Features Examples Formats How It Works Type Support Customization
Documentation →
v3.22/Python 3.10–3.14

Serialize anything.
Ridiculously fast.

Turn typed Python data into JSON, MessagePack, YAML, or TOML with a single line of code. No schemas, no boilerplate — just your types.

$pip install mashumaro

Everything you need,
nothing you don't

Built for developers who value clean APIs, type safety, and performance. Mashumaro generates optimized code for your exact schema at compile time.

Seriously Fast

Generates specialized encoder and decoder functions for your exact data schema. No runtime introspection, no overhead.

Comprehensive Type Coverage

Supports generics, enums, datetime, paths, UUIDs, IP addresses, TypedDict, NamedTuple, and much more out of the box.

Multi-Format Out of the Box

JSON, YAML, TOML, and MessagePack. Each format has preconfigured codecs and mixin classes ready to use.

Deeply Customizable

Per-field methods, reusable strategies, config inheritance, field aliases, and pluggable dialects.

JSON Schema Generation

Automatically generate JSON Schema from dataclasses for validation, documentation, and interoperability.

Lifecycle Hooks

Tap into the serialization pipeline with pre/post hooks. Transform data without polluting your model logic.

Two lines of code.
That's all it takes.

Inherit a mixin or create a codec. Mashumaro stays out of your way while handling all the complexity.

mixin_example.py
from dataclasses import dataclass
from datetime import datetime
from mashumaro.mixins.json import DataClassJSONMixin

@dataclass
class Event(DataClassJSONMixin):
    name: str
    timestamp: datetime
    attendees: list[str]
    is_virtual: bool = False

# Serialize to JSON
event = Event(
    name="PyCon",
    timestamp=datetime(2026, 5, 13, 9, 0),
    attendees=["Alice", "Bob"],
)
json_str = event.to_json()
# Deserialize back
restored = Event.from_json(json_str)
codec_example.py
from dataclasses import dataclass
from mashumaro.codecs.json import JSONDecoder, JSONEncoder

@dataclass
class Coordinate:
    lat: float
    lon: float
    city: str

# Create typed encoder/decoder once, reuse everywhere
encoder = JSONEncoder(list[Coordinate])
decoder = JSONDecoder(list[Coordinate])

waypoints = [
    Coordinate(52.52, 13.40, "Berlin"),
    Coordinate(48.86, 2.35, "Paris"),
]
json_str = encoder.encode(waypoints)
restored = decoder.decode(json_str)
custom_example.py
from dataclasses import dataclass
from datetime import datetime
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import SerializationStrategy

class Timestamp(SerializationStrategy):
    def serialize(self, value: datetime) -> float:
        return value.timestamp()

    def deserialize(self, value: float) -> datetime:
        return datetime.fromtimestamp(value)

@dataclass
class Event(DataClassDictMixin):
    created_at: datetime

    class Config(BaseConfig):
        serialization_strategy = {datetime: Timestamp()}

One library, every format

Each format ships with a mixin class for dataclasses and a codec pair for arbitrary types.

{ }
JSON
Built-in & orjson
Y
YAML
PyYAML
T
TOML
tomli / tomllib
MP
MessagePack
msgpack
{d}
Basic
Basic Python objects

How Mashumaro works

Instead of reflecting on types at every call, Mashumaro analyzes your schema once and generates optimized Python code.

1

Analyze Schema

Inspects your dataclass fields and type annotations to understand the complete data shape.

2

Generate Code

A specialized encoder and decoder are compiled for your exact schema at import time or on codec creation.

3

Execute Instantly

Every subsequent call runs pre-compiled code. No type checking, no branching — just direct execution.

If Python has it,
Mashumaro handles it

From basic primitives to deeply nested generics — every type is handled automatically with zero configuration.

strintfloatboolbyteslistdicttuplesetfrozensetOptionalUnionLiteralAnnotated TypeVarTypeVarTupleNewTypeFinalSelfTypedDictNamedTupleEnumIntEnumStrEnumFlagdatetimedate timetimedeltatimezoneZoneInfoPathPurePathUUIDDecimalFractionIPv4AddressIPv6AddressIPv4Network re.PatternDequeOrderedDictDefaultDictCounterChainMapSequenceMappingMappingProxyType
View full type reference →

Your data, your rules

Control every aspect of how your data is serialized — from individual fields to entire type hierarchies.

Config Options

Set global behaviors like omit_none, omit_default, field aliases, discriminated unions, and key sorting from a single inner class.

Explore Config →

Field Options

Override serialization per field — custom encoders, aliases, pass-through mode, and alternative datetime parsers.

See Field Options →

Serialization Strategy

Reusable conversion rules for any type. Apply globally via Config or per-field. Perfect for third-party types you can't modify.

Learn Strategies →

Dialects

Context-dependent serialization schemes that can be switched at call time. Same model, different output depending on the consumer.

Discover Dialects →

Lifecycle Hooks

Intercept data before and after serialization/deserialization. Normalize input, redact fields, validate constraints.

View Hooks →

SerializableType

Full control interface for your own custom classes. Define exactly how instances are packed and unpacked.

Read More →

Ready to serialize?

Install Mashumaro and start building. It takes seconds to set up and works with your existing codebase.

$pip install mashumaro

Getting Started

Mashumaro is a fast serialization library built around Python type annotations and dataclasses. It generates specialized packing and unpacking functions for your exact type shape, so the normal hot path does not repeatedly inspect fields or walk annotations.

Use a mixin when the root object is a dataclass and methods such as to_json() are the most convenient API. Use a reusable codec when the root shape is anything else — for example list[User], dict[str, Event | None], a TypedDict, or a scalar type.

Installation and Python compatibility

Install the mashumaro package from PyPI with pip:

bash
pip install mashumaro

Mashumaro 3.22 supports Python 3.10–3.14. For older Python versions, use the last compatible Mashumaro release listed below.

PythonRecommended mashumaro versionStatus
3.10–3.14Current releaseSupported
3.93.20Last compatible release
3.83.14Last compatible release
3.73.9.1Last compatible release
3.63.1.1Last compatible release

A Python version that has reached end of life no longer receives fixes from CPython. Pinning an old mashumaro release preserves compatibility, but upgrading Python is the safer choice.

The dictionary and standard-library JSON APIs need no extra dependencies. Install only the package extras required by your wire formats:

bash
pip install "mashumaro[orjson]"
pip install "mashumaro[yaml]"
pip install "mashumaro[toml]"
pip install "mashumaro[msgpack]"

# Multiple extras may be installed together
pip install "mashumaro[orjson,yaml,toml,msgpack]"
ExtraDependencyWhat it enables
orjsonorjsonFast JSON bytes and the orjson mixin/codec
yamlPyYAMLYAML mixin and codec
tomltomli-w, plus tomli on Python 3.10TOML mixin and codec
msgpackmsgpackMessagePack mixin and codec

Your first model

Declare ordinary dataclasses and add a mixin only to the root model that needs serialization methods. Nested dataclasses do not need to inherit a mixin.

python
from dataclasses import dataclass
from datetime import datetime, timezone
from uuid import UUID

from mashumaro.mixins.json import DataClassJSONMixin


@dataclass
class Address:
    city: str
    postal_code: str


@dataclass
class User(DataClassJSONMixin):
    id: UUID
    name: str
    address: Address
    created_at: datetime
    tags: list[str]


user = User(
    id=UUID("20f16666-90f3-4d73-a034-4e73a57e8f30"),
    name="Alice",
    address=Address(city="Belgrade", postal_code="11000"),
    created_at=datetime(2026, 8, 16, 12, 30, tzinfo=timezone.utc),
    tags=["admin", "beta"],
)

payload = user.to_json()
restored = User.from_json(payload)

assert restored == user

The same model also has to_dict() and from_dict() because every format-specific mixin derives from DataClassDictMixin:

python
basic = user.to_dict()

assert basic["id"] == "20f16666-90f3-4d73-a034-4e73a57e8f30"
assert basic["address"] == {"city": "Belgrade", "postal_code": "11000"}
assert User.from_dict(basic) == user

to_dict() does not mean dataclasses.asdict(). Mashumaro converts values according to their annotations: UUIDs and dates become strings, sets become lists, nested dataclasses become dictionaries, and custom strategies are applied.

Mixins or codecs?

Both APIs use the same generated conversion engine. Choose the entry point that expresses the root shape most naturally.

QuestionMixinCodec
Root valueDataclass instanceAny supported type shape
APIMethods on the modelReusable encoder/decoder object
Compile timeClass creation, or first use with lazy compilationCodec construction
Best fitApplication/domain modelsCollections, scalars, adapters, framework boundaries

Mixin example

python
from dataclasses import dataclass

from mashumaro.mixins.json import DataClassJSONMixin


@dataclass
class ServiceConfig(DataClassJSONMixin):
    host: str
    port: int
    debug: bool = False


config = ServiceConfig(host="localhost", port=8080)
text = config.to_json()
assert ServiceConfig.from_json(text) == config

Codec example

A codec takes the entire shape type at construction time. Build it once and reuse it when performance matters.

python
from mashumaro.codecs.json import JSONDecoder, JSONEncoder

encoder = JSONEncoder(list[ServiceConfig])
decoder = JSONDecoder(list[ServiceConfig])

configs = [
    ServiceConfig("api.internal", 443),
    ServiceConfig("worker.internal", 8080, True),
]

payload = encoder.encode(configs)
assert decoder.decode(payload) == configs

For a one-off conversion, each codec module also exposes functions:

python
from mashumaro.codecs.json import decode, encode

payload = encode(configs, list[ServiceConfig])
restored = decode(payload, list[ServiceConfig])

These functions create a disposable codec internally. Reuse an encoder or decoder for repeated work so code generation happens once.

The serialization pipeline

It helps to distinguish three layers:

  • Typed object — the value your application uses, such as User or list[User].
  • Basic form — dictionaries, lists, and scalar values produced by Mashumaro's generated packer.
  • Encoded payload — JSON text, YAML text, TOML text, or MessagePack bytes produced by the format library.

For JSON encoding, the flow is:

signature
User -> generated packer -> dict/list/scalars -> json.dumps -> str
str -> json.loads -> dict/list/scalars -> generated unpacker -> User

Format dialects can deliberately keep native values in the basic form. For example, MessagePack keeps bytes, TOML keeps native date/time/datetime, and orjson handles several native scalar types itself. The Supported Types chapter documents these differences.

Conversion and validation

Deserialization is annotation-directed conversion, not merely assignment:

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin


@dataclass
class Invoice(DataClassDictMixin):
    number: int
    issued_on: date
    paid: bool


invoice = Invoice.from_dict(
    {"number": "42", "issued_on": "2026-08-16", "paid": 1}
)

assert invoice == Invoice(42, date(2026, 8, 16), True)

When conversion fails, Mashumaro raises a typed exception such as InvalidFieldValue; missing and unexpected keys have separate exceptions. See Errors and Troubleshooting.

Mashumaro is not a business-rule validator. Constraints such as “price must be positive” belong in your model, a validation layer, or generated JSON Schema. Serialization hooks can normalize data but should not hide invalid domain states.

For a medium or large codebase:

  • Put shared defaults in a project base mixin with an inner Config.
  • Keep wire-format choices at system boundaries.
  • Use field options for genuinely local exceptions.
  • Use reusable SerializationStrategy objects for third-party types.
  • Use dialects when the same model must speak more than one external representation.
  • Reuse codecs for repeated serialization of the same root shape.
  • Enable forbid_extra_keys at strict API boundaries.
python
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


class APIModel(DataClassDictMixin):
    class Config(BaseConfig):
        forbid_extra_keys = True
        allow_deserialization_not_by_alias = True

Configuration is inherited, and a dataclass can override only the options it needs.

Where to go next

Supported Formats

Mashumaro provides a dictionary “basic form” plus JSON, orjson, YAML, TOML, and MessagePack integrations. Every integration has a reusable codec API; dataclass roots also have mixins with format-specific methods.

API overview

FormatMixin methodsReusable codecOne-off functionsEncoded result
Basic formto_dict, from_dictBasicEncoder, BasicDecoderencode, decodePython object
JSONto_json, from_jsonJSONEncoder, JSONDecoderjson_encode, json_decodestr by default
orjsonto_jsonb, to_json, from_jsonORJSONEncoder, ORJSONDecoderjson_encode, json_decodebytes from encoder/to_jsonb
YAMLto_yaml, from_yamlYAMLEncoder, YAMLDecoderyaml_encode, yaml_decodestr or bytes
TOMLto_toml, from_tomlTOMLEncoder, TOMLDecodertoml_encode, toml_decodestr
MessagePackto_msgpack, from_msgpackMessagePackEncoder, MessagePackDecodermsgpack_encode, msgpack_decodebytes

Each module also exports its one-off functions as encode and decode, so an application can use a uniform import style.

Basic form

The basic form is the typed conversion layer used underneath most other formats. It is also useful directly when passing values to a database driver, web framework, template renderer, or another serializer.

Dataclass mixin

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin


@dataclass
class Release(DataClassDictMixin):
    version: str
    published: date


release = Release("1.0", date(2024, 1, 1))
assert release.to_dict() == {
    "version": "1.0",
    "published": "2024-01-01",
}
assert Release.from_dict(release.to_dict()) == release

Every format mixin derives from DataClassDictMixin, so do not inherit it separately when you already use DataClassJSONMixin, DataClassYAMLMixin, or another format mixin.

Reusable and one-off codecs

python
from mashumaro.codecs import BasicDecoder, BasicEncoder
from mashumaro.codecs.basic import decode, encode

shape = dict[str, list[Release]]
value = {"stable": [release]}

encoder = BasicEncoder(shape)
decoder = BasicDecoder(shape)

basic = encoder.encode(value)
assert decoder.decode(basic) == value
assert decode(encode(value, shape), shape) == value

BasicDecoder accepts an optional pre_decoder_func; BasicEncoder accepts post_encoder_func. These functions wrap the generated typed conversion and are useful for integrating a storage adapter without defining a new codec.

Standard-library JSON

The standard JSON integration uses json.loads and json.dumps by default and requires no extra package.

Dataclass mixin

python
from dataclasses import dataclass

from mashumaro.mixins.json import DataClassJSONMixin


@dataclass
class Point(DataClassJSONMixin):
    x: int
    y: int


text = Point(10, 20).to_json()
assert Point.from_json(text) == Point(10, 20)

to_json() accepts a replacement encoder as its first argument and forwards other keyword arguments to to_dict(). from_json() similarly accepts a decoder and forwards the rest to from_dict().

python
import json

pretty = Point(10, 20).to_json(
    encoder=lambda value: json.dumps(value, indent=2, sort_keys=True)
)

Codec

python
import json

from mashumaro.codecs.json import JSONDecoder, JSONEncoder

encoder = JSONEncoder(
    list[Point],
    post_encoder_func=lambda value: json.dumps(value, separators=(",", ":")),
)
decoder = JSONDecoder(list[Point])

payload = encoder.encode([Point(1, 2), Point(3, 4)])
assert decoder.decode(payload) == [Point(1, 2), Point(3, 4)]

The decoder accepts str, bytes, or bytearray. The default encoder returns str, but a custom post_encoder_func may define a different boundary contract.

One-off functions

python
from mashumaro.codecs.json import json_decode, json_encode

payload = json_encode(Point(1, 2), Point)
assert json_decode(payload, Point) == Point(1, 2)

# Equivalent short aliases
from mashumaro.codecs import json as json_codec

payload = json_codec.encode(Point(3, 4), Point)
assert json_codec.decode(payload, Point) == Point(3, 4)

orjson

Install the optional dependency first:

bash
pip install "mashumaro[orjson]"

The orjson documentation describes its bytes-oriented API and native types. orjson produces UTF-8 JSON bytes and natively handles datetime, date, time, and UUID on the serialization side. Mashumaro's OrjsonDialect passes those values through so orjson can encode them.

python
from dataclasses import dataclass
from datetime import datetime, timezone

from mashumaro.mixins.orjson import DataClassORJSONMixin


@dataclass
class AuditRecord(DataClassORJSONMixin):
    action: str
    at: datetime


record = AuditRecord("login", datetime(2026, 8, 16, tzinfo=timezone.utc))

binary_payload = record.to_jsonb()
text_payload = record.to_json()

assert isinstance(binary_payload, bytes)
assert isinstance(text_payload, str)
assert AuditRecord.from_json(binary_payload) == record

Use Config.orjson_options for a model-wide bitmask or pass orjson_options= to to_jsonb() for one call:

python
import orjson

binary_payload = record.to_jsonb(
    orjson_options=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
)

The reusable API is ORJSONEncoder(shape) and ORJSONDecoder(shape) from mashumaro.codecs.orjson. The encoder returns bytes; the one-off json_encode and encode functions do the same.

orjson is not simply a drop-in speed flag for standard JSON. Its option set, return type, native-type handling, and error behavior come from orjson. Choose the integration as part of your external API contract.

YAML

Install PyYAML:

bash
pip install "mashumaro[yaml]"
python
from dataclasses import dataclass

from mashumaro.mixins.yaml import DataClassYAMLMixin


@dataclass
class Pipeline(DataClassYAMLMixin):
    name: str
    steps: list[str]


pipeline = Pipeline("checks", ["lint", "test"])
yaml_text = pipeline.to_yaml()
assert Pipeline.from_yaml(yaml_text) == pipeline

Mashumaro selects CSafeLoader when available and falls back to SafeLoader; for dumping it selects the C dumper when available. You can supply another encoder/decoder to the mixin or a post_encoder_func/pre_decoder_func to the codec:

python
import yaml

from mashumaro.codecs.yaml import YAMLDecoder, YAMLEncoder

