mrkeyoor.com_
Thu 06 Aug 05:57 UTC
PyPIUtilsupdated 06 Aug 2026

marshmallow

marshmallow converts complex Python objects to and from plain data types. You declare a Schema whose class attributes are fields, then call schema.dump(obj) to turn an object into JSON-ready primitives, or schema.load(data) to validate incoming data and turn it back into Python values. Loading is where the work happens: fields coerce types, run validators, rename keys through data_key, reject unknown keys, and collect every failure into one nested dictionary on a ValidationError instead of stopping at the first problem. It is deliberately framework-agnostic, so the same schema works under Flask, Django, a Kafka consumer, or a plain script, and a large ecosystem (webargs, apispec, marshmallow-sqlalchemy, environs) is built on top of it.

Verdict

Still the best pick when serialization has to stay decoupled from your models and when you want fine-grained control over the wire format, especially inside the webargs and apispec toolchain. For new services where the validated object is the model, pydantic gives you typing and speed that marshmallow cannot match.

API stability3/5marshmallow 4 (April 2025) removed Schema.context, implicit field creation, the default and missing arguments, and made decorator arguments keyword-only, so 3.x and 4.x code look different enough that tutorials mislead; inside 4.x releases have been additive with one CVE fix in 4.1.2.
Docs4/5readthedocs covers a quickstart, per-field API reference, and explicit upgrading guides for 3.24, 3.26 and 4.0; the weaker areas are custom field authoring and recipes that still assume 3.x idioms.
Maintenance4/5Pushed 3 August 2026 with 120 open issues (142 counting PRs) and a steady 2026 release cadence under Steven Loria, funded through Open Collective, Tidelift and a sponsor, though day-to-day work rests on very few people.
Ecosystem5/5Around 29.3 million weekly downloads and a maintained wiki of dependent libraries: webargs for request parsing, apispec for OpenAPI, marshmallow-sqlalchemy for ORM-derived schemas, environs for settings.

Use it if

  • You need serialization and validation to be separate from your model classes, for example one SQLAlchemy model exposed through three different API shapes with different visible fields
  • You want every validation error at once as a nested dict keyed by field name, which maps directly onto a 422 response body instead of a single exception message
  • Your input and output shapes differ from your internal names: data_key renames keys on the wire, load_only and dump_only split write-side and read-side fields, and @pre_load lets you clean payloads before validation
  • You are already in the marshmallow ecosystem, using webargs for request parsing, apispec for OpenAPI generation, or marshmallow-sqlalchemy to derive schemas from ORM models
Skip it if

Setup reality

pip install marshmallow is about as easy as it gets: pure Python, requires Python 3.10 or newer, and no third-party dependencies at all on 3.11+ (older Pythons pull in typing-extensions and a fromisoformat backport). The cost is in the shape of the code, not the install. Every model needs a parallel Schema class, so field definitions live in two places and drift. load() returns a dict, so you write a @post_load hook on every schema that should produce an object. Unknown keys raise by default, which breaks callers that send extra fields until you set unknown = EXCLUDE in class Meta. Errors arrive as a nested dict of lists on ValidationError.messages that you have to translate into whatever your API returns. And because marshmallow 4 dropped Schema.context, code that passed request state into fields has to move to marshmallow.experimental.context.Context or contextvars.

Patterns

Declare a schema and serialize an objectdefine-schema-and-dump

from datetime import date
from marshmallow import Schema, fields

class AlbumSchema(Schema):
    title = fields.Str()
    release_date = fields.Date()
    plays = fields.Int()

album = {"title": "Hunky Dory", "release_date": date(1971, 12, 17), "plays": 12}
AlbumSchema().dump(album)
# {"title": "Hunky Dory", "release_date": "1971-12-17", "plays": 12}

dump() never validates; it only converts. It accepts objects or dicts, reading attributes first and falling back to keys. Use dumps() if you want a JSON string in one step.

Validate input and read the error dictload-and-handle-errors

from marshmallow import Schema, fields, ValidationError

class UserSchema(Schema):
    email = fields.Email(required=True)
    age = fields.Int(required=True)

try:
    UserSchema().load({"email": "nope", "age": "x"})
except ValidationError as err:
    print(err.messages)
    # {"email": ["Not a valid email address."], "age": ["Not a valid integer."]}
    print(err.valid_data)  # the fields that did pass

load() collects every field error before raising, so err.messages is a dict of lists ready to return as a 422 body. err.valid_data holds whatever validated cleanly, which is useful for partial recovery.

Decide what happens to unexpected keysunknown-fields

from marshmallow import Schema, fields, EXCLUDE, INCLUDE, RAISE

class StrictSchema(Schema):
    name = fields.Str()
    class Meta:
        unknown = RAISE  # the default

class TolerantSchema(Schema):
    name = fields.Str()
    class Meta:
        unknown = EXCLUDE

# or per call:
TolerantSchema().load(payload, unknown=INCLUDE)

RAISE is the default and is the single most common surprise when a client adds a field. EXCLUDE silently drops extras, INCLUDE passes them straight through unvalidated.

Rename wire keys and split read from writerename-and-scope-fields

from marshmallow import Schema, fields

