# pyserde: dataclass serialization with generated code instead of runtime reflection

> pyserde turns an ordinary dataclass into a serializer by decorating it and generating code for it, in the spirit of serde-rs. It covers JSON, YAML, TOML, MsgPack, Pickle and dict, supports a wide set of typing constructs, and requires Python 3.11 or newer.

**yukinarit/pyserde** — Yet another serialization library on top of dataclasses, inspired by serde-rs.

- Repository: https://github.com/yukinarit/pyserde
- Website: https://yukinarit.github.io/pyserde
- Stars: 852 · Forks: 66
- Language: Python
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/yukinarit-pyserde

## A decorator over a dataclass is the entire public surface

pyserde is a serialization library built on top of dataclasses, and it describes itself as yet another serialization library inspired by serde-rs. The usage pattern is one decorator and type annotations.

```python
@serde
class Foo:
    i: int
    s: str
    f: float
    b: bool
```

Once the class is decorated, the conversion functions exist and work in both directions.

```python
>>> to_json(Foo(i=10, s='foo', f=100.0, b=True))
'{"i":10,"s":"foo","f":100.0,"b":true}'
```

```python
>>> from_json(Foo, '{"i": 10, "s": "foo", "f": 100.0, "b": true}')
Foo(i=10, s='foo', f=100.0, b=True)
```

Notice that the class is not written with the @dataclass decorator in the example, and notice the argument order of from_json: the target class comes first, then the payload. Those two details are the shape of the whole library. The annotations are PEP 484 style, so the serialized field set is derived from the type hints rather than from an explicit schema.

The audience is a Python team that has already decided dataclasses are the data model and does not want to hand-write to_dict and from_dict for every class. The value proposition is that adding a field to the dataclass adds it to the serialized form, with no second declaration to keep in step.

## The type coverage is where the effort went

The feature list is long, and the type list is the part that tells you where the library has been exercised. Primitives are supported, and so are the container abstractions rather than only the concrete types: list alongside collections.abc.Sequence and MutableSequence, tuple, set alongside Set and MutableSet, and dict alongside Mapping and MutableMapping. frozenset, defaultdict, deque and Counter are called out by name.

The typing constructs are covered too. Optional, Union, dataclass-typed fields, NewType over primitives, Any, Literal, Generic, ClassVar and InitVar are all listed as supported.

The standard library types are supported directly: pathlib.Path, decimal.Decimal, uuid.UUID, date, time and datetime, and the ipaddress types. Two PyPI libraries appear in the list as well, numpy types and SQLAlchemy Declarative Dataclass Mapping, and the SQLAlchemy support is marked experimental in the documentation.

The union handling is worth singling out, because it is where serialization libraries usually get complicated. A Union field has no single representation on the wire, so pyserde documents a separate Union Representation page, and there is a Type Check page as well. That means the library can both check that a deserialized value matches the declared type and decide how to represent a union, rather than leaving you to guess.

The set of forward reference and typing support pages tells the same story. Forward references, PEP 563 postponed evaluation of annotations, PEP 585 generics in standard collections, PEP 604 union syntax, PEP 681 data class transform, PEP 695 type parameter syntax and PEP 747 annotating type forms are each documented, which is an unusual amount of typing surface for a serialization library.

## Field attributes are where serialization diverges from a plain dataclass

Beyond the type system, pyserde offers a set of attributes on fields and on classes that control the wire format. These are the features that decide whether a schema is acceptable to an existing API.

Case conversion is a class attribute, rename_all, and it renames fields on the way in and out. There is a per-field rename and an alias, which is the mechanism for accepting a differently named key on input. There is a whole family of skip options: skip, skip_if, skip_if_false, skip_if_none and skip_if_default, each usable at field level or class level.

Custom serializers are supported at two levels. You can attach a custom field serializer for a single field, and you can supply a custom class serializer for a whole class, which is how you make a type that pyserde does not know about work with your format. The examples directory in the repository shows this in practice with separate files for custom field serializers, custom class serializers and a legacy variant of the latter.

The example files also reveal what is supported beyond the bullet list. There are examples for alias, any, class_var, collection, counter, deque, default, default_dict, deny_unknown_fields, enum, environment variables, field order, flatten, flatten_dict, forward_reference, frozen_set, generics and nested generics, including a PEP 695 generics example.

deny_unknown_fields and flatten are the two worth understanding. Denying unknown fields makes deserialization strict, so an unexpected key in the payload raises rather than being silently dropped, which is the behaviour you want when the payload comes from outside your control. Flattening embeds a nested structure into the parent rather than nesting it, which is how you match an existing flat JSON contract while keeping a nested dataclass in the code.

## Formats come from optional extras, so the dependency set is your choice

pyserde supports a list of data formats: dict, tuple, JSON, YAML, TOML, MsgPack and Pickle. JSON is in the core, and the rest arrive as optional dependencies, which is visible in the project metadata.

```toml
[project.optional-dependencies]
msgpack = ["msgpack"]
toml = [
  "tomli-w",
]
yaml = ["pyyaml"]
numpy = [
  "numpy>1.22.0,<3.0.0",
]
jaxtyping = ["jaxtyping"]
orjson = ["orjson"]
sqlalchemy = ["sqlalchemy>2"]
```

There is also an all extra that pulls in every one of them. The interesting entries are orjson and jaxtyping, because those are not formats. orjson is an alternative JSON implementation, and jaxtyping is a runtime type checking library, which lines up with the Type Check feature rather than with the format list.