encoder = YAMLEncoder(
    Pipeline,
    post_encoder_func=lambda value: yaml.safe_dump(value, sort_keys=False),
)
decoder = YAMLDecoder(Pipeline, pre_decoder_func=yaml.safe_load)

Only load YAML from trusted or appropriately restricted sources. Mashumaro controls typed conversion after parsing; the safety characteristics of parsing are defined by the supplied PyYAML loader.

TOML

Install the TOML extra:

bash
pip install "mashumaro[toml]"

Python 3.11+ uses the standard-library tomllib for reading. Python 3.10 uses tomli. Writing uses tomli-w on every supported Python version.

python
from dataclasses import dataclass
from datetime import date

from mashumaro.mixins.toml import DataClassTOMLMixin


@dataclass
class AppConfig(DataClassTOMLMixin):
    title: str
    released: date
    description: str | None = None


config = AppConfig("Mashumaro", date(2026, 5, 26))
toml_text = config.to_toml()

assert "released = 2026-05-26" in toml_text
assert "description" not in toml_text
assert AppConfig.from_toml(toml_text) == config

TOML 1.0 has native date/time values, so TOMLDialect passes datetime, date, and time through to tomli-w. TOML has no null value, so fields containing None are omitted by default. TOML documents are tables at the top level; use a dataclass or mapping-compatible shape rather than a scalar root.

Reusable codecs are TOMLEncoder and TOMLDecoder; one-off functions are toml_encode/toml_decode and their encode/decode aliases.

MessagePack

Install msgpack:

bash
pip install "mashumaro[msgpack]"
python
from dataclasses import dataclass

from mashumaro.mixins.msgpack import DataClassMessagePackMixin


@dataclass
class Blob(DataClassMessagePackMixin):
    media_type: str
    body: bytes


blob = Blob("application/octet-stream", b"\x00\x01\x02")
payload = blob.to_msgpack()

assert isinstance(payload, bytes)
assert Blob.from_msgpack(payload) == blob

MessagePackDialect passes bytes through and reconstructs bytearray explicitly. The default encoder calls msgpack.packb(..., use_bin_type=True) and the default decoder calls msgpack.unpackb(..., raw=False).

Use codec transforms to customize msgpack options without changing the typed layer:

python
import msgpack

from mashumaro.codecs.msgpack import MessagePackDecoder, MessagePackEncoder

encoder = MessagePackEncoder(
    Blob,
    post_encoder_func=lambda value: msgpack.packb(value, use_bin_type=True),
)
decoder = MessagePackDecoder(
    Blob,
    pre_decoder_func=lambda value: msgpack.unpackb(value, raw=False),
)

Custom format boundaries with codec transforms

All reusable codecs follow the same two-stage idea:

  • A decoder's pre_decoder_func turns an encoded payload into the basic form before typed deserialization.
  • An encoder's post_encoder_func turns the generated basic form into the final payload.

The basic codec permits None for either function; YAML and MessagePack do as well. This makes codecs useful even when another system already parses or renders the transport format.

python
from urllib.parse import parse_qs, urlencode

from mashumaro.codecs.basic import BasicDecoder, BasicEncoder

query_encoder = BasicEncoder(
    dict[str, str], post_encoder_func=urlencode
)
query_decoder = BasicDecoder(
    dict[str, list[str]], pre_decoder_func=parse_qs
)

assert query_encoder.encode({"page": "2", "sort": "name"}) == (
    "page=2&sort=name"
)
assert query_decoder.decode("tag=python&tag=typing") == {
    "tag": ["python", "typing"]
}

The standard-library urllib.parse documentation defines the query-string behavior of urlencode and parse_qs used in this adapter.

Choosing a format

NeedGood default
Public interoperable APIStandard JSON
Maximum JSON throughput and bytes outputorjson
Human-edited nested configurationYAML, with deliberate safe-loader policy
Human-edited application configuration with strict semanticsTOML
Compact internal binary messagesMessagePack
Already have a transport/storage adapterBasic codec with transforms

Serialization format does not replace a compatibility strategy. For durable data, define aliases, defaults, discriminators, and migration policy explicitly; test old payloads against new models.

Supported Types

Mashumaro supports dataclasses, standard collections, modern typing constructs, enums, date/time objects, paths, network addresses, decimals, UUIDs, patterns, and user-defined extensions. Support applies recursively: a type can appear at the root of a codec, as a dataclass field, inside a collection, or as a type argument of another supported generic.

This chapter describes the default basic representation. A format-specific dialect may keep selected native values — notably bytes in MessagePack, date/time values in TOML, and several scalars in orjson.

Representation reference

Scalars and special values

Python typeBasic serialized formDeserialization behavior
None / NoneTypeNoneProduces None
strstrCalls str conversion where needed
intintCalls int conversion and wraps failure
floatfloatCalls float conversion and wraps failure
boolboolCalls bool conversion
bytesBase64 ASCII strBase64-decodes to bytes
bytearrayBase64 ASCII strBase64-decodes to bytearray
AnyPassed throughPassed through without typed conversion

The default bytes encoder uses base64.encodebytes, whose output includes a trailing newline:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class Payload(DataClassDictMixin):
    body: bytes
    mutable_body: bytearray


payload = Payload(b"123", bytearray(b"123"))
assert payload.to_dict() == {
    "body": "MTIz\n",
    "mutable_body": "MTIz\n",
}
assert Payload.from_dict(payload.to_dict()) == payload

MessagePack overrides this basic behavior and stores binary values natively. For URL-safe or newline-free Base64 JSON, define a SerializationStrategy.

Date and time

Python typeBasic serialized formDefault constructor/parsing rule
datetime.datetimeISO 8601 strdatetime.fromisoformat
datetime.dateISO 8601 strdate.fromisoformat
datetime.timeISO 8601 strtime.fromisoformat
datetime.timedeltaTotal seconds as floattimedelta(seconds=value)
datetime.timezonetzname(None) stringParses UTC or a UTC±HH:MM offset
zoneinfo.ZoneInfoIANA zone key as strZoneInfo(value)
python
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo

from mashumaro import DataClassDictMixin


@dataclass
class Schedule(DataClassDictMixin):
    starts_at: datetime
    day: date
    local_time: time
    timeout: timedelta
    fixed_zone: timezone
    named_zone: ZoneInfo


schedule = Schedule(
    starts_at=datetime(2026, 8, 16, 9, 30, 15, 123456),
    day=date(2026, 8, 16),
    local_time=time(9, 30),
    timeout=timedelta(seconds=2.5),
    fixed_zone=timezone.utc,
    named_zone=ZoneInfo("Europe/Belgrade"),
)

assert schedule.to_dict() == {
    "starts_at": "2026-08-16T09:30:15.123456",
    "day": "2026-08-16",
    "local_time": "09:30:00",
    "timeout": 2.5,
    "fixed_zone": "UTC",
    "named_zone": "Europe/Belgrade",
}

Field deserialization can also use the optional ciso8601 or pendulum engine. See Field Options.

Numeric and identifier types

Python typeBasic serialized formExample
decimal.DecimalstrDecimal("1.330")"1.330"
fractions.FractionstrFraction(1, 3)"1/3"
uuid.UUIDCanonical str"3c25dd74-f208-46a2-9606-dd3919e975b7"

String representations preserve decimal precision and the exact fraction value across JSON-compatible formats. RoundedDecimal is a built-in strategy for applying decimal quantization at serialization time.

python
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP

from mashumaro import DataClassDictMixin, field_options
from mashumaro.types import RoundedDecimal


@dataclass
class Price(DataClassDictMixin):
    amount: Decimal = field(
        metadata=field_options(
            serialization_strategy=RoundedDecimal(
                places=2, rounding=ROUND_HALF_UP
            )
        )
    )


assert Price(Decimal("10.235")).to_dict() == {"amount": "10.24"}

IP addresses and networks

The complete ipaddress family is represented as strings:

  • IPv4Address and IPv6Address
  • IPv4Network and IPv6Network
  • IPv4Interface and IPv6Interface
python
from dataclasses import dataclass
from ipaddress import IPv4Address, IPv6Network

from mashumaro import DataClassDictMixin


@dataclass
class NetworkRule(DataClassDictMixin):
    gateway: IPv4Address
    destination: IPv6Network


rule = NetworkRule(
    gateway=IPv4Address("192.168.1.1"),
    destination=IPv6Network("2001:db8::/32"),
)
assert NetworkRule.from_dict(rule.to_dict()) == rule

Paths and patterns

Mashumaro supports pathlib.Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath, custom subclasses, and os.PathLike. Values are serialized with os.fspath() and reconstructed according to the annotation and operating system.

re.Pattern, re.Pattern[str], re.Pattern[bytes], and typing.Pattern serialize to their .pattern value and deserialize with re.compile. String patterns produce strings; bytes patterns produce bytes.

Collections

Collection contents are converted recursively. Abstract collection annotations deserialize to a useful concrete implementation.

Annotation familyBasic serialized formDeserialized concrete form
list[T], typing.List[T]listlist
tuple[...], typing.Tuple[...]listtuple
set[T], collections.abc.Set[T]listset
frozenset[T]listfrozenset
collections.deque[T]listdeque
Sequence[T], MutableSequence[T]listlist
dict[K, V], Mapping[K, V], MutableMapping[K, V]dictdict
OrderedDict[K, V]dictOrderedDict
defaultdict[K, V]dictdefaultdict
Counter[K]dictCounter
ChainMap[K, V]List of mapsChainMap
types.MappingProxyType[K, V]dictRead-only mapping proxy

Both legacy names from typing and PEP 585 built-in generic syntax are supported. On supported Python versions, prefer list[int] and dict[str, User] unless your project needs a compatibility style.

Tuples

Fixed, variable, and empty tuple shapes retain their typed meaning even though their basic representation is a list:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class TupleShapes(DataClassDictMixin):
    point: tuple[int, int]
    labels: tuple[str, ...]
    empty: tuple[()]


value = TupleShapes((10, 20), ("a", "b"), ())
assert value.to_dict() == {
    "point": [10, 20],
    "labels": ["a", "b"],
    "empty": [],
}
assert TupleShapes.from_dict(value.to_dict()) == value

Variadic tuple shapes based on TypeVarTuple and Unpack are supported; see Generics and Modern Typing.

Mapping keys

The basic codec can convert typed mapping keys recursively, but the final format still sets the wire constraint:

  • JSON object keys are strings.
  • TOML keys are strings.
  • YAML and MessagePack can represent more key types, but downstream consumers may not.

For an interoperable JSON/TOML contract, prefer dict[str, V] or define a strategy that turns keys into an unambiguous string.

Copy behavior

Collections are normally copied while their elements are converted. A dialect's no_copy_collections option can pass selected collection types through when it is safe. Built-in format dialects use this for some native containers; custom use is an optimization that can expose mutable input objects to the downstream encoder.

Dataclasses

Nested dataclasses are supported even if only the root class inherits a mixin:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class CPU:
    cores: int


@dataclass
class Machine(DataClassDictMixin):
    name: str
    cpu: CPU
    replicas: list[CPU]


machine = Machine("builder", CPU(12), [CPU(4), CPU(8)])
assert Machine.from_dict(machine.to_dict()) == machine

Supported dataclass features include inheritance, slots=True and kw_only=True, defaults, default factories, forward references, recursive models, generic dataclasses, ClassVar, InitVar, and typing.Self.

ClassVar is not an instance field and is ignored. InitVar participates in construction rather than stored output, following dataclass semantics.

Named tuples

Typed and untyped named tuples are supported, including defaults and generic named tuples. The default representation is a list. Set namedtuple_as_dict = True globally, use a dialect, or select as_dict for one field.

python
from dataclasses import dataclass, field
from typing import NamedTuple

from mashumaro import DataClassDictMixin


class Point(NamedTuple):
    x: int
    y: int = 0


@dataclass
class Shapes(DataClassDictMixin):
    compact: Point
    readable: Point = field(
        metadata={"serialize": "as_dict", "deserialize": "as_dict"}
    )


value = Shapes(Point(1, 2), Point(3, 4))
assert value.to_dict() == {
    "compact": [1, 2],
    "readable": {"x": 3, "y": 4},
}

When dictionary representation is enabled, missing named-tuple items with defaults use those defaults during deserialization.

Typed dictionaries

TypedDict can be a field type or codec root. Mashumaro understands total and non-total dictionaries, Required, NotRequired, and ReadOnly markers from both typing and typing_extensions.

python
from typing import NotRequired, TypedDict

from mashumaro.codecs.basic import BasicDecoder, BasicEncoder


class Patch(TypedDict):
    user_id: int
    display_name: NotRequired[str]


decoder = BasicDecoder(Patch)
encoder = BasicEncoder(Patch)

patch = decoder.decode({"user_id": "42"})
assert patch == {"user_id": 42}
assert encoder.encode(patch) == {"user_id": 42}

On Python 3.10, import NotRequired, Required, and ReadOnly from typing_extensions.

Enums and literals

Mashumaro supports Enum, IntEnum, StrEnum, Flag, and IntFlag. The default serialized value is .value, and deserialization calls the enum type with that value.

python
from dataclasses import dataclass
from enum import Enum
from typing import Literal

from mashumaro import DataClassDictMixin


class Status(Enum):
    OPEN = "open"
    CLOSED = "closed"


@dataclass
class Ticket(DataClassDictMixin):
    status: Status
    priority: Literal["low", "high"]


ticket = Ticket.from_dict({"status": "open", "priority": "high"})
assert ticket == Ticket(Status.OPEN, "high")
assert ticket.to_dict() == {"status": "open", "priority": "high"}

Literal supports strings, integers, booleans, None, bytes, and enum members. A value outside the allowed literal set causes InvalidFieldValue.

To encode all enum subclasses by name instead of by value, register a strategy with match_subclasses=True.

Optional and union types

Both typing.Optional[T]/typing.Union[A, B] and PEP 604 T | None/A | B are supported. Recursive unions and unions nested inside collections are supported as well.

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class Success:
    value: int


@dataclass
class Failure:
    error: str


@dataclass
class Response(DataClassDictMixin):
    result: Success | Failure | None

An untagged union can be ambiguous when variants accept the same representation — for example bool | int or multiple dataclasses with overlapping fields. Use Discriminator for a stable polymorphic wire contract.

Other typing constructs

Annotated

The underlying type is serialized normally. Mashumaro-specific metadata such as Alias and Discriminator, plus JSON Schema annotations, can be attached with Annotated without replacing the type:

python
from dataclasses import dataclass
from typing import Annotated

from mashumaro import DataClassDictMixin
from mashumaro.types import Alias


@dataclass
class User(DataClassDictMixin):
    user_id: Annotated[int, Alias("userId")]

NewType

A NewType is serialized using its underlying supertype while keeping the declared shape for type analysis.

python
from typing import NewType

UserId = NewType("UserId", int)

TypeVar and bounds

Type variables are resolved from a concrete generic use. Unbound variables fall back to their bound, constraint, default, or Any as appropriate. TypeVar defaults from PEP 696 are supported through typing_extensions and natively on newer Python versions.

Final, LiteralString, Self, and ReadOnly

These markers are supported from typing where available and from typing_extensions otherwise. They refine static meaning while Mashumaro converts the underlying runtime value.

PEP 695 type aliases

On Python 3.12+, the PEP 695 type statement is supported as a codec shape or field annotation, including parameterized and recursive aliases:

python
type JSONValue = (
    None | bool | int | float | str | list[JSONValue] | dict[str, JSONValue]
)

Recursive aliases are guarded during generated-code and JSON Schema construction.

Python version guide

The package supports Python 3.10–3.14. Many newer typing objects can be used on older supported interpreters through the mandatory typing_extensions dependency, but new grammar cannot be backported.

FeatureNative PythonEarlier supported Python
A | B union syntax, list[int]3.10All supported versions already have it
Required, NotRequired3.11Import from typing_extensions on 3.10
Self3.11Import from typing_extensions
TypeVarTuple, Unpack3.11Import from typing_extensions
PEP 695 class Box[T] and type Alias = ... syntax3.12No syntax backport; use Generic and assignment aliases
TypeVar defaults (PEP 696)3.13Use typing_extensions.TypeVar(default=...)
ReadOnly for TypedDict3.13Import from typing_extensions
Deferred annotation evaluation model3.14Use string references or from __future__ import annotations

Generic syntax by Python version

The following two definitions express the same model.

Python 3.10 and 3.11:

python
from dataclasses import dataclass
from typing import Generic, TypeVar

T = TypeVar("T")


@dataclass
class Box(Generic[T]):
    value: T

Python 3.12 and newer:

python
from dataclasses import dataclass


@dataclass
class Box[T]:
    value: T

Both forms work as Box[date] fields and codec roots. See Generics and Modern Typing for inheritance, variadic generics, forward references, and generic extension types.

Format-specific representation differences

TypeBasic/JSON/YAML defaultorjson defaultTOML defaultMessagePack default
bytesBase64 stringBase64 stringBase64 stringNative binary
bytearrayBase64 stringBase64 stringBase64 stringNative binary, restored as bytearray
datetimeISO stringPassed to orjsonNative TOML datetimeISO string
dateISO stringPassed to orjsonNative TOML dateISO string
timeISO stringPassed to orjsonNative TOML timeISO string
UUIDStringPassed to orjsonStringString
None fieldPresent unless omitted by configPresent unless omittedOmitted by TOML dialectPresent unless omitted

Format behavior is implemented with dialects, so you can merge it with your own representation rules instead of reimplementing the format integration.

Custom and third-party types

An arbitrary class is not serialized by guessing its __dict__. Choose an explicit extension mechanism:

