13

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?

Teyras
  • 1,262
  • 11
  • 22

3 Answers3

5

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)
Moses Koledoye
  • 77,341
  • 8
  • 133
  • 139
  • 2
    Defining the class with `setattr` prevents me from specifying validation rules, which is the reason I bother with Marshmallow after all. I could use a `collections.namedtuple`, but that seems rather clumsy when nested schemas are involved. – Teyras Aug 23 '17 at 06:39
  • @Teyras For such cases, I guess you'll have to repeat the fields. The docs explicitly does that. – Moses Koledoye Aug 23 '17 at 06:41
2

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)

user3832673
  • 369
  • 2
  • 7
  • This is pretty interesting! Is this a new feature of Marshmallow 3? I can't seem to find any documentation for it... – Teyras Jan 26 '19 at 11:33
  • Going to update my post with a relevant pip list so you can see what versions I'm on. It's not version 3. I was surprised by it myself, albeit pleasantly. – user3832673 Jan 28 '19 at 01:36
  • Oh, the feature you're using is provided by Flask-Marshmallow (https://flask-marshmallow.readthedocs.io/en/latest/)! Now I understand why there's no mention of this in Marshmallow docs. Anyway, thanks for your answer! – Teyras Jan 28 '19 at 12:36
2

Unless you need to deserialize as a specific class or you need custom serialization logic, you can simply do this (adapted from https://kimsereylam.com/python/2019/10/25/serialization-with-marshmallow.html):

from marshmallow import Schema, fields
from datetime import datetime

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

schema = UserSchema()
data = { "name": "Some Guy", "email": "sguy@google.com": datetime.now() }
user = schema.load(data)

You could also create a function in your class that creates a dict with validation rules, though it would still be redundant, it would allow you to keep everything in your model class:

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

        @classmethod
        def Schema(cls):
            return {"name": fields.Str(), "email": fields.Email(), "created_at": fields.DateTime()}

UserSchema = Schema.from_dict(User.Schema)

If you need to strong typing and full validation functionality, consider flask-pydantic or marshmallow-dataclass.

marshmallow-dataclass offers a lot of similar validation features to marshmallow. It kind of ties your hands though. It doesn't have built-in support for custom fields/polymorphism (have to use using marshmallow-union instead) and doesn't seem to play well with stack-on packages like flask-marshmallow and marshmallow-sqlalchemy. https://pypi.org/project/marshmallow-dataclass/

from typing import ClassVar, Type
from marshmallow_dataclass import dataclasses
from marshmallow import Schema, field, validate


@dataclass
class Person:
    name: str = field(metadata=dict(load_only=True))
    height: float = field(metadata=dict(validate=validate.Range(min=0)))
    Schema: ClassVar[Type[Schema]] = Schema


Person.Schema().dump(Person('Bob', 2.0))
# => {'height': 2.0}

flask-pydantic is less elegant from a validation standpoint, but offers many of the same features and the validation is built into the class. Note that simple validations like min/max are more awkward than in marshmallow. Personally, I prefer to keep view/api logic out of the class though. https://pypi.org/project/Flask-Pydantic/

from typing import Optional
from flask import Flask, request
from pydantic import BaseModel

from flask_pydantic import validate

app = Flask("flask_pydantic_app")

class QueryModel(BaseModel):
  age: int

class ResponseModel(BaseModel):
  id: int
  age: int
  name: str
  nickname: Optional[str]

# Example 1: query parameters only
@app.route("/", methods=["GET"])
@validate()
def get(query:QueryModel):
  age = query.age
  return ResponseModel(
    age=age,
    id=0, name="abc", nickname="123"
    )
VoteCoffee
  • 4,692
  • 1
  • 41
  • 44
  • If I recall correctly what I wanted when asking the question, the de-serialized result should have a definite type for static type checking (mypy, pyright) and IDE autocompletion. – Teyras Mar 23 '21 at 09:12
  • @Teyras I updated it with a second example using an alternative package that keeps everything in one class. It's more of choosing the best of 2 imperfect solutions though. – VoteCoffee Mar 23 '21 at 12:02
  • 1
    Huh, I guess this just became the best answer thanks to libraries getting better over time. I'd just like to add that you can use dataclasses with Pydantic as well (out of the box) - I actually like that approach more. – Teyras Mar 24 '21 at 13:16