mrkeyoor.com_
Thu 06 Aug 10:56 UTC
PyPIUtilsupdated 06 Aug 2026

dataclasses-json

dataclasses-json bolts JSON serialization onto plain stdlib dataclasses. You add a @dataclass_json decorator above @dataclass, or inherit from DataClassJsonMixin, and the class gains four methods: to_json, from_json, to_dict, and from_dict. It walks nested dataclasses, lists, dicts, and other collections, and knows how to encode datetime, UUID, and Decimal without you writing a custom JSONEncoder. Field-level metadata controls renaming, camelCase conversion, per-field encoder and decoder callables, and what happens to keys in the JSON that your class does not declare. A separate .schema() method generates a marshmallow schema from the same class, which is where actual type validation lives; the plain from_json path does not validate anything and will happily hand you a Person whose name is the integer 42.

Verdict

A convenient layer over dataclasses that solved a real problem in 2018 and still works, but it has no release since June 2024, no official Python 3.13 support, a hard cap on marshmallow 3, and no validation on the path most people use. Fine to keep in a working codebase; hard to justify for something new when cattrs, mashumaro, and pydantic all cover the same ground.

API stability4/5The public surface (dataclass_json, DataClassJsonMixin, config, LetterCase, Undefined, CatchAll, global_config) has not moved in years, so upgrades within 0.6.x are uneventful. It is formally pre-1.0 with a documented convention that minor bumps may break things, and the stability here is at least partly the stability of a project that is not changing much.
Docs4/5The README is genuinely good: runnable examples for every feature, an explicit section on unknown fields with all three strategies, warnings about the datetime asymmetry and about deferred annotations, and a Python compatibility table. It is also the entire documentation, mirrored to a GitHub Pages site, with no API reference beyond it.
Maintenance2/5Last PyPI release 0.6.7 in June 2024, more than two years ago, despite repository pushes as recently as May 2026. 129 open issues (165 counting PRs), the README still lists Python 3.13 and later as unsupported, and the marshmallow dependency is capped below the current major version. It works, but nobody should plan on a fix arriving.
Ecosystem3/5Around 15.8 million downloads a week, which reflects how many older projects and libraries list it as a dependency rather than fresh adoption. There is no plugin ecosystem, and the interop story is limited to the generated marshmallow schema, which ties you to marshmallow 3.

Use it if

  • You already model your data as stdlib dataclasses and want them on and off the wire without rewriting them as pydantic models
  • Your JSON is camelCase and your Python is snake_case, and you want that mapping declared once at the class or field level rather than in every call site
  • You are consuming an API that adds fields without warning and want them captured in a CatchAll field or rejected loudly, chosen per class
  • You want zero-cost objects at runtime: these are still ordinary dataclasses with no validation layer on attribute access, so instances behave exactly like the ones you already have
  • Your project cannot take on pydantic's compiled Rust core, for example because you ship to a platform with no wheel
Skip it if

Setup reality

pip install dataclasses-json is quick and pure Python, but it brings marshmallow and typing-inspect with it, and the marshmallow pin is <4.0.0, which becomes a resolver conflict the moment anything else in your tree wants marshmallow 4. After that the friction is behavioural rather than mechanical. Decorator order matters: @dataclass_json goes above @dataclass or nothing is generated. Type checkers do not see the four methods the decorator adds, so mypy and pyright report to_json as unknown unless you use the DataClassJsonMixin inheritance style instead. Datetimes round-trip as float timestamps by default, and because a naive datetime is interpreted in system local time on the way out and comes back timezone-aware, encode and decode are not inverses until you override the codec. Recursive types need a quoted forward reference such as Optional['Tree'], and you must not reach for from __future__ import annotations to avoid the quotes because the library cannot read deferred annotations. None of these are hard, but each one costs somebody an afternoon the first time.

Patterns

Add JSON methods to a dataclassbasic-roundtrip

from dataclasses import dataclass
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass
class Person:
    name: str
    age: int = 0