SituationRecommended mechanism
You own the classSerializableType
You own a generic class and want annotated conversionSerializableType with use_annotations=True
You cannot modify the classSerializationStrategy
Only one field differsCallable or strategy in Field Options
The representation changes by API/formatDialect
Raw object must pass unchangedpass_through, only when the final encoder accepts it

Keeping the conversion explicit makes the wire representation reviewable, testable, and compatible with JSON Schema generation.

SerializableType

Use SerializableType when you own a class and want the class itself to define its stable serialized representation. The contract is explicit: _serialize() returns the representation, and _deserialize() rebuilds an instance.

For a third-party class you cannot modify, use SerializationStrategy. For a representation that changes by external API, prefer a Dialect so the domain type stays independent of one wire contract.

Basic contract

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializableType


class Airport(SerializableType):
    def __init__(self, code: str, city: str):
        self.code = code
        self.city = city

    def _serialize(self):
        return [self.code, self.city]

    @classmethod
    def _deserialize(cls, value):
        return cls(*value)

    def __eq__(self, other):
        return isinstance(other, Airport) and (
            self.code, self.city
        ) == (other.code, other.city)


@dataclass
class Flight(DataClassDictMixin):
    origin: Airport
    destination: Airport


data = {
    "origin": ["BEG", "Belgrade"],
    "destination": ["NRT", "Tokyo"],
}
flight = Flight.from_dict(data)

assert flight.origin == Airport("BEG", "Belgrade")
assert flight.to_dict() == data

Without annotation processing, the value passed to _deserialize() is raw input and the value returned from _serialize() is accepted as the final basic representation. This is ideal when your methods perform the entire conversion themselves.

Annotation-aware conversion

Set use_annotations=True when Mashumaro should recursively convert the function annotations on the input of _deserialize() and the return of _serialize().

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializableType


@dataclass
class Stop:
    airport: str
    day: date


class Itinerary(SerializableType, use_annotations=True):
    def __init__(self, stops: list[Stop]):
        self.stops = stops

    def _serialize(self) -> list[Stop]:
        return self.stops

    @classmethod
    def _deserialize(cls, stops: list[Stop]) -> "Itinerary":
        return cls(stops)


@dataclass
class Trip(DataClassDictMixin):
    itinerary: Itinerary


trip = Trip.from_dict(
    {
        "itinerary": [
            {"airport": "BEG", "day": "2026-08-16"},
            {"airport": "NRT", "day": "2026-08-17"},
        ]
    }
)

assert trip.itinerary.stops[0].day == date(2026, 8, 16)
assert trip.to_dict()["itinerary"][1]["day"] == "2026-08-17"

Both annotations matter. The _deserialize() parameter tells Mashumaro how to unpack raw data before your method runs. The _serialize() return type tells it how to pack the value your method returns.

use_annotations is intentionally opt-in for compatibility with older code. Missing annotations while it is enabled make the conversion contract incomplete; annotate both directions.

Generic owned types

Annotation substitution works for classic Generic classes, PEP 695 generics on Python 3.12+, and variadic generics.

python
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializableType

K = TypeVar("K")
V = TypeVar("V")


class DictWrapper(dict[K, V], SerializableType, use_annotations=True):
    def _serialize(self) -> dict[K, V]:
        return dict(self)

    @classmethod
    def _deserialize(cls, value: dict[K, V]) -> "DictWrapper[K, V]":
        return cls(value)


@dataclass
class Index(DataClassDictMixin):
    by_date: DictWrapper[date, str]
    dates: DictWrapper[str, date]


raw = {
    "by_date": {"2026-08-16": "release"},
    "dates": {"release": "2026-08-16"},
}
index = Index.from_dict(raw)

assert date(2026, 8, 16) in index.by_date
assert index.dates["release"] == date(2026, 8, 16)
assert index.to_dict() == raw

On Python 3.12+, the class header may be written as class DictWrapper[K, V](dict[K, V], SerializableType, use_annotations=True):.

GenericSerializableType

GenericSerializableType is a lower-level alternative. Instead of having Mashumaro substitute method annotations, your methods receive a list of concrete type arguments.

python
from datetime import date
from typing import Generic, TypeVar

from mashumaro.types import GenericSerializableType

T = TypeVar("T")


class LegacyBox(Generic[T], GenericSerializableType):
    def __init__(self, value):
        self.value = value

    def _serialize(self, types):
        item_type = types[0]
        if item_type is date:
            return self.value.isoformat()
        return self.value

    @classmethod
    def _deserialize(cls, value, types):
        item_type = types[0]
        if item_type is date:
            value = date.fromisoformat(value)
        return cls(value)

Prefer annotation-aware SerializableType for new code: it composes naturally with nested generic types and gives Mashumaro enough information for JSON Schema generation. Use GenericSerializableType when the concrete type objects themselves drive custom runtime logic.

Dataclass types that implement the interface

A class may be both a dataclass and a SerializableType. Its explicit interface wins over ordinary dataclass field packing for uses of that type. This is useful when the in-memory field layout must not leak into the wire representation.

python
from dataclasses import dataclass

from mashumaro.types import SerializableType


@dataclass
class Coordinate(SerializableType, use_annotations=True):
    latitude: float
    longitude: float

    def _serialize(self) -> tuple[float, float]:
        return self.latitude, self.longitude

    @classmethod
    def _deserialize(cls, value: tuple[float, float]) -> "Coordinate":
        return cls(*value)

JSON Schema interaction

For annotation-aware classes, JSON Schema follows the _serialize() return annotation. Without a usable return annotation, the schema builder cannot reliably infer the external representation. This is one reason to annotate custom serialization even when runtime conversion would work without it.

Design guidance

  • Keep _serialize() deterministic and side-effect free.
  • Make _deserialize() accept only the documented external shape.
  • Version the representation deliberately; changing a list to a mapping is a wire breaking change.
  • Use annotation-aware conversion when nested values should follow normal Mashumaro rules.
  • Test round trips and exact basic output, not only object equality.
  • Use a strategy or dialect instead when the representation belongs to a particular API rather than to the class itself.

SerializationStrategy

A SerializationStrategy adds or overrides conversion for a type without modifying that type. It is the main extension point for third-party classes, alternate scalar formats, and reusable organization-wide representation rules.

Strategies can be attached to one field, registered by type in a model Config, or packaged in a switchable dialect.

A reusable strategy

python
from dataclasses import dataclass, field
from datetime import datetime

from mashumaro import DataClassDictMixin, field_options
from mashumaro.types import SerializationStrategy


class FormattedDateTime(SerializationStrategy):
    def __init__(self, fmt: str):
        self.fmt = fmt

    def serialize(self, value: datetime) -> str:
        return value.strftime(self.fmt)

    def deserialize(self, value: str) -> datetime:
        return datetime.strptime(value, self.fmt)


@dataclass
class Report(DataClassDictMixin):
    short_time: datetime = field(
        metadata=field_options(
            serialization_strategy=FormattedDateTime("%Y%m%d")
        )
    )
    readable_time: datetime = field(
        metadata=field_options(
            serialization_strategy=FormattedDateTime("%d %B %Y")
        )
    )


report = Report(
    short_time=datetime(2026, 8, 16),
    readable_time=datetime(2026, 8, 16),
)
assert report.to_dict() == {
    "short_time": "20260816",
    "readable_time": "16 August 2026",
}

The strategy instance can hold configuration, which makes one implementation reusable for multiple fields. Python documents the directives accepted by strftime() and strptime() in its format-code reference.

Register by type in Config

Register a strategy once when every field of a type should use it:

python
from dataclasses import dataclass
from datetime import datetime

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


@dataclass
class AuditEvent(DataClassDictMixin):
    created_at: datetime
    processed_at: datetime

    class Config(BaseConfig):
        serialization_strategy = {
            datetime: FormattedDateTime("%Y-%m-%d %H:%M:%S"),
        }

The mapping key is the target Python type. A field-level serialization_strategy is the more local rule and should be used when one field differs from the model convention.

Dictionary form

You do not have to define a strategy class. A config or dialect entry may contain serialize and deserialize callables:

python
from dataclasses import dataclass
from uuid import UUID

from mashumaro import DataClassDictMixin


@dataclass
class BinaryId(DataClassDictMixin):
    value: UUID

    class Config:
        serialization_strategy = {
            UUID: {
                "serialize": lambda value: value.hex,
                "deserialize": UUID,
            }
        }


item = BinaryId(UUID("20f16666-90f3-4d73-a034-4e73a57e8f30"))
assert item.to_dict() == {
    "value": "20f1666690f34d73a0344e73a57e8f30"
}

Entries may define only one direction. The missing direction falls back to normal handling where that is meaningful. The UUID.hex attribute used above produces the 32-character form without hyphens. For a stable round-trip contract, define and test both directions.

Annotation-aware strategies

With use_annotations=True, Mashumaro converts according to the deserialize() parameter annotation before calling the method, and converts the serialize() result according to its return annotation afterward.

python
from datetime import datetime, timezone

from mashumaro.types import SerializationStrategy


class UnixTimestamp(
    SerializationStrategy, use_annotations=True
):
    def serialize(self, value: datetime) -> float:
        return value.timestamp()

    def deserialize(self, value: float) -> datetime:
        return datetime.fromtimestamp(value, tz=timezone.utc)

As a result, an input string such as "1723800000" is converted to float before deserialize() runs. The return annotation also lets JSON Schema describe the serialized type as a number.

Annotation processing changes the boundary of your method. Without it, the method sees raw input and its return value is final. With it, Mashumaro applies recursive typed conversion on both sides.

Match subclasses

By default a strategy registered for Base matches exactly Base. Opt into subclass matching when a base-class policy should apply across a hierarchy:

python
from dataclasses import dataclass
from enum import Enum

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializationStrategy


class EnumByName(
    SerializationStrategy, match_subclasses=True
):
    def serialize(self, value: Enum) -> str:
        return value.name

    def deserialize(self, value: str) -> Enum:
        raise NotImplementedError


class Color(Enum):
    RED = "#f00"
    BLUE = "#00f"


@dataclass
class Theme(DataClassDictMixin):
    color: Color

    class Config:
        serialization_strategy = {Enum: EnumByName()}


assert Theme(Color.RED).to_dict() == {"color": "RED"}

When more than one registered base type matches, Mashumaro follows the target type's method-resolution order and uses the first registered match. Register narrow rules when multiple hierarchies could overlap.

Deserialization by a generic base such as Enum cannot know the concrete subclass from only the base strategy method. The generated field still knows Color, but the example intentionally leaves deserialization undefined to emphasize that each direction needs a real policy.

Generic third-party types

A strategy can itself be generic. Register it under the target origin type; Mashumaro substitutes the field's concrete type arguments into the strategy annotations. The example uses the third-party multidict.MultiDict container.

python
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar

from multidict import MultiDict

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializationStrategy

T = TypeVar("T")


class MultiDictStrategy(SerializationStrategy, Generic[T]):
    def serialize(self, value: MultiDict[T]) -> list[tuple[str, T]]:
        return list(value.items())

    def deserialize(
        self, value: list[tuple[str, T]]
    ) -> MultiDict[T]:
        return MultiDict(value)


@dataclass
class Query(DataClassDictMixin):
    dates: MultiDict[date]

    class Config:
        serialization_strategy = {MultiDict: MultiDictStrategy()}

Generic strategies use their annotations implicitly; there is no need to pass use_annotations=True. The number and order of strategy type variables must match the target generic type.

Built-in RoundedDecimal

RoundedDecimal(places=None, rounding=None) serializes a Decimal to a string and optionally quantizes it first. Deserialization constructs a Decimal from the incoming value.

python
from decimal import Decimal, ROUND_DOWN

from mashumaro.types import RoundedDecimal

money_strategy = RoundedDecimal(places=2, rounding=ROUND_DOWN)
assert money_strategy.serialize(Decimal("12.349")) == "12.34"

Attach it per field when different currencies or measurements use different scales, or register it for Decimal when the rule is model-wide.

pass_through

mashumaro.pass_through is a strategy whose two directions return the input unchanged:

python
from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin, pass_through


@dataclass
class Envelope(DataClassDictMixin):
    raw: object = field(
        metadata={
            "serialize": pass_through,
            "deserialize": pass_through,
        }
    )

It is safe only when the next layer accepts the raw value. MessagePack can accept bytes; standard JSON cannot encode an arbitrary object. Pass-through also skips normal reconstruction, so use it deliberately at trusted boundaries.

Strategies and dialects

Put a strategy in a dialect when the same Python model needs different external representations:

python
from datetime import datetime

from mashumaro.dialect import Dialect


class PublicAPIDialect(Dialect):
    serialization_strategy = {
        datetime: FormattedDateTime("%Y-%m-%dT%H:%M:%S"),
    }


class LegacyAPIDialect(Dialect):
    serialization_strategy = {
        datetime: FormattedDateTime("%d/%m/%Y %H:%M:%S"),
    }

See Dialects for default, call-time, and codec usage.

Precedence and design rules

  • A field-specific strategy is the narrowest and clearest override.
  • A model config expresses one model family's stable default.
  • A dialect expresses an external representation that may be selected or reused.
  • Built-in type behavior is used only when no applicable override replaces it.
  • Method annotations affect annotation-aware runtime conversion and JSON Schema output.

Avoid strategies that depend on unrelated global state. Immutable strategy objects with explicit constructor settings are easier to cache, test, and reason about.

Field Options

Field options customize one dataclass field. They live in dataclasses.field(metadata=...), so they compose with defaults, factories, init, repr, and metadata used by other libraries. You provide the metadata mapping when declaring the field; after the field is created, dataclasses exposes it as a read-only mapping intended for third-party extensions. Mashumaro reads its options from that mapping.

Use field options for a local exception. If all fields of a type share a rule, move it to Config Options or a Dialect.

The field_options helper

python
from dataclasses import dataclass, field
from datetime import datetime

from mashumaro import DataClassDictMixin, field_options


@dataclass
class Event(DataClassDictMixin):
    created_at: datetime = field(
        metadata=field_options(
            serialize=lambda value: value.timestamp(),
            deserialize=lambda value: datetime.fromtimestamp(float(value)),
            alias="createdAt",
        )
    )

field_options() returns a normal metadata dictionary. Its named parameters are:

OptionValuePurpose
serializeCallable, engine name, or pass_throughOverride packing
deserializeCallable, engine name, or pass_throughOverride unpacking
serialization_strategySerializationStrategy instanceOverride both directions
aliasstrExternal field name

Additional keyword arguments are copied into the result, which lets Mashumaro metadata coexist with schema descriptions or another library's metadata.

You may also write the dictionary directly:

python
value: int = field(metadata={"alias": "externalValue"})

Custom serialize callable

The callable receives the field value and returns its serialized representation:

python
from dataclasses import dataclass, field
from datetime import datetime

from mashumaro import DataClassDictMixin


def datetime_to_millis(value: datetime) -> int:
    return int(value.timestamp() * 1000)


@dataclass
class LogEntry(DataClassDictMixin):
    at: datetime = field(metadata={"serialize": datetime_to_millis})

Add a return annotation when JSON Schema should reflect the overridden representation. Here the field schema becomes an integer rather than the default date-time string.

The paired deserializer is independent:

python
def datetime_from_millis(value: int) -> datetime:
    return datetime.fromtimestamp(value / 1000)


@dataclass
class LogEntry(DataClassDictMixin):
    at: datetime = field(
        metadata={
            "serialize": datetime_to_millis,
            "deserialize": datetime_from_millis,
        }
    )

Without the matching deserializer, normal datetime parsing expects an ISO string and will not round-trip the integer representation.

Custom deserialize callable

The callable receives the external field value and must return the field's runtime value:

python
from dataclasses import dataclass, field
from decimal import Decimal

from mashumaro import DataClassDictMixin


@dataclass
class Payment(DataClassDictMixin):
    amount: Decimal = field(
        metadata={"deserialize": lambda value: Decimal(str(value))}
    )

Exceptions raised by field conversion are reported as InvalidFieldValue with the field name, annotated type, input value, and holder class. See Errors and Troubleshooting.

Serialization engines

String engine names select optimized built-in behavior for a small set of types.

Named tuple engines

For a NamedTuple field, both directions accept as_list and as_dict:

python
from dataclasses import dataclass, field
from typing import NamedTuple

from mashumaro import DataClassDictMixin


class Point(NamedTuple):
    x: int
    y: int


@dataclass
class Drawing(DataClassDictMixin):
    compact: Point = field(
        metadata={"serialize": "as_list", "deserialize": "as_list"}
    )
    readable: Point = field(
        metadata={"serialize": "as_dict", "deserialize": "as_dict"}
    )

as_list is the default unless config or dialect enables namedtuple_as_dict. A field engine can override that broader default.

Omit engine

Set serialize="omit" to exclude a field unconditionally:

python
from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin


@dataclass
class Session(DataClassDictMixin):
    user_id: int
    internal_token: str = field(metadata={"serialize": "omit"})


assert Session(42, "secret").to_dict() == {"user_id": 42}

This changes serialization only. Deserialization behavior still follows the field definition, alias, and default. Use a default when omitted input must still construct the dataclass.

Datetime parser engines

The deserialize option supports ciso8601 and pendulum for datetime, date, and time fields:

python
from dataclasses import dataclass, field
from datetime import datetime

from mashumaro import DataClassDictMixin


@dataclass
class Event(DataClassDictMixin):
    at: datetime = field(metadata={"deserialize": "ciso8601"})

Install the named third-party module yourself. If it is missing, Mashumaro raises ThirdPartyModuleNotFoundError identifying the dependency and field. These parser engines affect only deserialization; choose a matching serializer if you need a non-default output format.

Per-field SerializationStrategy

A strategy keeps both directions and constructor options in one object:

python
from dataclasses import dataclass, field
from datetime import datetime

