User Guide¶
This library offers Serializer classes that handle (de)serialization of (possibly deeply nested) Python objects. Objects are serialized by instantiating and configuring a Serializer object and then passing objects to its serialize method, while deserialization works by passing the serialized data to its deserialize method.
This uninstrusive approach avoids subclassing and implementing any abstract methods for (de)serialization on the classes that are to be serialized. It also enables easy composition and mixing of different serialization methods for one and the same class by using separate, differently configured serializers.
This library supports many built-in classes out of the box, including attrs classes. Custom classes are supported through a highly flexible system of (de)serialization hook registration.
This closely follows the philosophy of cattrs and json dumping and loading.
Objects are (de)serialized using an instance of Serializer or any of its preconfigured subclasses, like DeterministicJsonSerializer or FastJsonSerializer[1]. For the following example we will be using DeterministicJsonSerializer. See the other subclasses’ docstrings for their specific differences.
# Example 1:
from parityos_serialization.serializer import DeterministicJsonSerializer
serializer = DeterministicJsonSerializer()
obj = {1, 2, 3}
dct = serializer.serialize(obj)
obj2 = serializer.deserialize(dct)
assert obj == obj2
print(dct)
# {'_data': [1, 2, 3], '_cls': 'set'}
This is very similar in spirit to json.dumps and json.loads, where json is the “serializer” and dumps and loads are similar in spirit to serialize and deserialize.
You will have noticed that that the serializer produces a dict with two fields:
_data: containing the serialized data_cls: containing an identifier tag of the serialized object’s class
The class identifier tag is necessary for deserialization, such that the serializer knows which class object to construct from the data.
This also works for arbitrarily nested objects:
# Example 2:
from dataclasses import dataclass
from enum import Enum
from attrs import frozen
from parityos_serialization.serializer import DeterministicJsonSerializer, FallBackStrategy
from typing_extensions import override
@frozen
class AFoo:
bar: int
baz: str
@dataclass
class DFoo:
x: int
s: str
class Parity(Enum):
even = False
odd = True
class Bar:
qux: int
fizz: str
__slots__: list[str] = ["qux", "__dict__"]
def __init__(self, qux: int, fizz: str):
self.qux = qux
self.fizz = fizz
@override
def __eq__(self, other: object) -> bool:
return isinstance(other, type(self)) and self.__getstate__() == other.__getstate__()
serializer = DeterministicJsonSerializer(
fallback_strategy=FallBackStrategy.PICKLE_INTERFACE
) # we will explain fallback_strategy in a minute
serializer.class_tagger.register_classes(
AFoo, DFoo, Bar, Parity
) # we will explain this in a minute
obj = [
{1, 2, 3},
{"a": [2, 3], "b": [3, 4]},
(42, "foo", True, None),
Parity.odd,
dict,
list[str],
AFoo(42, "qux"),
DFoo(42, "qux"),
Bar(42, "buzz"), # uses pickle fallback strategy
]
dct = serializer.serialize(obj)
obj2 = serializer.deserialize(dct)
assert obj == obj2
print(dct)
# [
# {"_data": [1, 2, 3], "_cls": "set"},
# {"_data": [["a", [2, 3]], ["b", [3, 4]]], "_cls": "dict"},
# {"_data": [42, "foo", True, None], "_cls": "tuple"},
# {"_data": "odd", "_cls": "Parity"},
# {"_data": "dict", "_cls": "type"},
# {
# "_data": {
# "origin": {"_data": "list", "_cls": "type"},
# "args": {"_data": [{"_data": "str", "_cls": "type"}], "_cls": "tuple"},
# },
# "_cls": "GenericAlias",
# },
# {"_data": {"bar": 42, "baz": "qux"}, "_cls": "AFoo"},
# {"_data": {"x": 42, "s": "qux"}, "_cls": "DFoo"},
# {"_data": {"fizz": "buzz", "qux": 42}, "_cls": "Bar"},
# ]
Notice how all objects are converted to simple python built-in classes. Some objects are respresented
as dicts with a _data field containing serialized data and a _cls field containing the original
object’s class name like in the set example above. Notice also how most elementary types like str, int, bool and None are passed through unaltered without class information. Finally notice, how lists don’t come with a class tag, while e.g. tuple, set or dict do. This particular output is due to how DeterministicJsonSerializer is preconfigured.
The most basic serializer with no preconfiguration is the Serializer class, which takes 4 required parameters:
typed_data_converter: Data converter that combines and extracts serialized data and type information in form of a class tag (see Serialization Formats).class_tagger: Class tagger object that manages mapping between classes and their class tags (see Class Tagging Strategies).pass_through_classes: Classes for which objects of that type are serialized by being returned unaltered (see Pass-Through Classes and Containers).fallback_strategy: Strategy for dealing with objects that are not caught by any hooks. Can be a preconfigured strategy from theFallBackStrategyenum or a customHookPair(see Fallback Strategies).
Let’s break these down in the following sections.