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…