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

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.

TaskCode
Serializeschema.dump(obj)
Validate inputschema.load(data)
Output-only fieldfields.Int(dump_only=True)
Embed relatedfields.Nested(AuthorSchema)

🏆 Mini Challenge: A Validated Order Schema

Build a schema for an order containing a nested list of line items.

# 🏆 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

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.

Related lessons