Using Marshmallow without repeating myself

2020-08-12 03:27发布

According to the official Marshmallow docs, it's recommended to declare a Schema and then have a separate class that receives loaded data, like this:

class UserSchema(Schema):
    name = fields.Str()
    email = fields.Email()
    created_at = fields.DateTime()

    @post_load
    def make_user(self, data):
        return User(**data)

However, my User class would look something like this:

class User:
    def __init__(name, email, created_at):
        self.name = name
        self.email = email
        self.created_at = created_at

This seems like repeating myself unnecessarily and I really don't like having to write the attribute names three more times. However, I do like IDE autocompletion and static type checking on well-defined structures.

So, is there any best practice for loading serialized data according to a Marshmallow Schema without defining another class?

2条回答
太酷不给撩
2楼-- · 2020-08-12 04:08

For vanilla Python classes, there isn't an out-of-box way to define the class for the schema without repeating the field names.

If you're using SQLAlchemy for example, you can define the schema directly from the model with marshmallow_sqlalchemy.ModelSchema:

from marshmallow_sqlalchemy import ModelSchema
from my_alchemy_models import User

class UserSchema(ModelSchema):
    class Meta:
        model = User

Same applies to flask-sqlalchemy which uses flask_marshmallow.sqla.ModelSchema.

In the case of vanilla Python classes, you may define the fields once and use it for both schema and model/class:

USER_FIELDS = ('name', 'email', 'created_at')

class User:
    def __init__(self, name, email, created_at):
        for field in USER_FIELDS:
            setattr(self, field, locals()[field])

class UserSchema(Schema):
    class Meta:
        fields = USER_FIELDS

    @post_load
    def make_user(self, data):
        return User(**data)
查看更多
男人必须洒脱
3楼-- · 2020-08-12 04:11

You'll have to create the two classes, but the good news is you won't have to enter the attribute names multiple times in most cases. One thing I've found, if you are using Flask, SQLAlchemy, and Marshmallow, is that if you define some of the validation attributes in your Column definition, the Marshmallow Schema will automatically pick up on these and the validations supplied in them. For example:

import (your-database-object-from-flask-init) as db
import (your-marshmallow-object-from-flask-init) as val

class User(db.Model):
  name = db.Column(db.String(length=40), nullable=False)
  email = db.Column(db.String(length=100))
  created_at = db.Column(db.DateTime)

class UserSchema(val.ModelSchema):
  class Meta:
    model = User

In this example, if you were take a dictionary of data and put it into UserSchema().load(data) , you would see errors if, in this example, name didn't exist, or name was longer than 40 characters, or email is longer than 100 characters. Any custom validations beyond that you'd still have to code within your schema.

It also works if you've created the model class as an extension of another model class, carrying over its attributes. For example, if you wanted every class to have created/modified information, you could put those attributes in the parent model class and the child would inherit those along with their validation parameters. Marshmallow doesn't allow your parent model to have a schema, so I don't have information on custom validations there.

I know you've probably already completed your project, but I hope this helps for other developers that come across this.

Relevant pip list: Flask (1.0.2) flask-marshmallow (0.9.0) Flask-SQLAlchemy (2.3.2) marshmallow (2.18.0) marshmallow-sqlalchemy (0.15.0) SQLAlchemy (1.2.16)

查看更多
登录 后发表回答