mrkeyoor.com_
Mon 21 Sept 02:02 UTC
PyPIUtilsupdated 20 Sept 2026

msgspec review

msgspec 0.21.1 combines serialization and typed validation, most often by decoding JSON or MessagePack straight into classes derived from `msgspec.Struct`. The same annotations also support YAML and TOML adapters, conversion of existing Python values, tagged unions, field constraints, custom hooks, and JSON Schema generation. Its speed comes partly from a compiled extension and reusable codec objects. The 0.21.1 patch stops rewrapping `ValidationError` and `DecodeError` raised by `dec_hook`, fixes a possible null dereference and an integer-constraint reference leak, adds the missing `ref_template` type stub, and documents migration from orjson.

Verdict

msgspec 0.21.1 installed as a 1 MB compiled package in 0.2 seconds on our sandbox and imported in 0.15 seconds, making it a lean typed-codec candidate. Choose it for message boundaries where one-error validation and `Struct` fit; choose Pydantic when model hooks, ecosystem integration, and aggregated errors matter more.

We installed it

Lab card: what happened when we installed msgspecScreenshot of msgspec documentation
Install✓ · 0.2s1 package on disk · 1 MB
Importimport msgspec in 0.15s · compiled extensions · py.typed · requires Python >=3.10
Known vulns0(pip-audit)

Answers from our run

Does msgspec install cleanly?

Yes. In a fresh container with an empty cache, pip install msgspec finished in 0.2s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does msgspec need to run?

Python >=3.10, and a platform wheel with compiled extensions. In our run import msgspec succeeded in 0.15s, and the package ships py.typed for type checkers.

msgspec or pydantic: which should you use?

pydantic: Use it for richer validators, aggregated errors, settings, FastAPI, and a larger extension ecosystem. msgspec 0.21.1 installed as a 1 MB compiled package in 0.2 seconds on our sandbox and imported in 0.15 seconds, making it a lean typed-codec candidate.

When should you not use msgspec?

A form or public API must return all invalid fields in one response. msgspec reports the first decode validation error.

API stability4/5Recent releases retain the same core vocabulary: `Struct`, codec-level `encode` and `decode`, reusable `Encoder` and `Decoder`, `convert`, `to_builtins`, field metadata, and tagged unions. The remaining caution is the 0.x version. Release 0.21 made replacement invoke `__post_init__`, while earlier releases removed or adjusted APIs, so a minor update can alter behavior even when ordinary JSON calls stay familiar.
Docs5/5The official site explains Struct options, supported annotations, constraints, tagged unions, hooks, conversion, inspection, JSON Schema, and each data format with executable examples. It states that decoding validates while encoding does not and describes strict coercion. Version 0.21.1 also adds an orjson porting guide and corrects a schema typing detail, giving users both task guides and precise API reference material.
Maintenance4/5Version 0.21.1 shipped on 2026-04-12, the repository was pushed on 2026-08-12, and GitHub reports 232 open issues and pull requests in an unarchived project. That patch addressed a possible null dereference, a reference leak, hook error propagation, typing, and documentation. Native code raises the maintenance burden, though recent project work covers current Python versions and additional wheel targets.
Ecosystem3/5The supplied count is 8,767,144 weekly downloads, and GitHub reports 4,062 stars. Standard annotations, JSON Schema output, multiple codecs, and integrations in projects such as Litestar make msgspec practical beyond isolated scripts. It remains much smaller than Pydantic's settings, ORM, FastAPI, and validator ecosystem, so teams adopting it for application models will write more adapters themselves.

Use it if

  • Profiles show that decoding and validating JSON or MessagePack consumes meaningful CPU in a queue, RPC, or API boundary.
  • One annotated message type should drive several codecs, in-memory conversion, and JSON Schema output.
  • Compact slot-based records and reusable typed decoders fit the service better than feature-heavy application models.
  • Validation can be expressed through types, bounds, patterns, tagged unions, and limited `__post_init__` checks.
Skip it if

Setup reality

We installed msgspec 0.21.1 in a clean Python 3.12 Bookworm container in 0.2 seconds. It left 1 package and 1 MB on disk; pip-audit found 0 known vulnerabilities. The measured metadata declares 3 direct dependencies and no recognized license value. Python 3.10 or newer is required. The wheel includes compiled .so code and py.typed, and import msgspec completed in 0.15 seconds.

JSON and MessagePack use the main codec implementation. YAML and TOML need their optional supporting packages, so the minimal install does not guarantee those calls work. Struct is slot-based and is not a dataclass: undeclared attributes fail, mutable defaults belong in msgspec.field(default_factory=...), and dataclass helpers are the wrong conversion tools. Use msgspec.structs, msgspec.convert, or msgspec.to_builtins instead.

Typed decoding validates incoming data. Encoding assumes the object is already valid, so later assignment of a wrong runtime type can still reach the wire. Strict mode rejects coercions such as a numeric string into an integer. strict=False suits boundaries that can only supply strings, including environment values, but it is a poor default for JSON request bodies. Validation stops at the first error, which keeps the hot path short and limits form-style feedback.

Create Encoder and typed Decoder objects once for frequently used schemas. Version 0.21.1 preserves ValidationError and DecodeError raised inside dec_hook instead of nesting them inside another validation error. In 0.21, both msgspec.structs.replace and copy.replace invoke __post_init__; replacement can therefore repeat validation, logging, or other side effects placed there. Test source builds separately because the 1 MB wheel result depends on receiving a compatible compiled artifact.

Patterns

Decode JSON into a typed Struct define-struct

