mashumaro
mashumaro serialises and deserialises ordinary Python dataclasses, and it does it by generating source code. When your class is created, mashumaro reads the type annotations and writes a from_dict and a to_dict function tailored to exactly those fields, then compiles them and attaches them to the class. Nothing is inspected at call time, so decoding a nested structure costs roughly what a hand-written loop would cost. You opt in either by inheriting a mixin such as DataClassDictMixin or DataClassJSONMixin, or by building a standalone codec object for any shape you like, including List[Dict[str, MyClass]] or a bare TypedDict that owns no mixin. Built-in codecs cover plain dicts, the standard json module, orjson, YAML, TOML and MessagePack, all sharing the same type handling. It is a converter, not a validator: it turns wire data into the annotated types and complains when it cannot, but it does not enforce field constraints or collect a list of errors for you.
If your domain objects are already dataclasses and serialisation shows up in your profiles, mashumaro is the fastest way to keep them plain and still get fast codecs for five wire formats. Do not reach for it as a pydantic replacement at an API boundary, because it converts types without validating them.
Use it if
- Your models are already plain dataclasses, NamedTuples or TypedDicts and you want serialisation without rewriting them as a model framework's base class
- Decoding sits in a hot path where per-record overhead shows up in profiles, for example a worker chewing through millions of JSON lines or MessagePack frames
- You need one type-handling story across several wire formats: the same dataclass round-trips through json, orjson, YAML, TOML and MessagePack with the same field options
- You have to serialise third-party types you cannot modify, which is what SerializationStrategy exists for, or tagged unions of subclasses, which is what Discriminator exists for
- You want to decode shapes that are not dataclasses at all, since BasicDecoder and JSONDecoder accept any typing construct rather than requiring a model class
- You need validation at a trust boundary. mashumaro converts values to the annotated type and raises InvalidFieldValue when that fails, but it does not check ranges, lengths or patterns, and it does not return a structured list of every error in the payload. The Minimum and MaxLength style annotations only feed the generated JSON Schema; from_dict ignores them. Public API input wants pydantic.
- You dislike code generation. The from_dict body is synthesised and compiled, so a traceback points into generated source and reading it means switching on the debug config option to print what was written. Static analysers also cannot see the methods the mixin adds.
- Import time matters and you have hundreds of models. Mixin-based classes compile their methods at import, which is exactly why the lazy_compilation config option exists; you should not have to reach for it, but on a large model package you will.
- You want a small surface to learn. Field options, config options, dialects, discriminators, serialization strategies, code generation flags and JSON Schema annotations are six overlapping ways to change behaviour, all documented on one very long README page.
- You already run pydantic elsewhere in the service. Two serialisation systems over the same domain objects means two sets of aliasing rules and two failure modes, and the speed win rarely pays for that unless you are actually CPU bound.
- You need a large maintainer bench. This is effectively a one-person project at 933 stars, and the biggest downstream consumer, dbt-core, currently pins mashumaro[msgpack] below 3.18 rather than tracking the current release.
Setup reality
pip install mashumaro is genuinely light: the only hard dependency is typing_extensions 4.14 or newer, and Python 3.10 or newer is required. Wire formats come as extras, so you want mashumaro[orjson], mashumaro[msgpack], mashumaro[yaml] or mashumaro[toml] depending on what you actually encode, and asking for DataClassORJSONMixin without the extra fails at import. The first real trip hazard is that the mixin does not make your class a dataclass. You still write @dataclass yourself above it, and forgetting produces a class whose generated methods see no fields at all. The second is forward references: because code is generated when the class is created, a type name that is not resolvable at that moment raises UnresolvedTypeReferenceError, which bites as soon as you add from __future__ import annotations or define a model inside a function. The allow_postponed_evaluation config option defers resolution to first call and is the usual fix. The third is startup cost: with mixins, every model compiles its methods at import time, so a package with hundreds of dataclasses adds measurable seconds to process boot until you enable lazy_compilation or move to codecs, which compile when the codec is constructed.
Patterns
Round-trip a dataclass through a plain dictdataclass-to-dict
from dataclasses import dataclass
from datetime import datetime
from mashumaro import DataClassDictMixin
@dataclass
class Event(DataClassDictMixin):
id: int
name: str
at: datetime
Event.from_dict({"id": 1, "name": "push", "at": "2026-08-07T10:00:00"})
Event(1, "push", datetime.now()).to_dict()The @dataclass decorator is still yours to write; the mixin only adds the generated methods. Leave it off and you get a class with no fields, from_dict returning an empty instance, and no error to tell you why.
Encode and decode JSON strings directlyjson-mixin
from dataclasses import dataclass
from mashumaro.mixins.json import DataClassJSONMixin
@dataclass
class User(DataClassJSONMixin):
id: int
email: str
User.from_json('{"id": 1, "email": "a@b.c"}')
User(1, "a@b.c").to_json()DataClassJSONMixin already inherits DataClassDictMixin, so you get from_dict and to_dict for free and should not list both mixins. It uses the standard library json module; swap to the orjson mixin if encoding shows up in your profile.
Build a codec for a shape that is not a dataclassreusable-codec
from typing import Dict, List
from mashumaro.codecs.json import JSONDecoder, JSONEncoder
decoder = JSONDecoder(Dict[str, List[Event]])
encoder = JSONEncoder(Dict[str, List[Event]])
data = decoder.decode(payload)
wire = encoder.encode(data)Build the decoder once at module scope and reuse it. The convenience functions such as json_decode(payload, SomeType) construct a throwaway decoder on every call, which means recompiling the generated code each time and losing the entire performance argument for using this library.
Map wire names onto Python namesfield-aliases
from dataclasses import dataclass, field
from mashumaro import DataClassDictMixin, field_options
from mashumaro.config import BaseConfig
@dataclass
class Item(DataClassDictMixin):
item_id: int = field(metadata=field_options(alias="itemId"))
unit_price: float = 0.0
class Config(BaseConfig):
aliases = {"unit_price": "unitPrice"}
serialize_by_alias = True
allow_deserialization_not_by_alias = TrueAliases apply to input only until you set serialize_by_alias, so the default behaviour is asymmetric: from_dict wants itemId but to_dict emits item_id. allow_deserialization_not_by_alias is what lets both spellings decode, which matters when you are migrating a payload format.
Drop None and default values from outputomit-empty-fields
from dataclasses import dataclass
from typing import Optional
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
@dataclass
class Patch(DataClassDictMixin):
name: Optional[str] = None
retries: int = 3
class Config(BaseConfig):
omit_none = True
omit_default = True
Patch().to_dict() # {}omit_default compares against the declared default or the default_factory result on every serialisation, so it is not free on wide models. These are class-level switches; if you need it decided per call, add TO_DICT_ADD_OMIT_NONE_FLAG to code_generation_options and pass to_dict(omit_none=True).
Teach it a type you do not owncustom-serialization-strategy
from dataclasses import dataclass
from decimal import Decimal
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
from mashumaro.types import SerializationStrategy
class DecimalAsString(SerializationStrategy):
def serialize(self, value: Decimal) -> str:
return format(value, "f")
def deserialize(self, value: str) -> Decimal:
return Decimal(value)
@dataclass
class Order(DataClassDictMixin):
total: Decimal
class Config(BaseConfig):
serialization_strategy = {Decimal: DecimalAsString()}Registering the strategy in Config applies it to every Decimal field on the class and anything it inherits, which is what you want for money. For a one-off field use field_options(serialization_strategy=...) instead, and remember the field option always beats the config entry.
Decode a subclass hierarchy by a tag fielddiscriminated-union
from dataclasses import dataclass
from typing import Annotated, List
from mashumaro import DataClassDictMixin
from mashumaro.types import Discriminator
@dataclass
class Message(DataClassDictMixin):
pass
@dataclass
class Text(Message):
type = "text"
body: str
@dataclass
class Image(Message):
type = "image"
url: str
@dataclass
class Batch(DataClassDictMixin):
items: List[Annotated[Message, Discriminator(field="type", include_subtypes=True)]]The tag attribute has to be defined on the subclass itself, because mashumaro reads it from that class's __dict__; a subclass that inherits the tag rather than declaring it is skipped. Only Final, Literal and StrEnum tag annotations are written back out during serialisation, so a bare class attribute decodes but does not re-encode.
Use orjson for the fast pathorjson-codec
# pip install mashumaro[orjson]
from dataclasses import dataclass
from datetime import datetime
from mashumaro.mixins.orjson import DataClassORJSONMixin
@dataclass
class Tick(DataClassORJSONMixin):
symbol: str
at: datetime
Tick.from_json(raw)
Tick("AAPL", datetime.now()).to_jsonb() # bytes, no decode steporjson handles datetime, date, time and UUID natively here, so those fields skip mashumaro's own conversion and the output format is orjson's rather than isoformat() through the standard mixin. to_jsonb returns bytes and is the one to use when the next hop is a socket or a Kafka producer.
Encode to MessagePackmsgpack-codec
# pip install mashumaro[msgpack]
from dataclasses import dataclass
from mashumaro.mixins.msgpack import DataClassMessagePackMixin
@dataclass
class Frame(DataClassMessagePackMixin):
seq: int
payload: bytes
blob = Frame(1, b"\x00\x01").to_msgpack()
Frame.from_msgpack(blob)MessagePack keeps bytes fields as bytes instead of base64-encoding them the way the JSON path has to, which is the main reason to pick it. This is also the exact extra dbt-core depends on, so if you are inside a dbt plugin the package is already installed.
Reject payloads with unknown fieldsforbid-extra-keys
from dataclasses import dataclass
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
@dataclass
class Config_(DataClassDictMixin):
host: str
class Config(BaseConfig):
forbid_extra_keys = True
Config_.from_dict({"host": "a", "prot": 80}) # ExtraKeysErrorOff by default, so a typo in a config file or a renamed API field is silently dropped rather than reported. Turning it on is the closest thing this library has to validation, and it is worth doing on anything humans hand-edit.
Normalise data before and after conversionserialization-hooks
from dataclasses import dataclass
from typing import Any, Dict
from mashumaro import DataClassDictMixin
@dataclass
class Legacy(DataClassDictMixin):
user_id: int
@classmethod
def __pre_deserialize__(cls, d: Dict[Any, Any]) -> Dict[Any, Any]:
return {k.lower(): v for k, v in d.items()}
def __post_serialize__(self, d: Dict[Any, Any]) -> Dict[Any, Any]:
d.pop("internal", None)
return dThe four hooks are __pre_deserialize__ and __post_deserialize__ as classmethods, __pre_serialize__ and __post_serialize__ as instance methods. They run on every call, so a dict comprehension over the whole payload in __pre_deserialize__ undoes a good chunk of the speed you came for.
See what was generated, and defer generating itinspect-generated-code
from dataclasses import dataclass
from mashumaro import DataClassDictMixin
from mashumaro.config import BaseConfig
@dataclass
class Model(DataClassDictMixin):
x: int
class Config(BaseConfig):
debug = True # prints the generated from_dict/to_dict
lazy_compilation = True # compile on first call, not at importdebug is the only practical way to understand a confusing traceback, since the frame it points at is synthesised source with no file on disk. With lazy_compilation on, do not stash a reference to Model.from_dict at import time: the real method does not exist yet, so capture it in a lambda instead.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pydantic | PyPI | You need real validation with field constraints and error reports, or you want the framework integrations that come with it |
| msgspec | PyPI | You want the speed plus schema validation in a C extension and can accept a smaller set of supported Python types |
| cattrs | PyPI | You prefer converters configured outside the class over generated methods attached to it, especially with attrs classes |