msgspec: schemas as Python type annotations, decoded in C
A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML
At a glance
- What is it?
- A serialization and validation library that treats a Struct subclass as the schema and generates the decoder for it. The README shows a three-line workflow; the interesting decisions are in the build flags.
- Who is it for?
- msgspec is the library to reach for when you want validation and you do not want a validation framework. The whole contract is that a Struct subclass is the schema, the decoder is generated from it, and validation happens during the decode rather than as a second pass over already-parsed objects.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A Struct subclass is the whole schema
The README recommends a three-step workflow rather than describing a library API. Define your message schemas using standard Python type annotations, encode messages as JSON or one of the other supported protocols, then decode messages back into Python objects with optional schema validation. There is no separate schema object, no class to register, and no code generation step.
The definition is an ordinary class declaration:
>>> import msgspec
>>> class User(msgspec.Struct):
... """A new type describing a User"""
... name: str
... groups: set[str] = set()
... email: str | None = NoneEncoding and decoding are module-level function calls rather than methods on the schema:
>>> alice = User("alice", groups={"admin", "engineering"})
>>> msg = msgspec.json.encode(alice)
>>> msg
b'{"name":"alice","groups":["admin","engineering"],"email":null}'What is worth noticing is what the type annotations buy you. `set[str]` is not a JSON type, so the encoder knows to emit an array. `str | None` produces a JSON `null`. The schema and the wire format are two readings of the same declaration, which is the design that makes validation free at decode time rather than a second traversal.
The library is described as performing as well as it can while retaining some of the conveniences of validation libraries like pydantic, and the README frames the choice as open to you: you may use it for serialization alone, as a faster JSON or MessagePack library, if you do not need validation.
Validation happens during decode, and it reports the path
Validation is not a separate step you opt into with a different API. You pass the type to the decoder, and a mismatch raises immediately:
>>> msgspec.json.decode(msg, type=User)
User(name='alice', groups={"admin", "engineering"}, email=None)
>>> msgspec.json.decode(b'{"name":"bob","groups":[123]}', type=User)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
msgspec.ValidationError: Expected `str`, got `int` - at `$.groups[0]`The error message is the part that justifies the design. It names the expected type, the type actually found, and a JSONPath-style location: `$.groups[0]`. When you are handling a request body that came from somewhere else, that pointer is the difference between a log line you can act on and one you have to reproduce by hand.
The README's framing of this is zero-cost schema validation, and it goes further, claiming that in its benchmarks msgspec decodes and validates JSON faster than orjson can decode it alone. That is a claim on the project's own benchmark page, so treat it as a claim about the shape of the comparison rather than as a result you can reproduce from the README. The comparison page is linked, which is more than most projects offer.
The library also advertises support for a wide variety of Python types, with additional types available through extensions, and a Struct type intended to feel familiar if you already use dataclasses or attrs while being considerably faster for common operations. Those speed figures are again attributed to the linked benchmarks, and the range is wide enough that the exact operation matters.
Four wire formats, and optional extras that pull in real dependencies
The builtin format list is JSON, MessagePack, YAML and TOML, and each has its own module namespace under `msgspec`. The interesting detail is what that costs. The `pyproject.toml` declares `requires-python` as `>=3.10` and states the license as BSD-3-Clause with the LICENSE file listed in `license-files`, and the README closes by pointing at the same license file.
The core has no required dependencies, and the optional extras are small and honest about it. TOML pulls in `tomli` on Python below 3.11 plus `tomli_w`, which is the standard-library split for TOML support. YAML pulls in `pyyaml`. So the only format with a hard third-party dependency is YAML, and it is behind an extra rather than bundled.
The classifiers list Python 3.10 through 3.15 and include CPython as the implementation, plus an Emscripten entry for WebAssembly. The `Development Status :: 4 - Beta` classifier is the project's own self-assessment, and with 227 open issues against 4125 stars it is a busy beta. Worth noting for planning: an `examples/` directory in the tree ships four runnable projects, an asyncio key-value store, conda repodata tooling, GeoJSON handling and pyproject.toml parsing, which are the fastest way to see how the library is meant to be used in something larger than a README snippet.
The build is a C extension, and that is a real constraint
`setup.py` explains more about the project than the README does, and the headline is that this is a compiled extension rather than pure Python. The script builds an `Extension` and adjusts compile and link flags per platform.
Two of those adjustments are worth knowing about. On macOS `arm64`, it adds `-flto=thin` to both compile and link arguments, which the 0.20.0 notes confirm was enabled for macOS `aarch64` builds. On Windows it adds C11 standard and atomic support flags, with a comment linking to the free-threading FAQ about building a library on Windows where MSVC reports that C11 atomics are not enabled. The free-threading support is not incidental: version 0.20.0 added Python 3.14 support including freethreaded mode.
There is also a hard refusal. If you are on Windows with a 32-bit Python build, the script prints a message saying msgspec currently does not support 32-bit Python Windows builds, points at issue 845, and calls `exit(1)`. So the failure is explicit at install time rather than a mysterious crash later.
Three environment variables control the build: `MSGSPEC_SANITIZE` adds address and undefined-behavior sanitizers and links them, `MSGSPEC_COVERAGE` adds gcov instrumentation, and `MSGSPEC_DEBUG` defaults to whichever of the two is set. Debug builds get `-O0`, `-g` and `-UNDEBUG`; otherwise non-Windows builds get `-g0`, which strips debug info for size. None of that is documentation, it is the build script, and it tells you the project treats its C core as something that gets actively debugged.
What the 0.20 and 0.21 lines changed, including a breaking one
The release notes are short enough to read, which is a courtesy, and 0.21.0 carries one clearly flagged breaking change. `msgspec.structs.replace` and `copy.replace` now call `__post_init__`. If your Struct defines `__post_init__` with a side effect, or expects the old behavior, that is the change to check first.
The rest of 0.21.0 is fixes with a few additions. A segfault on Python 3.13 and newer that could occur when creating structs containing a `__dict__` instead of only using `__slots__` was fixed. Hashing of decoded tuples on Python 3.14 was fixed. A memory leak when decoding sets was fixed. `msgspec.UNSET` type annotations were improved, a `ref_template` argument was added to `msgspec.json.schema`, `uniqueItems` was added to set and frozenset JSON schemas, and missing `libm` linkage was added for some architectures.
Version 0.21.1, published in April 2026, tightened error behavior: `ValidationError` and `DecodeError` raised inside a `dec_hook` were being wrapped in another `ValidationError`, which is now fixed. It also fixed a potential NULL dereference in `structmeta_get_module_ns` and a reference leak in `ms_passes_big_int_constraints`, and added a porting guide for users migrating from orjson. That guide is the single most useful document for anyone arriving from a faster-JSON-only setup, since the mental model changes when validation enters the decode.
Back in 0.20.0 the shape changed more: the `StructMeta` metaclass was exposed, `msgspec.inspect.is_struct` and `is_struct_type` were added, and `setuptools-scm` replaced a vendored versioning tool.
Where msgspec is the wrong choice
The library positions itself against pydantic and orjson, and the honest reading of that is a trade in three directions. Against a full validation framework, you lose declarative validators, computed fields, and the ecosystem of custom types, and you gain a decoder with no second pass. Against a serialization-only library, you gain validation at no measured cost. Against hand-written parsing, you gain a schema that is checked instead of assumed.
Where it stops being the right tool is more concrete than the marketing comparison. If your schemas change shape across services that you do not all control, a type-annotation-driven decoder is only as good as your ability to keep the annotations honest. If you need coercion, msgspec is deliberately not a coercion library, and the linked extensions mechanism is the intended escape hatch. If your deployment pipeline cannot compile a C extension, or you need a 32-bit Windows build, it is simply unavailable to you, and the setup script will tell you so at install time rather than letting you discover it later.
There is one more practical consideration that has nothing to do with speed. The project is a single-maintainer effort in the sense that the README sponsors link is personal, and 227 open issues is a real backlog to factor into your risk assessment. The code is BSD-3-Clause and the CI, codecov and documentation badges are all wired up, so the maintenance signal is good even though the bus factor is not stated.
Editorial conclusion
msgspec is the library to reach for when you want validation and you do not want a validation framework. The whole contract is that a Struct subclass is the schema, the decoder is generated from it, and validation happens during the decode rather than as a second pass over already-parsed objects. The practical consequences are a Python 3.10 floor, a BSD-3-Clause license, no required dependencies, and a README that is accurate about how little you need to learn. The catch is that the library is compiled C, so a source build is a compiler toolchain rather than a `pip install`, and 32-bit Windows Python is rejected outright by the setup script. Start with the Struct example in the README, add `msgspec.Meta` constraints only where you need them, and read the porting guide linked in the 0.21.1 notes if you are moving off orjson.
Frequently asked questions
What is msgspec used for in Python?
It is a serialization and validation library with builtin support for JSON, MessagePack, YAML and TOML. You define schemas as Struct subclasses with standard Python type annotations, then encode and decode through the format modules such as `msgspec.json.encode`, passing `type=` to the decoder to validate during decoding.
Does msgspec validate JSON against a schema?
Yes, as part of decoding rather than as a separate step. Passing `type=User` to `msgspec.json.decode` validates while parsing, and a mismatch raises `msgspec.ValidationError` with the expected type, the type found and a JSONPath location such as `$.groups[0]`.
How do I install msgspec?
It is on PyPI and conda-forge, and the packaging metadata requires Python 3.10 or newer with no required dependencies. Note that it is a compiled C extension: the build script enables ThinLTO on macOS arm64 and sets C11 flags on Windows, and it refuses to install on 32-bit Windows Python builds.
Does msgspec support YAML and TOML without extra dependencies?
TOML and YAML are both supported, but the extras are declared separately in the project metadata. The `toml` extra pulls in `tomli` on Python below 3.11 along with `tomli_w`, and the `yaml` extra pulls in `pyyaml`. JSON and MessagePack need no extras.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/msgspec-msgspec)