Encode/Decode#
Both of these operations are performed by a model.
Just like when using Python native json or pickle in Serious we #load(value)
and #dump(dataclass)
. The argument and return types of this methods are defined by the model in use
(JsonModel, DictModel).
So having a dataclass:
from dataclasses import dataclass
@dataclass
class Character:
name: str
Create an instance of JsonModel
:
from serious import JsonModel
model = JsonModel(Character)
And use its load/dump methods:
lancelot = Character('Sir Lancelot')
assert model.load('{"name": "Sir Lancelot"}') == lancelot
robin = Character('Sir Robin')
galahad = Character('Galahad')
assert model.dump_many([robin, galahad]) == '[{"name": "Sir Robin"}, {"name": "Galahad"}]'
bridgekeeper = Character('Bridgekeeper')
assert model.dump(bridgekeeper) == '{"name": "Bridgekeeper"}'
king = Character('King Arthur')
bedevere = Character('Sir Bedevere')
assert model.load_many('[{"name": "King Arthur"}, {"name": "Sir Bedevere"}]') == [king, bedevere]
Multiple values can be manipulated by corresponding #load_many(values)
and #dump_many(dataclasses)
model methods.
Field Serializers#
Internally serialization is performed by field serializers (subclasses of serious.serialization.FieldSerializer
).
A list of them is provided to the model, each checked for fitness against the dataclass field.
The first serializer class that fits the field is instantiated and used to load/dump fields values.
Field Serializer API#
def __init__(self, field: FieldDescriptor, root_model: SeriousModel):
-
A constructor from a field descriptor and root model.
The descriptor contains information about field type, generic parameters and field metadata.
Root model can be accessed for model configuration.
@classmethod @abstractmethod def fits(cls, field: FieldDescriptor) -> bool:
- A predicate returning
True
if this serializer fits to load/dump data for the provided field. @abstractmethod def load(self, value: Primitive, ctx: Context) -> Any:
- Loads a primitive value to a value supported by the serializer (e.g. dict -> dataclass).
@abstractmethod def dump(self, value: Any, ctx: Context) -> Primitive:
- Dumps the field value to a primitive value (e.g. datetime -> str).
Custom Field Serializers#
To create a custom field serializer you need to subclass the FieldSerializer
and
implement its fits
, load
and dump
methods.
For a new serializer to be used in model it should be included in the serializers
constructor parameter.
You can make use of serious.serialization.field_serializers
instead of constructing this list yourself.
field_serializers
function returns a frozen collection of field serializers in the default order
when called without parameters.
You can provide a list of custom field serializers to include them after metadata and optional serializers:
def field_serializers(custom: Iterable[Type[FieldSerializer]] = tuple()) -> Tuple[Type[FieldSerializer], ...]:
return tuple([
OptionalSerializer,
AnySerializer,
EnumSerializer,
*custom,
DictSerializer,
CollectionSerializer,
TupleSerializer,
StringSerializer,
BooleanSerializer,
IntegerSerializer,
FloatSerializer,
DataclassSerializer,
UtcTimestampSerializer,
DateTimeIsoSerializer,
DateIsoSerializer,
TimeIsoSerializer,
UuidSerializer,
DecimalSerializer,
])
Example#
from dataclasses import dataclass
from decimal import Decimal
from typing import Union, List
from serious import DictModel, ValidationError, TypeDescriptor
from serious.serialization import FieldSerializer, field_serializers
Number = Union[int, float, Decimal]
class Point:
x: Decimal
y: Decimal
def __init__(self, x: Number, y: Number):
self.x = round(Decimal(x), 2)
self.y = round(Decimal(y), 2)
def __repr__(self):
return f'<Point x:{self.x} y:{self.y}>'
class PointSerializer(FieldSerializer):
@classmethod
def fits(cls, desc: TypeDescriptor) -> bool:
return issubclass(desc.cls, Point)
def load(self, value, ctx):
self._validate_raw(value)
return Point(Decimal(value[0]), Decimal(value[1]))
def dump(self, value, ctx):
return [str(value.x), str(value.y)]
@staticmethod
def _validate_raw(value):
if not isinstance(value, list) or len(value) != 2:
raise ValidationError('Point should be an array with x and y coordinates')
@dataclass
class Area:
"""An area inside the bounds defined by a set of points."""
points: List[Point]
model = DictModel(Area, serializers=field_serializers([PointSerializer]))
This gets us:
>>> print('Loaded:', model.load({'points': [['1', '1'], ['2', '3'], ['4', '3.2']]}))
Loaded: Area(points=[<Point x:1.00 y:1.00>, <Point x:2.00 y:3.00>, <Point x:4.00 y:3.20>])
>>> print('Dumped:', model.dump(Area([Point(1.11, 2.54), Point(3.1, 2.54), Point(2.1, 0)])))
Dumped: {'points': [['1.11', '2.54'], ['3.10', '2.54'], ['2.10', '0.00']]}