Skip to content

Conversation

@mobias17
Copy link

Issues Addressed

Fixes #476, #477, #405

Problem Description

The generated OpenAPI models using Pydantic's oneOf schema were failing to properly deserialize JSON data. This affected multiple model types across both REST API and WebSocket API implementations:

  1. Filter Models (AssetFilters, ExchangeFilters, SymbolFilters) - The filterType field was not being used as a discriminator, causing deserialization to fail or be extremely inefficient
  2. Event Models (UserDataStreamEventsResponse) - The e field (event type) was not routing to the correct event schema
  3. Parent Model Chain - Models containing oneOf fields were not properly deserializing nested oneOf structures

Root Cause: The auto-generated from_dict() methods attempted to validate against all possible schemas sequentially (O(n) complexity), and model_validate() bypassed the custom discriminator logic entirely, leaving actual_instance as None.

Changes Made

1. Discriminator-Based Routing

Added discriminator maps to all oneOf models:

  • Filter Models (6 files): Use filterType field to directly instantiate the correct filter class
    • rest_api.models.asset_filters (1 filter type)
    • rest_api.models.exchange_filters (4 filter types)
    • rest_api.models.symbol_filters (16 filter types)
    • websocket_api.models.asset_filters (1 filter type)
    • websocket_api.models.exchange_filters (4 filter types)
    • websocket_api.models.symbol_filters (16 filter types)
  • Event Model (1 file): Use e field to route to correct event type
    • websocket_api.models.user_data_stream_events_response (6 event types)
@classmethod
def from_dict(cls, parsed) -> Self:
    if isinstance(parsed, dict) and "filterType" in parsed:
        filter_type_map = {"PRICE_FILTER": PriceFilter, "LOT_SIZE": LotSizeFilter, ...}
        target_cls = filter_type_map.get(parsed.get("filterType"))
        if target_cls is not None:
            instance = cls.model_construct()
            instance.actual_instance = target_cls.from_dict(parsed)
            return instance
    # fallback for unrecognized types

2. model_validate() Override

Added model_validate() classmethod to all oneOf models to delegate to discriminator logic:

@classmethod
def model_validate(cls, obj: Any) -> Self:
    if isinstance(obj, dict):
        return cls.from_dict(obj)
    return super().model_validate(obj)

This ensures both Model.from_dict() and Model.model_validate() use the same efficient discriminator routing.

3. Parent Model Support

Added model_validate() overrides to parent models in the deserialization chain (7 files):

  • REST API: ExchangeInfoResponse, ExchangeInfoResponseSymbolsInner, RateLimits
  • WebSocket API: ExchangeInfoResponse, ExchangeInfoResponseResult, ExchangeInfoResponseResultSymbolsInner, ExchangeInfoResponseResultSorsInner, RateLimits

Updated their from_dict() methods to use BaseModel.model_validate.__func__(cls, obj) to avoid infinite recursion while properly deserializing nested oneOf fields.

4. Legacy Code Removal

Removed 300+ lines of inefficient sequential validation code that attempted to try each schema in order.

Why This Works

  1. O(1) Lookup: Discriminator fields provide direct routing to the correct schema instead of O(n) trial-and-error
  2. Consistent API: Both from_dict() and model_validate() now work identically
  3. Proper Nesting: Parent models correctly delegate to child oneOf models through the entire hierarchy
  4. No Recursion: Using BaseModel.model_validate.__func__() calls Pydantic's native validation, avoiding infinite loops

Testing

All existing tests pass (54 tests total):

  • ✅ 26 REST API tests (including symbol/exchange filters)
  • ✅ 28 WebSocket API tests (including user data stream events)

Validated discriminator routing for:

  • 16 different symbol filter types
  • 4 exchange filter types
  • 1 asset filter type
  • 6 user data stream event types

Comparison to PR #423

Unlike PR #423 which only addressed UserDataStreamEventsResponse by wrapping input data with {"actual_instance": ...}, this PR:

  • Fixes all affected oneOf models (7 models vs 1)
  • Uses proper discriminator fields instead of workarounds
  • Provides O(1) performance instead of sequential validation
  • Fixes nested oneOf deserialization through parent models

Review Notes

Key files to review:

  1. Any *_filters.py file - check discriminator map completeness
  2. user_data_stream_events_response.py - verify event type routing
  3. Parent models with model_validate overrides - ensure no recursion

Testing suggestion:

# Should work with both methods now:
data = {"filterType": "PRICE_FILTER", "minPrice": "0.1", ...}
filter1 = SymbolFilters.from_dict(data)  # Works
filter2 = SymbolFilters.model_validate(data)  # Also works
assert filter1.actual_instance.__class__ == filter2.actual_instance.__class__

# Warning
# filter3 = SymbolFilters(**data)  # Does not work, but also not applied in codebase.

@mobias17 mobias17 marked this pull request as ready for review January 28, 2026 21:52
@mobias17
Copy link
Author

Closing due to improvements in binance-sdk-spot 7.0.0 and binance-common 3.5.0. Evaluating whether new PR is required.

@mobias17 mobias17 closed this Jan 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SPOT : SymbolFilters does not deserialize correctly, instance is always None

1 participant