Skip to content

Commit

Permalink
improve Union deserialization when "__type" field specifier is not pr…
Browse files Browse the repository at this point in the history
…esent (#478)
  • Loading branch information
idbentley authored Sep 12, 2023
1 parent b7ea954 commit 5207d26
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 16 deletions.
14 changes: 7 additions & 7 deletions dataclasses_json/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,17 +314,17 @@ def _decode_generic(type_, value, infer_missing):
type_options = _get_type_args(type_)
res = value # assume already decoded
if type(value) is dict and dict not in type_options:
# FIXME if all types in the union are dataclasses this
# will just pick the first option -
# maybe find the best fitting class in that case instead?
for type_option in type_options:
if is_dataclass(type_option):
res = _decode_dataclass(type_option, value, infer_missing)
break
try:
res = _decode_dataclass(type_option, value, infer_missing)
break
except (KeyError, ValueError):
continue
if res == value:
warnings.warn(
f"Failed to encode {value} Union dataclasses."
f"Expected Union to include a dataclass and it didn't."
f"Failed to decode {value} Union dataclasses."
f"Expected Union to include a matching dataclass and it didn't."
)
return res

Expand Down
27 changes: 18 additions & 9 deletions dataclasses_json/mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,25 @@ def _deserialize(self, value, attr, data, **kwargs):
if is_dataclass(type_) and type_.__name__ == dc_name:
del tmp_value['__type']
return schema_._deserialize(tmp_value, attr, data, **kwargs)
for type_, schema_ in self.desc.items():
if isinstance(tmp_value, _get_type_origin(type_)):
return schema_._deserialize(tmp_value, attr, data, **kwargs)
else:
elif isinstance(tmp_value, dict):
warnings.warn(
f'The type "{type(tmp_value).__name__}" (value: "{tmp_value}") '
f'is not in the list of possible types of typing.Union '
f'(dataclass: {self.cls.__name__}, field: {self.field.name}). '
f'Value cannot be deserialized properly.')
return super()._deserialize(tmp_value, attr, data, **kwargs)
f'Attempting to deserialize "dict" (value: "{tmp_value}) '
f'that does not have a "__type" type specifier field into'
f'(dataclass: {self.cls.__name__}, field: {self.field.name}).'
f'Deserialization may fail, or deserialization to wrong type may occur.'
)
return super()._deserialize(tmp_value, attr, data, **kwargs)
else:
for type_, schema_ in self.desc.items():
if isinstance(tmp_value, _get_type_origin(type_)):
return schema_._deserialize(tmp_value, attr, data, **kwargs)
else:
warnings.warn(
f'The type "{type(tmp_value).__name__}" (value: "{tmp_value}") '
f'is not in the list of possible types of typing.Union '
f'(dataclass: {self.cls.__name__}, field: {self.field.name}). '
f'Value cannot be deserialized properly.')
return super()._deserialize(tmp_value, attr, data, **kwargs)


class _TupleVarLen(fields.List):
Expand Down
42 changes: 42 additions & 0 deletions tests/test_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,21 @@ class Aux2:
f1: str


@dataclass_json
@dataclass
class Aux3:
f2: str

@dataclass_json
@dataclass
class C4:
f1: Union[Aux1, Aux2]

@dataclass_json
@dataclass
class C12:
f1: Union[Aux2, Aux3]


@dataclass_json
@dataclass
Expand Down Expand Up @@ -198,3 +208,35 @@ def test_deserialize_with_error(cls, data):
s = cls.schema()
with pytest.raises(ValidationError):
assert s.load(data)

def test_deserialize_without_discriminator():
# determine based on type
json = '{"f1": {"f1": 1}}'
s = C4.schema()
obj = s.loads(json)
assert obj.f1 is not None
assert type(obj.f1) == Aux1

json = '{"f1": {"f1": "str1"}}'
s = C4.schema()
obj = s.loads(json)
assert obj.f1 is not None
assert type(obj.f1) == Aux2

# determine based on field name
json = '{"f1": {"f1": "str1"}}'
s = C12.schema()
obj = s.loads(json)
assert obj.f1 is not None
assert type(obj.f1) == Aux2
json = '{"f1": {"f2": "str1"}}'
s = C12.schema()
obj = s.loads(json)
assert obj.f1 is not None
assert type(obj.f1) == Aux3

# if no matching types, type should remain dict
json = '{"f1": {"f3": "str2"}}'
s = C12.schema()
obj = s.loads(json)
assert type(obj.f1) == dict

12 comments on commit 5207d26

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 3.354s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py247996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142199%139
   test_str_subclass.py22195%9
TOTAL261312395% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.728s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py248996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py141299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py213399%20, 234, 240
   test_annotations.py804248%50–67, 78–102, 106–122
   test_api.py140299%139–140
   test_str_subclass.py22195%9
TOTAL249212495% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.258s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py247996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py234399%20, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142299%139–140
   test_str_subclass.py22195%9
TOTAL261312495% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.870s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 5.081s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 7.371s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py247996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py234399%20, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142299%139–140
   test_str_subclass.py22195%9
TOTAL261312495% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.629s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py247996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142199%139
   test_str_subclass.py22195%9
TOTAL261312395% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.497s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 3.498s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 3.423s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py248996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2042986%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py141299%24, 38
   utils.py1312978%12–25, 45–50, 61–65, 75, 100–101, 109–110, 163, 182, 207
tests
   entities.py213399%20, 234, 240
   test_annotations.py804248%50–67, 78–102, 106–122
   test_api.py140299%139–140
   test_str_subclass.py22195%9
TOTAL249212495% 

Tests Skipped Failures Errors Time
301 1 💤 0 ❌ 0 🔥 3.646s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
dataclasses_json
   cfg.py51492%80, 84–86
   core.py2481096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2053085%33–36, 42–45, 53–56, 62–65, 88, 170–171, 176, 180, 184, 189, 193, 197, 205, 211, 216, 225, 230, 235, 244, 253–260
   stringcase.py25388%59, 76, 97
   undefined.py143299%24, 38
   utils.py1313673%12–25, 45–50, 61–65, 75, 100–101, 109–110, 125–133, 163, 182, 207
tests
   entities.py234399%22, 234, 240
   test_annotations.py814248%50–67, 78–102, 106–122
   test_api.py142497%88, 99, 139–140
   test_str_subclass.py22195%9
   test_union.py1311092%97–104, 118–125
TOTAL261514594% 

Tests Skipped Failures Errors Time
301 3 💤 0 ❌ 0 🔥 5.562s ⏱️

Please sign in to comment.