Guide

Building a schema

A schema is a class with type annotations. The type annotations declare the names of the attributes on a model, what types they must be, and constraints on their content. A schema must be registered to its model using the marsha.schema decorator.

class Player(object):
    def __init__(self, name, number):
        self.name = name
        self.number = number

@marsha.schema(Player)
class PlayerSchema(object):
    name: str
    number: int

Once the schema has been registered to the model, it is no longer needed. For convenience, the type annotations can be added directly to the model and registered as a schema without declaring a separate schema class.

@marsha.schema()
class Player(object):
    name: str
    number: int

    def __init__(self, name, number):
        self.name = name
        self.number = number

If the data does not already have a model, or all the schema attributes can be added directly to the model instance after deserialization, the model can subclass the marsha.Object class to avoid constructor boilerplate.

@marsha.schema()
class Player(marsha.Object):
    name: str
    number: int

If the model needs to store the data like a dictionary, the model can subclass the dict class to inherit the dictionary constructor and use a format that knows how to interact with the model like a dictionary.

@marsha.schema()
class Player(dict):
    name: str
    number: int

Building custom types

The builtin types have default formats that define how the type should be validated and serialized in general. These formats also define constraints that can be customized when building custom types with the marsha.type method.

name_string = marsha.type(str, min=1, max=80)

Some types, like datetime, have required constraints which must be defined on a custom type. This prevents the builtin type from being used in a schema directly.

iso_date = marsha.type(datetime, encoding='%Y-%m-%d')

Deserializing data

Data can be deserialized using the marsha.load method. If the data is the wrong type, is missing values, or contains extra values a TypeError is raised. If the data fails any content constraints, then a ValueError is raised.

data = {'name': 'Rick', number: 42}
player = marsha.load(data, Player)

It can also be used as a utility for deserializing a single value.

number = marsha.load(42, int)

The typing generic List can be used to deserialize a list of models.

from typing import List

data = [{'name': 'Rick', number: 42}, {'name': 'Jerry', number: 0}]
number = marsha.load(data, List[Player])

Serializing data

Data can be serialized using the marsha.dump method.

player = Player(name='Rick', number=42)
data = marsha.dump(player)

It can also be used as a utility for serializing a single value.

value = marsha.dump(42, int)

The typing generic List can be used to serialize a list of models.

from typing import List

players = [Player(name='Rick', number=42), Player(name='Jerry', number=0)]
data = marsha.dump(players, List[Player])

An alternate schema can be used as a “view” to filter values differently than the model’s schema would by default.

@marsha.schema()
class PublicPlayerView(object):
    name: str

player = Player(name='Rick', number=42)
data = marsha.dump(player, PublicPlayerView)

The output of a model’s method can be serialized by using the Callable type annotation. Since callables do not need a value to deserialize, they will be treated like an unexpected key if they are provided during a load.

from typing import Callable

@marsha.schema()
class Player(marsha.Object):
    first_name: str
    last_name: str
    full_name: Callable

    def full_name(self):
        return '{} {}'.format(self.first_name, self.last_name)

The method’s output can be formatted using the typing subscription syntax to specify the return type.

integer_string = marsha.type(IntegerString)

@marsha.schema()
class Player(dict):
    get_number: Callable[..., integer_string]

    def get_number(self):
        return 42

A method can be serialized to a different key using the map constraint.

@marsha.schema()
class Player(dict):
    get_number: marsha.type(Callable, map='number')

    def get_number(self):
        return 42

Adding custom validation

Custom content constraints can be declared for types using the marsha.validate decorator. Validator functions must accept the data to validate as their only argument and return an error message if validation fails.

import re

name_string = marsha.type(str)

@marsha.validates(name_string)
def check_name_format(name):
    if not re.match(r'[a-zA-Z]+'):
        return 'Only ASCII letters are allowed in a name.'

Custom constraints can be declared for models to ensure that multiple values are compatible with each other.

@marsha.validates(Player)
def reserve_number(player):
    if player.number == 23 and player.name != 'Michael Jordan':
        return 'Number 23 is reserved for Michael Jordan.'

Building custom formats

Coming soon…