Working with Schemas

Now that we know about fields, let’s focus on schemas:

Defining Schemas

We already know how to define schemas: subclass lima.Schema (the shortcut for lima.schema.Schema) and add fields as class attributes.

But there’s more to schemas than this. First of all – schemas are composible:

from lima import Schema, fields

class PersonSchema(Schema):
    first_name = fields.String()
    last_name = fields.String()

class AccountSchema(Schema):
    login = fields.String()
    password_hash = fields.String()

class UserSchema(PersonSchema, AccountSchema):
    pass

list(UserSchema.__fields__)
# ['password_hash', 'login', 'last_name', 'first_name']

Secondly, it’s possible to remove fields from subclasses that are present in superclasses. This is done by setting a special class attribute __lima_args__ like so:

class UserProfileSchema(UserSchema):
    __lima_args__ = {'exclude': ['last_name', 'password_hash']}

list(UserProfileSchema.__fields__)
# ['login', 'first_name']

If there’s only one field to exclude, you don’t have to put its name inside a list - lima does that for you:

class NoLastNameSchema(UserSchema):
    __lima_args__ = {'exclude': 'last_name'}  # string instead of list

list(NoLastNameSchema.__fields__)
# ['password_hash', 'login', 'first_name']

And finally, we can’t just exclude fields, we can include them too. So here is a user schema with fields provided via __lima_args__:

class UserSchema(Schema):
    __lima_args__ = {
        'include': {
            'first_name': fields.String(),
            'last_name': fields.String(),
            'login': fields.String(),
            'password_hash': fields.String()
        }
    }

list(UserSchema.__fields__)
# ['password_hash', 'last_name', 'first_name', 'login']

Note

It’s possible to mix and match all those features to your heart’s content. lima tries to fail early if something doesn’t add up.

Note

The inheritance and precedence rules for fields are intuitive, but should there ever arise the need for clarification, you can read about how a schema’s fields are determined in the documentation of lima.schema.SchemaMeta.

Automated Schema Definition

Validating ORM agnosticism for a moment, let’s see how we could utilize __lima_args__['include'] to create our Schema automatically.

We start with this SQLAlchemy model (skip this section if you don’t want to install SQLAlchemy):

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Account(Base):
    __tablename__ = 'accounts'
    id = sa.Column(sa.Integer, primary_key=True)
    login = sa.Column(sa.String)
    password_hash = sa.Column(sa.String)

lima.fields defines a mapping lima.fields.type_mapping of some Python types to field classes. We can utilize this as follows:

from lima import fields

def fields_for_model(model):
    result = {}
    for name, col in model.__mapper__.columns.items():
        field_class = fields.type_mapping[col.type.python_type]
        result[name] = field_class()
    return result

Defining lima schemas becomes a piece of cake now:

from lima import Schema

class AccountSchema(Schema):
    __lima_args__ = {'include': fields_for_model(Account)}

AccountSchema.__fields__
# {'id': <lima.fields.Integer at 0x...>,
#  'login': <lima.fields.String at 0x...>,
#  'password_hash': <lima.fields.String at 0x...>}

... and of course you still can manually add, exclude or inherit anything you like.

Warning

Neither lima.fields.type_mapping nor the available field classes are as exhaustive as they should be. Expect above code to fail on slightly exotic column types. There is still work to be done.

Schema Objects

Up until now we only ever needed a single instance of a schema class to marshal the fields defined in this class. But schema objects can do more.

Providing the keyword-only argument exclude, we may exclude certain fields from being serialized. This saves the need to define lots of almost similar schema classes:

import datetime
from lima import Schema, fields

# again, our model
class Person:
    def __init__(self, first_name, last_name, birthday):
        self.first_name = first_name
        self.last_name = last_name
        self.birthday = birthday

# again, our schema
class PersonSchema(Schema):
    first_name = fields.String()
    last_name = fields.String()
    date_of_birth = fields.Date(attr='birthday')

# again, our person
person = Person('Ernest', 'Hemingway', datetime.date(1899, 7, 21))

# as before, for reference
person_schema = PersonSchema()
person_schema.dump(person)
# {'date_of_birth': '1899-07-21',
#  'first_name': 'Ernest',
#  'last_name': 'Hemingway'}

birthday_schema = PersonSchema(exclude=['first_name', 'last_name'])
birthday_schema.dump(person)
# {'date_of_birth': '1899-07-21'}

The same thing can be achieved via the only keyword-only argument:

birthday_schema = PersonSchema(only='date_of_birth')
birthday_schema.dump(person)
# {'date_of_birth': '1899-07-21'}

You may have already guessed: both exclude and only take lists of field names as well as simple strings for a single field name – just like __lima_args__['exclude'].

Marshalling Collections

Consider this:

persons = [
    Person('Ernest', 'Hemingway', datetime.date(1899, 7, 21)),
    Person('Virginia', 'Woolf', datetime.date(1882, 1, 25)),
    Person('Stefan', 'Zweig', datetime.date(1881, 11, 28)),
]

Instead of looping over this collection ourselves, we can ask the schema object to do this for us - either for a single call (by specifying many=True to the dump() method), or for every call of dump() (by specifying many=True to the schema’s constructor):

person_schema = PersonSchema(only='last_name')
person_schema.dump(persons, many=True)
# [{'last_name': 'Hemingway'},
#  {'last_name': 'Woolf'},
#  {'last_name': 'Zweig'}]

many_persons_schema =  PersonSchema(only='last_name', many=True)
many_persons_schema.dump(persons)
# [{'last_name': 'Hemingway'},
#  {'last_name': 'Woolf'},
#  {'last_name': 'Zweig'}]

Schema Recap

  • You now know how to compose bigger schemas from smaller ones (inheritance of schema classes).
  • You know how to exclude certain fields from schemas (__lima_args__['exclude']).
  • You know three different ways to add fields to schemas (class attributes, __lima_args__['include'] and inheriting from other schemas)
  • You are now able to create schemas automatically (__lima_args__['include'] with some model-specific code)
  • You can fine-tune what gets dumped by a schema object (only and exclude keyword-only arguments) and you can serialize collections of objects (many=True)