p = Person("Ada", 36)
p.to_json()                    # '{"name": "Ada", "age": 36}'
p.to_dict()                    # {'name': 'Ada', 'age': 36}
Person.from_json('{"name": "Ada", "age": 36}')
Person.from_dict({"name": "Ada"})   # Person(name='Ada', age=0)

@dataclass_json must sit above @dataclass; reversed, the decorator sees a plain class and nothing is generated. from_json performs no type checking, so Person.from_json('{"name": 42}') returns a Person whose name is an int and fails much later.

Use the mixin so mypy and pyright see the methodsmixin-for-type-checkers

from dataclasses import dataclass
from dataclasses_json import DataClassJsonMixin

@dataclass
class Person(DataClassJsonMixin):
    name: str

assert Person.from_json(Person("Ada").to_json()) == Person("Ada")

Runtime behaviour is identical to the decorator, but static analysis only understands the inheritance form. With the decorator, every call to obj.to_json() is an unresolved attribute as far as your type checker is concerned.

Map snake_case fields to camelCase JSONcamel-case-json

from dataclasses import dataclass, field
from dataclasses_json import LetterCase, config, dataclass_json

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Person:
    given_name: str
    family_name: str

Person("Alice", "Liddell").to_json()  # '{"givenName": "Alice", "familyName": "Liddell"}'

# or per field
@dataclass_json
@dataclass
class Mixed:
    given_name: str = field(metadata=config(letter_case=LetterCase.CAMEL))
    family_name: str

LetterCase offers CAMEL, KEBAB, SNAKE, and PASCAL. The conversion assumes your Python field names are already snake_case; the README says behaviour is undefined if they are not, which in practice means quietly wrong key names.

Rename one field, keep another out of the outputrename-and-exclude-fields

from dataclasses import dataclass, field
from dataclasses_json import Exclude, config, dataclass_json

@dataclass_json
@dataclass
class User:
    user_id: str = field(metadata=config(field_name="id"))
    password_hash: str = field(metadata=config(exclude=Exclude.ALWAYS))

User("u1", "secret").to_dict()   # {'id': 'u1'}
User.from_dict({"id": "u1", "password_hash": "secret"})

exclude affects only the encoding direction. The field is still read back on decode and still required by __init__, so excluding a field without a default makes round-tripping your own output fail.

Nested dataclasses and lists of themnested-and-collections

from dataclasses import dataclass
from typing import List
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass(frozen=True)
class Minion:
    name: str

@dataclass_json
@dataclass(frozen=True)
class Boss:
    minions: List[Minion]

Boss([Minion("a"), Minion("b")]).to_json(indent=4)

# many=True for a bare JSON array of one class
Minion.schema().dumps([Minion("a")], many=True)
Minion.schema().loads('[{"name": "a"}]', many=True)

Nested classes need the decorator too. to_json passes extra keyword arguments such as indent and sort_keys straight through to json.dumps. Use the .schema() path when the top level of the document is an array rather than an object.

Decide what a missing key meansmissing-and-optional-fields

from dataclasses import dataclass
from typing import Optional
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass
class Student:
    id: int
    name: str = "student"

Student.from_json('{"id": 1}')            # Student(id=1, name='student')

@dataclass_json
@dataclass
class Tutor:
    id: int
    student: Optional[Student] = None

Tutor.from_json('{"id": 1}', infer_missing=True)

Ordinary dataclass defaults already cover the common case. infer_missing fills any absent field with None regardless of its declared type, so an Optional-typed field without a default gets None while a non-Optional int field can also silently become None.

Control what happens to keys you did not declareunknown-fields

from dataclasses import dataclass
from typing import Any, Dict
from dataclasses_json import CatchAll, Undefined, dataclass_json

@dataclass_json(undefined=Undefined.RAISE)
@dataclass
class Strict:
    endpoint: str

@dataclass_json(undefined=Undefined.INCLUDE)
@dataclass
class Tolerant:
    endpoint: str
    data: Dict[str, Any]
    extras: CatchAll

