msgspec tagged union Structs over MessagePack and JSON: working versions (tested 2026-10-16)
Model a tagged union of msgspec Structs, round-trip it through MessagePack and JSON, and verify tag selection and strict-field validation under pytest.
passed6 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38883899161
The code
Exactly the files that passed. Your agent can pull them with get_recipe("msgspec-msgpack-tagged-unions") (install), or see them on GitHub.
msgspec==0.22.0 pytest==9.1.1
from typing import Union
import msgspec
class Created(msgspec.Struct, tag="created", forbid_unknown_fields=True):
id: int
name: str
class Deleted(msgspec.Struct, tag="deleted", forbid_unknown_fields=True):
id: int
reason: str
Event = Union[Created, Deleted]
DECODER = msgspec.json.Decoder(Event)
def to_msgpack(event: Event) -> bytes:
return msgspec.msgpack.encode(event)
def from_msgpack(data: bytes) -> Event:
return msgspec.msgpack.decode(data, type=Event)
def to_json(event: Event) -> bytes:
return msgspec.json.encode(event)
def from_json(data: bytes) -> Event:
return msgspec.json.decode(data, type=Event)
import msgspec
import pytest
from models import (
DECODER,
Created,
Deleted,
from_json,
from_msgpack,
to_json,
to_msgpack,
)
def test_msgpack_roundtrip_picks_subtype():
data = to_msgpack(Deleted(2, "spam"))
assert b"deleted" in data
event = from_msgpack(data)
assert isinstance(event, Deleted) and event.reason == "spam"
def test_json_carries_the_tag_field():
assert to_json(Created(1, "a")) == b'{"type":"created","id":1,"name":"a"}'
assert from_json(b'{"type":"deleted","id":9,"reason":"z"}') == Deleted(9, "z")
def test_unknown_tag_is_rejected():
with pytest.raises(msgspec.ValidationError):
from_json(b'{"type":"archived","id":1}')
def test_missing_tag_is_rejected():
with pytest.raises(msgspec.ValidationError):
from_msgpack(msgspec.msgpack.encode({"id": 1, "name": "a"}))
def test_strict_fields_reject_extras():
with pytest.raises(msgspec.ValidationError):
from_json(b'{"type":"created","id":1,"name":"a","extra":true}')
def test_msgpack_is_compact_and_decoder_is_reusable():
event = Created(1, "a")
assert len(to_msgpack(event)) < len(to_json(event))
assert DECODER.decode(b'{"type":"created","id":1,"name":"a"}') == event
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
msgspec Structs with tagged unions, constraints and fast codecs
Define msgspec Structs with tagged unions and Meta constraints, encode and decode JSON and MessagePack, inspect ValidationError paths, and convert plain dicts, all tested offline.
orjson and msgspec encode/decode parity helpers
Run orjson and msgspec side by side with canonical sort-key and non-str-key options, a shared Struct hook and typed decoding, and pin down where their bytes diverge.