Migwell
Migwell

Reputation: 20127

Handling multiple variants of a marshmallow schema

I have a simple Flask-SQLAlchemy model, which I'm writing a REST API for:

class Report(db.Model, CRUDMixin):
    report_id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.user_id'), index=True)
    report_hash = Column(Unicode, index=True, unique=True)
    created_at = Column(DateTime, nullable=False, default=dt.datetime.utcnow)
    uploaded_at = Column(DateTime, nullable=False, default=dt.datetime.utcnow)

Then I have the corresponding Marshmallow-SQLAlchemy schema:

class ReportSchema(ModelSchema):
    class Meta:
        model = Report

However, in my rest API, I need to be able to dump and load slightly different variants of this model:

How can I reasonably maintain 3 (or more) versions of this schema? Should I:

Upvotes: 12

Views: 4235

Answers (1)

Migwell
Migwell

Reputation: 20127

Since asking this question, I've done a ton of work using Marshmallow, so hopefully I can explain somewhat.

My rule of thumb is this: do as much as you can with the schema constructor (option #2), and only resort to inheritance (option #3) if you absolutely have to. Never use option #1, because that will result in unnecessary, duplicated code.

The schema constructor approach is great because:

  • You end up writing the least code
  • You never have to duplicate logic (e.g. validation)
  • The only, exclude, partial and unknown arguments to the schema constructor give you more than enough power to customize the individual schemas (see the documentation)
  • Schema subclasses can add extra settings to the schema constructor. For example marshmallow-jsonapi addds include_data, which lets you control the amount of data you return for each related resource

My original post is a situation where using the schema constructor is sufficient. You should first define a schema that includes all possibly related fields, including relationships that might be a Nested field. Then, if there are related resources or superfluous fields you don't want to include in the response sometimes, you can simply use Report(exclude=['some', 'fields']).dump() in that view method.

However, an example I've encountered where using inheritance was a better fit was when I modelled the arguments for certain graphs I was generating. Here, I wanted general arguments that would be passed into the underlying plotting library, but I wanted the child schemas to refine the schema and use more specific validations:

class PlotSchema(Schema):
    """
    Data that can be used to generate a plot
    """
    id = f.String(dump_only=True)
    type = f.String()
    x = f.List(f.Raw())
    y = f.List(f.Raw())
    text = f.List(f.Raw())
    hoverinfo = f.Str()


class TrendSchema(PlotSchema):
    """
    Data that can be used to generate a trend plot
    """
    x = f.List(f.DateTime())
    y = f.List(f.Number())

Upvotes: 8

Related Questions