Skip to content

[Bug] Response models declare numeric fields as strict str, causing silent fallback of .data() to raw dict (simple-earn & staking) #553

Description

@zcnova

[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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions