Fatal1ty / mashumaro

Fast and well tested serialization library
Apache License 2.0
751 stars 44 forks source link

Not parsing Generics correctly #193

Closed GuyKh closed 6 months ago

GuyKh commented 6 months ago

Description

I use this module parsing an API response where there's a DataWrapper for the content - e.g.

{
    "data": {
        "fieldA": 0,
        "fieldB": 0
    },
    "responseDescriptor": {
        "isSuccess": true,
        "code": null,
        "description": null
    }
}

Where data field varies based on the endpoint.

I modeled responseDescriptor:

@dataclass
class ResponseDescriptor(DataClassDictMixin):
    """Response Descriptor"""

    is_success: bool = field(metadata=field_options(alias="isSuccess"))
    code: Optional[str]
    description: Optional[str]

and the full response object:

T = TypeVar("T")

@dataclass
class ResponseWithDescriptor(Generic[T], DataClassDictMixin):
    """Response With Descriptor"""

    data: T
    response_descriptor: ResponseDescriptor = field(metadata=field_options(alias="reponseDescriptor"))

but when trying to parse - it doesn't recognize the correct T:

def _get_response_with_descriptor(token: JWT, url: str) -> T:
    ...
response_with_descriptor: ResponseWithDescriptor[T] = ResponseWithDescriptor[T].from_dict(response.json())
return response_with_descriptor.data

What I Did

see above

I guess I'm doing something wrong here but what should I do otherwise?

Fatal1ty commented 6 months ago

Hi @GuyKh

ResponseWithDescriptor[T].from_dict is equal to ResponseWithDescriptor.from_dict at runtime due to type erasure (this is how generic types work in Python). In your example, you just call from_dict, which belongs to ResponseWithDescriptor[Any], so to speak.

if you look at how does it work, you will see that method from_dict is compiled and set as attribute to your class. You have only one class here — ResponseWithDescriptor. It’s not possible to set an attribute to ResponseWithDescriptor[T] because at runtime it’s the same type.

I can see different options for you: 1) Create a subclass of ResponseWithDescriptor with T replaced by something specific (see example) 2) Use ResponseWithDescriptor[T] as a field type in another generic dataclass with parameter T that will be parametrized later (as above) 3) Use codecs instead of mixins

from mashumaro.codecs import BasicDecoder
data_type_1_decoder =  BasicDecoder(ResponseWithDescriptor[DataType1])
# create and reuse this decoder, because it’s expensive to create it from scratch each time you need it 
data_type_1_decoder.decode({…})
GuyKh commented 6 months ago

@Fatal1ty works like a charm. I took the 3rd option, creating for every DataType a member of the class (decoder):

// Contract.py
decoder = BasicDecoder(ResponseWithDescriptor[Contracts])

And then using it as follows:

from contract import Contract, Contracts
from contract import decoder as contract_decoder
...

def get_default_contract(token: JWT, bp_number: str) -> Contract:
    return _get_response_with_descriptor(token, url, contract_decoder)

...
def _get_response_with_descriptor(jwt_token: JWT, request_url: str,
                                  decoder: BasicDecoder[ResponseWithDescriptor[T]]) -> T:
            ...
            return decoder.decode(response.json())

Thank you for this quick response - especially with the broad option range. Truly appreciated