from mashumaro import DataClassDictMixin, field_options
from mashumaro.types import SerializationStrategy


class EpochSeconds(SerializationStrategy):
    def serialize(self, value: datetime) -> float:
        return value.timestamp()

    def deserialize(self, value: float) -> datetime:
        return datetime.fromtimestamp(value)


@dataclass
class Event(DataClassDictMixin):
    at: datetime = field(
        metadata=field_options(serialization_strategy=EpochSeconds())
    )

See SerializationStrategy for annotation-aware and generic strategies.

Aliases

An alias is the external key expected during deserialization:

python
from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin


@dataclass
class APIResponse(DataClassDictMixin):
    status_code: int = field(metadata={"alias": "statusCode"})
    error_message: str = field(metadata={"alias": "errorMessage"})


response = APIResponse.from_dict(
    {"statusCode": 200, "errorMessage": ""}
)

Aliases are used for input by default. Output keeps Python field names unless serialize_by_alias=True, a dynamic by_alias=True flag is enabled, or a dialect selects alias output.

python
@dataclass
class AliasedAPIResponse(DataClassDictMixin):
    status_code: int = field(metadata={"alias": "statusCode"})
    error_message: str = field(metadata={"alias": "errorMessage"})

    class Config:
        serialize_by_alias = True


response = AliasedAPIResponse.from_dict(
    {"statusCode": 200, "errorMessage": ""}
)


assert response.to_dict() == {
    "statusCode": 200,
    "errorMessage": "",
}

Set allow_deserialization_not_by_alias=True when both the alias and Python name should be accepted during a migration.

Annotated aliases

Aliases can stay next to the type with Annotated instead of in field() metadata:

python
from typing import Annotated

from mashumaro.types import Alias


@dataclass
class User(DataClassDictMixin):
    user_id: Annotated[int, Alias("userId")]

Alias precedence is intentionally local:

  • field(metadata={"alias": ...}) wins.
  • Otherwise Annotated[..., Alias(...)] wins.
  • Otherwise Config.aliases supplies the model-wide alias.

This lets a field override an inherited naming convention without changing the base config.

Pass values through unchanged

Use pass_through in either direction to skip generated conversion:

python
from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin, pass_through


@dataclass
class CachedValue(DataClassDictMixin):
    payload: object = field(
        metadata={
            "serialize": pass_through,
            "deserialize": pass_through,
        }
    )

Pass-through does not make a value serializable by the final format. A raw custom object may be acceptable to a database adapter but will still fail in json.dumps. It also bypasses input conversion, so it should be used only where the caller already guarantees the runtime type.

Defaults and omission

Field defaults are regular dataclass defaults and default factories. During deserialization, a missing input key uses default or default_factory; a required field without either raises MissingField.

Omission options have different meanings:

FeatureConditionScope
serialize="omit"AlwaysOne field
omit_noneCurrent value is NoneConfig, dialect, or call flag
omit_defaultCurrent value equals its default/factory resultConfig or dialect

Do not use omission to conceal a required external field. A receiver still needs a compatible default or migration rule.

Common mistakes

  • Defining only a serializer and expecting the new representation to round-trip.
  • Using an alias and assuming output changes automatically.
  • Using pass_through before a format encoder that cannot accept the object.
  • Forgetting a return annotation when JSON Schema should see the overridden type.
  • Selecting ciso8601 or pendulum without installing it.
  • Hiding a secret only with a post-serialization hook when serialize="omit" would make the policy explicit at the field.

Config Options

An inner Config class defines model-wide serialization behavior. Configuration follows normal Python class inheritance, so a project can create a base mixin and let individual dataclasses override only their exceptions.

python
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


class APIModel(DataClassDictMixin):
    class Config(BaseConfig):
        forbid_extra_keys = True
        allow_deserialization_not_by_alias = True

Inheriting BaseConfig is recommended for discoverability, type checking, and documented defaults, though a plain nested class with recognized attributes also works.

Complete option reference

OptionDefaultEffect
debugFalsePrint generated source code
code_generation_options[]Add optional method parameters/features
serialization_strategy{}Type-to-strategy mapping
aliases{}Field-name-to-alias mapping
serialize_by_aliasUnsetEmit aliases by default
allow_deserialization_not_by_aliasFalseAccept Python names for aliased fields
omit_noneUnsetOmit fields whose value is None
omit_defaultUnsetOmit fields equal to defaults
namedtuple_as_dictUnsetRepresent named tuples as mappings
allow_postponed_evaluationTrueDefer compilation when a type reference is unresolved
dialectNoneFixed default serialization dialect
orjson_options0Default orjson option bitmask
json_schema{}Dataclass-level JSON Schema overrides
discriminatorNonePolymorphic subtype selection
lazy_compilationFalseCompile generated methods on first use
sort_keysFalseSort serialized dictionary keys
forbid_extra_keysFalseReject unexpected input keys

“Unset” is an internal sentinel rather than False, allowing a format dialect to provide a default while an explicit model value can override it.

debug

Set debug = True to print the generated packing and unpacking source. This is useful when investigating configuration precedence, a performance issue, or an unexpected conversion.

python
class Config(BaseConfig):
    debug = True

Generated source is an implementation detail. Use it for diagnosis, not as an API to copy or patch.

code_generation_options

Optional features change generated method signatures and are therefore opt-in:

python
from mashumaro.config import (
    ADD_DIALECT_SUPPORT,
    ADD_SERIALIZATION_CONTEXT,
    TO_DICT_ADD_BY_ALIAS_FLAG,
    TO_DICT_ADD_OMIT_NONE_FLAG,
)


class Config(BaseConfig):
    code_generation_options = [
        TO_DICT_ADD_BY_ALIAS_FLAG,
        TO_DICT_ADD_OMIT_NONE_FLAG,
        ADD_DIALECT_SUPPORT,
        ADD_SERIALIZATION_CONTEXT,
    ]

See Code Generation Options for the exact method parameters and propagation rules.

serialization_strategy

Map target types to SerializationStrategy instances or dictionaries containing serialize/deserialize callables:

python
from datetime import date


class Config(BaseConfig):
    serialization_strategy = {
        date: {
            "serialize": date.toordinal,
            "deserialize": date.fromordinal,
        }
    }

See SerializationStrategy for subclass and generic matching.

Aliases

aliases

Define external names centrally:

python
class Config(BaseConfig):
    aliases = {
        "user_id": "userId",
        "created_at": "createdAt",
    }

A per-field metadata alias overrides an Annotated alias, which overrides this mapping.

serialize_by_alias

Aliases are input names by default. Set serialize_by_alias = True to use them for output too:

python
from dataclasses import dataclass


@dataclass
class User(APIModel):
    user_id: int

    class Config(APIModel.Config):
        aliases = {"user_id": "userId"}
        serialize_by_alias = True


assert User(42).to_dict() == {"userId": 42}

allow_deserialization_not_by_alias

With the default False, an aliased field accepts its alias and rejects its Python name. Set this option to accept both:

python
class Config(BaseConfig):
    aliases = {"user_id": "userId"}
    allow_deserialization_not_by_alias = True

This is valuable for gradual API migrations. If both names are present, avoid depending on precedence; reject such payloads before conversion or normalize them in __pre_deserialize__.

Omission rules

omit_none

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class Sparse(DataClassDictMixin):
    name: str
    description: str | None = None

    class Config(BaseConfig):
        omit_none = True


assert Sparse("item").to_dict() == {"name": "item"}

Only a current value of None is omitted. Empty strings, zero, False, and empty collections remain.

omit_default

This option omits a field when its current value equals the declared default or the value produced by its default factory:

python
from dataclasses import dataclass, field


@dataclass
class Preferences(DataClassDictMixin):
    theme: str = "system"
    tags: list[str] = field(default_factory=list)

    class Config(BaseConfig):
        omit_default = True


assert Preferences().to_dict() == {}
assert Preferences(theme="dark").to_dict() == {"theme": "dark"}

Default factories are evaluated to determine the comparison value. Keep them cheap and deterministic.

namedtuple_as_dict

Set this option to represent every named tuple field as a mapping instead of the default list. A field-level as_list or as_dict engine can override the model-wide choice.

python
class Config(BaseConfig):
    namedtuple_as_dict = True

Dictionary representation is more self-describing and tolerates omitted defaulted items; list representation is more compact.

Forward references

allow_postponed_evaluation

The default True lets Mashumaro postpone generated-method compilation when a referenced type is not defined yet. This supports string annotations, from __future__ import annotations, mutually recursive models, and Python 3.14's deferred annotation behavior.

python
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Node(DataClassDictMixin):
    value: int
    next: Node | None = None

With allow_postponed_evaluation = False, an unresolved type raises UnresolvedTypeReferenceError during class processing instead of deferring. Disable it when eager failure is more valuable than declaration-order flexibility.

dialect

Set a fixed default dialect for this model:

python
class Config(BaseConfig):
    dialect = PublicAPIDialect

A dialect class is required, not an instance. See Dialects for call-time switching and codec defaults.

orjson_options

Store the integer bitmask forwarded to orjson.dumps by DataClassORJSONMixin.to_jsonb():

python
import orjson


class Config(BaseConfig):
    orjson_options = orjson.OPT_SORT_KEYS | orjson.OPT_UTC_Z

The call-time to_jsonb(orjson_options=...) argument overrides this value for one call.

json_schema

Dataclass-level schema overrides currently support the JSON Schema properties and additionalProperties keywords:

python
from mashumaro.jsonschema.models import JSONSchema


class Config(BaseConfig):
    json_schema = {
        "properties": {
            "name": {
                "type": "string",
                "description": "Public display name",
            }
        },
        "additionalProperties": JSONSchema(type=None),
    }

For field-level keywords, prefer Annotated[..., JSONSchema(...)]. See JSON Schema.

discriminator

Configure polymorphic deserialization for a model hierarchy:

python
from mashumaro.types import Discriminator


class Config(BaseConfig):
    discriminator = Discriminator(
        field="type",
        include_subtypes=True,
    )

At least one of include_subtypes or include_supertypes must be enabled. See Discriminator.

Compilation controls

lazy_compilation

With the default False, mixin methods are normally generated during class creation. Set True to defer work until the first serialization/deserialization call:

python
class Config(BaseConfig):
    lazy_compilation = True

This can reduce import time in applications that define many models but use only a subset. The first call pays the compilation cost; subsequent calls use the generated method.

sort_keys

Sort keys in each generated dictionary:

python
class Config(BaseConfig):
    sort_keys = True

This makes basic output deterministic and propagates to nested dataclasses according to their own config. JSON encoders may also have a separate sort option; basic sorting happens before the format encoder.

Strict input with forbid_extra_keys

The default ignores input keys that do not correspond to fields. Set strict mode to raise ExtraKeysError:

python
from dataclasses import dataclass

from mashumaro.exceptions import ExtraKeysError


@dataclass
class Command(DataClassDictMixin):
    action: str

    class Config(BaseConfig):
        forbid_extra_keys = True


try:
    Command.from_dict({"action": "deploy", "force": True})
except ExtraKeysError as exc:
    assert exc.extra_keys == {"force"}
    assert exc.target_type is Command

Alias names count as expected keys. When allow_deserialization_not_by_alias=True, both the alias and Python field name are accepted.

Inheritance pattern

Configuration follows normal Python inheritance. Derive from the parent config when overriding options so inherited policy remains obvious:

python
class PublicModel(DataClassDictMixin):
    class Config(BaseConfig):
        forbid_extra_keys = True
        omit_none = True


class InternalModel(PublicModel):
    class Config(PublicModel.Config):
        forbid_extra_keys = False

Use immutable-by-convention mappings and lists: do not mutate an inherited config collection at runtime, because class attributes can be shared.

Dialects

A dialect is a reusable serialization profile. It separates an external representation from the data model and can be selected as a model default, passed at call time, or supplied to a codec.

Typical uses include public versus legacy API shapes, database versus network representations, date formats by partner, compact versus readable named tuples, and format-native pass-through rules.

Define a dialect

python
from datetime import date, datetime

from mashumaro.dialect import Dialect


class ISOAPIDialect(Dialect):
    serialization_strategy = {
        date: {
            "serialize": date.isoformat,
            "deserialize": date.fromisoformat,
        }
    }
    serialize_by_alias = True
    omit_none = True


class OrdinalStorageDialect(Dialect):
    serialization_strategy = {
        date: {
            "serialize": date.toordinal,
            "deserialize": date.fromordinal,
        }
    }
    omit_none = False

Dialect attributes are class attributes. Pass dialect classes such as ISOAPIDialect, not instances.

The two date strategies above use the standard date.isoformat(), date.fromisoformat(), date.toordinal(), and date.fromordinal() representations.

Supported options

OptionPurpose
serialization_strategyType-to-strategy mapping
serialize_by_aliasEmit external alias names
namedtuple_as_dictRepresent named tuples as mappings
omit_noneOmit fields containing None
omit_defaultOmit fields equal to defaults
no_copy_collectionsPass selected collection types through without copying

The unset sentinel lets a model or another dialect layer supply a value. Explicit False is different from leaving an option unset.

Fixed model default

Set Config.dialect when a dataclass always uses one representation:

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


@dataclass
class Release(DataClassDictMixin):
    name: str
    day: date

    class Config(BaseConfig):
        dialect = OrdinalStorageDialect


release = Release("1.0", date(2024, 1, 1))
assert release.to_dict() == {"name": "1.0", "day": 738886}

A fixed dialect does not add a dialect= method parameter. It is compiled into the model's normal behavior.

Select a dialect at call time

Enable ADD_DIALECT_SUPPORT to generate a keyword argument for to_dict() and from_dict() and the corresponding format mixin methods:

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin
from mashumaro.config import ADD_DIALECT_SUPPORT, BaseConfig


@dataclass
class Release(DataClassDictMixin):
    name: str
    day: date

    class Config(BaseConfig):
        code_generation_options = [ADD_DIALECT_SUPPORT]


release = Release("1.0", date(2024, 1, 1))

assert release.to_dict(dialect=ISOAPIDialect) == {
    "name": "1.0",
    "day": "2024-01-01",
}
assert release.to_dict(dialect=OrdinalStorageDialect) == {
    "name": "1.0",
    "day": 738886,
}

If the model also defines Config.dialect, that class is the default when the call omits dialect=; the explicit call argument selects another profile.

Dynamic dialect support propagates through supported nested dataclasses, unions, typed dictionaries, named tuples, and generic shapes when their generated methods participate in dialect handling.

Codec default dialect

Every reusable encoder and decoder accepts default_dialect=:

python
from datetime import date

from mashumaro.codecs.json import JSONDecoder, JSONEncoder

encoder = JSONEncoder(
    list[date], default_dialect=OrdinalStorageDialect
)
decoder = JSONDecoder(
    list[date], default_dialect=OrdinalStorageDialect
)

payload = encoder.encode([date(2026, 8, 16)])
assert payload == "[739844]"
assert decoder.decode(payload) == [date(2026, 8, 16)]

Codec dialects are the cleanest way to customize a non-dataclass root shape.

Built-in format dialects

Mashumaro uses dialects internally for format-native behavior:

DialectBehavior
OrjsonDialectPasses datetime, date, time, and UUID to orjson; avoids copying lists/dicts
TOMLDialectPasses native TOML date/time values, omits None, avoids copying lists/dicts
MessagePackDialectPasses bytes/bytearray in MessagePack binary form, avoids copying lists/dicts

When a TOML, MessagePack, or orjson codec receives default_dialect=YourDialect, the format dialect is merged with your custom dialect so required native behavior remains available.

Merging dialects

Dialect.merge(other) creates a new dialect class. Serialization-strategy dictionaries are copied and overlaid; other wins for the same type/direction. This resembles a dictionary union for strategy entries. omit_none, omit_default, and no_copy_collections also take the explicit value from other, falling back to the left-hand dialect when unset.

python
class CompactDialect(Dialect):
    omit_none = True
    omit_default = True


CombinedDialect = ISOAPIDialect.merge(CompactDialect)

Current merge behavior is intentionally narrow: serialize_by_alias and namedtuple_as_dict are not copied by Dialect.merge. If a merged profile needs them, define them on the final dialect class explicitly.

python
class CombinedDialect(ISOAPIDialect.merge(CompactDialect)):
    serialize_by_alias = True
    namedtuple_as_dict = True

no_copy_collections

This option is a sequence of collection origins that may be returned unchanged when no element conversion is required:

python
class FastInternalDialect(Dialect):
    no_copy_collections = (list, dict)

It can reduce allocations, but it changes isolation guarantees: a downstream encoder or hook may see the caller's mutable collection. Use it only for trusted internal pipelines and benchmark the actual workload. Built-in format dialects use it where their encoders can safely consume the native containers.

Config versus dialect

Model config describes the model's default policy; a dialect describes a reusable external profile. An explicit model value can override a dialect default for options such as omission because “unset” and False are distinct.

Choose one source of truth per concern:

  • Put invariant security behavior such as forbid_extra_keys in config; dialects do not define it.
  • Put wire naming, omission, and type representations in a dialect when they vary by destination.
  • Put an unchanging representation in config when the model has only one contract.
  • Put a one-field exception in field metadata.

Failure modes

  • Passing a dialect instance instead of a class raises BadDialect.
  • Passing dialect= without ADD_DIALECT_SUPPORT raises TypeError because the generated method has no such parameter.
  • Pass-through rules inherited from a format dialect still depend on the final encoder's capabilities.
  • A custom dialect used for serialization must define a compatible reverse rule if round trips are required.
  • Aggressive no_copy_collections can leak mutations across a serialization boundary.

Discriminator

Discriminator chooses the concrete dataclass variant when deserializing a union or class hierarchy. A tagged discriminator performs direct lookup and gives the wire format an explicit compatibility contract; an untagged discriminator tries candidate shapes in sequence.

