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?
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": "[email protected]": 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"
)
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With