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 serpyco.Serializer for your dataclass type:

from dataclasses import dataclass

from serpyco import Serializer


@dataclass
class Point(object):
    x: float
    y: float


serializer = Serializer(Point)
print(serializer.dump(Point(x=3.14, y=1.5)))
{"x": 3.14, "y": 1.5}

More complex dataclass can be serialized just as easily:

import enum
import typing
from dataclasses import dataclass

from serpyco import Serializer


@dataclass
class Point(object):
    x: float
    y: float


class PolygonColor(enum.Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


@dataclass
class Polygon(object):
    points: typing.List[Point]
    color: PolygonColor
    name: typing.Optional[str] = None


serializer = Serializer(Polygon)
serializer.dump(
    Polygon(points=[Point(1, 2), Point(2, 3), Point(4, 5)], color=PolygonColor.RED)
)
{"color": 1, "points": [{"x": 1, "y": 2}, {"x": 2, "y": 3}, {"x": 4, "y": 5}]}

Loading data works the same:

>>> 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=<PolygonColor.RED:1>,
    name=None,
)

Validate data

Serpyco can also validate your data when dumping/loading objects. This is done by the validate=True parameter of serpyco.Serializer.dump() and serpyco.Serializer.load():

>>> 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 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 serpyco.string_field():

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 serpyco.number_field():

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:

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:

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 serpyco.field():

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 serpyco.field() and specific versions for string/number/nested types are compatible with dataclasses.field() signature.

Nested fields serialization options

Nested dataclasses serialization can be tuned to only keep or exclude some fields by using serpyco.nested_field():

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 serpyco.Serializer.dump_json() and serpyco.Serializer.load_json() are provided. They are equivalent as calling:

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:

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:

Pre-processing and post-processing methods

It is possible to specify additional processing to take place before and after either loading or dumping:

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 serpyco.field() function:

@dataclasses.dataclass
class CastedOnLoad:
    value: int = serpyco.field(cast_on_load=True)

serializer = serpyco.Serializer(CastedOnLoad)
>>> serializer.load({"value": "42"})
CastedOnLoad(value=42)

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:

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:

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')