Parameters

ParameterDefaultMeaning
fieldNoneInput key containing the variant tag
include_subtypesFalseInclude descendants of annotated/configured classes
include_supertypesFalseInclude the listed/annotated classes as fallback variants
variant_tagger_fnNoneCompute one tag or a list of tags from a variant class

At least one of include_subtypes or include_supertypes must be true. Otherwise construction raises ValueError.

Tagged class hierarchy

Use Annotated when only one field needs polymorphic behavior:

python
from dataclasses import dataclass
from ipaddress import IPv4Address
from typing import Annotated, Literal

from mashumaro import DataClassDictMixin
from mashumaro.types import Discriminator


@dataclass
class ClientEvent:
    pass


@dataclass
class Connected(ClientEvent):
    type: Literal["connected"] = "connected"
    client_ip: IPv4Address = IPv4Address("127.0.0.1")


@dataclass
class Disconnected(ClientEvent):
    type: Literal["disconnected"] = "disconnected"
    client_ip: IPv4Address = IPv4Address("127.0.0.1")


@dataclass
class Batch(DataClassDictMixin):
    events: list[
        Annotated[
            ClientEvent,
            Discriminator(field="type", include_subtypes=True),
        ]
    ]


batch = Batch.from_dict(
    {
        "events": [
            {"type": "connected", "client_ip": "10.0.0.42"},
            {"type": "disconnected", "client_ip": "10.0.0.43"},
        ]
    }
)

assert isinstance(batch.events[0], Connected)
assert isinstance(batch.events[1], Disconnected)

The tag can come from a class attribute or a field declared with ClassVar, Final, Literal, or a string enum. Literal and StrEnum fields are especially convenient because the tag is also naturally included in serialization.

The discriminator attribute must be defined on the concrete descendant itself. A descendant without the field is skipped along with that branch for tagged discovery, preventing an inherited tag from accidentally identifying multiple variants.

Class-level discriminator

Put the discriminator in the base model's Config when every use of that base class should deserialize polymorphically:

python
from dataclasses import dataclass
from typing import Literal

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import Discriminator


@dataclass
class Event(DataClassDictMixin):
    class Config(BaseConfig):
        discriminator = Discriminator(
            field="kind", include_subtypes=True
        )


@dataclass
class Created(Event):
    kind: Literal["created"] = "created"
    object_id: int = 0


@dataclass
class Deleted(Event):
    kind: Literal["deleted"] = "deleted"
    object_id: int = 0


event = Event.from_dict({"kind": "deleted", "object_id": 42})
assert event == Deleted(object_id=42)

This works for nested Event fields and direct Event.from_dict() calls. Do not set include_supertypes=True on a class-level discriminator: selecting the configured base as its own fallback would recurse. Use Annotated on a union/field when supertypes are needed.

Discriminated unions

Use a union when variants do not share a useful base class or only a subset of a hierarchy is legal:

python
from dataclasses import dataclass
from typing import Annotated, Literal

from mashumaro import DataClassDictMixin
from mashumaro.types import Discriminator


@dataclass
class Email:
    channel: Literal["email"] = "email"
    address: str = ""


@dataclass
class SMS:
    channel: Literal["sms"] = "sms"
    number: str = ""


Notification = Annotated[
    Email | SMS,
    Discriminator(field="channel", include_supertypes=True),
]


@dataclass
class Job(DataClassDictMixin):
    notification: Notification


job = Job.from_dict(
    {"notification": {"channel": "sms", "number": "+381..."}}
)
assert isinstance(job.notification, SMS)

For a tagged union, variant selection is direct rather than “try every union branch”. This is faster and avoids accidental success when models have overlapping fields.

Untagged shape matching

Omit field when the variants are distinguishable by their required fields:

python
from dataclasses import dataclass
from typing import Annotated, Literal

from mashumaro import DataClassDictMixin
from mashumaro.types import Discriminator


@dataclass
class Ingredient:
    name: str


@dataclass
class Hummus(Ingredient):
    made_of: Literal["chickpeas", "beet", "artichoke"]
    grams: int


@dataclass
class Celery(Ingredient):
    pieces: int


@dataclass
class Plate(DataClassDictMixin):
    ingredients: list[
        Annotated[
            Ingredient,
            Discriminator(include_subtypes=True),
        ]
    ]

Mashumaro attempts candidate descendants until one deserializes. This is slower than tag lookup and can be ambiguous when classes have similar required fields. Use a tag whenever you control the contract.

Fallback to a supertype

Enable both directions on an Annotated discriminator to prefer a known subtype and fall back to the base model for a forward-compatible payload:

python
@dataclass
class Plate(DataClassDictMixin):
    ingredients: list[
        Annotated[
            Ingredient,
            Discriminator(
                include_subtypes=True,
                include_supertypes=True,
            ),
        ]
    ]

Subtypes are attempted first; supertypes are attempted afterward. An input with only {"name": "cumin"} can therefore become Ingredient("cumin"), while a hummus payload still selects Hummus.

This fallback is useful for additive evolution, but it also hides unknown variants. If unknown kinds must be rejected, use a tagged discriminator without a broad supertype fallback.

Custom tag generation

variant_tagger_fn receives each variant class and returns its tag:

python
def class_name_tag(cls):
    return cls.__name__.removesuffix("Event").lower()


class Config(BaseConfig):
    discriminator = Discriminator(
        field="type",
        include_subtypes=True,
        variant_tagger_fn=class_name_tag,
    )

Return a list to accept multiple tags for one variant during migrations:

python
def compatible_tags(cls):
    current = cls.__name__.removesuffix("Event").lower()
    return [current, f"v1:{current}"]

Keep tag generation deterministic and collision-free. A tagger maps classes to accepted input tags; it does not automatically inject a missing tag into serialized output. Add a field, hook, or explicit strategy when output must contain it.

Error behavior

SituationException
Configured tag key is missingMissingDiscriminatorError
Tag has no registered variantSuitableVariantNotFoundError
Untagged candidates all fail inside a fieldUsually wrapped as InvalidFieldValue
Neither subtype nor supertype inclusion enabledValueError when creating Discriminator

Catch these exceptions at an input boundary to produce a stable API error. Their attributes expose the field/tag or variant information; see Errors and Troubleshooting.

Best practices

  • Prefer a Literal or StrEnum dataclass field for a self-serializing tag.
  • Keep tag values stable even if Python class names change.
  • Use Annotated for local union behavior and Config for hierarchy-wide behavior.
  • Avoid untagged matching when variants overlap or the list may grow large.
  • Test missing, unknown, legacy, and duplicate tags explicitly.
  • Generate JSON Schema for the union and verify it matches the consumer's OpenAPI discriminator expectations.

Code Generation Options

Mashumaro keeps common generated methods small. Features that add runtime branches or method parameters are enabled through Config.code_generation_options.

Available flags

ConstantGenerated behavior
TO_DICT_ADD_OMIT_NONE_FLAGAdds omit_none= to serialization methods
TO_DICT_ADD_BY_ALIAS_FLAGAdds by_alias= to serialization methods
ADD_DIALECT_SUPPORTAdds dialect= to serialization and deserialization methods
ADD_SERIALIZATION_CONTEXTAdds context= to serialization methods and hooks
python
from mashumaro.config import (
    ADD_DIALECT_SUPPORT,
    ADD_SERIALIZATION_CONTEXT,
    TO_DICT_ADD_BY_ALIAS_FLAG,
    TO_DICT_ADD_OMIT_NONE_FLAG,
    BaseConfig,
)

Calling one of these keyword arguments without enabling its flag raises normal TypeError because the generated method does not accept it.

Dynamic omit_none

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig, TO_DICT_ADD_OMIT_NONE_FLAG


@dataclass
class SearchResult(DataClassDictMixin):
    title: str
    snippet: str | None = None

    class Config(BaseConfig):
        code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG]


result = SearchResult("Mashumaro")
assert result.to_dict() == {"title": "Mashumaro", "snippet": None}
assert result.to_dict(omit_none=True) == {"title": "Mashumaro"}

The no-argument default comes from Config.omit_none or the active dialect. The explicit call value overrides that default:

python
class Config(BaseConfig):
    omit_none = True
    code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG]

# to_dict() omits None; to_dict(omit_none=False) includes it

Dynamic aliases

python
from dataclasses import dataclass, field

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig, TO_DICT_ADD_BY_ALIAS_FLAG


@dataclass
class User(DataClassDictMixin):
    user_id: int = field(metadata={"alias": "userId"})

    class Config(BaseConfig):
        code_generation_options = [TO_DICT_ADD_BY_ALIAS_FLAG]


user = User(42)
assert user.to_dict() == {"user_id": 42}
assert user.to_dict(by_alias=True) == {"userId": 42}

If serialize_by_alias=True, the no-argument result uses aliases and by_alias=False temporarily restores Python names.

Dynamic dialects

ADD_DIALECT_SUPPORT adds dialect= to to_dict(), from_dict(), and format-specific mixin methods:

python
from dataclasses import dataclass
from datetime import date

from mashumaro import DataClassDictMixin
from mashumaro.config import ADD_DIALECT_SUPPORT, BaseConfig


@dataclass
class Release(DataClassDictMixin):
    day: date

    class Config(BaseConfig):
        code_generation_options = [ADD_DIALECT_SUPPORT]


wire = Release(date(2026, 8, 16)).to_dict(
    dialect=OrdinalStorageDialect
)
assert Release.from_dict(wire, dialect=OrdinalStorageDialect) == Release(
    date(2026, 8, 16)
)

See Dialects for model and codec defaults.

Serialization context

ADD_SERIALIZATION_CONTEXT adds a caller-defined context value and passes it to __pre_serialize__ and __post_serialize__ hooks that accept the extra parameter. Any is convenient for open-ended examples, but applications can annotate a TypedDict, protocol, or mapping for a stricter context contract.

python
from dataclasses import dataclass
from typing import Any

from mashumaro import DataClassDictMixin
from mashumaro.config import ADD_SERIALIZATION_CONTEXT, BaseConfig


@dataclass
class Account(DataClassDictMixin):
    username: str
    email: str

    class Config(BaseConfig):
        code_generation_options = [ADD_SERIALIZATION_CONTEXT]

    def __post_serialize__(
        self, data: dict[str, Any], context: dict | None = None
    ) -> dict[str, Any]:
        if context and context.get("public"):
            data.pop("email")
        return data


account = Account("alice", "alice@example.com")
assert account.to_dict(context={"public": True}) == {
    "username": "alice"
}

Context is serialization-only. Deserialization hooks do not receive it. The context type and mutation policy belong to your application; a small immutable Mapping is often easiest to reason about.

Combine flags

Options compose in a single keyword-only call:

python
class Config(BaseConfig):
    code_generation_options = [
        TO_DICT_ADD_OMIT_NONE_FLAG,
        TO_DICT_ADD_BY_ALIAS_FLAG,
        ADD_DIALECT_SUPPORT,
        ADD_SERIALIZATION_CONTEXT,
    ]


data = model.to_dict(
    omit_none=True,
    by_alias=True,
    dialect=PublicAPIDialect,
    context={"public": True},
)

Nested propagation

Dynamic flags propagate only where the nested generated converter supports the same feature. This distinction is deliberate:

python
from dataclasses import dataclass


@dataclass
class Inner(DataClassDictMixin):
    value: int | None = None


@dataclass
class Outer(DataClassDictMixin):
    inner: Inner
    value: int | None = None

    class Config(BaseConfig):
        code_generation_options = [TO_DICT_ADD_OMIT_NONE_FLAG]


assert Outer(Inner()).to_dict(omit_none=True) == {
    "inner": {"value": None}
}

Outer.value is omitted, while Inner.value remains because Inner did not opt into the dynamic flag. Add the option to a shared base config when an entire model graph should honor it.

The same principle prevents a parent call from silently changing a nested model that intentionally has a different external contract.

Performance guidance

Each option adds generated code and sometimes a branch per call. The cost is usually small, but do not enable every option globally without a use case. Fixed Config or dialect values produce a simpler API when behavior never changes at runtime.

Generics and Modern Typing

Mashumaro resolves type variables through generic dataclass inheritance, parameterized fields, codecs, serialization strategies, and custom serializable types. It supports classic Generic, PEP 646 variadic generics, PEP 695 syntax and type aliases, PEP 696 TypeVar defaults, and recursive generic models.

Parameterized field types

The most common pattern is a generic dataclass used with different concrete arguments:

python
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar

from mashumaro import DataClassDictMixin

T = TypeVar("T")


@dataclass
class Box(Generic[T]):
    value: T


@dataclass
class Document(DataClassDictMixin):
    published: Box[date]
    title: Box[str]


raw = {
    "published": {"value": "2026-08-16"},
    "title": {"value": "Mashumaro"},
}
document = Document.from_dict(raw)

assert document.published.value == date(2026, 8, 16)
assert document.title.value == "Mashumaro"
assert document.to_dict() == raw

The generic dataclass itself does not need a mixin when it is nested. For direct typed methods, create a concrete subclass such as DateBox(Box[date], DataClassDictMixin); calling an origin method through Box[date] does not specialize that generated method at runtime.

Concrete generic inheritance

Subclass a generic dataclass to create a named concrete model:

python
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar

from mashumaro import DataClassDictMixin

K = TypeVar("K")
V = TypeVar("V")


@dataclass
class Index(Generic[K, V]):
    entries: Mapping[K, V]


@dataclass
class ReleaseIndex(Index[str, date], DataClassDictMixin):
    pass


index = ReleaseIndex.from_dict(
    {"entries": {"stable": "2026-08-16"}}
)
assert index.entries["stable"] == date(2026, 8, 16)

Partial specialization and replacing a parent TypeVar with another TypeVar are supported. Unresolved variables use a bound, constraints, a default, or Any depending on the declaration.

Bounds, constraints, and defaults

python
from dataclasses import dataclass
from datetime import date
from typing import Generic, TypeVar

BoundDate = TypeVar("BoundDate", bound=date)
DateOrString = TypeVar("DateOrString", date, str)


@dataclass
class Bounded(Generic[BoundDate]):
    value: BoundDate


@dataclass
class Constrained(Generic[DateOrString]):
    value: DateOrString

PEP 696 defaults can provide a concrete fallback:

python
from typing_extensions import TypeVar

T = TypeVar("T", default=int)

typing_extensions.TypeVar makes defaults available on all supported Python versions; Python 3.13+ also provides native syntax/API support.

Variadic generics (PEP 646)

TypeVarTuple and Unpack allow a tuple shape to carry any number of type parameters:

python
from dataclasses import dataclass
from typing import Generic
from typing_extensions import TypeVarTuple, Unpack

from mashumaro import DataClassDictMixin

Ts = TypeVarTuple("Ts")


@dataclass
class Row(Generic[Unpack[Ts]]):
    values: tuple[Unpack[Ts]]


@dataclass
class Table(DataClassDictMixin):
    row: Row[int, float, str]


table = Table.from_dict({"row": {"values": [1, 2.5, "ok"]}})
assert table.row.values == (1, 2.5, "ok")

On Python 3.11+, star-unpacking syntax such as tuple[*Ts] is also available. Mashumaro supports fixed items around an unpack, arbitrary-length tuple arguments, and empty variadic tuples where Python's type syntax permits them.

PEP 695 syntax (Python 3.12+)

Python 3.12 can declare type parameters directly:

python
from dataclasses import dataclass


@dataclass
class Box[T]:
    value: T


@dataclass
class Pair[K, V]:
    key: K
    value: V

These classes work in parameterized fields, concrete inheritance, codecs, annotation-aware SerializableType, and generic strategies just like classic Generic classes.

The grammar is Python 3.12-only; a library supporting Python 3.10/3.11 should keep PEP 695 declarations in version-specific modules or use classic syntax.

PEP 695 type aliases

The type statement can define simple, generic, and recursive shape aliases:

python
type UserMap = dict[str, int]
type Page[T] = list[T]
type JSONValue = (
    None | bool | int | float | str | list[JSONValue] | dict[str, JSONValue]
)

Aliases can be codec roots or dataclass field types:

python
from mashumaro.codecs.basic import BasicDecoder, BasicEncoder

encoder = BasicEncoder(Page[date])
decoder = BasicDecoder(Page[date])

Mashumaro resolves parameterized aliases and guards direct, wrapped, and mutually recursive aliases from infinite recursion. JSON Schema definitions use stable alias names.

Recursive generic models

Forward references and Self work with generic dataclasses:

python
from __future__ import annotations

from dataclasses import dataclass
from typing import Generic, TypeVar

from mashumaro import DataClassDictMixin

T = TypeVar("T")


@dataclass
class Node(Generic[T]):
    value: T
    children: list[Node[T]]


@dataclass
class Tree(DataClassDictMixin):
    root: Node[int]


tree = Tree.from_dict(
    {
        "root": {
            "value": "1",
            "children": [{"value": "2", "children": []}],
        }
    }
)
assert tree.root.children[0].value == 2

With postponed annotations enabled by default, compilation can wait until referenced types exist.

Calling an inherited mixin method through a runtime generic alias such as Node[int].from_dict(...) does not specialize that method with int; Python forwards the attribute to the unspecialized origin class. Put the specialized type in a containing model, use BasicDecoder(Node[int]), or create a concrete subclass when direct methods are required.

Generic SerializableType

For a generic class you own, SerializableType(use_annotations=True) substitutes concrete type arguments into _serialize() and _deserialize() annotations. This is usually more concise than inspecting type objects manually.

See SerializableType for a complete DictWrapper[K, V] example.

Generic SerializationStrategy

For a third-party generic, make the strategy generic and register it under the target origin:

