Skip to content

Commit 514a38a

Browse files
authored
Merge pull request #459 from yukinarit/custom-global-serializer
Implement global (de)serializer
2 parents 06a1d47 + 43721b2 commit 514a38a

File tree

10 files changed

+258
-26
lines changed

10 files changed

+258
-26
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ Foo(i=10, s='foo', f=100.0, b=True)
9292
- [Rename](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#rename)
9393
- [Alias](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#alias)
9494
- Skip (de)serialization ([skip](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#skip), [skip_if](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#skip_if), [skip_if_false](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#skip_if_false), [skip_if_default](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#skip_if_default))
95-
- [Custom class (de)serializer](https://github.com/yukinarit/pyserde/blob/main/docs/en/class-attributes.md#serializer--deserializer)
9695
- [Custom field (de)serializer](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#serializerdeserializer)
96+
- [Custom class (de)serializer](https://github.com/yukinarit/pyserde/blob/main/docs/en/class-attributes.md#class_serializer--class_deserializer)
97+
- [Custom global (de)serializer](https://github.com/yukinarit/pyserde/blob/main/docs/en/extension.md#custom-global-deserializer)
9798
- [Flatten](https://github.com/yukinarit/pyserde/blob/main/docs/en/field-attributes.md#flatten)
9899

99100
## Contributors ✨

docs/en/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
- [Field Attributes](field-attributes.md)
1010
- [Union](union.md)
1111
- [Type Check](type-check.md)
12+
- [Extension](extension.md)
1213
- [FAQ](faq.md)

docs/en/class-attributes.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ New in v0.7.0. See [Union](union.md).
7575
If you want to use a custom (de)serializer at class level, you can pass your (de)serializer object in `class_serializer` and `class_deserializer` class attributes. Class custom (de)serializer depends on a python library [plum](https://github.com/beartype/plum) which allows multiple method overloading like C++. With plum, you can write robust custom (de)serializer in a quite neat way.
7676
7777
```python
78-
class MySerializer(ClassSerializer):
78+
class MySerializer:
7979
@dispatch
8080
def serialize(self, value: datetime) -> str:
8181
return value.strftime("%d/%m/%y")
8282
83-
class MyDeserializer(ClassDeserializer):
83+
class MyDeserializer:
8484
@dispatch
8585
def deserialize(self, cls: Type[datetime], value: Any) -> datetime:
8686
return datetime.strptime(value, "%d/%m/%y")
@@ -97,6 +97,28 @@ Also,
9797
* If both field and class serializer specified, field serializer is prioritized
9898
* If both legacy and new class serializer specified, new class serializer is prioritized
9999

100+
> 💡 Tip: If you implements multiple `serialize` methods, you will receive "Redefinition of unused `serialize`" warning from type checker. In such case, try using `plum.overload` and `plum.dispatch` to workaround it. See [plum's documentation](https://beartype.github.io/plum/integration.html) for more information.
101+
>
102+
> ```python
103+
> from plum import dispatch, overload
104+
>
105+
> class Serializer:
106+
> # use @overload
107+
> @overload
108+
> def serialize(self, value: int) -> Any:
109+
> return str(value)
110+
>
111+
> # use @overload
112+
> @overload
113+
> def serialize(self, value: float) -> Any:
114+
> return int(value)
115+
>
116+
> # Add method time and make sure to add @dispatch. Plum will do all the magic to erase warnings from type checker.
117+
> @dispatch
118+
> def serialize(self, value: Any) -> Any:
119+
> ...
120+
> ```
121+
100122
See [examples/custom_class_serializer.py](https://github.com/yukinarit/pyserde/blob/main/examples/custom_class_serializer.py) for complete example.
101123
102124
New in v0.13.0.

docs/en/extension.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Extending pyserde
2+
3+
pyserde offers three ways to extend pyserde to support non builtin types.
4+
5+
## Custom field (de)serializer
6+
7+
See [custom field serializer](./field-attributes.md#serializerdeserializer).
8+
9+
> 💡 Tip: wrapping `serde.field` with your own field function makes
10+
>
11+
> ```python
12+
> import serde
13+
>
14+
> def field(*args, **kwargs):
15+
> serde.field(*args, **kwargs, serializer=str)
16+
>
17+
> @serde
18+
> class Foo:
19+
> a: int = field(default=0) # Configuring field serializer
20+
> ```
21+
22+
## Custom class (de)serializer
23+
24+
See [custom class serializer](./class-attributes.md#class_serializer--class_deserializer).
25+
26+
## Custom global (de)serializer
27+
28+
You apply the custom (de)serialization for entire codebase by registering class (de)serializer by `add_serializer` and `add_deserializer`. Registered class (de)serializers are stacked in pyserde's global space and automatically used for all the pyserde classes.
29+
30+
e.g. Implementing custom (de)serialization for `datetime.timedelta` using [isodate](https://pypi.org/project/isodate/) package.
31+
32+
Here is the code of registering class (de)serializer for `datetime.timedelta`.
33+
34+
```python
35+
from datetime import timedelta
36+
from plum import dispatch
37+
from typing import Type, Any
38+
import isodate
39+
import serde
40+
41+
class Serializer:
42+
@dispatch
43+
def serialize(self, value: timedelta) -> Any:
44+
return isodate.duration_isoformat(value)
45+
46+
class Deserializer:
47+
@dispatch
48+
def deserialize(self, cls: Type[timedelta], value: Any) -> timedelta:
49+
return isodate.parse_duration(value)
50+
51+
def init() -> None:
52+
serde.add_serializer(Serializer())
53+
serde.add_deserializer(Deserializer())
54+
```
55+
56+
Users of this package can reuse custom (de)serialization functionality for `datetime.timedelta` just by calling `serde_timedelta.init()`.
57+
58+
```python
59+
import serde_timedelta
60+
from serde import serde
61+
from serde.json import to_json, from_json
62+
from datetime import timedelta
63+
64+
serde_timedelta.init()
65+
66+
@serde
67+
class Foo:
68+
a: timedelta
69+
70+
f = Foo(timedelta(hours=10))
71+
json = to_json(f)
72+
print(json)
73+
print(from_json(Foo, json))
74+
```
75+
and you get `datetime.timedelta` to be serialized in ISO 8601 duration format!
76+
```bash
77+
{"a":"PT10H"}
78+
Foo(a=datetime.timedelta(seconds=36000))
79+
```
80+
81+
> 💡 Tip: You can register as many class (de)serializer as you want. This means you can use as many pyserde extensions as you want.
82+
> Registered (de)serializers are stacked in the memory. A (de)serializer can be overridden by another (de)serializer.
83+
>
84+
> e.g. If you register 3 custom serializers in this order, the first serializer will completely overridden by the 3rd one. 2nd one works because it is implemented for a different type.
85+
> 1. Register Serializer for `int`
86+
> 2. Register Serializer for `float`
87+
> 3. Register Serializer for `int`
88+
89+
New in v0.13.0.

examples/custom_class_serializer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,19 @@
33
from datetime import datetime
44
from serde import (
55
serde,
6-
ClassSerializer,
7-
ClassDeserializer,
86
field,
97
)
108
from serde.json import from_json, to_json
119
from typing import Type, Any, List
1210

1311

14-
class MySerializer(ClassSerializer):
12+
class MySerializer:
1513
@dispatch
1614
def serialize(self, value: datetime) -> str:
1715
return value.strftime("%d/%m/%y")
1816

1917

20-
class MyDeserializer(ClassDeserializer):
18+
class MyDeserializer:
2119
@dispatch
2220
def deserialize(self, cls: Type[datetime], value: Any) -> datetime:
2321
return datetime.strptime(value, "%d/%m/%y")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from plum import dispatch
2+
from dataclasses import dataclass
3+
from datetime import datetime
4+
from serde import serde, add_serializer, add_deserializer
5+
from serde.json import from_json, to_json
6+
from typing import Type, Any
7+
8+
9+
class MySerializer:
10+
@dispatch
11+
def serialize(self, value: datetime) -> str:
12+
return value.strftime("%d/%m/%y")
13+
14+
15+
class MyDeserializer:
16+
@dispatch
17+
def deserialize(self, cls: Type[datetime], value: Any) -> datetime:
18+
return datetime.strptime(value, "%d/%m/%y")
19+
20+
21+
class MySerializer2:
22+
@dispatch
23+
def serialize(self, value: int) -> str:
24+
return str(value)
25+
26+
27+
class MyDeserializer2:
28+
@dispatch
29+
def deserialize(self, cls: Type[int], value: Any) -> int:
30+
return int(value)
31+
32+
33+
class MySerializer3:
34+
@dispatch
35+
def serialize(self, value: float) -> str:
36+
return str(value)
37+
38+
39+
class MyDeserializer3:
40+
@dispatch
41+
def deserialize(self, cls: Type[float], value: Any) -> float:
42+
return float(value)
43+
44+
45+
add_serializer(MySerializer())
46+
add_serializer(MySerializer2())
47+
add_deserializer(MyDeserializer())
48+
add_deserializer(MyDeserializer2())
49+
50+
51+
@serde(class_serializer=MySerializer3(), class_deserializer=MyDeserializer3())
52+
@dataclass
53+
class Foo:
54+
a: datetime
55+
b: int
56+
c: float
57+
58+
59+
def main() -> None:
60+
dt = datetime(2021, 1, 1, 0, 0, 0)
61+
f = Foo(dt, 10, 100.0)
62+
print(f"Into Json: {to_json(f)}")
63+
64+
s = '{"a": "01/01/21", "b": "10", "c": "100.0"}'
65+
print(f"From Json: {from_json(Foo, s)}")
66+
67+
68+
if __name__ == "__main__":
69+
main()

serde/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
init,
4545
logger,
4646
should_impl_dataclass,
47+
add_serializer,
48+
add_deserializer,
4749
)
4850
from .de import (
4951
DeserializeFunc,
@@ -103,6 +105,8 @@
103105
"logger",
104106
"ClassSerializer",
105107
"ClassDeserializer",
108+
"add_serializer",
109+
"add_deserializer",
106110
]
107111

108112

serde/core.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,3 +1057,22 @@ class ClassDeserializer(Protocol):
10571057

10581058
def deserialize(self, cls: Any, value: Any) -> Any:
10591059
pass
1060+
1061+
1062+
GLOBAL_CLASS_SERIALIZER: List[ClassSerializer] = []
1063+
1064+
GLOBAL_CLASS_DESERIALIZER: List[ClassDeserializer] = []
1065+
1066+
1067+
def add_serializer(serializer: ClassSerializer) -> None:
1068+
"""
1069+
Register custom global serializer.
1070+
"""
1071+
GLOBAL_CLASS_SERIALIZER.append(serializer)
1072+
1073+
1074+
def add_deserializer(deserializer: ClassDeserializer) -> None:
1075+
"""
1076+
Register custom global deserializer.
1077+
"""
1078+
GLOBAL_CLASS_DESERIALIZER.append(deserializer)

serde/de.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@
55

66
from __future__ import annotations
77
import abc
8+
import itertools
89
import collections
910
import dataclasses
1011
import functools
1112
import typing
1213
from dataclasses import dataclass, is_dataclass
13-
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, overload, Union, Sequence
14+
from typing import (
15+
Any,
16+
Callable,
17+
Dict,
18+
Generic,
19+
List,
20+
Optional,
21+
TypeVar,
22+
overload,
23+
Union,
24+
Sequence,
25+
Iterable,
26+
)
1427

1528
import jinja2
16-
import plum
1729
from typing_extensions import Type, dataclass_transform
1830

1931
from .compat import (
@@ -57,6 +69,7 @@
5769
typename,
5870
)
5971
from .core import (
72+
GLOBAL_CLASS_DESERIALIZER,
6073
ClassDeserializer,
6174
FROM_DICT,
6275
FROM_ITER,
@@ -231,6 +244,12 @@ def wrap(cls: Type[T]) -> Type[T]:
231244
scope = Scope(cls, reuse_instances_default=reuse_instances_default)
232245
setattr(cls, SERDE_SCOPE, scope)
233246

247+
class_deserializers: List[ClassDeserializer] = list(
248+
itertools.chain(
249+
GLOBAL_CLASS_DESERIALIZER, [class_deserializer] if class_deserializer else []
250+
)
251+
)
252+
234253
# Set some globals for all generated functions
235254
g["cls"] = cls
236255
g["serde_scope"] = scope
@@ -250,7 +269,7 @@ def wrap(cls: Type[T]) -> Type[T]:
250269
g["coerce"] = coerce
251270
g["_exists_by_aliases"] = _exists_by_aliases
252271
g["_get_by_aliases"] = _get_by_aliases
253-
g["class_deserializer"] = class_deserializer
272+
g["class_deserializers"] = class_deserializers
254273
if deserializer:
255274
g["serde_legacy_custom_class_deserializer"] = functools.partial(
256275
serde_legacy_custom_class_deserializer, custom=deserializer
@@ -685,16 +704,20 @@ def render(self, arg: DeField[Any]) -> str:
685704
"""
686705
Render rvalue
687706
"""
688-
implemented_methods: Dict[Type[Any], plum.Signature] = {}
689-
if self.class_deserializer:
690-
implemented_methods = {
691-
get_args(sig.types[1])[0]: sig
692-
for sig in self.class_deserializer.__class__.deserialize.methods # type: ignore
693-
}
707+
implemented_methods: Dict[Type[Any], int] = {}
708+
class_deserializers: Iterable[ClassDeserializer] = itertools.chain(
709+
GLOBAL_CLASS_DESERIALIZER, [self.class_deserializer] if self.class_deserializer else []
710+
)
711+
for n, class_deserializer in enumerate(class_deserializers):
712+
for sig in class_deserializer.__class__.deserialize.methods: # type: ignore
713+
implemented_methods[get_args(sig.types[1])[0]] = n
694714

695715
custom_deserializer_available = arg.type in implemented_methods
696716
if custom_deserializer_available and not arg.deserializer:
697-
res = f"class_deserializer.deserialize({typename(arg.type)}, {arg.data})"
717+
res = (
718+
f"class_deserializers[{implemented_methods[arg.type]}].deserialize("
719+
f"{typename(arg.type)}, {arg.data})"
720+
)
698721
elif arg.deserializer and arg.deserializer.inner is not default_deserializer:
699722
res = self.custom_field_deserializer(arg)
700723
elif is_generic(arg.type):

0 commit comments

Comments
 (0)