Package: typed-json-dataclass
typed_json_dataclasstyped_json_dataclassis a library that augments the Python3.7dataclassfeature in two
major ways:Add a way to recursively grab class dictionary definitions, thus making your
dataclass JSON serializableAdd a light amount of type validation to your dataclasses, so that you can
validate that the JSON you're being given matches the data types that you're
expecting.By expressing your data as dataclasses, and by having your incoming data
validated as it is received, you can easily implement theData Transfer Object
(DTO)pattern in
your Python code.This library can be thought of as a combination ofattrs,cattrs, andmarshamllowGetting StartedInstall the library from PyPI:pip install typed_json_dataclassUse the dataclass decorator just like normal, but add theTypedJsonMixinfrom
this library, to your class definition. This will add 4 new methods to all of your dataclasses:from_dict()@classmethoddeffrom_dict(cls,raw_dict,*,mapping_mode=MappingMode.NoMap):"""Given a python dict, create an instance of the implementing class.:raw_dict: A dictionary that represents the DTO to create:mapping_mode: Format for properties:returns: Returns an instance of the DTO, instantiated via the dict"""from_json()@classmethoddeffrom_json(cls,raw_json,*,mapping_mode=MappingMode.NoMap):"""Given a raw json string, create an instance of the implementing class.:raw_json: A json string that represents the DTO to create:mapping_mode: Format for properties:returns: Returns an instance of the DTO, instantiated via the json"""to_dict()defto_dict(self,*,keep_none=False,mapping_mode=MappingMode.NoMap,warn_on_initvar=True):"""Express the DTO as a dictionary.:keep_none: Filter keys that are None:mapping_mode: Format for properties:warn_on_initvar: Emit a warning if the instance contains non-defaultinit-only variables.:returns: Returns the instantiated DTO as a dictionary"""to_json()defto_json(self,*,keep_none=False,mapping_mode=MappingMode.NoMap,warn_on_initvar=True):"""Express the DTO as a json string.:keep_none: Filter keys that are None:mapping_mode: Format for properties:warn_on_initvar: Emit a warning if the instance contains non-defaultinit-only variables.:returns: Returns the instantiated DTO as a json string"""ExamplesConverting your dataclass to a JSON serializable formatfromtypingimportListfromdataclassesimportdataclassfromtyped_json_dataclassimportTypedJsonMixin@dataclassclassPerson(TypedJsonMixin):name:strage:int@dataclassclassFamily(TypedJsonMixin):people:List[Person]bob=Person(name='Bob',age=24)alice=Person(name='Alice',age=32)family=Family(people=[bob,alice])print(family.to_json())# => {"people": [{"name": "Bob", "age": 24}, {"name": "Alice", "age": 32}]}If your data doesn't match the type definitions, you'll get a helpful error:fromdataclassesimportdataclassfromtyped_json_dataclassimportTypedJsonMixin@dataclassclassPerson(TypedJsonMixin):name:strage:intrequest_data='{"name":"Bob","age":"24"}'bob=Person.from_json(request_data)# => TypeError: Person.age is expected to be <class 'int'>, but value 24 with type <class 'str'> was found insteadAnd you can parse data from a Pythondictas well. Just use the.from_dict()function instead:fromdataclassesimportdataclassfromtyped_json_dataclassimportTypedJsonMixin@dataclassclassPerson(TypedJsonMixin):name:strage:intrequest_data_as_dict={'name':'Alice','age':'32'}alice=Person.from_dict(request_data_as_dict)# => TypeError: Person.age is expected to be <class 'int'>, but value 32 with type <class 'str'> was found insteadSetting a mapping_mode for auto mappingfromdataclassesimportdataclassfromtyped_json_dataclassimportTypedJsonMixin,MappingMode@dataclassclassPerson(TypedJsonMixin):person_name:strperson_age:intrequest_data_as_dict={'personName':'Alice','personAge':32}alice=Person.from_dict(request_data_as_dict,mapping_mode=MappingMode.SnakeCase)# => Person(person_name='Alice', person_age=32)This mapping mode is useful for when you get requests that have the JSON in a
camel case format, but you want your objects to be snake case and stay PEP8
compliant.Limitations and CaveatsDataclasses with init-only variablesSupport for dataclasses withinit-only variablesis limited. Althoughto_dictandto_jsonwill convert the dataclass, the
resulting dict or JSON string will not contain the init-only variables, since
their values are not available after initialization. This also means that such
dataclasses cannot later be instantiated from a dict or JSON string, since the
init-only variables are a required parameter in the dataclass'__init__method.TypedJsonMixindetects the usage of dataclasses with init-only
variables, emits a warning when it is converted to a dict or JSON string, and
refuses to instantiate a dataclass with init-only variables.A first workaround consists of providing a default value to the init-only
variables:@dataclassclassPerson(TypedJsonMixin):person_name:InitVar[str]=''person_first_name:str=''person_last_name:str=''def__post_init__(self,person_name):ifperson_name:# Instantiated directlyself.person_first_name,self.person_last_name=person_name.split()# Call TypedJsonMixin __post_init__ methodsuper().__post_init__()Note: Instantiations without arguments, such asPerson(), are now
possible, although the created instance would then be invalid.The second workaround is to remove init-only variables from the dataclass, and
perform the__post_init__instantiation using a class method instead:@dataclassclassPerson(TypedJsonMixin):person_first_name:strperson_last_name:str@classmethoddefcreate(cls,person_name):first_name,last_name=person_name.split()cls(first_name,last_name)Finally, if the dataclass is not meant to ever be instantiated from a dict or
JSON string, and only theto_dictorto_jsonmethods are called, the
warnings can be suppressed by passingwarn_on_initvar=Falseas a keyword
argument in the method call.