python
class Config:
    serialization_strategy = {
        ThirdPartyContainer: ThirdPartyContainerStrategy()
    }

Mashumaro maps field arguments such as ThirdPartyContainer[date] into the strategy's method annotations. See SerializationStrategy.

Generic codec roots

Codecs accept any fully parameterized shape:

python
from datetime import date

from mashumaro.codecs.json import JSONDecoder, JSONEncoder

shape = dict[str, list[Box[date] | None]]
encoder = JSONEncoder(shape)
decoder = JSONDecoder(shape)

Construct and reuse one codec per concrete shape. A bare generic with unresolved parameters has less conversion information and may fall back to bounds/defaults/Any.

Version matrix

CapabilityPython 3.103.113.123.13–3.14
Classic Generic[T]YesYesYesYes
TypeVarTuple/Unpack objectstyping_extensionsNativeNativeNative
tuple[*Ts] syntaxLimited; use UnpackYesYesYes
PEP 695 class/type-alias syntaxNoNoYesYes
TypeVar defaultstyping_extensionstyping_extensionstyping_extensionsNative or extension
Deferred 3.14 annotationsFuture import modelFuture import modelFuture import modelNative in 3.14

Troubleshooting generics

  • Parameterize the root shape: prefer Box[date] over bare Box.
  • Keep strategy TypeVars aligned with the third-party generic's parameter order.
  • Use typing_extensions imports for a source file shared with Python 3.10.
  • Put Python 3.12-only grammar in a module that older interpreters never parse.
  • Leave allow_postponed_evaluation=True for forward and recursive references.
  • Inspect generated code with Config.debug=True when a TypeVar resolves to an unexpected fallback.

Serialization Hooks

Hooks intercept a dataclass at four points around generated conversion. They work through dictionary and format mixins and when the dataclass is nested inside a codec shape.

Lifecycle

HookRequired kindInputMust return
__pre_deserialize__classmethodRaw mappingMapping to unpack
__post_deserialize__classmethodConstructed instanceFinal instance
__pre_serialize__Instance methodselfInstance to pack
__post_serialize__Instance methodPacked dictionaryFinal dictionary

With serialization context enabled, the two serialization hooks may also accept context.

Before deserialization

Normalize or migrate raw keys before field lookup:

python
from dataclasses import dataclass
from typing import Any

from mashumaro.mixins.json import DataClassJSONMixin


@dataclass
class User(DataClassJSONMixin):
    name: str
    age: int

    @classmethod
    def __pre_deserialize__(
        cls, data: dict[str, Any]
    ) -> dict[str, Any]:
        normalized = {key.lower(): value for key, value in data.items()}
        if "years" in normalized and "age" not in normalized:
            normalized["age"] = normalized.pop("years")
        return normalized


assert User.from_json('{"NAME": "Alice", "years": "30"}') == User(
    "Alice", 30
)

The hook runs after the format parser has produced a Mapping but before typed field conversion. It can therefore rename keys and reshape input while leaving date/enum/nested conversion to Mashumaro.

After deserialization

Inspect or replace the fully constructed object:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class Score(DataClassDictMixin):
    value: int

    @classmethod
    def __post_deserialize__(cls, obj: "Score") -> "Score":
        obj.value = max(0, min(100, obj.value))
        return obj


assert Score.from_dict({"value": 120}) == Score(100)

This hook receives an instance whose fields already have their annotated runtime types. Returning an instance is mandatory; it may be the same object or a replacement compatible with the class contract.

Before serialization

Select or prepare the object before generated field packing:

python
from dataclasses import dataclass
from typing import ClassVar

from mashumaro import DataClassDictMixin


@dataclass
class Metered(DataClassDictMixin):
    value: int
    serializations: ClassVar[int] = 0

    def __pre_serialize__(self) -> "Metered":
        type(self).serializations += 1
        return self

ClassVar keeps the counter out of the dataclass field set. Avoid mutating ordinary instance fields just to serialize them: repeated calls should usually produce the same result. A strategy or __post_serialize__ transformation is easier to reason about for pure representation changes.

After serialization

Transform the complete dictionary after all fields have been packed:

python
from dataclasses import dataclass
from typing import Any

from mashumaro import DataClassDictMixin


@dataclass
class Credentials(DataClassDictMixin):
    username: str
    password: str

    def __post_serialize__(
        self, data: dict[str, Any]
    ) -> dict[str, Any]:
        data.pop("password")
        data["kind"] = "credentials"
        return data


assert Credentials("alice", "secret").to_dict() == {
    "username": "alice",
    "kind": "credentials",
}

For a field that must never be emitted, serialize="omit" is more explicit. A post hook is best for transformations that need multiple fields or the complete mapping.

Serialization context

Enable ADD_SERIALIZATION_CONTEXT to make per-call policy available to serialization hooks:

python
from dataclasses import dataclass
from typing import Any

from mashumaro import DataClassDictMixin
from mashumaro.config import ADD_SERIALIZATION_CONTEXT, BaseConfig


