Model is the entry point to Serious. You create it to serialize data and validate created objects. When does multiple things upon creation:

  • builds a hierarchy of descriptors unwrapping the generics, optionals, etc;
  • checks that this model conforms to your requirements;
  • forms a tree of serializers to executed upon load/dump.

There are two model types at this point:

  1. JsonModel for working with JSON strings
  2. DictModel for working with Python dictionaries

Protocol#

Model protocol is defined by 5 methods.

def __init__(cls: Type[T], *, **options):
A constructor from a dataclass type and implementation specific options.
def load(data: D) -> T:
Create a new dataclass instance from a model-specific encoded data.
def dump(obj: T) -> D:
Encode a dataclass to model-specific type.
def load_many(data: DC) -> List[T]:
Load a list of dataclass instances from model-specific encoded data collection.
def dump_many(obj: Iterable[T]) -> DC:
Dump multiple objects at once to model-specific collection.

Common Options#

serializers#

Type: Iterable[Type[FieldSerializer]] Default: serious.serialization.field_serializers()

An ordered collection of field serializer classes. Pass a non-default collection to override how particular fields are serialized by the model. For more refer to custom serialization guide.

allow_any#

Type: bool Default: False

By default the dataclass and its fields cannot contain unambiguous fields annotated with Any. This also includes generics like list which is equal to List[Any].

Both examples below are ambiguous in this manner:

@dataclass
class User:
    metadata: dict

@dataclass
class Message:
    extras: Any

Loading them will result in error:

>>> JsonModel(Message, allow_any=False)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "..serious/serious/json/model.py", line 80, in __init__
    key_mapper=JsonKeyMapper() if camel_case else None,
  File "..serious/serious/serialization/model.py", line 71, in __init__
    raise ModelContainsAny(descriptor.cls)
serious.errors.ModelContainsAny: <class '__main__.Message'>

You can pass allow_any=True to model to serialize load/dump Any fields as is, although this can lead to uncertain behaviour.

allow_missing#

Type: bool Default: False

By default serious is strict in respect to missing data. A LoadError will be raised if some field exists in dataclass but is missing from loaded the data.

But some APIs prefer to omit null values. To handle such use-case the fields should be marked Optional:

@dataclass
class Dinosaur:
    name: str 
    height: Optional[int] 

Then loading JSON to a model with allow_missing=True without height will set it to None:

>>> JsonModel(Dinosaur, allow_missing=True).load('{"name": "Yoshi"}')
Dinosaur(name='Yoshi', height=None)

allow_unexpected#

Type: bool Default: False

If there will be some redundant data serious default is to error. The idea here is to fail fast. But if you would like to just skip extra fields, then pass allow_unexpected=True to your model:

>>> JsonModel(Dinosaur, allow_unexpected=True).load('{"name": "Yoshi", "height": null, "clothing": "orange boots"}')
Dinosaur(name='Yoshi', height=None)

JsonModel#

def \_\_init\_\_(
    self,
    cls: Type[T],
    *,
    serializers: Iterable[Type[FieldSerializer]] = field_serializers(),
    allow_any: bool = False,
    allow_missing: bool = False,
    allow_unexpected: bool = False,
    indent: Optional[int] = None,
):
  • cls — the descriptor of the dataclass to load/dump.
  • serializers — field serializer classes in an order they will be tested for fitness for each field.
  • allow_anyFalse to raise if the model contains fields annotated with Any (this includes generics like List[Any], or simply list).
  • allow_missingFalse to raise during load if data is missing the optional fields.
  • allow_unexpectedFalse to raise during load if data contains some unknown fields.
  • indent — number of spaces JSON output will be indented by; `None` for most compact representation.
def load(self, json_: str) -> T:
Creates an instance of dataclass from a JSON string.
def dump(self, o: Any) -> str:
Dumps an instance of dataclass to a JSON string.
def load_many(self, json_: str) -> List[T]:
Loads multiple T dataclass objects from JSON array of objects string.
def dump_many(self, items: Collection[T]) -> str:
Dumps a list/set/collection of objects to an array of objects JSON string.

DictModel#

