Map a dictionary to a custom object, or a list of dictionaries to a list of custom objects and reversely
Here we refer to a dictionary but we can expand the idea to a JSON also because the JSON load of a string/input file(such as a flask request json value) will be dumped in the python side as a dictionary or a list of dictionary.
A mapping without customization: @post_load to load the model and nothing to dump the model
Here the idea is to declare in the model and in the schema representing the model the exact same field attributes and to specify in a method annotated
with @post_load how to instantiate the model after the load() method is invoked: in the general case, we need only to invoke the constructor
of the model by passing to it the attributes bound in the schema instance.
Model and mapping:
from dataclasses import dataclass from datetime import date from enum import Enum from enum import auto from marshmallow import EXCLUDE from marshmallow import Schema from marshmallow import fields from marshmallow import post_load class Gender(Enum): MALE = auto() FEMALE = auto() UNKNOWN = auto() @dataclass() class Person: first_name: str last_name: str birthday: date gender: Gender class PersonSchema(Schema): class Meta: unknown = EXCLUDE first_name = fields.Str() last_name = fields.Str() birthday = fields.Date() gender = fields.Enum(Gender) # Register a method to invoke after deserializing an object. The method receives the deserialized # data and returns the processed data @post_load def post_load(self, data, **kwargs): return Person(**data) |
Processing
def create_person_dump(gender: str) -> dict: person_dump: dict = { 'first_name': 'David', 'last_name': 'Doe', 'birthday': '2022-12-31', 'gender': gender } return person_dump def create_person(gender: Gender) -> Person: return Person(first_name='David', last_name='Doe', birthday=date(2022, 12, 31), gender=gender) def load_person_from_dicts(): print(f'load_person_from_dicts()') # dict to person person_dump: dict = create_person_dump(gender='MALE') person: Person = PersonSchema().load(person_dump) print(f'person={person}') # list of dict to list of person person_dump: list[dict] = [create_person_dump(gender='MALE'), create_person_dump(gender='UNKNOWN')] persons: list[Person] = PersonSchema().load(person_dump, many=True) print(f'persons={persons}') def dump_person_to_dicts(): print(f'dump_person_to_dicts()') # person to dict person: Person = create_person(gender=Gender.MALE) person_dump: dict = PersonSchema().dump(person) print(f'person_dump={person_dump}') # list of person to list of dict persons: list[Person] = [create_person(gender=Gender.MALE), create_person(gender=Gender.UNKNOWN)] persons_dump: list[dict] = PersonSchema().dump(persons, many=True) print(f'persons_dump={persons_dump}') load_person_from_dicts() dump_person_to_dicts() |
Output:
load_person_from_dicts() person=Person(first_name='David', last_name='Doe', birthday=datetime.date(2022, 12, 31), gender=<Gender.MALE: 1>) persons=[Person(first_name='David', last_name='Doe', birthday=datetime.date(2022, 12, 31), gender=<Gender.MALE: 1>), Person(first_name='David', last_name='Doe', birthday=datetime.date(2022, 12, 31), gender=<Gender.UNKNOWN: 3>)] dump_person_to_dicts() person_dump={'first_name': 'David', 'last_name': 'Doe', 'birthday': '2022-12-31', 'gender': 'MALE'} persons_dump=[{'first_name': 'David', 'last_name': 'Doe', 'birthday': '2022-12-31', 'gender': 'MALE'}, {'first_name': 'David', 'last_name': 'Doe', 'birthday': '2022-12-31', 'gender': 'UNKNOWN'}] |
A mapping with load object customization: @pre_load to adjust the input data and @post_load to load the adjusted model
Model and mapping:
class PersonSchema(Schema): class Meta: unknown = EXCLUDE first_name = fields.Str() last_name = fields.Str() birthday = fields.Date() gender = fields.Enum(Gender) @post_load def post_load(self, data, **kwargs): return Person(**data) @pre_load def pre_load(self, data, **kwargs): gender: str = data['gender']['type'] data['gender'] = gender return data |
Processing
person_dump: dict = { 'first_name': 'a', 'last_name': 'a', 'birthday': '2022-12-31', 'gender': { 'description': 'a man or a boy', 'type': 'MALE' } } person: Person = PersonSchema().load(person_dump) print(f'person={person}') |
Output:
person=Person(first_name='a', last_name='a', birthday=datetime.date(2022, 12, 31), gender=<Gender.MALE: 'a man or a boy'>) |
A mapping with dump customization: @post_dump to adjust the dumped data
Model and mapping:
from dataclasses import dataclass from datetime import date from enum import Enum from marshmallow import EXCLUDE from marshmallow import Schema from marshmallow import fields from marshmallow import post_dump class Gender(Enum): MALE = 'a man or a boy' FEMALE = 'a woman or a girl' UNKNOWN = "we don't know" @dataclass() class Person: first_name: str last_name: str birthday: date gender: Gender class PersonSchema(Schema): class Meta: unknown = EXCLUDE first_name = fields.Str() last_name = fields.Str() birthday = fields.Date() @post_dump(pass_original=True) def post_dump(self, data: dict, original_data: Person, **kwargs): data['gender'] = { 'description': original_data.gender.value, 'type': original_data.gender.name } return data |
Possessing:
person = Person(first_name='David', last_name='Doe', birthday=date(2022, 12, 31), gender=Gender.MALE) dump: dict = PersonSchema().dump(person) print(f'dump={dump}') |
Output:
dump={'first_name': 'David', 'last_name': 'Doe', 'birthday': '2022-12-31', 'gender': {'description': 'a man or a boy', 'type': 'MALE'}} |