class AccountSchema(Schema):
    id = fields.Int(dump_only=True)
    email = fields.Email(required=True)
    password = fields.Str(load_only=True, required=True)
    created_at = fields.DateTime(data_key="createdAt", dump_only=True)

data_key controls the external name in both directions. dump_only fields are ignored on load (so clients cannot set an id), load_only fields never appear in output (so passwords do not leak into responses).

Nest schemas and handle listsnested-and-many

from marshmallow import Schema, fields

class ArtistSchema(Schema):
    name = fields.Str(required=True)

class AlbumSchema(Schema):
    title = fields.Str(required=True)
    artist = fields.Nested(ArtistSchema)
    tags = fields.List(fields.Str())

# a list of albums
AlbumSchema(many=True).load(payload_list)

# only some nested fields
fields.Nested(ArtistSchema, only=("name",))

For self-referencing or circular schemas, pass a callable or the class name as a string ("ArtistSchema"); passing the string "self" was removed. Nested errors nest in messages too, keyed by index for lists.

Turn loaded data into your own classpost-load-object

from dataclasses import dataclass
from marshmallow import Schema, fields, post_load

@dataclass
class User:
    name: str
    email: str

class UserSchema(Schema):
    name = fields.Str(required=True)
    email = fields.Email(required=True)

    @post_load
    def make_user(self, data, **kwargs):
        return User(**data)

UserSchema().load({"name": "Ada", "email": "ada@example.com"})  # User(...)

Without @post_load you get a dict. Always accept **kwargs: marshmallow passes many, partial and unknown to hook methods, and a fixed signature breaks on upgrade.

Validate single fieldsfield-validators

from marshmallow import Schema, fields, validate, validates, ValidationError

class ProductSchema(Schema):
    sku = fields.Str(validate=validate.Regexp(r"^[A-Z]{3}-\d{4}$"))
    price = fields.Decimal(validate=validate.Range(min=0), as_string=True)
    size = fields.Str(validate=validate.OneOf(["s", "m", "l"]))

    @validates("sku")
    def check_sku_not_reserved(self, value, data_key):
        if value.startswith("ZZZ"):
            raise ValidationError("Reserved prefix.")

In marshmallow 4 methods decorated with @validates receive data_key as a keyword argument, and custom validators must raise ValidationError; returning False no longer fails validation. @validates can take several field names at once.

Validate across fieldsschema-level-validation

from marshmallow import Schema, fields, validates_schema, ValidationError

class DateRangeSchema(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("start must be before end", field_name="start")

Schema validators only run when the individual fields passed, so data["start"] is safe to read. Pass field_name to attach the message to a field instead of the _schema key.

Accept PATCH-style partial payloadspartial-updates

# ignore required= for every field
UserSchema().load({"email": "new@example.com"}, partial=True)

# or only for specific fields
UserSchema().load(payload, partial=("name",))

# fields absent from the payload simply do not appear in the result

partial only relaxes required. Missing keys are omitted from the returned dict rather than set to None, so update code can distinguish "not sent" from "explicitly null" (which needs allow_none=True).

Set defaults for load and dump separatelydefaults-and-none

from marshmallow import Schema, fields

class SettingsSchema(Schema):
    theme = fields.Str(load_default="light", dump_default="light")
    nickname = fields.Str(allow_none=True, load_default=None)
    retries = fields.Int(load_default=3)

The old default and missing arguments were removed in 4.0: missing became load_default (used when a key is absent from input) and default became dump_default (used when an attribute is absent on output). allow_none is what permits an explicit null.

Handle enums, datetimes and timedeltasenum-and-datetime

import enum
from marshmallow import Schema, fields

class Status(enum.Enum):
    ACTIVE = "active"
    BANNED = "banned"

class AccountSchema(Schema):
    status = fields.Enum(Status, by_value=True)
    created_at = fields.DateTime(format="iso")
    ttl = fields.TimeDelta(precision="seconds")

by_value=True serializes "active" instead of the member name "ACTIVE". marshmallow 4 parses ISO input with the standard library fromisoformat, so time offsets and bare YYYY-MM-DD datetimes now load where 3.x rejected them, and TimeDelta keeps microseconds instead of truncating.

Give fields access to request contextpass-context

import typing
from marshmallow import Schema, fields
from marshmallow.experimental.context import Context

class Ctx(typing.TypedDict):
    current_user_id: int

class PostSchema(Schema):
    is_mine = fields.Function(
        lambda post: post["author_id"] == Context[Ctx].get()["current_user_id"]
    )

with Context[Ctx]({"current_user_id": 42}):
    PostSchema().dump({"author_id": 42})  # {"is_mine": True}

Schema.context was removed in 4.0. The replacement is a contextvar-backed manager, still marked experimental, so isolate it behind a helper if you would rather not depend on the exact import path.

Alternatives

PackageRegistryPick it when
pydanticPyPIYou want typed model instances, editor autocompletion, and much faster validation, and you accept coupling your data shape to the validation library.
msgspecPyPIYou are decoding large volumes of JSON or MessagePack and want the fastest option, with far fewer validation features.
cattrsPyPIYour classes are already attrs or dataclasses and you only need structuring and unstructuring, not a separate schema layer.