a python serializer
The serializer package adds serialization, type-enforcement, and other features to a python class.
By borrowing some of the syntax used by the dataclasses package, serializer is able to leverage standard python, but with more nuanced control than that provided by a dataclass.
A dataclass is a decorator that magically adds methods to a class based on that class's fields and their annotations. By contrast, the serializer package provides a Serializable superclass that brings it's own dunder functions that provide features like: type-enforcement, attribute constraint, user-defined types, nested objects, read-only fields, optional fields with no default values, and serialization.
from serializer import Serializable, Optional, serialize
class Apartment(Serializable):
floor: int
unit: str
balcony: bool = Optional
is_studio: bool = False
class Person(Serializable):
name: str
address: Apartment
Here two classes are defined.
The Apartment class has four attributes, two of which are required—an Apartment can't be created that doesn't include floor and unit. Here is an Apartment being created in code (these produce equivalent instances):
apt = Apartment(3, "A")
apt = Apartment(floor=3, unit="A")
apt = Apartment("3", unit="A", is_studio=False)
The instance can be serialized to a dict:
>> serialize(apt)
{'floor': 3, 'unit': 'A', is_studio: False}
The instance fields operate as expected:
>> apt.balcony = True
>> apt.is_studio = True
>> serialize(apt)
{'floor': 3, 'unit': 'A', 'is_studio': True, 'balcony': True}
>> apt.bathrooms = 1
UndefinedAttributeError: 'Apartment' object has no attribute 'bathrooms'
apt.floor = "three"
ValueError: invalid <int> value (three) for field 'floor': not an integer
Optional fields only appear if assigned. Fields that are not defined in the class cannot be used. Values that don't match the annotation's type are not accepted.
The Person class includes a field—address—whose type is Apartment. A Serializable class can be used as an annotated type, creating a nested object. These four ways to create a new Person are equivalent:
tenant = Person(name="John Doe", address={"floor": 3, "unit": "A"})
tenant = Person("John Doe", {"floor": 3, "unit": "A"})
tenant = Person("John Doe", [3, "A"]) # a list is treated as *args
apt = Apartment(3, "A")
tenant = Person("John Doe", apt)
Serialize the tenant:
serialize(tenant)
{'name': 'John Doe', 'address': {'floor': 3, 'unit': 'A', 'is_studio': False}}
The serialized version of an object can be used to create a new object—a method which even works with nested objects:
ser = serialize(tenant)
tenant = Person(**ser)
Access the Apartment attribute in the expected way:
print(tenant.address.unit)
A