@dataclass
class Profile(DataClassDictMixin):
    username: str
    email: str

    class Config(BaseConfig):
        code_generation_options = [ADD_SERIALIZATION_CONTEXT]

    def __pre_serialize__(
        self, context: dict[str, Any] | None = None
    ) -> "Profile":
        return self

    def __post_serialize__(
        self,
        data: dict[str, Any],
        context: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        if context and context.get("audience") == "public":
            data.pop("email")
        return data


profile = Profile("alice", "alice@example.com")
assert profile.to_dict(context={"audience": "public"}) == {
    "username": "alice"
}

Context propagates through nested models whose generated methods support it. Models without the option keep their ordinary hook signature and behavior.

Hook inheritance

Hooks can be defined in a parent class and are discovered on descendants:

python
class LowercaseInput:
    @classmethod
    def __pre_deserialize__(cls, data):
        return {key.lower(): value for key, value in data.items()}


@dataclass
class Model(LowercaseInput, DataClassDictMixin):
    value: int

This is useful for cross-cutting migrations, but a base hook affects every descendant. Keep it generic and test inheritance combinations.

Signature validation

Mashumaro validates whether pre/post deserialization hooks are classmethods and whether hook signatures match the enabled features. Invalid definitions raise BadHookSignature during class processing instead of failing much later in a request.

Correct signatures are:

python
@classmethod
def __pre_deserialize__(cls, data): ...

@classmethod
def __post_deserialize__(cls, obj): ...

def __pre_serialize__(self): ...

def __post_serialize__(self, data): ...

With context, add an optional second argument to pre-serialize and third argument to post-serialize as shown above.

Hooks versus other extensions

RequirementPrefer
Convert one fieldField callable or strategy
Convert every value of a typeConfig strategy
Switch representation by destinationDialect
Rename one keyAlias
Migrate or reshape a whole old payloadPre-deserialize hook
Add/remove output based on several fieldsPost-serialize hook
Choose a polymorphic classDiscriminator

Hooks are powerful because they see broad state. Use the narrowest extension that expresses the rule; narrow rules produce better schemas and fewer surprising interactions.

JSON Schema

Mashumaro builds JSON Schema for any supported type shape, not only dataclasses. The result is a typed JSONSchema object with to_dict() and to_json() methods.

Built-in schema dialects cover JSON Schema Draft 2020-12 and OpenAPI 3.1.

Build a schema

python
from dataclasses import dataclass, field
from uuid import UUID

from mashumaro.jsonschema import build_json_schema


@dataclass
class User:
    id: UUID
    name: str = field(metadata={"description": "Public display name"})
    email: str | None = None


schema = build_json_schema(User)
schema_dict = schema.to_dict()
schema_json = schema.to_json()

The essential output is equivalent to:

json
{
  "type": "object",
  "title": "User",
  "properties": {
    "id": {"type": "string", "format": "uuid"},
    "name": {"type": "string", "description": "Public display name"},
    "email": {
      "anyOf": [{"type": "string"}, {"type": "null"}],
      "default": null
    }
  },
  "additionalProperties": false,
  "required": ["id", "name"]
}

Dataclass fields without defaults are required. Defaults are serialized into the schema using the model's serialization rules. Aliases become property names.

Non-dataclass root shapes

python
from mashumaro.jsonschema import build_json_schema

users_schema = build_json_schema(list[User])
lookup_schema = build_json_schema(dict[str, User | None])
tuple_schema = build_json_schema(tuple[int, str, bool])

Codecs and schema generation therefore share the same “shape type” vocabulary.

build_json_schema options

ArgumentDefaultPurpose
instance_typeRequiredDataclass or any supported shape
contextNew contextReuse definitions and plugin state
with_definitionsTrueAttach accumulated definitions to the result
all_refsDialect defaultPut all dataclasses in definitions and reference them
with_dialect_uriFalseEmit the $schema keyword
dialectDraft 2020-12Select schema dialect
ref_prefixDialect defaultOverride the reference root
pluginsEmptyExtend or modify generated schemas

Schema dialects

Draft 2020-12 is the default:

python
from mashumaro.jsonschema import DRAFT_2020_12, build_json_schema

schema = build_json_schema(
    User,
    dialect=DRAFT_2020_12,
    with_dialect_uri=True,
)
assert schema.to_dict()["$schema"] == (
    "https://json-schema.org/draft/2020-12/schema"
)

OpenAPI 3.1 uses #/components/schemas references and references dataclasses by default:

python
from mashumaro.jsonschema import OPEN_API_3_1, build_json_schema

schema = build_json_schema(
    list[User],
    dialect=OPEN_API_3_1,
    with_dialect_uri=True,
)
DialectDefinition pointerDefault all_refs
DRAFT_2020_12#/$defsFalse
OPEN_API_3_1#/components/schemasTrue

References and definitions

Set all_refs=True to replace every dataclass occurrence with a reference and collect definitions:

python
schema = build_json_schema(list[User], all_refs=True)

document = schema.to_dict()
assert document["items"]["$ref"].endswith("/User")
assert "User" in document["$defs"]

Set with_definitions=False when embedding the returned fragment in a larger document that stores definitions elsewhere:

python
fragment = build_json_schema(
    list[User],
    dialect=OPEN_API_3_1,
    with_definitions=False,
)

Override the reference root when the destination uses another component location:

python
fragment = build_json_schema(
    list[User],
    all_refs=True,
    with_definitions=False,
    ref_prefix="#/components/responses",
)

Trailing slashes are normalized. Recursive dataclasses and recursive type aliases force definitions/references as needed even when all_refs=False, preventing infinite inline expansion.

Incremental JSONSchemaBuilder

Use a builder when assembling a larger OpenAPI or schema document from many models:

python
from dataclasses import dataclass
from uuid import UUID

from mashumaro.jsonschema import JSONSchemaBuilder, OPEN_API_3_1


@dataclass
class Device:
    id: UUID
    model: str


builder = JSONSchemaBuilder(dialect=OPEN_API_3_1)

users_fragment = builder.build(list[User]).to_dict()
devices_fragment = builder.build(list[Device]).to_dict()
definitions = builder.get_definitions().to_dict()

assert "User" in definitions
assert "Device" in definitions

The builder shares one Context, so definitions accumulate across calls. Constructor options are dialect, all_refs, ref_prefix, and plugins.

Type-to-schema mapping

Python shapeSchema shape
str{"type": "string"}
int{"type": "integer"}
float{"type": "number"}
Decimal, FractionString with Mashumaro's decimal/fraction format extension
bool{"type": "boolean"}
None{"type": "null"}
AnyEmpty unrestricted schema
Literal[...], Enumenum values
A | BanyOf
list[T], sequencesArray with items
Fixed tuple[...]Array with prefixItems, minItems, maxItems
set[T], frozenset[T]Array with uniqueItems
MappingObject with propertyNames/additionalProperties
TypedDictObject properties and required keys
DataclassClosed object with properties and required keys
datetime, date, timeString with standard format
UUID, IP addressesString with standard format
bytesString with Mashumaro's base64 format extension
PathsString with Mashumaro's path format extension

Mashumaro also defines extension formats for time zones, timedelta, networks, IP interfaces, decimal, fraction, and Base64 where the standard JSON Schema format vocabulary has no exact built-in format.

Constraints with Annotated

Constraint objects in mashumaro.jsonschema.annotations add Draft 2020-12 validation keywords while preserving the runtime type with Annotated:

python
from dataclasses import dataclass
from typing import Annotated

from mashumaro.jsonschema.annotations import (
    MaxItems,
    MaxLength,
    Maximum,
    MinItems,
    MinLength,
    Minimum,
    Pattern,
    UniqueItems,
)


@dataclass
class Product:
    sku: Annotated[str, Pattern(r"^[A-Z]{2}\d{6}$")]
    name: Annotated[str, MinLength(1), MaxLength(100)]
    price: Annotated[float, Minimum(0), Maximum(1_000_000)]
    tags: Annotated[
        list[str], MinItems(1), MaxItems(20), UniqueItems(True)
    ]

Numeric constraints

  • Minimum(value)
  • Maximum(value)
  • ExclusiveMinimum(value)
  • ExclusiveMaximum(value)
  • MultipleOf(value)

String constraints

  • MinLength(value)
  • MaxLength(value)
  • Pattern(value)

Array constraints

  • MinItems(value)
  • MaxItems(value)
  • UniqueItems(value)
  • Contains(JSONSchema(...))
  • MinContains(value)
  • MaxContains(value)

MinContains and MaxContains are emitted only when Contains is present, matching the JSON Schema array-keyword semantics.

Object constraints

  • MinProperties(value)
  • MaxProperties(value)
  • DependentRequired(mapping)

Apply collection constraints to the collection's Annotated layer and element constraints to the element layer:

python
from mashumaro.jsonschema.annotations import MaxItems, Maximum

Scores = Annotated[
    list[Annotated[int, Maximum(100)]],
    MaxItems(10),
]

Schema overlays with JSONSchema

Attach a JSONSchema instance inside Annotated for keywords that do not have a dedicated constraint class:

python
from dataclasses import dataclass
from typing import Annotated

from mashumaro.jsonschema.models import JSONSchema, JSONSchemaInstanceType


@dataclass
class Upload:
    file: Annotated[
        bytes,
        JSONSchema(
            description="PDF document",
            contentEncoding="base64",
            contentMediaType="application/pdf",
        ),
    ]
    metadata: Annotated[
        str,
        JSONSchema(
            contentMediaType="application/json",
            contentSchema=JSONSchema(
                type=JSONSchemaInstanceType.OBJECT
            ),
        ),
    ]

Overlay values are applied after automatic type generation and combine with constraint annotations. If multiple overlays set the same attribute, the last one wins. Explicit None for const and default is preserved.

Structural keywords $schema, $ref, and $defs are ignored in field overlays; they are controlled by builder context and reference options.

Field metadata and model overrides

A field's metadata={"description": ...} supplies its description:

python
name: str = field(metadata={"description": "Public display name"})

For dataclass-level overrides, Config.json_schema supports property replacements and additionalProperties:

python
from mashumaro.config import BaseConfig


@dataclass
class Flexible:
    name: str

    class Config(BaseConfig):
        json_schema = {
            "properties": {
                "name": {
                    "type": "string",
                    "description": "Overridden field schema",
                }
            },
            "additionalProperties": True,
        }

An additionalProperties override may be True, False, or a JSONSchema object. A property override replaces that field's automatically generated schema; an Annotated overlay modifies the generated schema. Prefer overlays for additive field customization.

Custom serialization and schema output

Schema describes the serialized representation, not only the Python annotation. Give custom serializers return annotations so Mashumaro can follow the output type:

python
from dataclasses import dataclass, field

from mashumaro.config import BaseConfig


def string_as_characters(value: str) -> list[str]:
    return list(value)


def integer_as_string(value: int) -> str:
    return str(value)


@dataclass
class CustomWireShape:
    name: str = field(metadata={"serialize": string_as_characters})
    count: int = 0

    class Config(BaseConfig):
        serialization_strategy = {
            int: {"serialize": integer_as_string}
        }

The schema for name becomes an array of strings and count becomes a string. If a callable lacks a return annotation or the annotation cannot be resolved, the builder warns and falls back to an unrestricted schema.

The same principle applies to annotation-aware SerializableType and SerializationStrategy methods.

Plugins

Plugins can add an unsupported type or modify an existing schema. They run in order for every instance in the shape.

The built-in docstring plugin adds Python class docstrings from dataclasses:

python
from mashumaro.jsonschema import build_json_schema
from mashumaro.jsonschema.plugins import DocstringDescriptionPlugin


@dataclass
class Region:
    """A deployment region."""

    name: str


schema = build_json_schema(
    Region, plugins=[DocstringDescriptionPlugin()]
)
assert schema.description == "A deployment region."

Custom plugin

python
from pathlib import Path

from mashumaro.jsonschema.models import (
    Context,
    JSONSchema,
    JSONSchemaInstanceType,
)
from mashumaro.jsonschema.plugins import BasePlugin
from mashumaro.jsonschema.schema import Instance


class PathDescriptionPlugin(BasePlugin):
    def get_schema(
        self,
        instance: Instance,
        ctx: Context,
        schema: JSONSchema | None = None,
    ) -> JSONSchema | None:
        try:
            if issubclass(instance.type, Path) and schema is not None:
                schema.type = JSONSchemaInstanceType.STRING
                schema.description = "Filesystem path"
                return schema
        except TypeError:
            return None
        return None

Returning a schema replaces/continues the current result. Returning None or raising NotImplementedError means the plugin does not handle that instance. A plugin may receive schema=None for a type unsupported by built-in creators and can return a complete schema to add support.

Register the same plugin list on build_json_schema(..., plugins=[...]) or JSONSchemaBuilder(plugins=[...]).

Recursive models and aliases

Direct recursion, mutual recursion, typing.Self, and PEP 695 recursive aliases are supported. The builder creates definitions automatically when it encounters a cycle:

python
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class Node:
    value: int
    children: list[Node]


schema = build_json_schema(Node)
assert "$defs" in schema.to_dict()

Practical checklist

  • Build schemas from the same concrete shape passed to your codec.
  • Use aliases consistently so schema property names match actual output.
  • Annotate custom serializer return types.
  • Choose Draft 2020-12 or OpenAPI 3.1 before deciding reference layout.
  • Use JSONSchemaBuilder when assembling multiple components.
  • Prefer Annotated constraints and overlays for field-local additions.
  • Snapshot or structurally test generated schemas as part of API compatibility checks.
  • Remember that JSON Schema documents the serialized contract; it does not execute Mashumaro deserialization.

Errors and Troubleshooting

Mashumaro raises specific exceptions for schema construction, input conversion, hooks, dialects, and optional integrations. Catch narrow exceptions at external boundaries and keep programming/configuration errors visible during development.

Exception reference

ExceptionTypical stageMeaning
MissingFieldDeserializationRequired field key is absent
ExtraKeysErrorDeserializationUnexpected keys with forbid_extra_keys=True
InvalidFieldValueDeserializationA field value could not be converted
UnserializableDataErrorCode generationBase error for unsupported data shapes
UnserializableFieldCode generationA particular dataclass field is unsupported
UnsupportedSerializationEngineCode generationUnknown/inapplicable serialize engine
UnsupportedDeserializationEngineCode generationUnknown/inapplicable deserialize engine
MissingDiscriminatorErrorDeserializationConfigured discriminator key is absent
SuitableVariantNotFoundErrorDeserializationNo discriminator variant matched
BadHookSignatureClass processingHook kind or parameters are invalid
ThirdPartyModuleNotFoundErrorClass processing/useRequested parser integration is not installed
UnresolvedTypeReferenceErrorClass processingA forward reference cannot be resolved
BadDialectCode generation/callDialect is invalid or passed in the wrong form

All live in mashumaro.exceptions.

Missing required fields

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.exceptions import MissingField


@dataclass
class User(DataClassDictMixin):
    name: str
    age: int = 0


try:
    User.from_dict({"age": 30})
except MissingField as exc:
    assert exc.field_name == "name"
    assert exc.field_type is str
    assert exc.holder_class is User

A dataclass default or default_factory makes missing input legal. Optional[T] does not automatically make a field optional in the mapping; it only allows None. Give it a default when omission is valid:

python
nickname: str | None = None

For an alias, the alias is the required input key unless allow_deserialization_not_by_alias=True.

Unexpected keys

python
from dataclasses import dataclass

from mashumaro.config import BaseConfig
from mashumaro.exceptions import ExtraKeysError


@dataclass
class Command(DataClassDictMixin):
    action: str

    class Config(BaseConfig):
        forbid_extra_keys = True


try:
    Command.from_dict({"action": "deploy", "force": True})
except ExtraKeysError as exc:
    assert exc.extra_keys == {"force"}
    assert exc.target_type is Command

Strict mode is best at public boundaries. During a rolling migration, normalize legacy keys in __pre_deserialize__ before strict checking or explicitly allow both an alias and the Python name.

Invalid values

InvalidFieldValue wraps a failure to construct the annotated type:

python
from dataclasses import dataclass

from mashumaro.exceptions import InvalidFieldValue


@dataclass
class Item(DataClassDictMixin):
    count: int


try:
    Item.from_dict({"count": "many"})
except InvalidFieldValue as exc:
    assert exc.field_name == "count"
    assert exc.field_type is int
    assert exc.field_value == "many"
    assert exc.holder_class is Item

Inspect exc.msg and the chained exception when available. The most common causes are a wrong wire representation, a serializer/deserializer pair that disagrees, an invalid literal/enum value, or an ambiguous union.

Do not catch every ValueError around a full payload. Catch InvalidFieldValue and report the structured field context.

Unsupported fields and engines

An arbitrary class is not serialized from __dict__ automatically. This example uses the standard library's queue.Queue:

python
from queue import Queue


@dataclass
class Unsupported(DataClassDictMixin):
    queue: Queue

Depending on compilation mode, defining or first using this class raises UnserializableField. Add an explicit SerializableType, strategy, field callable, or pass-through rule appropriate to the final format.

Engine names are type-specific. as_dict/as_list apply to named tuples; ciso8601/pendulum apply to date/time types; omit is serialization-only. A typo or incompatible engine raises UnsupportedSerializationEngine or UnsupportedDeserializationEngine.

Missing optional parser modules

Selecting deserialize="ciso8601" or deserialize="pendulum" does not install ciso8601 or pendulum. Missing modules raise ThirdPartyModuleNotFoundError with:

  • module_name
  • field_name
  • holder_class

Install the dependency explicitly and include it in your application's requirements or lock file.

Format mixins have their own extras; see Supported Formats.

Forward-reference failures

UnresolvedTypeReferenceError identifies the holder class and unresolved name:

python
class Config(BaseConfig):
    allow_postponed_evaluation = False

Common fixes:

  • Keep allow_postponed_evaluation=True, the default.
  • Add from __future__ import annotations on Python 3.10–3.13.
  • Import the referenced type before the model is first used.
  • Avoid references hidden only under TYPE_CHECKING when runtime evaluation needs them.
  • For local-scope models, ensure the referenced object remains resolvable in the expected namespace.

Use Config.debug=True to see when compilation happens and what concrete type was resolved.

Discriminator failures

MissingDiscriminatorError.field_name identifies an absent tag key. SuitableVariantNotFoundError exposes:

  • variants_type
  • discriminator_name
  • discriminator_value

If a valid subclass is ignored, check that the tag attribute is defined directly on that concrete descendant, not only inherited. If a custom variant_tagger_fn is used, verify that it returns stable unique values and that the input tag has the same basic type.

For untagged variants, enable forbid_extra_keys where suitable and make required fields sufficiently distinct; otherwise a broader class may succeed before the intended one.

Hook failures

The two deserialization hooks must be classmethods:

python
@classmethod
def __pre_deserialize__(cls, data): ...

@classmethod
def __post_deserialize__(cls, obj): ...

Serialization hooks are instance methods. Context parameters are legal only with ADD_SERIALIZATION_CONTEXT. A wrong signature raises BadHookSignature during code generation.

Every hook must return the transformed value. Forgetting return data, return obj, or return self often produces a later error that looks unrelated.

Dialect failures

Pass a dialect class:

python
model.to_dict(dialect=PublicAPIDialect)

Do not pass PublicAPIDialect(). Also enable ADD_DIALECT_SUPPORT before using the call-time keyword. A fixed Config.dialect does not require the flag.

If a merged dialect appears to lose alias or named-tuple behavior, remember that Dialect.merge() currently merges strategies, omission flags, and no_copy_collections; define serialize_by_alias and namedtuple_as_dict explicitly on the final class.

“Object is not JSON serializable”

This message usually comes from json.dumps, after Mashumaro's typed stage. Typical causes:

  • pass_through left a custom object in the basic form.
  • A custom serializer returned a non-JSON value.
  • A dictionary has unsupported key types.
  • A codec transform returned the wrong layer.
  • A format-native dialect was reused with a different encoder.

Inspect obj.to_dict() or a BasicEncoder result first. If the unsupported object is already present there, fix the strategy. If the basic form is JSON-safe, inspect the custom JSON encoder.

Why did my alias not appear in output?

Aliases are used for input by default. Enable one of:

  • Config.serialize_by_alias = True
  • TO_DICT_ADD_BY_ALIAS_FLAG, then to_dict(by_alias=True)
  • A dialect with serialize_by_alias = True

The alias may come from field metadata, Annotated[..., Alias(...)], or Config.aliases in that precedence order.

Why was None not omitted in a nested model?

A dynamic omit_none=True flag only propagates through nested converters that support the option. Put TO_DICT_ADD_OMIT_NONE_FLAG on a shared base config or use fixed omit_none=True on the nested model/dialect.

Why is TOML different from to_dict()?

TOML has no null and has native date/time types. Its built-in dialect omits None and passes date/time objects through to tomli-w. This is deliberate format behavior, not a failed basic round trip.

Debugging workflow

  • Reduce the failure to to_dict()/from_dict() or a Basic codec first.
  • Print the exact annotated shape passed to the codec.
  • Enable Config.debug=True to inspect generated code.
  • Check field metadata, then model config, then active dialect.
  • Verify custom serialize/deserialize functions are true inverses.
  • Test the final format's constraints independently.
  • Reproduce under the oldest and newest supported Python versions if annotations are involved.
  • Turn the reproduction into a focused test before changing configuration.

Boundary error handling

At an HTTP or message-consumer boundary, a useful pattern is:

python
from mashumaro.exceptions import (
    ExtraKeysError,
    InvalidFieldValue,
    MissingDiscriminatorError,
    MissingField,
    SuitableVariantNotFoundError,
)


INPUT_ERRORS = (
    MissingField,
    ExtraKeysError,
    InvalidFieldValue,
    MissingDiscriminatorError,
    SuitableVariantNotFoundError,
)

Treat configuration exceptions such as UnserializableField, BadHookSignature, and BadDialect as deployment/programming failures rather than malformed user input.

Practical Recipes

These patterns combine the lower-level features into common application contracts. Each recipe keeps typed conversion, wire representation, and compatibility policy explicit.

Camel-case API fields

For a small stable model, declare aliases directly:

python
from dataclasses import dataclass
from typing import Annotated

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import Alias


@dataclass
class APIUser(DataClassDictMixin):
    user_id: Annotated[int, Alias("userId")]
    display_name: Annotated[str, Alias("displayName")]

    class Config(BaseConfig):
        serialize_by_alias = True
        allow_deserialization_not_by_alias = True


user = APIUser.from_dict({"userId": 42, "displayName": "Alice"})
assert user.to_dict() == {"userId": 42, "displayName": "Alice"}

Mashumaro deliberately uses an explicit alias mapping rather than guessing a naming convention. For a generated model set, create the Config.aliases dictionaries in your model-generation layer and snapshot-test them.

Rename a field without breaking old payloads

An alias plus allow_deserialization_not_by_alias supports two names: the external alias and current Python field name.

For more than two historical names, normalize them before deserialization:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin


@dataclass
class Customer(DataClassDictMixin):
    display_name: str

    @classmethod
    def __pre_deserialize__(cls, data):
        data = dict(data)
        for old_name in ("name", "full_name", "displayName"):
            if old_name in data and "display_name" not in data:
                data["display_name"] = data.pop(old_name)
        return data

Choose one canonical output name and stop emitting old names. Input compatibility can be wider than output compatibility.

Strict public payload, flexible internal model

Use a boundary wrapper with strict keys while keeping reusable nested domain objects permissive:

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig


@dataclass
class Address:
    city: str
    postal_code: str


@dataclass
class CreateUserRequest(DataClassDictMixin):
    name: str
    address: Address

    class Config(BaseConfig):
        forbid_extra_keys = True

Strictness on the root catches request typos. Add strict config to nested types too if unknown nested fields must be rejected.

Partial updates with TypedDict

A dataclass describes a constructible object and normally requires fields without defaults. A non-total TypedDict is a better shape for PATCH-style data:

python
from typing import TypedDict

from mashumaro.codecs.json import JSONDecoder


class UserPatch(TypedDict, total=False):
    display_name: str
    age: int
    active: bool


decode_patch = JSONDecoder(UserPatch)
patch = decode_patch.decode('{"age": "31"}')

assert patch == {"age": 31}

Apply the resulting keys to your domain object in a separate update layer where authorization and business validation live.

Unix timestamps

python
from dataclasses import dataclass
from datetime import datetime, timezone

from mashumaro import DataClassDictMixin
from mashumaro.types import SerializationStrategy


class UTCUnixTimestamp(
    SerializationStrategy, use_annotations=True
):
    def serialize(self, value: datetime) -> float:
        if value.tzinfo is None:
            raise ValueError("timezone-aware datetime required")
        return value.timestamp()

    def deserialize(self, value: float) -> datetime:
        return datetime.fromtimestamp(value, tz=timezone.utc)


@dataclass
class Event(DataClassDictMixin):
    at: datetime

    class Config:
        serialization_strategy = {datetime: UTCUnixTimestamp()}

Requiring aware datetimes avoids machine-local timezone behavior. The strategy uses datetime.timestamp() and datetime.fromtimestamp(); document whether fractional seconds are allowed and whether the number is seconds or milliseconds.

URL-safe Base64 without newlines

python
import base64
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.dialect import Dialect


class URLSafeBase64Dialect(Dialect):
    serialization_strategy = {
        bytes: {
            "serialize": lambda value: base64.urlsafe_b64encode(value).decode(
                "ascii"
            ),
            "deserialize": lambda value: base64.urlsafe_b64decode(
                value.encode("ascii")
            ),
        }
    }


@dataclass
class Token(DataClassDictMixin):
    raw: bytes

    class Config:
        dialect = URLSafeBase64Dialect


token = Token(b"\xfb\xff")
assert token.to_dict() == {"raw": "-_8="}
assert Token.from_dict(token.to_dict()) == token

This replaces the default Base64 encoder that includes a trailing newline with base64.urlsafe_b64encode() and base64.urlsafe_b64decode().

Public and internal representations

Use dialects instead of duplicate models when fields are identical but scalar representation differs:

python
from datetime import date

from mashumaro.dialect import Dialect


class PublicDialect(Dialect):
    serialization_strategy = {
        date: {
            "serialize": date.isoformat,
            "deserialize": date.fromisoformat,
        }
    }
    omit_none = True


class StorageDialect(Dialect):
    serialization_strategy = {
        date: {
            "serialize": date.toordinal,
            "deserialize": date.fromordinal,
        }
    }
    omit_none = False

Enable ADD_DIALECT_SUPPORT for call-time selection, or construct separate codecs with different default_dialect values.

Context-aware redaction

python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin
from mashumaro.config import ADD_SERIALIZATION_CONTEXT, BaseConfig


@dataclass
class Account(DataClassDictMixin):
    username: str
    email: str
    api_key: str

    class Config(BaseConfig):
        code_generation_options = [ADD_SERIALIZATION_CONTEXT]

    def __post_serialize__(self, data, context=None):
        audience = (context or {}).get("audience", "internal")
        if audience == "public":
            data.pop("email")
            data.pop("api_key")
        elif audience == "support":
            data["api_key"] = "***"
        return data

For a field that must never leave the process, prefer serialize="omit". Context redaction is appropriate only when multiple explicitly tested audiences are a real requirement.

Versioned event envelopes

Keep protocol versioning separate from variant tagging:

python
from dataclasses import dataclass
from typing import Annotated, Literal

from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import Discriminator


@dataclass
class UserCreated:
    type: Literal["user.created"] = "user.created"
    user_id: int = 0


@dataclass
class UserDeleted:
    type: Literal["user.deleted"] = "user.deleted"
    user_id: int = 0


Event = Annotated[
    UserCreated | UserDeleted,
    Discriminator(field="type", include_supertypes=True),
]


@dataclass
class Envelope(DataClassDictMixin):
    schema_version: Literal[1]
    event: Event

    class Config(BaseConfig):
        forbid_extra_keys = True

The Literal version selects an envelope migration; the discriminator selects an event class. Keep tag values stable and add a new version when representation meaning changes incompatibly.

Human-readable JSON with standard library

python
import json
from functools import partial

pretty_json = partial(
    json.dumps,
    indent=2,
    sort_keys=True,
    ensure_ascii=False,
)

text = model.to_json(encoder=pretty_json)

The example binds options with functools.partial and passes them to json.dumps. For codecs:

python
from mashumaro.codecs.json import JSONEncoder

encoder = JSONEncoder(Model, post_encoder_func=pretty_json)

The callable receives Mashumaro's basic form, so normal typed conversion is preserved.

Decimal money as fixed strings

python
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_EVEN

from mashumaro import DataClassDictMixin, field_options
from mashumaro.types import RoundedDecimal


@dataclass
class LineItem(DataClassDictMixin):
    total: Decimal = field(
        metadata=field_options(
            serialization_strategy=RoundedDecimal(
                places=2, rounding=ROUND_HALF_EVEN
            )
        )
    )

Rounding at serialization does not mutate the in-memory Decimal. The ROUND_HALF_EVEN mode resolves halfway cases toward the nearest even value. Decide whether deserialized values must already have the same scale and validate that rule separately.

Codec adapter around an existing parser

If a framework already parses the transport, use a Basic codec:

python
from mashumaro.codecs.basic import BasicDecoder, BasicEncoder

request_decoder = BasicDecoder(CreateUserRequest)
response_encoder = BasicEncoder(APIUser)

request = request_decoder.decode(framework_request_json)
framework_response_json = response_encoder.encode(response_model)

This avoids encoding JSON to text and parsing it again just to reach the typed conversion layer.

Compatibility tests

For a durable contract, test exact representations in both directions:

python
def test_user_wire_contract():
    wire = {"userId": 42, "displayName": "Alice"}
    model = APIUser(42, "Alice")

    assert APIUser.from_dict(wire) == model
    assert model.to_dict() == wire

Add fixtures from previous released versions, unknown keys/tags, missing optional fields, and boundary numeric/date values. Round-trip equality alone can miss an unintended but symmetrical wire-format change.

Performance

Mashumaro generates specialized Python functions for a concrete type shape. After compilation, conversion runs through direct code tailored to those fields and type arguments instead of repeatedly inspecting dataclasses and annotations.

When compilation happens

APIDefault compilation point
Dataclass mixinClass creation/import time
Mixin with lazy_compilation=TrueFirst serialization/deserialization call
Reusable codecEncoder/decoder construction
One-off codec functionEvery function call creates a disposable codec

This gives two distinct performance dimensions: setup latency and steady-state throughput.

Reuse codecs

The most important optimization for non-dataclass roots is to construct once:

python
from mashumaro.codecs.json import JSONDecoder, JSONEncoder

encoder = JSONEncoder(list[Event])
decoder = JSONDecoder(list[Event])


def publish(events: list[Event]) -> str:
    return encoder.encode(events)


def consume(payload: str) -> list[Event]:
    return decoder.decode(payload)

The convenient module functions are appropriate for one-time scripts:

python
from mashumaro.codecs.json import encode

payload = encode(events, list[Event])

Do not call a one-off function repeatedly for the same shape on a hot path; it regenerates a codec each time.

Eager versus lazy mixins

Eager compilation makes the first call fast and catches unsupported fields early. Lazy compilation reduces import work when an application declares many models but uses few of them:

python
class Config(BaseConfig):
    lazy_compilation = True

Choose based on application startup behavior:

  • CLI/serverless workloads may benefit from lazy models that are rarely touched.
  • Long-running services usually benefit from eager failure and predictable first-request latency.
  • If you choose lazy compilation, warm critical models during startup when latency spikes are unacceptable.

Measure total startup plus first-use cost; import time alone can hide deferred work.

Choose the format deliberately

The typed conversion stage and the final encoder both contribute to runtime.

RequirementCandidate
No dependency, maximum compatibilityStandard-library JSON
High JSON throughput, bytes outputorjson
Compact binary internal payloadMessagePack
Human-edited configurationTOML or YAML; optimize readability first
Framework already parsed JSONBasic encoder/decoder to avoid re-encoding

orjson can encode datetime, date, time, and UUID natively through its built-in dialect. MessagePack keeps bytes native. These paths can avoid intermediate conversions as well as using a faster final encoder.

Benchmark with your real shapes: deeply nested unions, large primitive lists, custom strategies, and many optional fields stress different parts of the pipeline.

Avoid unnecessary copies carefully

Dialect no_copy_collections can pass selected collection types through:

python
class InternalDialect(Dialect):
    no_copy_collections = (list, dict)

Use it only when elements need no conversion and the downstream consumer will not mutate input. Eliminating a shallow copy is valuable for large containers but irrelevant for small models, and aliasing mutable objects can be far more expensive than the allocation it saved.

Built-in orjson, TOML, and MessagePack dialects already select safe native-container paths for their encoders.

Prefer fixed behavior when it is fixed

Dynamic code-generation flags add per-call parameters and branches. If a model always omits None, use:

python
class Config(BaseConfig):
    omit_none = True

Enable TO_DICT_ADD_OMIT_NONE_FLAG only when callers genuinely switch behavior. The same applies to aliases and dialect selection.

This is a small optimization, but it also produces a simpler and more stable API.

Tagged unions scale better

An untagged union or discriminator may try multiple variants until one succeeds. A field discriminator maps a tag directly to the intended variant:

python
Discriminator(field="type", include_subtypes=True)

The benefit grows with the number and overlap of variants. Tagged payloads also make failure deterministic and easier to debug.

Custom strategy costs

Strategies are compiled into generated calls, but the callable's work still matters.

  • Reuse immutable strategy instances instead of allocating them per value.
  • Avoid reparsing constant format strings or calling re.compile() inside serialize().
  • Use annotation processing only when recursive conversion is needed.
  • Return the final simple representation directly when possible.
  • Keep hooks free of network, disk, and global-lock operations.

A Python lambda and a strategy method have similar call overhead in the hot path; choose the clearer reusable design first, then profile.

Omission and payload size

omit_none and omit_default reduce output size and final encoder work, but equality checks and default-factory evaluation have a cost. They are most useful for sparse models and network/storage payloads, not as a blanket micro-optimization.

Do not change omission policy only for speed if consumers distinguish missing from explicit null/default.

Sorting keys

sort_keys=True creates deterministic dictionary order but adds sorting work. Enable it for reproducible snapshots, signatures, or human comparison; leave it off on throughput-sensitive paths that do not require canonical ordering.

The final JSON encoder may sort again if configured separately. Avoid paying for both basic-form and encoder sorting unless nested model policy requires it.

Measure correctly

A useful benchmark separates:

  • Import/class construction.
  • Codec construction.
  • First lazy call.
  • Repeated encode/decode.
  • Typed conversion alone with Basic codecs.
  • Final format encoding alone.
  • Allocation/peak memory for large collections.

Use representative payloads and a benchmark runner such as pyperf; follow its guidance for reproducible runs and use enough processes to reduce noise. Compare exact semantics — bytes versus strings, validation strictness, omission, aliases, datetime handling, and unknown keys — before comparing numbers.

The repository benchmark uses real nested models and logarithmic charts. Results are workload- and configuration-dependent, so treat published comparisons as orientation rather than a guarantee for your application.

Performance checklist

  • Reuse encoders and decoders.
  • Avoid round-tripping through JSON when a framework already provides dictionaries.
  • Pick eager or lazy compilation based on first-use latency.
  • Use tagged discriminators for large polymorphic sets.
  • Select orjson/MessagePack when their boundary contract fits.
  • Benchmark before enabling no_copy_collections.
  • Keep strategies and hooks deterministic and local.
  • Test payload semantics before optimizing representation size.

API Reference

This is a compact map of the public entry points. Detailed behavior and examples live in the linked chapters.

Top-level imports

python
from mashumaro import (
    DataClassDictMixin,
    MissingField,
    field_options,
    pass_through,
)

DataClassDictMixin adds the basic-form methods. field_options() builds dataclass metadata. pass_through is the identity serialization strategy.

Format mixins intentionally live in their own modules:

python
from mashumaro.mixins.json import DataClassJSONMixin
from mashumaro.mixins.msgpack import DataClassMessagePackMixin
from mashumaro.mixins.orjson import DataClassORJSONMixin
from mashumaro.mixins.toml import DataClassTOMLMixin
from mashumaro.mixins.yaml import DataClassYAMLMixin

Mixin methods

Basic form

signature
obj.to_dict(**generated_options) -> dict
Model.from_dict(mapping, **generated_options) -> Model

Possible generated options are omit_none, by_alias, dialect, and serialization-only context; each requires its code generation flag.

Standard JSON

The default callables are the standard-library json.dumps and json.loads.

signature
obj.to_json(encoder=json.dumps, **to_dict_kwargs) -> str | bytes | bytearray
Model.from_json(data, decoder=json.loads, **from_dict_kwargs) -> Model

orjson

The orjson API defines the accepted options, native values, bytes return type, and errors.

signature
obj.to_jsonb(
    encoder=orjson.dumps,
    *,
    orjson_options=...,
    **to_dict_kwargs,
) -> bytes
obj.to_json(**kwargs) -> str
Model.from_json(data, decoder=orjson.loads, **from_dict_kwargs) -> Model

YAML

Default loaders and dumpers come from PyYAML.

signature
obj.to_yaml(encoder=default_encoder, **to_dict_kwargs) -> str | bytes
Model.from_yaml(data, decoder=default_decoder, **from_dict_kwargs) -> Model

TOML

Reading uses tomllib where available and writing uses tomli-w.

signature
obj.to_toml(encoder=tomli_w.dumps, **to_dict_kwargs) -> str
Model.from_toml(data, decoder=tomllib.loads, **from_dict_kwargs) -> Model

MessagePack

The defaults wrap the msgpack API.

signature
obj.to_msgpack(encoder=default_encoder, **to_dict_kwargs) -> bytes
Model.from_msgpack(data, decoder=default_decoder, **from_dict_kwargs) -> Model

See Supported Formats for dependencies and native representations.

Reusable codecs

Basic

signature
from mashumaro.codecs import BasicDecoder, BasicEncoder

BasicDecoder(
    shape_type,
    *,
    default_dialect=None,
    pre_decoder_func=None,
)
BasicEncoder(
    shape_type,
    *,
    default_dialect=None,
    post_encoder_func=None,
)

Instances expose .decode(data) and .encode(obj).

Standard JSON

signature
from mashumaro.codecs.json import JSONDecoder, JSONEncoder

JSONDecoder(
    shape_type,
    *,
    default_dialect=None,
    pre_decoder_func=json.loads,
)
JSONEncoder(
    shape_type,
    *,
    default_dialect=None,
    post_encoder_func=json.dumps,
)

orjson

signature
from mashumaro.codecs.orjson import ORJSONDecoder, ORJSONEncoder

ORJSONDecoder(shape_type, *, default_dialect=None)
ORJSONEncoder(shape_type, *, default_dialect=None)

The encoder returns bytes. The built-in orjson parser/renderer is fixed in the codec.

YAML

signature
from mashumaro.codecs.yaml import YAMLDecoder, YAMLEncoder

YAMLDecoder(shape_type, *, default_dialect=None, pre_decoder_func=...)
YAMLEncoder(shape_type, *, default_dialect=None, post_encoder_func=...)

TOML

signature
from mashumaro.codecs.toml import TOMLDecoder, TOMLEncoder

TOMLDecoder(shape_type, *, default_dialect=None)
TOMLEncoder(shape_type, *, default_dialect=None)

MessagePack

signature
from mashumaro.codecs.msgpack import (
    MessagePackDecoder,
    MessagePackEncoder,
)

MessagePackDecoder(
    shape_type, *, default_dialect=None, pre_decoder_func=...
)
MessagePackEncoder(
    shape_type, *, default_dialect=None, post_encoder_func=...
)

One-off codec functions

ModuleNamed functionsShort aliases
mashumaro.codecs.basicencode, decodeSame
mashumaro.codecs.jsonjson_encode, json_decodeencode, decode
mashumaro.codecs.orjsonjson_encode, json_decodeencode, decode
mashumaro.codecs.yamlyaml_encode, yaml_decodeencode, decode
mashumaro.codecs.tomltoml_encode, toml_decodeencode, decode
mashumaro.codecs.msgpackmsgpack_encode, msgpack_decodeencode, decode

Every encode function takes (obj, shape_type) and every decode function takes (data, shape_type). Standard JSON additionally accepts its transform function as an optional third argument.

Configuration

python
from mashumaro.config import (
    ADD_DIALECT_SUPPORT,
    ADD_SERIALIZATION_CONTEXT,
    TO_DICT_ADD_BY_ALIAS_FLAG,
    TO_DICT_ADD_OMIT_NONE_FLAG,
    BaseConfig,
)

BaseConfig attributes:

  • debug
  • code_generation_options
  • serialization_strategy
  • aliases
  • serialize_by_alias
  • namedtuple_as_dict
  • allow_postponed_evaluation
  • dialect
  • omit_none
  • omit_default
  • orjson_options
  • json_schema
  • discriminator
  • lazy_compilation
  • sort_keys
  • allow_deserialization_not_by_alias
  • forbid_extra_keys

See Config Options for defaults and precedence.

Dialect

python
from mashumaro.dialect import Dialect

Subclass attributes are serialization_strategy, serialize_by_alias, namedtuple_as_dict, omit_none, omit_default, and no_copy_collections. Dialect.merge(other) returns a new dialect class; see Dialects.

Extension types

python
from mashumaro.types import (
    Alias,
    Discriminator,
    GenericSerializableType,
    RoundedDecimal,
    SerializableType,
    SerializationStrategy,
)

SerializableType

python
class Custom(SerializableType, use_annotations=False):
    def _serialize(self): ...

    @classmethod
    def _deserialize(cls, value): ...

GenericSerializableType

python
def _serialize(self, types): ...

@classmethod
def _deserialize(cls, value, types): ...

SerializationStrategy

python
class Strategy(
    SerializationStrategy,
    use_annotations=False,
    match_subclasses=False,
):
    def serialize(self, value): ...
    def deserialize(self, value): ...

RoundedDecimal

signature
RoundedDecimal(places: int | None = None, rounding: str | None = None)

Discriminator

signature
Discriminator(
    field: str | None = None,
    include_supertypes: bool = False,
    include_subtypes: bool = False,
    variant_tagger_fn=None,
)

Alias

python
Annotated[int, Alias("externalName")]

Annotated preserves the underlying static type while carrying Mashumaro's runtime metadata.

Field helper

signature
from mashumaro import field_options

field_options(
    serialize=None,
    deserialize=None,
    serialization_strategy=None,
    alias=None,
    **extra_metadata,
) -> dict

See Field Options.

JSON Schema

The supported targets are JSON Schema Draft 2020-12 and the OpenAPI 3.1 Schema Object.

python
from mashumaro.jsonschema import (
    DRAFT_2020_12,
    OPEN_API_3_1,
    JSONSchemaBuilder,
    build_json_schema,
)
python
build_json_schema(
    instance_type,
    context=None,
    with_definitions=True,
    all_refs=None,
    with_dialect_uri=False,
    dialect=None,
    ref_prefix=None,
    plugins=(),
)
python
JSONSchemaBuilder(
    dialect=DRAFT_2020_12,
    all_refs=None,
    ref_prefix=None,
    plugins=(),
)

The builder exposes .build(instance_type) and .get_definitions().

Exceptions

Import exceptions from mashumaro.exceptions. Their attributes and recommended handling are documented in Errors and Troubleshooting.

Stable versus internal modules

Build integrations from the public modules shown here. Names under mashumaro.core and generated private methods are implementation details and may change without serving as a user-facing extension contract.

Migration and Compatibility

Mashumaro follows Semantic Versioning 2.0.0. Review the GitHub Releases notes before upgrading across a major version and keep exact wire-contract tests for serialized data that outlives a deployment.

Supported Python versions

The current 3.22 release supports Python 3.10–3.14.

Retired PythonLast compatible mashumaro
3.93.20
3.83.14
3.73.9.1
3.63.1.1

If an application must stay on an end-of-life interpreter, pin both the library and optional format dependencies. The preferred migration is to upgrade Python and then use the current Mashumaro release.

Migrating from version 2 to 3

Format mixin imports moved

Use format-specific modules:

python
from mashumaro.mixins.json import DataClassJSONMixin
from mashumaro.mixins.msgpack import DataClassMessagePackMixin
from mashumaro.mixins.yaml import DataClassYAMLMixin

DataClassDictMixin remains available from mashumaro.

use_bytes was removed

Replace pass-through behavior with a dialect:

python
from mashumaro import pass_through
from mashumaro.dialect import Dialect


class BytesDialect(Dialect):
    serialization_strategy = {
        bytes: pass_through,
        bytearray: pass_through,
    }

Enable ADD_DIALECT_SUPPORT for call-time use or set Config.dialect. Ensure the final encoder supports raw binary; standard JSON does not.

use_enum was removed

Use a strategy/dialect with pass_through when the downstream format accepts enum objects, or define an explicit by-name/by-value strategy. Default Mashumaro behavior serializes Enum.value.

use_datetime was removed

Use a dialect that passes datetime, date, and time through. TOML and orjson already ship format dialects with appropriate native handling. For JSON, prefer an explicit textual or numeric strategy because json.dumps does not accept datetime objects by default.

Format method signatures changed

Version 2 accepted dict_params and arbitrary final encoder/decoder keyword arguments. Version 3 forwards extra method keywords to the generated dictionary conversion layer:

python
Model.from_json(data, decoder=custom_decoder, **from_dict_kwargs)
model.to_json(encoder=custom_encoder, **to_dict_kwargs)

Bind format-library options with a lambda or functools.partial:

python
import json
from decimal import Decimal
from functools import partial

loads_decimal = partial(json.loads, parse_float=Decimal)
dumps_unicode = partial(json.dumps, ensure_ascii=False)

model = Model.from_json(data, decoder=loads_decimal)
text = model.to_json(encoder=dumps_unicode)

Evolving a wire model safely

Add a field

Give a new field a dataclass default or default factory so old payloads remain readable:

python
@dataclass
class User(DataClassDictMixin):
    name: str
    tags: list[str] = field(default_factory=list)

Whether old readers tolerate the new output depends on their unknown-key policy. Coordinate forbid_extra_keys rollouts.

Rename a field

Keep one stable output alias and accept the Python name during migration:

python
class Config(BaseConfig):
    aliases = {"display_name": "displayName"}
    serialize_by_alias = True
    allow_deserialization_not_by_alias = True

For several historical names, normalize them in __pre_deserialize__.

Change a field representation

A date string changed to epoch seconds is a breaking wire change even if the Python field remains datetime. Introduce a protocol version or a new dialect, read both representations during a defined transition, and emit only the new canonical representation.

Add a polymorphic variant

Tagged discriminators make this additive for new readers, but old readers still reject unknown tags unless they have a supertype fallback. Decide explicitly whether unknown variants should fail, be stored raw, or map to a base model.

Tighten unknown-key handling

Enabling forbid_extra_keys is behaviorally breaking for payloads that previously contained ignored fields. Audit real traffic or stored fixtures first.

Compatibility test suite

Keep fixtures for every supported protocol/storage version and test:

  • Old payload → current model.
  • Current model → exact current payload.
  • Missing newly optional fields.
  • Legacy aliases and canonical output.
  • Unknown fields and discriminator tags.
  • Boundary dates, decimals, bytes, and enum values.
  • Generated JSON Schema snapshots.
  • Python 3.10 and 3.14 at minimum when modern annotations are used.

Exact output assertions catch symmetrical representation changes that a simple decode(encode(x)) == x round trip will miss.

Upgrade checklist

  • Read release notes and Python requirement changes.
  • Install all optional format extras in a clean environment.
  • Run tests with eager compilation so schema errors fail early.
  • Run the oldest/newest supported Python jobs.
  • Rebuild JSON Schema and compare contract snapshots.
  • Exercise stored production fixtures and unknown-key policy.
  • Benchmark hot codecs after, not before, semantic compatibility is confirmed.