Serialization with Marshmallow
Reviewed & published by Brayan K
Marshmallow is a Python library that converts complex objects to and from simple data types, serializing your models into JSON and validating incoming request data against a schema.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll declare a Schema, serialize objects with dump, validate input with load, nest schemas for related data, and read structured validation errors.
What You'll Learn in This Lesson
- Declaring a Schema with typed fields
- Serializing objects to dicts with dump()
- Validating and deserializing with load()
- Using dump_only and load_only fields
- Embedding related data with Nested
- Reading structured ValidationError messages
1 Declaring a Schema & Serializing with dump
A Marshmallow Schema is a class whose attributes are typed fields — fields.Str, fields.Int, fields.Email, and so on. Calling schema.dump(obj) reads those fields off the object and returns a plain dictionary, ready to hand to jsonify. A field marked dump_only=True appears only in output, perfect for a database-assigned id.
The runnable example serializes a user dictionary and a list of users.
# schemas.py — declare a schema and serialize (runnable)
from marshmallow import Schema, fields
class UserSchema(Schema):
id = fields.Int(dump_only=True) # output only; never accepted from input
name = fields.Str()
email = fields.Email()
schema = UserSchema()
# Serialize one object to a plain dict:
user = {"id": 1, "name": "Ada", "email": "ada@example.com"}
print("one:", schema.dump(user))
# Serialize a list with many=True:
users = [
{"id": 1, "name": "Ada", "email": "ada@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
]
print("many:", UserSchema(many=True).dump(users))
# ✅ Expected output:
# one: {'id': 1, 'name': 'Ada', 'email': 'ada@example.com'}
# many: [{'id': 1, 'name': 'Ada', 'email': 'ada@example.com'}, {'id': 2, 'name': 'Bob', 'email': 'bob@example.com'}]Output: a single dict with id, name, and email, then a list of two such dicts — exactly the shape you would return from an API.
2 Deserializing & Validating with load
The other direction is load(data): it takes incoming data (your parsed JSON body), checks every field's type and validate rules, and returns clean Python values. If anything is wrong it raises a ValidationError whose .messages is a dictionary keyed by field — perfect for a 400 response.
The example loads a valid payload, then a broken one to show the error structure.
# schemas.py — validate incoming data with load (runnable)
from marshmallow import Schema, fields, validate, ValidationError
class UserSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True, validate=validate.Length(min=2))
email = fields.Email(required=True)
age = fields.Int(validate=validate.Range(min=0, max=130))
schema = UserSchema()
# Valid input -> clean Python dict (note: no id, it's dump_only)
clean = schema.load({"name": "Ada", "email": "ada@example.com", "age": 36})
print("loaded:", clean)
# Invalid input -> ValidationError with a per-field message dict
try:
schema.load({"name": "A", "email": "not-an-email", "age": 999})
except ValidationError as err:
print("errors:", err.messages)
# ✅ Expected output:
# loaded: {'name': 'Ada', 'email': 'ada@example.com', 'age': 36}
# errors: {'name': ['Shorter than minimum length 2.'], 'email': ['Not a valid email address.'], 'age': ['Must be greater than or equal to 0 and less than or equal to 130.']}Output: the cleaned dict for the valid input, then an errors dict reporting the short name, the malformed email, and the out-of-range age — one entry per failing field.
3 Nested Schemas & Using It in a Flask Route
Related data nests with fields.Nested(OtherSchema). A BookSchema can embed its AuthorSchema, so dumping a book includes the author object and loading validates the nested fields too — errors are collected under the nested key.
# schemas.py — nested schemas (runnable)
from marshmallow import Schema, fields, validate, ValidationError
class AuthorSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True, validate=validate.Length(min=2))
class BookSchema(Schema):
id = fields.Int(dump_only=True)
title = fields.Str(required=True)
year = fields.Int(required=True, validate=validate.Range(min=0))
author = fields.Nested(AuthorSchema) # embed the author schema
schema = BookSchema()
# Dump: the nested author is serialized too
book = {"id": 1, "title": "1984", "year": 1949, "author": {"id": 5, "name": "Orwell"}}
print("dump:", schema.dump(book))
# Load valid nested data
print("load:", schema.load({"title": "Brave New World", "year": 1932,
"author": {"name": "Huxley"}}))
# Load with a bad nested field -> error nested under 'author'
try:
schema.load({"title": "X", "year": -5, "author": {"name": "A"}})
except ValidationError as err:
print("errors:", err.messages)
# ✅ Expected output:
# dump: {'id': 1, 'title': '1984', 'year': 1949, 'author': {'id': 5, 'name': 'Orwell'}}
# load: {'title': 'Brave New World', 'year': 1932, 'author': {'name': 'Huxley'}}
# errors: {'year': ['Must be greater than or equal to 0.'], 'author': {'name': ['Shorter than minimum length 2.']}}Output: the dumped book with its nested author, the cleanly loaded second book, then an errors dict where the year error and the nested author.name error sit side by side.
In a real project you often pair Marshmallow with SQLAlchemy via flask-marshmallow and SQLAlchemyAutoSchema, which generates fields from your model:
🎯 Your Turn: Fill in the Blanks
Complete the schema. Replace each ___ so price is required and non-negative, and serialize/validate correctly.
# 🎯 YOUR TURN: replace each ___ to finish the schema
from marshmallow import Schema, fields, validate, ValidationError
class ProductSchema(Schema):
id = fields.Int(___=True) # output only
name = fields.Str(required=True)
price = fields.Float(required=True, validate=validate.Range(min=___)) # no negative prices
schema = ProductSchema()
# serialize an object
print("dump:", schema.___({"id": 1, "name": "Mug", "price": 9.5})) # which method outputs a dict?
# validate good and bad input
print("load:", schema.load({"name": "Pen", "price": 2.0}))
try:
schema.load({"name": "Bad", "price": -3})
except ValidationError as err:
print("errors:", err.___) # which attribute holds messages?Common Errors & Quick Reference
❌ Forgetting to catch ValidationError
load() raises on bad input. Wrap it in try/except ValidationError and return err.messages with a 400, or your view crashes with a 500.
❌ Accepting a dump_only field from the client
Mark server-controlled fields like id as dump_only=True. Otherwise a client could try to set them on load().
❌ Serializing a list without many=True
To dump or load a collection, pass many=True (on the instance or per call). Without it, Marshmallow expects a single object.
| Task | Code |
|---|---|
| Serialize | schema.dump(obj) |
| Validate input | schema.load(data) |
| Output-only field | fields.Int(dump_only=True) |
| Embed related | fields.Nested(AuthorSchema) |
🏆 Mini Challenge: A Validated Order Schema
Build a schema for an order containing a nested list of line items.
- A LineItemSchema with a required sku and a positive qty
- An OrderSchema with a nested list of line items
- Load a valid order, then a bad one and print the errors
# 🏆 Mini Challenge: nested list validation
from marshmallow import Schema, fields, validate, ValidationError
class LineItemSchema(Schema):
sku = fields.Str(required=True)
qty = fields.Int(required=True, validate=validate.Range(min=1))
class OrderSchema(Schema):
id = fields.Int(dump_only=True)
customer = fields.Str(required=True)
items = fields.List(fields.Nested(LineItemSchema), required=True)
schema = OrderSchema()
good = schema.load({
"customer": "Ada",
"items": [{"sku": "A1", "qty": 2}, {"sku": "B2", "qty": 1}],
})
print("valid order:", good)
try:
schema.load({"customer": "Ada", "items": [{"sku": "A1", "qty": 0}]})
except ValidationError as err:
print("errors:", err.messages)❓ Frequently Asked Questions
Lesson complete — your API speaks clean, validated JSON!
You can declare a Schema, serialize with dump, validate input with load, separate input/output with dump_only/load_only, nest related data, and read structured ValidationError messages.
Practice quiz
What is a Marshmallow Schema?
- A database table
- A Flask route
- A class whose attributes are typed fields
- A Jinja template
Answer: A class whose attributes are typed fields. A Schema is a class with typed fields like fields.Str and fields.Int.
Which method serializes an object to a plain dict?
- load()
- validate()
- parse()
- dump()
Answer: dump(). schema.dump(obj) reads the fields and returns a dictionary.
Which method validates and deserializes incoming data?
- load()
- dump()
- serialize()
- render()
Answer: load(). schema.load(data) checks types and validators and returns clean values.
What does a field marked dump_only=True mean?
- It is required on input
- It appears only in output, never accepted from input
- It is always None
- It is encrypted
Answer: It appears only in output, never accepted from input. dump_only fields appear in output but are ignored on load(), ideal for a server-set id.
What does load() raise when input is invalid?
- KeyError
- TypeError
- ValidationError
- ValueError
Answer: ValidationError. Invalid input raises a marshmallow ValidationError.
Where are per-field error messages found on a ValidationError?
- err.args
- err.fields
- err.detail
- err.messages
Answer: err.messages. err.messages is a dict keyed by field name, perfect for a 400 response.
How do you embed related data in a schema?
- fields.Nested(OtherSchema)
- fields.Link(OtherSchema)
- fields.Child(OtherSchema)
- fields.Embed(OtherSchema)
Answer: fields.Nested(OtherSchema). fields.Nested(OtherSchema) embeds another schema for related objects.
How do you serialize a list of objects?
- Pass list=True to dump
- Use Schema(many=True)
- Call dump_all()
- Loop and call dump on each manually only
Answer: Use Schema(many=True). Schema(many=True).dump(items) serializes a collection.
Which field validator restricts a number to a range?
- validate.MinMax()
- validate.Bounds()
- validate.Range(min=..., max=...)
- validate.Limit()
Answer: validate.Range(min=..., max=...). validate.Range(min=0, max=130) enforces numeric bounds.
In a Flask view, how do you handle a ValidationError from load()?
- Catch it and return jsonify(e.messages), 400
- Let it crash with a 500
- Ignore it and continue
- Convert it to a redirect
Answer: Catch it and return jsonify(e.messages), 400. Wrap load() in try/except and return e.messages with a 400 status.
Continue this course
- Previous: API Authentication with JWT
- Next: Checkpoint: Build a REST API — Combine models, queries, schemas and pagination into a JSON API — then a quiz
- Quick reference: Flask cheat sheet
Frequently asked questions
What problem does Marshmallow solve?
Marshmallow handles the two directions of data at an API boundary: serialization (turning Python objects or model instances into JSON-friendly dictionaries) and deserialization (turning incoming JSON into validated Python data). It centralizes field definitions, type conversion, and validation so your views stay thin.
What is the difference between dump and load?
dump() serializes — it takes a Python object and produces an output dictionary you can return as JSON. load() deserializes — it takes incoming data, validates it against the schema, and returns clean Python values, raising a ValidationError if anything is wrong.
What does dump_only and load_only mean?
dump_only=True marks a field that only appears in output, like an auto-generated id you never accept from the client. load_only=True marks an input-only field, like a password you accept on load but never echo back in a dump. They keep input and output shapes separate and safe.
How do nested schemas work?
fields.Nested(OtherSchema) embeds one schema inside another, so a Book schema can include its Author as a nested object. On dump it serializes the related object; on load it validates the nested data too, collecting nested errors under the field's name.