Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class Based Discriminators do not work with aliases #254

Open
matthew-chambers-pushly opened this issue Oct 16, 2024 · 0 comments
Open

Class Based Discriminators do not work with aliases #254

matthew-chambers-pushly opened this issue Oct 16, 2024 · 0 comments

Comments

@matthew-chambers-pushly
Copy link
Contributor

Is your feature request related to a problem? Please describe.
When utilizing aliased fields (our use case is minifying datastructures on our edges to reduce costs), class based discriminators no longer function as intended with payloads that are aliased. The constructor for a Discriminator takes a single str as the parameter for field, which will throw an error when trying to deserialize a payload that has been aliased (or vice versa if you set the Discriminator.field to point to the aliased fieldname. MissingDiscriminatorError gets thrown immediately when calling .from_dict, but before __pre_deserialize__ runs (it has to because each subtype can have their implementation of __pre_deserialize__) so it is not possible to fix it with the hook; similarly __post_init__ is also off the table for the same reason.

Describe the solution you'd like
Ideally, for mashumaro to perform some resolution for discriminator.field if TO_DICT_ADD_BY_ALIAS_FLAG and allow_deserialization_not_by_alias are enabled, or another configuration option/flag if necessary. Alternatively, being able to provide a tuple of the possible field names for the parser to lazily loop through when performing the discrimination.

Describe alternatives you've considered
The alternatives seem to be using a preprocessor to do some operation like:

d = {"uf": "foobar"}
try:
    d["unaliased_field"] = d.pop("uf")
except KeyError:
    ...

MyModel.from_dict(d)

where the fields are set to the happiest expected path (this would require allow_deserialization_not_by_alias to be enabled). It does not feel appropriate to require a helper function to utilize mashumaro dataclasses around our codebase.

Alternatively, not utilizing the aliases feature on the discriminator field is a viable workaround.

Additional context
The major reason for submitting this request is consistency between human readable formats and aliased formats. The byte difference is small when leaving one field without an alias configuration but that can add up quickly when processing things at scale and it also brings a small amount of "gotcha" or tribal knowledge ("Why does this field not have an alias? Let me fix that real quick... oh no everything broke!"). There is no mention of aliases being incompatible with class based discriminators so at the very least the documentation can be updated to reflect this.

Here are some models and tests that reproduce the error:

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

from mashumaro import field_options
from mashumaro.config import BaseConfig, TO_DICT_ADD_BY_ALIAS_FLAG
from mashumaro.mixins.dict import DataClassDictMixin
from mashumaro.types import Discriminator


class Profession(str, Enum):
    UNKNOWN = "unknown"
    DENTIST = "dentist"
    SURGEON = "surgeon"


class BaseModel(DataClassDictMixin):
    class Config(BaseConfig):
        code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]
        allow_deserialization_not_by_alias = True


@dataclass
class Address(BaseModel):
    street: str = field(metadata=field_options(alias="s"))
    zip_code: str = field(metadata=field_options(alias="z"))


@dataclass
class Person(BaseModel):
    first_name: str = field(metadata=field_options(alias="fn"))
    last_name: str = field(metadata=field_options(alias="ln"))
    address: Optional[Address] = field(metadata=field_options(alias="a"), default=None)
    profession: Profession = field(metadata=field_options(alias="p"), default=Profession.UNKNOWN)

    class Config(BaseConfig):
        code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]
        discriminator = Discriminator(
            field="profession",
            include_subtypes=True,
        )
        allow_deserialization_not_by_alias = True


@dataclass
class Dentist(Person):
    profession: Profession = field(metadata=field_options(alias="p"), default=Profession.DENTIST)
    ...


@dataclass
class Surgeon(Person):
    profession: Profession = field(metadata=field_options(alias="p"), default=Profession.SURGEON)
    ...


def test_model_load():
    raw_data = {
        "first_name": "jerry",
        "last_name": "seefree",
        "address": {"street": "15256 green st", "zip_code": "16563"},
        "profession": "surgeon",
    }

    m = Person.from_dict(raw_data)
    assert m.first_name == "jerry"
    assert isinstance(m, Surgeon)


def test_model_load_alias():
    raw_data = {"fn": "jerry", "ln": "seefree", "a": {"s": "15256 green st", "z": "16563"}, "p": "surgeon"}

    p = Person.from_dict(raw_data)
    assert isinstance(p, Surgeon)

The first test will pass but the second will throw:

cls = <class 'tests.test_models.Person'>
value = {'a': {'s': '15256 green st', 'z': '16563'}, 'fn': 'jerry', 'ln': 'seefree', 'p': 'surgeon'}
_dialect = None, _default_dialect = None

>   ???
E   mashumaro.exceptions.MissingDiscriminatorError: Discriminator 'profession' is missing

<string>:6: MissingDiscriminatorError
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

No branches or pull requests

1 participant