The core dependency set is small and tells you something about the implementation. typing_extensions is there for the typing features, casefy handles the case conversion, jinja2 is a template engine, plum-dispatch provides multiple dispatch, and beartype does runtime type checking. The presence of jinja2 and plum-dispatch in the core is the strongest hint that this is a code generation library rather than a reflection-based one, because those are the tools you use to emit and specialise code at import time.

The Python floor is explicit: requires-python is >=3.11,<4.0, with classifiers for 3.11 through 3.14 and for both CPython and PyPy. The project is on PyPI under the name pyserde, and the license is MIT, with the licence file referenced from the project metadata rather than spelled out as a classifier in the snippet available here.

## The build is a Makefile over uv, with mkdocs and mike for the docs

The repository layout shows a fairly conventional Python project with an unusual amount of tooling around it. There is a serde/ directory holding the library, a tests/ directory, a docs/ directory with mkdocs.yml at the root, an examples/ directory, a bench/ directory, a Dockerfile, a Makefile and a uv.lock.

The Makefile is the entry point for everything, and it is driven by uv rather than by pip or poetry. The setup target syncs the environment, lists the installed packages and installs the pre-commit hooks.

```make
setup:
	$(UV) sync
	$(UV) run pip list
	$(UV) run pre-commit install
```

The test target runs pytest over the tests directory with doctest modules enabled on the serde package, which is a good sign, since it means the examples in the library's own docstrings are executed rather than left to rot.

```make
test:
	$(UV) run pytest tests --doctest-modules serde -v -nauto
```

Several other targets exist for a contributor. build runs uv build, examples runs the examples runner script, check runs all pre-commit hooks, fmt runs the black hook, and docs runs mkdocs build with a separate docs-serve target using mkdocs serve. Documentation deployment uses mike, with the version passed in as a variable and the latest alias updated, which is how a multi-version docs site is published.

The Dockerfile is a development image rather than a runtime image. It starts from ubuntu:24.04, adds the deadsnakes PPA to install Python 3.14 with a venv module, and then registers python3.14 as the default python3 through update-alternatives. There is no application entry point, so the container exists to give contributors a pinned interpreter rather than to serve the library.

## Where pyserde is the wrong tool

The first wrong-tool case is a project that cannot move to Python 3.11. The floor is stated in the project metadata, and a service pinned to an older interpreter cannot use this version at all. That is a hard boundary, not a soft one.

The second case is a data model that is not dataclass-shaped. pyserde derives the serialized form from annotated class fields. A codebase whose payloads come from deeply nested dicts with inconsistent keys, or from objects that carry behaviour, gets no benefit from decorating a class when the class is not the source of truth for the data.

The third case is a team that needs a format it does not want to add a dependency for. YAML needs pyyaml, TOML needs tomli-w, MsgPack needs msgpack, and numpy and SQLAlchemy support need their own packages. For a service that already depends on one of these, the extra is free. For a minimal service, the extras are real weight for functionality it may never use. And the SQLAlchemy Declarative Dataclass Mapping support is documented as experimental, so it should not be the basis of a production data layer.

On maintenance, the project is clearly alive. The last push was on 2026-10-18, and the release history shows v0.33.1 on 2026-09-19, v0.33.0 on 2026-09-17 and v0.32.2 on 2026-09-07, so the 0.33 line is still moving. The version number is dynamic in the project metadata, and the repository keeps a CHANGELOG-style history through its tags. A library at version 0.33 is pre-1.0 by convention, which means the API is not guaranteed stable, and that is a factor to weigh against a service where a serialization change means a wire format change.

## Conclusion

pyserde suits a Python service that already models its data as dataclasses and wants serialization that is generated once and typed at the field level, with no hand-written dict plumbing and no reflection at request time. It does not suit a codebase on Python 3.10 or older, since the project metadata requires Python 3.11 or newer, and it does not suit a project that needs a format the optional extras do not cover without adding a dependency. Before adopting, add pyserde with the extra matching the format you need, for example the yaml or msgpack extra, and check the field attribute documentation for rename, alias and skip, since those are where most of the behaviour differences from a plain dataclass dump live.

## FAQ

### How do I install pyserde and serialize a dataclass?

pyserde is published on PyPI under the name pyserde. You declare a class with the @serde decorator and PEP 484 annotations, then call to_json on an instance and from_json with the target class and a payload. The project metadata requires Python 3.11 or newer.

### Which serialization formats does pyserde support?

The documented formats are dict, tuple, JSON, YAML, TOML, MsgPack and Pickle. JSON is in the core, while the others are optional extras such as msgpack, toml with tomli-w, and yaml with pyyaml, and there is an all extra that installs every one of them.

### How do I rename or skip fields in pyserde?

The class attribute rename_all applies case conversion across the fields, a field can be renamed individually, an alias accepts a different key on input, and the skip family of attributes covers skip, skip_if, skip_if_false, skip_if_none and skip_if_default at field or class level.

### Can pyserde handle a field that is a Union or an Optional?

Both are listed among the supported types, and the project documents a separate Union Representation page, along with a Type Check page, so unions have a defined wire representation rather than an ad hoc fallback.

### What license is pyserde released under?

pyserde is MIT licensed, with the LICENSE file referenced from the project metadata through the license field.

## Sources

- [License: MIT](https://github.com/yukinarit/pyserde/blob/main/LICENSE)
- [Project website](https://yukinarit.github.io/pyserde)
- [README](https://github.com/yukinarit/pyserde/blob/main/README.md)
- [Releases](https://github.com/yukinarit/pyserde/releases)
- [yukinarit/pyserde on GitHub](https://github.com/yukinarit/pyserde)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/yukinarit-pyserde
