Skip to content

Commit 4039490

Browse files
committed
(de)serialize externally tagged union directly
This example works correctly now ``` @serde @DataClass class Foo: a: int @serde @DataClass class Bar: a: int bar = Bar(10) s = to_json(bar) print(s) print(from_json(Union[Foo, Bar], s)) ``` However, This will introduce a breaking change! The default behaviour when you pass Union directly was "Untagged", but since v0.12.0 it is "ExternalTagging". The following code prints `{"a": 10}` until v0.11.1, but prints `{"Bar": {"a": 10}}` since v0.12.0 ``` print(to_json(bar)) ```
1 parent 20d1353 commit 4039490

File tree

14 files changed

+461
-47
lines changed

14 files changed

+461
-47
lines changed

docs/en/union.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,84 @@ A class declaration with `AdjacentTagging` looks like below. If you serialize `F
6666
class Foo:
6767
a: Union[Bar, Baz]
6868
```
69+
70+
## (de)serializing Union types directly
71+
72+
New in v0.12.0.
73+
74+
Passing Union types directly in (de)serialize APIs (e.g. to_json, from_json) was partially supported prior to v0.12, but the union type was always treated as untagged. Users had no way to change the union tagging. The following example code wasn't able to correctly deserialize into `Bar` due to untagged.
75+
76+
```python
77+
@serde
78+
@dataclass
79+
class Foo:
80+
a: int
81+
82+
@serde
83+
@dataclass
84+
class Bar:
85+
a: int
86+
87+
bar = Bar(10)
88+
s = to_json(bar)
89+
print(s)
90+
# prints {"a": 10}
91+
print(from_json(Union[Foo, Bar], s))
92+
# prints Foo(10)
93+
```
94+
95+
Since v0.12.0, pyserde can handle union that's passed in (de)serialize APIs a bit nicely. The union type is treated as externally tagged as that is the default tagging in pyserde. So the above example can correctly (de)serialize as `Bar`.
96+
97+
```python
98+
@serde
99+
@dataclass
100+
class Foo:
101+
a: int
102+
103+
@serde
104+
@dataclass
105+
class Bar:
106+
a: int
107+
108+
bar = Bar(10)
109+
s = to_json(bar)
110+
print(s)
111+
# prints {"Bar" {"a": 10}}
112+
print(from_json(Union[Foo, Bar], s))
113+
# prints Bar(10)
114+
```
115+
116+
Also you can change the tagging using `serde.InternalTagging`, `serde.AdjacentTagging` and `serde.Untagged`.
117+
118+
Now try to change the tagging for the above example. You need to pass a new argument `cls` in `to_json`. Also union class must be wrapped in either `InternalTagging`, `AdjacentTaging` or `Untagged` with required parameters.
119+
120+
* InternalTagging
121+
```python
122+
from serde import InternalTagging
123+
124+
s = to_json(bar, cls=InternalTagging("type", Union[Foo, Bar]))
125+
print(s)
126+
# prints {"type": "Bar", "a": 10}
127+
print(from_json(InternalTagging("type", Union[Foo, Bar]), s))
128+
# prints Bar(10)
129+
```
130+
* AdjacentTagging
131+
```python
132+
from serde import AdjacentTagging
133+
134+
s = to_json(bar, cls=AdjacentTagging("type", "content", Union[Foo, Bar]))
135+
print(s)
136+
# prints {"type": "Bar", "content": {"a": 10}}
137+
print(from_json(AdjacentTagging("type", "content", Union[Foo, Bar]), s))
138+
# prints Bar(10)
139+
```
140+
* Untagged
141+
```python
142+
from serde import Untagged
143+
144+
s = to_json(bar, cls=Untagged(Union[Foo, Bar]))
145+
print(s)
146+
# prints {"a": 10}
147+
print(from_json(Untagged(Union[Foo, Bar]), s))
148+
# prints Foo(10)
149+
```

examples/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import type_decimal
4141
import union
4242
import union_tagging
43+
import union_directly
4344
import user_exception
4445
import variable_length_tuple
4546
import yamlfile
@@ -73,6 +74,7 @@ def run_all() -> None:
7374
run(type_decimal)
7475
run(type_datetime)
7576
run(union_tagging)
77+
run(union_directly)
7678
run(generics)
7779
run(generics_nested)
7880
run(nested)

examples/union_directly.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from dataclasses import dataclass
2+
from typing import Union
3+
4+
from serde import serde, Untagged, AdjacentTagging, InternalTagging
5+
from serde.json import from_json, to_json
6+
7+
8+
@serde
9+
@dataclass
10+
class Bar:
11+
b: int
12+
13+
14+
@serde
15+
@dataclass(unsafe_hash=True)
16+
class Baz:
17+
b: int
18+
19+
20+
def main() -> None:
21+
baz = Baz(10)
22+
23+
print("# external tagging (default)")
24+
a = to_json(baz, cls=Union[Bar, Baz])
25+
print("IntoJSON", a)
26+
print("FromJSON", from_json(Union[Bar, Baz], a))
27+
28+
print("# internal tagging")
29+
a = to_json(baz, cls=InternalTagging("type", Union[Bar, Baz]))
30+
print("IntoJSON", a)
31+
print("FromJSON", from_json(InternalTagging("type", Union[Bar, Baz]), a))
32+
33+
print("# adjacent tagging")
34+
a = to_json(baz, cls=AdjacentTagging("type", "content", Union[Bar, Baz]))
35+
print("IntoJSON", a)
36+
print("FromJSON", from_json(AdjacentTagging("type", "content", Union[Bar, Baz]), a))
37+
38+
print("# untagged")
39+
a = to_json(baz, cls=Untagged(Union[Bar, Baz]))
40+
print("IntoJSON", a)
41+
print("FromJSON", from_json(Untagged(Union[Bar, Baz]), a))
42+
43+
44+
if __name__ == "__main__":
45+
main()

serde/compat.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ def get_np_args(tp: Any) -> Tuple[Any, ...]:
114114
""" List of datetime types """
115115

116116

117+
@dataclasses.dataclass
118+
class _WithTagging(Generic[T]):
119+
"""
120+
Intermediate data structure for (de)serializaing Union without dataclass.
121+
"""
122+
123+
inner: T
124+
""" Union type .e.g Union[Foo,Bar] passed in from_obj. """
125+
tagging: Any
126+
""" Union Tagging """
127+
128+
117129
class SerdeError(Exception):
118130
"""
119131
Serde error class.
@@ -475,6 +487,13 @@ def is_union(typ: Any) -> bool:
475487
True
476488
"""
477489

490+
try:
491+
# When `_WithTagging` is received, it will check inner type.
492+
if isinstance(typ, _WithTagging):
493+
return is_union(typ.inner)
494+
except Exception:
495+
pass
496+
478497
# Python 3.10 Union operator e.g. str | int
479498
if sys.version_info[:2] >= (3, 10):
480499
try:

0 commit comments

Comments
 (0)