Skip to content

class_instance_serializer.py

ofrak.service.serialization.serializers.class_instance_serializer

ClassInstanceSerializer (SerializerInterface)

Serialize and deserialize class instances into PJSONType.

Implementation: the class is serialized, along with the attributes of the instance taken from its annotations. In other words, the annotations determine the exact attributes of the instance which are serialized and then re-created during deserialization.

Annotations are present in dataclasses by construction, but they need to be added for all the other classes if this default serializer is to process them correctly.

Invoking init and post_init during deserialization could have side effects, though.

For dataclasses, init corresponds perfectly to the annotations, and the default assumption is that side effects in post_init are desirable to re-invoke during deserialization (if not, write a custom serializer). For other classes, the default assumption is that init and post_init might not correspond to the annotations and that their side effects are undesirable for deserialization, so we only call new and then manually setattr the fields. Write a custom serializer if this behavior doesn't work well for some class. For example, the following class will be serialized and deserialized correctly, but wouldn't if init was called:

class Example:
    i: int
    def __init__(self, i: int):
        self.i = i + 1

The type hint (name of the class) given is ignored, instead we get the annotations from the class instance itself. This allows to deal with the frequent case of a type hint corresponding to a superclass of the class instance, e.g. an interface, which wouldn't provide the correct annotations and would require a custom serializer for the superclass (which would dispatch based on the actual type of the class instance given).

The more precise class is used in the serialized form, allowing deserialization to know which precise class to handle as well.

obj_to_pjson(self, cls_instance, _type_hint)

Serialize the object to PJSON. Note that the generic self._service.to_pjson can be used here.

Source code in ofrak/service/serialization/serializers/class_instance_serializer.py
def obj_to_pjson(self, cls_instance: Any, _type_hint: Any) -> Tuple[str, Dict[str, PJSONType]]:
    cls_fields_pjson: Dict[str, PJSONType] = {}
    fields_and_types = self._get_class_fields_and_types(
        cls_instance, as_dataclass=is_dataclass(cls_instance)
    )
    for field_name, field_type in fields_and_types.items():
        cls_fields_pjson[field_name] = self._service.to_pjson(
            getattr(cls_instance, field_name), field_type
        )
    cls_ref_pjson = self._service.to_pjson(cls_instance.__class__, Type)
    cls_ref_pjson = cast(str, cls_ref_pjson)
    return (cls_ref_pjson, cls_fields_pjson)

pjson_to_obj(self, pjson_obj, _type_hint)

Deserialize PJSON into the object. Note that the generic self._service.from_pjson can be used here.

Source code in ofrak/service/serialization/serializers/class_instance_serializer.py
def pjson_to_obj(self, pjson_obj: Tuple[str, Dict[str, PJSONType]], _type_hint: Any) -> Any:
    cls_ref_pjson, cls_fields_pjson = pjson_obj
    cls = self._service.from_pjson(cls_ref_pjson, Type)
    return self._deserialize_instance(cls, cls_fields_pjson)

_get_class_fields_and_types(cls, as_dataclass) private staticmethod

Return the field names and types for cls, a class or class instance.

Source code in ofrak/service/serialization/serializers/class_instance_serializer.py
@staticmethod
def _get_class_fields_and_types(cls: Any, as_dataclass: bool) -> Dict[str, Any]:
    """Return the field names and types for `cls`, a class or class instance."""
    if as_dataclass is True:
        # Fields for which init is False aren't serialized
        return {field.name: field.type for field in fields(cls) if field.init is True}
    else:
        # NOTE: in python >= 3.10, inspect.get_annotations() is a better choice
        return getattr(cls, "__annotations__", {})