Search code examples
pythonflasksqlalchemymarshmallow

How to dynamically generate marshmallow schemas for SQLAlchemy models


I'm creating a Flask API using SQLAlchemy models. I don't want to define a schema for every model I have, I don't want to do this every time:

class EntrySchema(ma.ModelSchema):
    class Meta:
        model = Entry

I would like each model to have a schema, so it can easily dump itself. Creating a default Schema and setting the Schema.Meta.model didn't work:

class Entry(db.Model):
    __tablename__ = 'entries'

    id = db.Column(db.Integer, primary_key=True)
    started_at = db.Column(db.DateTime)
    ended_at = db.Column(db.DateTime)
    description = db.Column(db.Text())

    def __init__(self, data):
        for key in data:
            setattr(self, key, data[key])

        self.Schema = Schema
        self.Schema.Meta.model = self.__class__

    def dump(self):
        schema = self.Schema()
        result = schema.dump(self)
        return result


class Schema(ma.ModelSchema):
    class Meta:
        pass

Why is a generic Schema with the model overwritten different than a Schema with the model declared?


Solution

  • You could create a class decorator that adds the Schema to your models:

    def add_schema(cls):
        class Schema(ma.ModelSchema):
            class Meta:
                model = cls
        cls.Schema = Schema
        return cls
    

    and then

    @add_schema
    class Entry(db.Model):
        ...
    

    The schema will be available as the class attribute Entry.Schema.

    The reason your original attempt fails is that marshmallow Schema classes are constructed using a custom metaclass, which inspects the namespace created from executing the class body and does its thing. When you modify the already constructed class, it is too late.

    If you're unfamiliar with metaclasses in Python, read about them in the language reference. They are a tool that allows for great things and great misuse.


    Some more complex types, such as enums, require additional information and dedicated field types to work properly. For example using marshmallow-enum and a decorator factory pattern it is possible to configure the model schema to accommodate enums:

    from marshmallow_enum import EnumField
    
    def add_schema(**kwgs):
        def decorator(cls): 
            class Meta:
                model = cls
    
            schema = type("Schema", (ma.ModelSchema,), {"Meta": Meta, **kwgs})
            cls.Schema = schema
            return cls
    
        return decorator
    
    ...
    
    
    @add_schema(
        my_enum=EnumField(MyEnumType, by_value=True)
    )
    class Entry(db.Model):
        ...
    

    Of course another way would be to make the decorator itself smarter and inspect the class before building the schema, so that it handles special cases such as enums.