Marshmallow serializes and deserializes Python objects. pip install marshmallow. Schema: from marshmallow import Schema, fields. Define: class UserSchema(Schema): name = fields.Str(required=True); email = fields.Email(required=True). Serialize (dump): schema = UserSchema(); result = schema.dump(user_obj). Deserialize (load): data = schema.load({"name":"Alice","email":"[email protected]"}). Many: schema.dump(users, many=True). schema.load(list_of_dicts, many=True). Validate only: schema.validate({"name":"","email":"bad"}) — returns error dict. Fields: fields.Int, fields.Float, fields.Bool, fields.DateTime(format="iso"), fields.Date, fields.Nested(OtherSchema), fields.List(fields.Str()), fields.Dict(values=fields.Int()). Optional: fields.Str(load_default=None). Dump only: fields.Str(dump_only=True). Load only: fields.Str(load_only=True). Rename: email = fields.Str(data_key="emailAddress"). Validators: from marshmallow import validate. fields.Str(validate=validate.Length(min=1,max=100)). validate.Range(min=0,max=100). validate.OneOf(["a","b","c"]). validate.Regexp(r"^\d{5}$"). validate.Email(). validate.URL(). Custom validator: @validates("password") def validate_password(self, value): if len(value)<8: raise ValidationError("Too short"). Cross-field: @validates_schema def check_dates(self, data, **kwargs): if data["end"]<data["start"]: raise ValidationError("end before start"). Hooks: @post_load def make_user(self, data, **kwargs): return User(**data). Meta: class Meta: unknown = EXCLUDE — ignore unknown fields. INCLUDE to keep them. RAISE (default) to error on unknown. marshmallow-dataclass: from marshmallow_dataclass import dataclass. @dataclass class User: name: str; email: str. UserSchema = User.Schema. marshmallow-sqlalchemy: from marshmallow_sqlalchemy import SQLAlchemyAutoSchema. class UserSchema(SQLAlchemyAutoSchema): class Meta: model = User. Claude Code generates marshmallow schemas, nested serializers, and SQLAlchemy auto-schemas.
CLAUDE.md for Marshmallow
## marshmallow Stack
- Version: marshmallow >= 3.21 | pip install marshmallow marshmallow-dataclass
- Schema: class S(Schema): field = fields.Str(required=True, validate=Length(min=1))
- Dump: schema.dump(obj) — serialize to dict | schema.dump(list, many=True)
- Load: schema.load(dict) — deserialize + validate | schema.validate(dict) only
- Meta: class Meta: unknown = EXCLUDE|INCLUDE|RAISE (default RAISE)
- Hook: @post_load def make_model(self, data, **kwargs): return Model(**data)
- Nested: fields.Nested(OtherSchema) | fields.List(fields.Nested(...))
Marshmallow Serialization Pipeline
# app/serializers.py — marshmallow schema definitions
from __future__ import annotations
import re
from datetime import datetime
from typing import Any
from marshmallow import (
EXCLUDE,
INCLUDE,
RAISE,
Schema,
ValidationError,
fields,
post_dump,
post_load,
pre_load,
validate,
validates,
validates_schema,
)
# ─────────────────────────────────────────────────────────────────────────────
# Data models (plain Python — no ORM required)
# ─────────────────────────────────────────────────────────────────────────────
class Address:
def __init__(self, street, city, state, postal_code, country="US"):
self.street = street
self.city = city
self.state = state
self.postal_code = postal_code
self.country = country
class User:
def __init__(self, id, email, first_name, last_name, role,
is_active=True, address=None, created_at=None):
self.id = id
self.email = email
self.first_name = first_name
self.last_name = last_name
self.role = role
self.is_active = is_active
self.address = address
self.created_at = created_at or datetime.utcnow()
self._password_hash = ""
def set_password(self, raw: str) -> None:
self._password_hash = f"hashed:{raw}"
class Product:
def __init__(self, id, sku, name, price, stock, category, is_active=True):
self.id = id
self.sku = sku
self.name = name
self.price = price
self.stock = stock
self.category = category
self.is_active = is_active
class OrderLine:
def __init__(self, product_id, sku, quantity, unit_price):
self.product_id = product_id
self.sku = sku
self.quantity = quantity
self.unit_price = unit_price
self.subtotal = quantity * unit_price
class Order:
def __init__(self, id, user_id, lines, status, total, created_at=None):
self.id = id
self.user_id = user_id
self.lines = lines
self.status = status
self.total = total
self.created_at = created_at or datetime.utcnow()
# ─────────────────────────────────────────────────────────────────────────────
# Address schema
# ─────────────────────────────────────────────────────────────────────────────
class AddressSchema(Schema):
street = fields.Str(required=True, validate=validate.Length(min=1, max=200))
city = fields.Str(required=True, validate=validate.Length(min=1, max=100))
state = fields.Str(required=True, validate=validate.Length(min=2, max=2))
postal_code = fields.Str(required=True, validate=validate.Regexp(r"^\d{5}(-\d{4})?$"))
country = fields.Str(load_default="US", validate=validate.Length(min=2, max=2))
@post_load
def make_address(self, data: dict, **kwargs) -> Address:
data["state"] = data["state"].upper()
data["country"] = data["country"].upper()
return Address(**data)
# ─────────────────────────────────────────────────────────────────────────────
# User schemas
# ─────────────────────────────────────────────────────────────────────────────
class UserSchema(Schema):
"""Response schema — safe to expose to clients."""
class Meta:
unknown = EXCLUDE # silently drop unknown fields during deserialization
id = fields.Int(dump_only=True)
email = fields.Email(required=True)
first_name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
last_name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
role = fields.Str(
required=True,
validate=validate.OneOf(["user", "moderator", "admin"]),
)
is_active = fields.Bool(load_default=True)
address = fields.Nested(AddressSchema, load_default=None)
created_at = fields.DateTime(format="iso", dump_only=True)
# Computed field — only in serialized output
full_name = fields.Method("get_full_name", dump_only=True)
def get_full_name(self, obj: User) -> str:
return f"{obj.first_name} {obj.last_name}"
class CreateUserSchema(Schema):
"""Request schema for POST /users — includes password input."""
class Meta:
unknown = RAISE # reject unexpected fields from clients
email = fields.Email(required=True)
first_name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
last_name = fields.Str(required=True, validate=validate.Length(min=1, max=100))
password = fields.Str(
required=True,
load_only=True, # never serialized in responses
validate=validate.Length(min=8, max=72),
)
role = fields.Str(load_default="user", validate=validate.OneOf(["user","moderator","admin"]))
address = fields.Nested(AddressSchema, required=False, load_default=None)
@validates("password")
def validate_password_complexity(self, value: str) -> None:
if not re.search(r"[A-Z]", value):
raise ValidationError("Password must contain at least one uppercase letter")
if not re.search(r"\d", value):
raise ValidationError("Password must contain at least one digit")
@post_load
def make_user(self, data: dict, **kwargs) -> User:
password = data.pop("password")
user = User(id=None, **data)
user.set_password(password)
return user
class UpdateUserSchema(Schema):
"""PATCH schema — all fields optional."""
class Meta:
unknown = RAISE
first_name = fields.Str(required=False, validate=validate.Length(min=1, max=100))
last_name = fields.Str(required=False, validate=validate.Length(min=1, max=100))
role = fields.Str(required=False, validate=validate.OneOf(["user","moderator","admin"]))
# ─────────────────────────────────────────────────────────────────────────────
# Product schema
# ─────────────────────────────────────────────────────────────────────────────
class ProductSchema(Schema):
id = fields.Int(dump_only=True)
sku = fields.Str(required=True, validate=validate.Regexp(r"^[A-Z0-9]+-\d{4,}$"))
name = fields.Str(required=True, validate=validate.Length(min=1, max=200))
price = fields.Float(required=True, validate=validate.Range(min=0.01, max=100_000))
stock = fields.Int(required=True, validate=validate.Range(min=0))
category = fields.Str(required=True, validate=validate.OneOf(
["Electronics", "Clothing", "Books", "Home", "Sports"]
))
is_active = fields.Bool(load_default=True)
@post_load
def make_product(self, data: dict, **kwargs) -> Product:
return Product(id=None, **data)
# ─────────────────────────────────────────────────────────────────────────────
# Order line and Order schemas — nested
# ─────────────────────────────────────────────────────────────────────────────
class OrderLineSchema(Schema):
product_id = fields.Int(required=True, validate=validate.Range(min=1))
sku = fields.Str(dump_only=True)
quantity = fields.Int(required=True, validate=validate.Range(min=1, max=10_000))
unit_price = fields.Float(dump_only=True) # set server-side from product
subtotal = fields.Float(dump_only=True)
class CreateOrderSchema(Schema):
user_id = fields.Int(required=True)
lines = fields.List(fields.Nested(OrderLineSchema), required=True,
validate=validate.Length(min=1))
notes = fields.Str(load_default=None, validate=validate.Length(max=500))
@validates_schema
def check_unique_products(self, data: dict, **kwargs) -> None:
product_ids = [line["product_id"] for line in data.get("lines", [])]
if len(product_ids) != len(set(product_ids)):
raise ValidationError("Duplicate product_id in order lines", field_name="lines")
class OrderSchema(Schema):
id = fields.Int(dump_only=True)
user_id = fields.Int()
lines = fields.List(fields.Nested(OrderLineSchema))
status = fields.Str(validate=validate.OneOf(
["pending","paid","shipped","delivered","cancelled"]
))
total = fields.Float(dump_only=True)
created_at = fields.DateTime(format="iso", dump_only=True)
# ─────────────────────────────────────────────────────────────────────────────
# Schema registry — access by name for generic endpoints
# ─────────────────────────────────────────────────────────────────────────────
SCHEMAS: dict[str, Schema] = {
"user": UserSchema(),
"create_user": CreateUserSchema(),
"update_user": UpdateUserSchema(),
"product": ProductSchema(),
"order": OrderSchema(),
"create_order": CreateOrderSchema(),
}
def serialize(schema_name: str, obj: Any, many: bool = False) -> dict | list:
return SCHEMAS[schema_name].dump(obj, many=many)
def deserialize(schema_name: str, data: dict | list, many: bool = False) -> Any:
return SCHEMAS[schema_name].load(data, many=many)
if __name__ == "__main__":
# Serialize a user object
user = User(id=1, email="[email protected]",
first_name="Alice", last_name="Smith", role="user")
schema = UserSchema()
serialized = schema.dump(user)
print("Serialized:", serialized)
# Deserialize + validate raw input
try:
new_user = CreateUserSchema().load({
"email": "[email protected]",
"first_name": "Bob",
"last_name": "Jones",
"password": "SecurePass1",
})
print("Created user:", new_user.first_name)
except ValidationError as e:
print("Validation errors:", e.messages)
# Many=True
users = [
User(id=1, email="[email protected]", first_name="A", last_name="B", role="user"),
User(id=2, email="[email protected]", first_name="C", last_name="D", role="admin"),
]
results = schema.dump(users, many=True)
print(f"Serialized {len(results)} users")
For the Pydantic alternative — Pydantic v2 validates at instantiation time using Rust-compiled validators and is 5–20× faster than marshmallow for large payloads, but marshmallow’s @post_load def make_model(self, data, **kwargs): return MyClass(**data) deserializes directly into arbitrary Python classes without those classes extending BaseModel — useful for legacy codebases or third-party models, and @validates_schema can check relationships between fields (start < end, product quantities don’t exceed warehouse capacity) with access to the full validated dict before instantiation. For the plain json.loads + manual validation alternative — if "email" not in data or "@" not in data["email"]: in every endpoint is unmaintainable across 50 fields, while a single marshmallow Schema centralizes validation (field types, ranges, regex), serialization (dump_only / load_only), and transformation (@pre_load to normalize keys, @post_load to construct objects) in one place that can be tested independently of HTTP handlers. The Claude Skills 360 bundle includes marshmallow skill sets covering Schema field definitions, fields Nested/List/Method/DateTime, validate Length/Range/OneOf/Regexp/Email, @validates and @validates_schema cross-field checks, @post_load for object construction, Meta unknown EXCLUDE/RAISE, dump_only/load_only fields, many=True for collections, marshmallow-dataclass integration, and marshmallow-sqlalchemy auto-schema. Start with the free tier to try serialization code generation.