Tolerant.from_dict({"endpoint": "/v1", "data": {}, "whatever": [1, 2]})
# Tolerant(endpoint='/v1', data={}, extras={'whatever': [1, 2]})

The default with no undefined setting is inconsistent: from_dict ignores unknown keys while the .schema() path raises. Undefined.INCLUDE requires exactly one CatchAll field or you get an UndefinedParameterError, and LetterCase conversion is not applied to keys landing in it.

Get actual validation through the marshmallow schemavalidate-with-schema

from dataclasses import dataclass
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass
class Person:
    name: str

Person.from_json('{"name": 42}')        # no error, name is an int
Person.schema().loads('{"name": 42}')   # marshmallow ValidationError

# cache it: .schema() rebuilds the schema on every call
person_schema = Person.schema()
person_schema.dump(people, many=True)

This is the only path that type-checks input, and it is the slow one. Building the schema is not cached, so calling Person.schema() inside a loop over a nested class regenerates the whole tree each time. Bind it to a module-level variable once.

Encode datetimes as ISO strings instead of timestampsiso-datetimes

from dataclasses import dataclass, field
from datetime import datetime
from marshmallow import fields
from dataclasses_json import config, dataclass_json

@dataclass_json
@dataclass
class Event:
    created_at: datetime = field(
        metadata=config(
            encoder=datetime.isoformat,
            decoder=datetime.fromisoformat,
            mm_field=fields.DateTime(format="iso"),
        )
    )

The default is a float epoch timestamp, and a naive datetime is converted using the machine's local timezone on the way out then decoded back as timezone-aware, so the value you get is not the value you gave. Set all three of encoder, decoder, and mm_field or the .schema() path disagrees with to_json.

Register a codec for a type once, project wideglobal-codec-override

from dataclasses import dataclass
from datetime import date
import dataclasses_json
from dataclasses_json import dataclass_json

dataclasses_json.global_config.encoders[date] = date.isoformat
dataclasses_json.global_config.decoders[date] = date.fromisoformat

@dataclass_json
@dataclass
class Record:
    created_on: date

This is process-global mutable state, so it has to run before the decorated classes are imported and it changes behaviour for every library in the process that also uses dataclasses-json. Put it in one module that everything imports early, not scattered across call sites.

Support numpy or pandas values with per-field hookscustom-field-codecs

from dataclasses import dataclass, field
import numpy as np
import pandas as pd
from dataclasses_json import config, dataclass_json

@dataclass_json
@dataclass
class Batch:
    values: np.ndarray = field(metadata=config(decoder=np.asarray))
    frame: pd.DataFrame = field(
        metadata=config(
            decoder=pd.DataFrame.from_records,
            encoder=lambda df: df.to_dict(orient="records"),
        )
    )

Third-party types are not supported out of the box, and a missing encoder fails at dump time rather than at class definition, so a rarely-taken branch can surface the error in production. The hooks apply to to_json and to_dict alike.

Declare a self-referencing typerecursive-dataclass

from dataclasses import dataclass
from typing import Optional
from dataclasses_json import dataclass_json

@dataclass_json
@dataclass
class Tree:
    value: str
    left: Optional["Tree"] = None
    right: Optional["Tree"] = None

Tree.from_json('{"value": "a", "left": {"value": "b"}}')

The quoted forward reference is required, and the README explicitly says not to use from __future__ import annotations as a shortcut: the library reads annotations at runtime and deferred ones break it. That constraint applies to the whole module, not just the recursive class.

Alternatives

PackageRegistryPick it when
pydanticPyPIYou want validation, coercion, JSON Schema, and a maintained core; this is the default choice for new code parsing external data.
msgspecPyPISpeed matters and you can define Struct classes; it validates on decode and is dramatically faster for JSON and MessagePack.
cattrsPyPIYou want to keep plain dataclasses or attrs classes untouched and configure structuring and unstructuring from outside, with no decorator on the class.
mashumaroPyPIYou want the same decorator-on-a-dataclass ergonomics but with code generated at class creation, so serialization is much faster than runtime type walking.