binance / binance/binance-connector-python

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

Open
#553 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
2.9k
Forks
697
PR merge metrics
No merged PRs in 30d

Description

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

```python
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`.

```python
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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reproducing the model_validate examples in the issue. Inspect the named models in binance-sdk-simple-earn/rest_api/models and binance-sdk-staking/rest_api/models, then read binance_common.utils.send_request to understand the raw-dict fallback. Done means numeric production-shaped payloads return model objects consistently, with the fallback behavior addressed as intended.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.