def __init__(
    self,
    cls: Type[T],
    *,
    serializers: Iterable[Type[FieldSerializer]] = field_serializers(),
    allow_any: bool = False,
    allow_missing: bool = False,
    allow_unexpected: bool = False,
):
  • cls — the descriptor of the dataclass to load/dump.
  • serializers — field serializer classes in an order they will be tested for fitness for each field.
  • allow_anyFalse to raise if the model contains fields annotated with Any (this includes generics like List[Any], or simply list).
  • allow_missingFalse to raise during load if data is missing the optional fields.
  • allow_unexpectedFalse to raise during load if data contains some unknown fields.
def load(self, data: Dict[str, Any]) -> T:
Creates an instance of T from a dictionary with string keys.
def dump(self, o: Any) -> Dict[str, Any]
Dumps an instance of dataclass to a Python dictionary.
def load_many(self, items: Iterable[Dict[str, Any]]) -> List[T]:
Loads multiple T dataclass objects from a list of dictionaries.
def dump_many(self, items: Collection[T]) -> List[Dict[str, Any]]:
Dumps a list/set/collection of objects to an list of primitive dictionaries.

Custom Model#

Models do not share any common parent class. Instead the idea is: "If it walks like a duck and it quacks like a duck, then it must be a duck".

So what you should do is implement the protocol described above.

Internally, your model will need to create a descriptor of your dataclass, specifying fields types and modifiers. A root dataclass TypeDescriptor is created by serious.descriptors.describe function.

Having a descriptor in place, serious.serialization.SeriousModel may be helpful. SeriousModel forms a tree of field serializers executed upon load and dump operations. It does so from the provided descriptor and a list of all possible field serializers.

Check implementation for more details on how existing code base works and check sources for JsonModel for a comprehensive example:

class JsonModel(Generic[T]):

    def __init__(
            self,
            cls: Type[T],
            serializers: Iterable[Type[FieldSerializer]] = field_serializers(),
            *,
            allow_any: bool = False,
            allow_missing: bool = False,
            allow_unexpected: bool = False,
            validate_on_load: bool = True,
            validate_on_dump: bool = False,
            ensure_frozen: Union[bool, Iterable[Type]] = False,
            camel_case: bool = True,
            indent: Optional[int] = None,
    ):
        self._descriptor = describe(cls)
        self._serializer: SeriousModel = SeriousModel(
            self._descriptor,
            serializers,
            allow_any=allow_any,
            allow_missing=allow_missing,
            allow_unexpected=allow_unexpected,
            validate_on_load=validate_on_load,
            validate_on_dump=validate_on_dump,
            ensure_frozen=ensure_frozen,
            key_mapper=JsonKeyMapper() if camel_case else None,
        )
        self._dump_indentation = indent

    @property
    def cls(self):
        return self._descriptor.cls

    def load(self, json_: str) -> T:
        data: MutableMapping = self._load_from_str(json_)
        check_that_loading_an_object(data, self.cls)
        return self._from_dict(data)

    def load_many(self, json_: str) -> List[T]:
        data: Collection = self._load_from_str(json_)
        check_that_loading_a_list(data, self.cls)
        return [self._from_dict(each) for each in data]

    def dump(self, o: T) -> str:
        check_is_instance(o, self.cls)
        return self._dump_to_str(self._serializer.dump(o))

    def dump_many(self, items: Collection[T]) -> str:
        dict_items = list(map(self._dump, items))
        return self._dump_to_str(dict_items)

    def _dump(self, o) -> Dict[str, Any]:
        return self._serializer.dump(check_is_instance(o, self.cls))

    def _from_dict(self, data: MutableMapping) -> T:
        return self._serializer.load(data)

    def _load_from_str(self, json_: str) -> Any:
        """Override to customize JSON loading behaviour."""
        return json.loads(json_)

    def _dump_to_str(self, dict_items: Any) -> str:
        """Override to customize JSON dumping behaviour."""
        return json.dumps(dict_items,
                          skipkeys=False,
                          ensure_ascii=False,
                          check_circular=True,
                          allow_nan=False,
                          indent=self._dump_indentation,
                          separators=None,
                          default=None,
                          sort_keys=False)