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

Adds a configuration option to allow mixed alias/unaliased field name use when deserializing #175

Merged
merged 8 commits into from
Nov 14, 2023
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Table of contents
* [`discriminator` config option](#discriminator-config-option)
* [`lazy_compilation` config option](#lazy_compilation-config-option)
* [`sort_keys` config option](#sort_keys-config-option)
* [`loose_deserialization` config option](#loose_deserialization-config-option)
* [Passing field values as is](#passing-field-values-as-is)
* [Extending existing types](#extending-existing-types)
* [Dialects](#dialects)
Expand Down Expand Up @@ -1391,6 +1392,36 @@ t = SortedDataClass(1, 2)
assert t.to_dict() == {"bar": 2, "foo": 1}
```

#### `loose_deserialization` config option

When using aliases, the deserializer defaults to requiring the keys to match
what is defined as the alias in the metadata.
If the flexibility to deserialize aliased and unaliased keys is required then
the config option `loose_deserialization = True` can be set to enable the
feature.

```python
from dataclasses import dataclass, field
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig, TO_DICT_ADD_BY_ALIAS_FLAG

@dataclass
class AliasedDataClass(DataClassDictMixin):
foo: int = field(metadata={"alias": "alias_foo"})
bar: int = field(metadata={"alias": "alias_bar"})

class Config(BaseConfig):
serialize_by_alias = True
loose_deserialization = True
code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]

no_alias_dict = {"bar": 2, "foo": 1}
# Will raise `mashumaro.exceptions.MissingField` if loose_deserialization is
# False
t = AliasedDataClass.from_dict(no_alias_dict)
assert t.to_dict(by_alias=False) == {"bar": 2, "foo": 1}
```

### Passing field values as is

In some cases it's needed to pass a field value as is without any changes
Expand Down
1 change: 1 addition & 0 deletions mashumaro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ class BaseConfig:
discriminator: Optional[Discriminator] = None
lazy_compilation: bool = False
sort_keys: bool = False
loose_deserialization: bool = False
38 changes: 29 additions & 9 deletions mashumaro/core/meta/code/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -554,16 +554,36 @@ def _unpack_method_set_value(
could_be_none=False if could_be_none else True,
)
)
if unpacked_value != "value":
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
elif has_default:
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
if self.get_config().loose_deserialization:
if unpacked_value != "value":
self.add_line(
f"value = d.get('{alias}', d.get('{fname}', MISSING))"
)
packed_value = "value"
elif has_default:
self.add_line(
f"value = d.get('{alias}', d.get('{fname}', MISSING))"
)
packed_value = "value"
else:
self.add_line(
f"__{fname} = d.get('{alias}', d.get('{fname}', MISSING))"
)
packed_value = f"__{fname}"
unpacked_value = packed_value
Fatal1ty marked this conversation as resolved.
Show resolved Hide resolved
else:
self.add_line(f"__{fname} = d.get('{alias or fname}', MISSING)")
packed_value = f"__{fname}"
unpacked_value = packed_value
if unpacked_value != "value":
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
elif has_default:
self.add_line(f"value = d.get('{alias or fname}', MISSING)")
packed_value = "value"
else:
self.add_line(
f"__{fname} = d.get('{alias or fname}', MISSING)"
)
packed_value = f"__{fname}"
unpacked_value = packed_value
if not has_default:
with self.indent(f"if {packed_value} is MISSING:"):
self.add_line(
Expand Down
17 changes: 17 additions & 0 deletions tests/test_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,20 @@ class Config(BaseConfig):

assert DataClass(x=123).to_dict() == {"alias": 123}
assert DataClass(x=None).to_dict() == {"alias": None}


def test_by_field_with_loose_deserialize():
@dataclass
class DataClass(DataClassDictMixin):
a: int = field(metadata={"alias": "alias_a"})
b: Optional[int] = field(metadata={"alias": "alias_b"})
Fatal1ty marked this conversation as resolved.
Show resolved Hide resolved

class Config(BaseConfig):
serialize_by_alias = True
code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]
loose_deserialization = True

instance = DataClass(a=123, b=456)
assert DataClass.from_dict({"a": 123, "alias_b": 456}) == instance
assert instance.to_dict() == {"alias_a": 123, "alias_b": 456}
assert instance.to_dict(by_alias=False) == {"a": 123, "b": 456}