marshmallow review
marshmallow 4.3.1 turns declared Schema fields into two directional operations: `load()` validates and deserializes mappings, while `dump()` turns Python objects into JSON-ready values. Schemas stay separate from ORM, dataclass, and domain classes, so create input and public output can use different fields. Version 4.3.1 changes two narrow cases: `Enum(by_value=...)` permits `None` by default when the enum contains a `None` value, and URL validation accepts a fragment after an empty path. Our Python 3.12 sandbox imported this pure Python, typed package in 0.24 seconds.
marshmallow 4.3.1 installed in 0.2 seconds and imported in 0.24 seconds in our sandbox, leaving one 1 MB package with 0 audit findings. It fits services that need wire contracts independent of model classes; choose a typed-model alternative when the return type from validation matters more than schema hooks.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 1 MB |
| Import | ✓ | import marshmallow in 0.24s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does marshmallow install cleanly?
Yes. In a fresh container with an empty cache, pip install marshmallow finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does marshmallow need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import marshmallow succeeded in 0.24s, and the package ships py.typed for type checkers.
marshmallow or pydantic: which should you use?
pydantic: Use it when validation should directly create typed model instances. marshmallow 4.3.1 installed in 0.2 seconds and imported in 0.24 seconds in our sandbox, leaving one 1 MB package with 0 audit findings.
When should you not use marshmallow?
load() must produce a statically typed model without custom code. Marshmallow returns plain data unless a post_load hook constructs the object.
Use it if
- One model needs distinct schemas for creation, patching, administration, and public responses.
- An HTTP API should return a nested map of all field errors instead of stopping after the first failure.
- Wire names, model attributes, input-only secrets, output-only IDs, partial loads, and processing hooks need explicit control.
- The service already uses webargs, apispec, or marshmallow-sqlalchemy and benefits from their shared Schema model.
- `load()` must produce a statically typed model without custom code. Marshmallow returns plain data unless a `post_load` hook constructs the object.
- Decode throughput dominates the request. msgspec and Pydantic's compiled validator are stronger benchmarks than pure Python field processing.
- A required plugin still targets marshmallow 3. Version 4 removes `Schema.context`, implicit Meta fields, old default names, and deprecated helpers.
- Dataclasses or attrs classes already define every field and only need structuring. cattrs avoids maintaining a second Schema declaration.
- Type checking must know the exact keys returned by `load()`. Runtime field declarations do not give a dict result a precise static shape.
Setup reality
We installed marshmallow 4.3.1 in a clean Python 3.12 Bookworm sandbox in 0.2 seconds. It left one package and 1 MB on disk. pip-audit reported 0 known vulnerabilities. The package measurement lists 2 direct dependencies, requires Python 3.10 or newer, and found pure Python code plus py.typed. import marshmallow worked in 0.24 seconds.
There is no compiler, credential, server, or config file to prepare. The real setup decision is where each wire schema lives and which unknown-field policy it uses. The default raises ValidationError. EXCLUDE drops extra keys, while INCLUDE carries them into loaded data without field validation. That last choice can become a mass-assignment bug if loaded mappings feed model constructors or update calls.
load() validates and deserializes, then returns a dict unless a post_load hook builds something else. dump() serializes without first validating the object. A load_only field never appears in output. A dump_only name is unknown during loading, and with unknown=INCLUDE a client can send that name through unvalidated. Use partial=True for PATCH semantics; a missing field and an explicit None remain different inputs.
A 3.x upgrade needs a deliberate pass. Schema.context moved to Context or Python context variables. missing and default became load_default and dump_default; hook options are keyword-only; pass_many became pass_collection; validators reject by raising ValidationError, not by returning false. Version 4.3.0 added field-level load processors. In 4.3.1, an Enum with a None member can change whether missing null allowance is inferred.
Patterns
Dump a Python object to primitives define-and-dump-schema
from datetime import date
from marshmallow import Schema, fields
class AlbumSchema(Schema):
title = fields.Str()
released = fields.Date()
album = {'title': 'Hunky Dory', 'released': date(1971, 12, 17)}
result = AlbumSchema().dump(album)`dump()` serializes values such as `date`; it does not run the input validators attached to `load()`.
Return all input errors together load-and-report-errors
from marshmallow import Schema, ValidationError, fields
class UserSchema(Schema):
email = fields.Email(required=True)
age = fields.Int(required=True)
try:
user = UserSchema().load(payload)
except ValidationError as error:
return {'errors': error.messages}, 422`ValidationError.messages` is keyed by field and can contain nested mappings or per-item lists.
Reject, drop, or retain unknown keys choose-unknown-policy
from marshmallow import EXCLUDE, Schema, fields
class PublicUserSchema(Schema):
name = fields.Str(required=True)
class Meta:
unknown = EXCLUDERAISE is the default. `INCLUDE` keeps undeclared values without validating them, so `EXCLUDE` is safer for many public inputs.
Give a field a wire name rename-wire-keys
class EventSchema(Schema):
created_at = fields.DateTime(
data_key='createdAt',
required=True,
)`data_key` controls the external name in both directions. Use `attribute` separately when the Python object uses another attribute.
Separate submitted and returned fields separate-input-output-fields
class AccountSchema(Schema):
id = fields.Int(dump_only=True)
email = fields.Email(required=True)
password = fields.Str(load_only=True, required=True)A submitted `dump_only` name counts as unknown. With `unknown=INCLUDE`, it can pass through without the field's validation.
Load a list of nested records load-nested-list
class ArtistSchema(Schema):
name = fields.Str(required=True)
class AlbumSchema(Schema):
title = fields.Str(required=True)
artist = fields.Nested(ArtistSchema, required=True)
albums = AlbumSchema(many=True).load(payload)With `many=True`, failures are indexed by list position and then by the nested field path.
Construct a domain object after load construct-domain-object
from dataclasses import dataclass
from marshmallow import post_load
@dataclass
class User:
name: str
class UserSchema(Schema):
name = fields.Str(required=True)
@post_load
def make_user(self, data, **kwargs):
return User(**data)A `post_load` method replaces the normal dict result. Accept extra keyword arguments because marshmallow passes load context to hooks.
Reject a reserved field value validate-one-field
from marshmallow import ValidationError, validates
class ProductSchema(Schema):
sku = fields.Str(required=True)
@validates('sku')
def reject_reserved_sku(self, value, data_key):
if value.startswith('SYS-'):
raise ValidationError('Reserved SKU prefix.')Marshmallow 4 passes `data_key` to this decorated validator. Rejection requires raising `ValidationError`.
Attach a cross-field error validate-across-fields
from marshmallow import ValidationError, validates_schema
class WindowSchema(Schema):
start = fields.Date(required=True)
end = fields.Date(required=True)
@validates_schema
def check_order(self, data, **kwargs):
if data['start'] > data['end']:
raise ValidationError('End precedes start.', field_name='end')Supplying `field_name='end'` places the failure under that field; omit it to report under `_schema`.
Validate a partial update load-partial-update
schema = UserSchema()
changes = schema.load(
{'email': 'new@example.com'},
partial=True,
)`partial=True` relaxes required checks. An absent key stays absent and should not be confused with a submitted null.
Set defaults by direction set-directional-defaults
class SettingsSchema(Schema):
retries = fields.Int(load_default=3)
theme = fields.Str(
load_default='light',
dump_default='light',
)Marshmallow 4 uses `load_default` and `dump_default`; the older `missing` and `default` arguments are gone.
Provide request data through Context pass-request-context
from typing import TypedDict
from marshmallow.experimental.context import Context
class RequestContext(TypedDict):
viewer_id: int
with Context[RequestContext]({'viewer_id': 42}):
output = PostSchema().dump(post)`Schema.context` was removed in 4.0. Its typed replacement currently lives under `marshmallow.experimental.context`.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic | PyPI | Use it when validation should directly create typed model instances. |
| cattrs | PyPI | Use it to structure existing dataclasses or attrs classes without parallel Schema classes. |
| msgspec | PyPI | Use it for typed JSON or MessagePack decoding where speed matters more than marshmallow hooks. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

