[Bug] Response models declare numeric fields as strict str, causing silent fallback of .data() to raw dict (simple-earn & staking)
Environment
binance-sdk-simple-earn 8.0.0
binance-sdk-staking 5.11.0
binance-common 4.0.0
- Python 3.14.6, Windows 11
Summary
Several REST response models declare fields as StrictStr while the live API returns JSON numbers for them. Pydantic strict validation therefore fails on every real response, and binance_common.utils.send_request silently falls back to returning the raw parsed JSON. As a result, ApiResponse.data() returns a plain dict instead of the documented model object for these endpoints — data.to_dict() / attribute access then raises AttributeError in user code, with no warning that validation failed.
All cases below were reproduced against the production API with real responses on 2026-07-07/08.
Case 1 (verified): SubscribeLockedProductResponse.position_id
POST /sapi/v1/simple-earn/locked/subscribe returns positionId as a number, but the model declares it as strict str.
from binance_sdk_simple_earn.rest_api.models.subscribe_locked_product_response import SubscribeLockedProductResponse
# actual live response of a successful subscription
payload = {'purchaseId': 351129167, 'positionId': 347608223, 'success': True, 'amount': '0.05'}
SubscribeLockedProductResponse.model_validate(payload)
1 validation error for SubscribeLockedProductResponse
positionId
Input should be a valid string [type=string_type, input_value=347608223, input_type=int]
Note: purchase_id is declared StrictInt in the same model, so the numeric purchaseId passes while the numeric positionId fails — the declarations are inconsistent within one model.
Case 2 (verified): GetLockedProductPositionResponse rows
GET /sapi/v1/simple-earn/locked/position rows contain numeric duration, accrualDays, payPeriod, redeemPeriod, but the rows-inner model declares them as strict str.
from binance_sdk_simple_earn.rest_api.models.get_locked_product_position_response import GetLockedProductPositionResponse
# actual live response row of a held position
row = {'positionId': 347608038, 'projectId': 'Bnb*120', 'asset': 'BNB', 'amount': '0.05',
'purchaseTime': 1783439066000, 'duration': 120, 'accrualDays': 0, 'rewardAsset': 'BNB',
'rewardAmt': '0', 'nextPay': '0.00000054', 'nextPayDate': 1783555200000, 'payPeriod': 1,
'redeemAmountEarly': '0.05', 'rewardsEndDate': 1793836800000, 'deliverDate': 1793959200000,
'redeemPeriod': 1, 'canRedeemEarly': True, 'canFastRedemption': True, 'autoSubscribe': False,
'type': 'NORMAL', 'status': 'HOLDING', 'canReStake': False, 'redeemTo': 'SPOT',
'totalBoostRewardAmt': '0', 'apy': '0.004'}
GetLockedProductPositionResponse.model_validate({'rows': [row], 'total': 1})
4 validation errors for GetLockedProductPositionResponse
rows.0.duration Input should be a valid string [input_value=120, input_type=int]
rows.0.accrualDays Input should be a valid string [input_value=0, input_type=int]
rows.0.payPeriod Input should be a valid string [input_value=1, input_type=int]
rows.0.redeemPeriod Input should be a valid string [input_value=1, input_type=int]
Important detail: with an empty rows the wrapper model validates fine, so the endpoint appears healthy until the account actually holds a position — then every call degrades to a raw dict.
Case 3 (same pattern, static): SubscribeOnChainYieldsLockedProductResponse.position_id
binance-sdk-staking, POST /sapi/v1/onchain-yields/locked/subscribe — the model declares:
purchase_id: StrictInt | None
position_id: StrictStr | None <-- same inconsistency as Case 1
The twin Simple Earn endpoint empirically returns positionId as a number (Case 1), and the official docs example for this endpoint also shows a numeric positionId.
Case 4 (same pattern, static): GetOnChainYieldsLockedProductPositionResponseRowsInner
Every field of the rows-inner model is declared strict str, including ones that are numbers on the wire for the twin locked endpoint (Case 2): purchase_time, duration, accrual_days, pay_period, next_pay_date, rewards_end_date, deliver_date, ...
Impact
ApiResponse.data() type is inconsistent per endpoint (model vs raw dict), and the switch is silent — user code calling .to_dict() or accessing attributes crashes with AttributeError.
- Particularly nasty on the subscribe endpoint: the subscription has already executed server-side, then response parsing fails client-side, which looks like a failed request.
Suggested fix
- Align model field types with the actual wire types (
int for positionId, duration, timestamps, etc.), or relax Strict to allow coercion.
- Independently of the type fixes: consider logging a warning in
binance_common.utils.send_request when model validation fails and the raw payload is returned, so the fallback is observable.
[Bug] Response models declare numeric fields as strict
str, causing silent fallback of.data()to raw dict (simple-earn & staking)Environment
binance-sdk-simple-earn8.0.0binance-sdk-staking5.11.0binance-common4.0.0Summary
Several REST response models declare fields as
StrictStrwhile the live API returns JSON numbers for them. Pydantic strict validation therefore fails on every real response, andbinance_common.utils.send_requestsilently falls back to returning the raw parsed JSON. As a result,ApiResponse.data()returns a plaindictinstead of the documented model object for these endpoints —data.to_dict()/ attribute access then raisesAttributeErrorin user code, with no warning that validation failed.All cases below were reproduced against the production API with real responses on 2026-07-07/08.
Case 1 (verified):
SubscribeLockedProductResponse.position_idPOST /sapi/v1/simple-earn/locked/subscribereturnspositionIdas a number, but the model declares it as strictstr.Note:
purchase_idis declaredStrictIntin the same model, so the numericpurchaseIdpasses while the numericpositionIdfails — the declarations are inconsistent within one model.Case 2 (verified):
GetLockedProductPositionResponserowsGET /sapi/v1/simple-earn/locked/positionrows contain numericduration,accrualDays,payPeriod,redeemPeriod, but the rows-inner model declares them as strictstr.Important detail: with an empty
rowsthe wrapper model validates fine, so the endpoint appears healthy until the account actually holds a position — then every call degrades to a raw dict.Case 3 (same pattern, static):
SubscribeOnChainYieldsLockedProductResponse.position_idbinance-sdk-staking,POST /sapi/v1/onchain-yields/locked/subscribe— the model declares:The twin Simple Earn endpoint empirically returns
positionIdas a number (Case 1), and the official docs example for this endpoint also shows a numericpositionId.Case 4 (same pattern, static):
GetOnChainYieldsLockedProductPositionResponseRowsInnerEvery field of the rows-inner model is declared strict
str, including ones that are numbers on the wire for the twin locked endpoint (Case 2):purchase_time,duration,accrual_days,pay_period,next_pay_date,rewards_end_date,deliver_date, ...Impact
ApiResponse.data()type is inconsistent per endpoint (model vs raw dict), and the switch is silent — user code calling.to_dict()or accessing attributes crashes withAttributeError.Suggested fix
intforpositionId,duration, timestamps, etc.), or relaxStrictto allow coercion.binance_common.utils.send_requestwhen model validation fails and the raw payload is returned, so the fallback is observable.