import msgspec

class User(msgspec.Struct):
    name: str
    email: str | None = None
    groups: set[str] = msgspec.field(default_factory=set)

user = msgspec.json.decode(
    b'{"name":"alice","groups":["admin"]}',
    type=User,
)

Passing `type=User` performs typed validation. Mutable collection defaults need `default_factory`.

Keep typed codecs at module scope reuse-codecs

import msgspec

class Event(msgspec.Struct):
    id: int
    kind: str

DECODER = msgspec.json.Decoder(Event)
ENCODER = msgspec.json.Encoder()

def handle(raw: bytes) -> bytes:
    event = DECODER.decode(raw)
    return ENCODER.encode(event)

Recreating a typed `Decoder` inside each request repeats schema preparation and discards part of its hot-path benefit.

Add numeric and string constraints constrain-fields

from typing import Annotated
import msgspec
from msgspec import Meta

Port = Annotated[int, Meta(ge=1, le=65535)]
Slug = Annotated[str, Meta(pattern=r"^[a-z0-9-]+$", max_length=64)]

class Service(msgspec.Struct):
    slug: Slug
    port: Port = 8080

Regex constraints search unless the pattern includes anchors; use `^` and `$` when the whole string must match.

Rename fields and reject unknown keys configure-wire-fields

import msgspec

class ApiUser(
    msgspec.Struct,
    rename="camel",
    omit_defaults=True,
    forbid_unknown_fields=True,
    frozen=True,
    kw_only=True,
):
    first_name: str
    last_name: str
    is_admin: bool = False

Unknown keys are ignored by default. `forbid_unknown_fields=True` turns them into a validation error.

Select a Struct through a wire tag decode-tagged-union

import msgspec

class Created(msgspec.Struct, tag="created", tag_field="type"):
    id: int

class Deleted(msgspec.Struct, tag="deleted", tag_field="type"):
    id: int
    reason: str

Event = Created | Deleted
event = msgspec.json.decode(raw, type=Event)

Every union member must use the same `tag_field`; msgspec does not guess between similar untagged Struct shapes.

Represent an omitted patch field distinguish-missing-null

import msgspec
from msgspec import UNSET, UnsetType

class UserPatch(msgspec.Struct, omit_defaults=True):
    name: str | UnsetType = UNSET
    bio: str | None | UnsetType = UNSET

patch = msgspec.json.decode(b'{"bio":null}', type=UserPatch)
assert patch.name is UNSET
assert patch.bio is None

`UNSET` means absent and `None` means explicit null; compare with the singleton so false values remain valid updates.

Convert an unsupported Python type write-custom-hooks

from ipaddress import IPv4Address
import msgspec

def enc_hook(obj):
    if isinstance(obj, IPv4Address):
        return str(obj)
    raise NotImplementedError

def dec_hook(type_, obj):
    if type_ is IPv4Address:
        return IPv4Address(obj)
    raise NotImplementedError

encoder = msgspec.json.Encoder(enc_hook=enc_hook)
decoder = msgspec.json.Decoder(IPv4Address, dec_hook=dec_hook)

Raise `NotImplementedError` for types the hook does not own. Version 0.21.1 no longer wraps hook validation errors a second time.

Coerce an in-memory settings mapping convert-python-values

import msgspec

class Config(msgspec.Struct):
    host: str
    port: int
    debug: bool = False

raw = {"host": "localhost", "port": "8080", "debug": "true"}
config = msgspec.convert(raw, Config, strict=False)
plain = msgspec.to_builtins(config)

Use `strict=False` only where string coercion is intentional. Strict mode is safer for already typed or decoded API data.

Produce schema components for Structs generate-json-schema

import msgspec

class Item(msgspec.Struct):
    sku: str
    quantity: int = 1

schemas, components = msgspec.json.schema_components(
    [Item, list[Item]],
    ref_template="#/components/schemas/{name}",
)

Checks implemented only in `__post_init__` cannot appear in generated JSON Schema.

Keep an envelope payload as Raw bytes defer-payload-decoding

import msgspec

class Envelope(msgspec.Struct):
    topic: str
    payload: msgspec.Raw

class Click(msgspec.Struct):
    url: str

envelope = msgspec.json.decode(raw_message, type=Envelope)
if envelope.topic == "click":
    click = msgspec.json.decode(envelope.payload, type=Click)

A `Raw` view keeps the source buffer alive. Copy it before retaining a small payload from a much larger message.

Decode optional TOML support decode-toml

# Install msgspec with its TOML extra first.
import msgspec

class Settings(msgspec.Struct):
    name: str
    workers: int = 4

with open("config.toml", "rb") as file:
    settings = msgspec.toml.decode(file.read(), type=Settings)

TOML and YAML need optional support packages; JSON and MessagePack use msgspec's main codec path.

Copy a Struct with one changed field replace-struct

import msgspec
from msgspec import structs

class Point(msgspec.Struct, frozen=True):
    x: int
    y: int

point = Point(1, 2)
updated = structs.replace(point, y=9)
assert structs.asdict(updated) == {"x": 1, "y": 9}

Version 0.21 invokes `__post_init__` on the replacement value, so side effects there run again.

Alternatives

PackageRegistryPick it when
pydanticPyPIUse it for richer validators, aggregated errors, settings, FastAPI, and a larger extension ecosystem.
orjsonPyPIUse it when fast JSON bytes are the goal and typed validation lives elsewhere.
cattrsPyPIUse it to structure existing attrs classes or dataclasses through configurable hooks.

More utils guides

lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.