Skip to content

Commit

Permalink
Fix for #239: Union inside List or Dict is not deserialized as the co…
Browse files Browse the repository at this point in the history
…rrespond… (#464)
  • Loading branch information
pawelwilczewski authored Sep 1, 2023
1 parent 54c6061 commit 02c561f
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 2 deletions.
18 changes: 16 additions & 2 deletions dataclasses_json/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,22 @@ def _decode_generic(type_, value, infer_missing):
res = _decode_generic(type_arg, value, infer_missing)
else:
res = _support_extended_types(type_arg, value)
else: # Union (already decoded or unsupported 'from_json' used)
res = value
else: # Union (already decoded or try to decode a dataclass)
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
if res == value:
warnings.warn(
f"Failed to encode {value} Union dataclasses."
f"Expected Union to include a dataclass and it didn't."
)
return res


Expand Down
172 changes: 172 additions & 0 deletions tests/test_collection_of_unions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from dataclasses import dataclass
from typing import Dict, Union, List
import json

from dataclasses_json import dataclass_json


@dataclass_json
@dataclass(frozen=True)
class TestChild:
some_field: int = None


@dataclass_json
@dataclass(frozen=True)
class TestOtherChild:
other_field: int = None


@dataclass_json
@dataclass(frozen=True)
class DictUnion:
d: Dict[str, Union[TestChild, TestOtherChild]]


@dataclass_json
@dataclass(frozen=True)
class ListUnion:
l: List[Union[TestChild, TestOtherChild]]


@dataclass_json
@dataclass(frozen=True)
class Player:
name: str


@dataclass_json
@dataclass(frozen=True)
class Team:
roster: List[Union[int, Player]]
roster_backup: Dict[int, Union[int, Player]]


class TestCollectionOfUnions:
def test_dict(self):
data = {
'd': {
'child' : {
'some_field' : 1
},
'other_child' : {
'other_field' : 2
}
}
}
json_str = json.dumps(data)
obj = DictUnion.from_json(json_str)

assert type(obj.d['child']) in (TestChild, TestOtherChild)

def test_list(self):
data = {
'l': [
{
'some_field' : 1
},
{
'other_field' : 1
}
]
}
json_str = json.dumps(data)
obj = ListUnion.from_json(json_str)

assert type(obj.l[0]) in (TestChild, TestOtherChild)

def test_int(self):
data = {
'roster': [
1,
2,
3
],
'roster_backup': {
1: 5,
2: 3,
3: 2
}
}
json_str = json.dumps(data)
obj: Team = Team.from_json(json_str)

assert type(obj.roster[0]) is int and type(obj.roster_backup[1]) is int

def test_dataclass(self):
data = {
'roster': [
{
'name': 'player1'
},
{
'name': 'player2'
},
{
'name': 'player3'
}
],
'roster_backup': {
1: {
'name': 'player1'
},
2: {
'name': 'player2'
},
3: {
'name': 'player3'
}
}
}
json_str = json.dumps(data)
obj: Team = Team.from_json(json_str)

assert type(obj.roster[0]) is Player and type(obj.roster_backup[1]) is Player

def test_mixed(self):
data = {
'roster': [
{
'name': 'player1'
},
{
'name': 'player2'
},
{
'name': 'player3'
}
],
'roster_backup': {
1: 5,
2: 3,
3: 2
}
}
json_str = json.dumps(data)
obj: Team = Team.from_json(json_str)

assert type(obj.roster[0]) is Player and type(obj.roster_backup[1]) is int

def test_mixed_inverse(self):
data = {
'roster': [
1,
2,
3
],
'roster_backup': {
1: {
'name': 'player1'
},
2: {
'name': 'player2'
},
3: {
'name': 'player3'
}
}
}
json_str = json.dumps(data)
obj: Team = Team.from_json(json_str)

assert type(obj.roster[0]) is int and type(obj.roster_backup[1]) is Player

6 comments on commit 02c561f

@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.py2451096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2023085%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 235, 244–251
   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.py981090%87–94, 108–115
TOTAL257614594% 

Tests Skipped Failures Errors Time
300 3 💤 0 ❌ 0 🔥 3.630s ⏱️

@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.py2451096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2023085%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 235, 244–251
   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.py981090%87–94, 108–115
TOTAL257614594% 

Tests Skipped Failures Errors Time
300 3 💤 0 ❌ 0 🔥 3.581s ⏱️

@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.py244996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2012986%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 244–251
   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
TOTAL257412395% 

Tests Skipped Failures Errors Time
300 1 💤 0 ❌ 0 🔥 3.547s ⏱️

@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.py245996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2012986%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 244–251
   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
TOTAL245512495% 

Tests Skipped Failures Errors Time
300 1 💤 0 ❌ 0 🔥 3.996s ⏱️

@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.py244996%38–41, 51, 64, 66, 81, 83, 169
   mm.py2012986%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 244–251
   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
TOTAL257412495% 

Tests Skipped Failures Errors Time
300 1 💤 0 ❌ 0 🔥 4.595s ⏱️

@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.py2451096%38–41, 51, 64, 66, 81, 83, 169, 197
   mm.py2023085%33–36, 42–45, 53–56, 62–65, 88, 161–162, 167, 171, 175, 180, 184, 188, 196, 202, 207, 216, 221, 226, 235, 244–251
   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.py981090%87–94, 108–115
TOTAL257614594% 

Tests Skipped Failures Errors Time
300 3 💤 0 ❌ 0 🔥 6.032s ⏱️

Please sign in to comment.