=============== Getting started =============== Serialize dataclasses instances ------------------------------- The classic use for Serpyco is to dump your dataclass objects to builtin Python types. This is done by creating a :class:`serpyco.Serializer` for your dataclass type: .. literalinclude:: examples/simple_dump.py :language: python More complex dataclass can be serialized just as easily: .. literalinclude:: examples/complex_dump.py :language: python Loading data works the same: .. code-block:: python >>> serializer.load( >>> {"color": 1, "points": [{"x": 1, "y": 2}, {"x": 2, "y": 3}, {"x": 4, "y": 5}]} >>> ) Polygon( points=[Point(x=1, y=2), Point(x=2, y=3), Point(x=4, y=5)], color=, name=None, ) Validate data ------------- Serpyco can also validate your data when dumping/loading objects. This is done by the `validate=True` parameter of :func:`serpyco.Serializer.dump` and :func:`serpyco.Serializer.load`: .. code-block:: python >>> serializer.load( >>> { >>> 'color': 4, >>> 'points': [ >>> {'x': "wrong", 'y': 2}, >>> {'x': 2, 'y': 3}, >>> {'x': 4, 'y': 5} >>> ] >>> }, validate=True) ValidationError: ('data["points"][0]["x"]: has type str, expected number.') Additional properties in loaded data are allowed by default, this can be changed using the `strict=True` parameter of :func:`serpyco.Serializer.__init__`. Customize data validation ------------------------- Typing the fields of a dataclass is not always enough for precisely validating input, that's why Serpyco offers additional field properties to enable fine-tuning of the validation. String fields ============= Tuning the validation of string fields is done using :func:`serpyco.string_field`: .. code-block:: python from dataclasses import dataclass from serpyco import Serializer, string_field, ValidationError @dataclass class StringFields: simple: str name: str = string_field(pattern="^[A-Z]") serializer = Serializer(StringFields) >>> serializer.load({"name": "Foo", "simple": "whatever"}, validate=True) StringFields(simple='whatever', name='Foo') >>> serializer.load({"name": "foo", "simple": "foo"}, validate=True) ValidationError: ('data["name"]: string does not match pattern, got "foo",expected "^[A-Z]".') Number fields ============= For numbers (`int` and `float`), the tuning is done with :func:`serpyco.number_field`: .. code-block:: python from dataclasses import dataclass from serpyco import Serializer, number_field, ValidationError @dataclass class NumberFields: simple: int range: float = number_field(minimum=0, maximum=10) serializer = Serializer(NumberFields) >>> serializer.load({"simple": 98, "range": 5}, validate=True) >>> NumberFields(simple=98, range=5) >>> serializer.load({"simple": 100, "range": 12}, validate=True) ValidationError: ('data["range"]: number must be <= 10, got 12.') Optional fields =============== A field can be specified as optional by typing it with `Optional`: .. code-block:: python from dataclasses import dataclass from serpyco import Serializer @dataclass class OptionalField: name: str option: typing.Optional[int] = None serializer = Serializer(OptionalField) >>> serializer.load({"name": "foo"}, validate=True) OptionalField(name="foo", option=None) Recognized types ---------------- The following python types are recognized out-of-the-box by Serpyco: - builtins: `str`, `float`, `int`, `bool` - containers: `typing.List`, `typing.Set`, `typing.Tuple` - unions: `typing.Optional`, `typing.Union` - generics: `typing.Generic` - enumerates: `enum.Enum` - dates: `datetime.datetime` - misc: `uuid.UUID` Advanced topics --------------- Keep only some fields/exclude some fields from serialization ============================================================ The fields dumped/loaded by a serializer object can be tuned when creating it: .. code-block:: python from dataclasses import dataclass from serpyco import field, Serializer @dataclasses.dataclass class Data: """Data test class""" foo: str bar: str >>> serializer = serpyco.Serializer(Data, only=["foo"]) >>> serializer.dump(Data(foo="bar", bar="foo")) {"foo": "bar"} >>> serializer = serpyco.Serializer(Data, exclude=["foo"]) >>> serializer.dump(Data(foo="bar", bar="foo")) {"bar": "foo"} General field serialization options =================================== Options can be defined on fields that changes the behaviour of the serialization. This is done by using :func:`serpyco.field`: .. code-block:: python from dataclasses import dataclass from serpyco import field, Serializer @dataclass class Example: name: str = field(dict_key="custom") serializer = Serializer(Example) >>> serializer.dump(Example(name="foo")) {"custom": "foo"} >>> serializer.load(Example({"custom": "foo"}) Example(name="foo") The :func:`serpyco.field` and specific versions for string/number/nested types are compatible with :func:`dataclasses.field` signature. Nested fields serialization options =================================== Nested dataclasses serialization can be tuned to only keep or exclude some fields by using :func:`serpyco.nested_field`: .. code-block:: python from dataclasses import dataclass from serpyco import Serializer, nested_field @dataclass class Nested: """Nested test class""" foo: str bar: str @dataclass class Parent: """Parent test class""" first: Nested = serpyco.nested_field(only=["foo"]) second: Nested = serpyco.nested_field(exclude=["foo"]) serializer = Serializer(Parent) >>> serializer.dump( >>> Parent(first=Nested(foo="foo", bar="bar"), second=Nested(foo="foo", bar="bar")) >>> ) {"first": {"foo": "foo"}, "second": {"bar": "bar"}} Dump and load to/from JSON ========================== The special methods :func:`serpyco.Serializer.dump_json` and :func:`serpyco.Serializer.load_json` are provided. They are equivalent as calling: .. code-block:: python data = serializer.dump(obj) js = json.dumps(data) data = json.loads(data) obj = serializer.load(data) But are faster, especially when using validation. Custom field encoder ==================== You can register your own field encoders for any type: .. code-block:: python from dataclasses import dataclass import typing from serpyco import Serializer, FieldEncoder class Rational: def __init__(self, numerator: int, denominator: int): self.numerator = numerator self.denominator = denominator def __repr__(self) -> str: return f"Rational({self.numerator}/{self.denominator})" class RationalEncoder(FieldEncoder): def load(self, value: typing.Tuple[int, int]) -> Rational: return Rational(value[0], value[1]) def dump(self, rational: Rational) -> typing.Tuple[int, int]: return (rational.numerator, rational.denominator) def json_schema(self) -> dict: # optional, but helpful to specify a custom validation # if you don't want any validation, return {} in your # implementation. return { "type": "array", "maxItems": 2, "minItems": 2, "items": {"type": "integer"}, } @dataclass class Custom: rational: Rational serializer = Serializer(Custom, type_encoders={Rational: RationalEncoder()}) >>> serializer.dump(Custom(rational=Rational(1, 2))) {'rational': (1, 2)} >>> serializer.load({"rational": (1, 2)}) Custom(rational=Rational(1/2)) serializer.load({"rational": (1, 2.1)}) ValidationError: ('data["rational"][1]: has type float, expected integer.') `type_encoders` also makes it possible to customize encoding of types that serpyco handles natively, for example to serialize `Enum` using their name instead of their value: .. code-block:: python import dataclasses import enum import typing import serpyco class EnumNameEncoder(serpyco.FieldEncoder): def __init__(self, enum_type: typing.Type[enum.Enum]) -> None: self._enum_type = enum_type def dump(self, value: enum.Enum) -> str: return value.name def load(self, value: str) -> enum.Enum: try: return next(e for e in self._enum_type if e.name == value) except StopIteration: raise serpyco.ValidationError(f"No member with name='{value}' in {self._enum_type.__name__}") def json_schema(self) -> dict: return { "type": "string", "enum": [e.name for e in self._enum_type] } class DictEnum(enum.Enum): FOO = {"name": "foo's name", "data": True} BAR = {"name": "bars's name", "data": False} @dataclasses.dataclass class Foobar: hello: DictEnum serializer = serpyco.Serializer(Foobar, type_encoders={DictEnum: EnumNameEncoder(DictEnum)}) print(serializer.dump(Foobar(DictEnum.FOO), validate=True)) print(serializer.load({"hello": "FOO"}, validate=True)) try: serializer.load({"hello": "SPAM"}, validate=True) except serpyco.ValidationError as e: print(f"Error while loading: {e}") Pre-processing and post-processing methods ========================================== It is possible to specify additional processing to take place before and after either loading or dumping: .. code-block:: python from dataclasses import dataclass from serpyco import Serializer, post_dump @dataclass class Custom: firstname: str lastname: str @post_dump def make_name(data: dict) -> dict: first = data["firstname"] last = data["lastname"] return {"name": f"{first} {last}"} serializer = Serializer(Custom) >>> serializer.dump(Custom(firstname="foo", lastname="bar")) {'name': 'foo bar'} Type casting when loading ========================= In some cases it is useful to be able to accept field values that can be cast to the field's type. This is possible by setting the `cast_on_load=True` argument of the :func:`serpyco.field` function: .. code-block:: python @dataclasses.dataclass class CastedOnLoad: value: int = serpyco.field(cast_on_load=True) serializer = serpyco.Serializer(CastedOnLoad) >>> serializer.load({"value": "42"}) CastedOnLoad(value=42) :class:`serpyco.ValidationError` will be raised if any exception is caught during the cast of the value. Serialize objects which are not dataclass instances =================================================== Serpyco is primarly made to serialize dataclass objects, but you can also use it to dump/load your existing classes: .. code-block:: python class Existing: def __init__(self, name: str, value: int) -> None: self.name = name self.value = value def __repr__(self) -> str: return f"Existing(name={self.name}, value={self.value})" @dataclasses.dataclass class Schema: name: str value: int serializer = serpyco.Serializer(Schema, load_as_type=Existing) >>> serializer.dump(Existing(name="hello", value=42)) {'name': 'hello', 'value': 42} >>> serializer.load({"name": "hello", "value": 42}) Existing(name=hello, value=42) Serialize generic dataclasses ============================= Dataclasses which are generic are supported, for example: .. code-block:: python T = typing.TypeVar("T") class Gen(typing.Generic[T]): name: str value: T serializer = serpyco.Serializer(Gen[int]) >>> serializer.dump(Gen(name="hello", value=42)) {'name': 'hello', 'value': 42} serializer = serpyco.Serializer(Gen[str]) >>> serializer.dump(Gen(name="hello", value="hello")) {'name': 'hello', 'value': "hello"} Mixin class providing load/dump methods ======================================= A helper class to provide load/dump methods to dataclass is provided: .. code-block:: python @dataclasses.dataclass class Foo(serpyco.SerializerMixin): name: str >>> Foo(name="hello").dump() {'name': 'hello'} >>> Foo.load({'name': 'hello'}) Foo(name='hello')