msgspec Structs with tagged unions, constraints and fast codecs: working versions (tested 2026-10-15)
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.
passed5 passed · 0 failed2026-10-15 13:31 UTC- Status
- passed
- Last passed
- 2026-10-15 13:31 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions2
msgspec@0.22.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("msgspec-struct-validation") (install), or see them on GitHub.
requirements.txt
msgspec==0.22.0 pytest==9.1.1
models.py
from typing import Annotated
import msgspec
PositiveInt = Annotated[int, msgspec.Meta(gt=0)]
Label = Annotated[str, msgspec.Meta(min_length=1, max_length=40)]
class Circle(msgspec.Struct, tag="circle"):
radius: PositiveInt
class Rect(msgspec.Struct, tag="rect"):
width: PositiveInt
height: PositiveInt
# Tagged union: the "type" field selects the variant during decode.
Shape = Circle | Rect
class Order(msgspec.Struct, kw_only=True, forbid_unknown_fields=True):
id: int
label: Label
shapes: list[Shape] = msgspec.field(default_factory=list)
def parse_order(raw: bytes) -> Order:
return msgspec.json.decode(raw, type=Order)
def order_from_dict(data: dict, strict: bool = True) -> Order:
return msgspec.convert(data, Order, strict=strict)
tests/test_models.py
import msgspec
import pytest
from models import Circle, Order, Rect, Shape, order_from_dict, parse_order
RAW = (
b'{"id":1,"label":"box","shapes":['
b'{"type":"circle","radius":3},{"type":"rect","width":2,"height":4}]}'
)
def test_tagged_union_roundtrip_json_and_msgpack():
order = parse_order(RAW)
assert isinstance(order.shapes[0], Circle)
assert isinstance(order.shapes[1], Rect)
as_json = msgspec.json.decode(msgspec.json.encode(order), type=Order)
as_mp = msgspec.msgpack.decode(msgspec.msgpack.encode(order), type=Order)
assert as_json == order
assert as_mp == order
def test_meta_constraint_error_reports_nested_path():
bad = RAW.replace(b'"radius":3', b'"radius":0')
with pytest.raises(msgspec.ValidationError, match=r"\$\.shapes\[0\]\.radius"):
parse_order(bad)
def test_constructor_does_not_enforce_meta():
# Meta constraints run on decode/convert, not on plain Struct __init__.
assert Circle(radius=-1).radius == -1
def test_unknown_tag_and_unknown_fields_rejected():
with pytest.raises(msgspec.ValidationError):
msgspec.json.decode(b'{"type":"hexagon","radius":1}', type=Shape)
extra = RAW.replace(b'"label":"box"', b'"label":"box","color":"red"')
with pytest.raises(msgspec.ValidationError, match="color"):
parse_order(extra)
def test_convert_from_dict_is_strict_by_default():
data = {"id": "7", "label": "box", "shapes": [{"type": "circle", "radius": 2}]}
with pytest.raises(msgspec.ValidationError):
order_from_dict(data)
order = order_from_dict(data, strict=False)
assert order.id == 7
assert order.shapes == [Circle(radius=2)]
pytest.ini
[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.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "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.