orjson + Pydantic v2 model round trip: working versions (tested 2026-10-15)
Serialize Pydantic v2 models to bytes with orjson through a default fallback and option flags, then validate them back offline with TypeAdapter.
passed6 passed · 0 failed2026-10-15 16:03 UTC- Status
- passed
- Last passed
- 2026-10-15 16:03 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 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("orjson-pydantic-v2-roundtrip") (install), or see them on GitHub.
orjson==3.13.0 pydantic==2.14.1 pytest==9.1.1
from decimal import Decimal
from enum import Enum
from typing import Any
import orjson
from pydantic import BaseModel, SecretStr, TypeAdapter
def _default(obj: Any) -> Any:
if isinstance(obj, BaseModel):
return obj.model_dump()
if isinstance(obj, Decimal):
return str(obj)
if isinstance(obj, SecretStr):
return str(obj)
if isinstance(obj, Enum):
return obj.value
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
def dumps(model: Any, *, sort_keys: bool = False, utc_z: bool = False) -> bytes:
option = orjson.OPT_NON_STR_KEYS
if sort_keys:
option |= orjson.OPT_SORT_KEYS
if utc_z:
option |= orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z
return orjson.dumps(model, default=_default, option=option)
def loads(data: bytes | str, model_type: type[BaseModel]) -> BaseModel:
return TypeAdapter(model_type).validate_python(orjson.loads(data))
import datetime
import uuid
from decimal import Decimal
from enum import Enum
import orjson
import pytest
from pydantic import BaseModel, SecretStr
from codec import dumps, loads
class Color(str, Enum):
red = "red"
class Event(BaseModel):
name: str
at: datetime.datetime
price: Decimal
uid: uuid.UUID
labels: dict[int, str]
color: Color
class Account(BaseModel):
user: str
token: SecretStr
class ZFirst(BaseModel):
z: int
a: int
class AFirst(BaseModel):
a: int
z: int
def make_event(**overrides) -> Event:
data = dict(
name="deploy",
at=datetime.datetime(2024, 1, 2, 3, 4, 5),
price=Decimal("19.90"),
uid=uuid.UUID(int=1),
labels={7: "prod"},
color=Color.red,
)
data.update(overrides)
return Event(**data)
def test_roundtrip_is_lossless_over_bytes():
event = make_event()
payload = dumps(event)
assert isinstance(payload, bytes)
restored = loads(payload, Event)
assert restored == event
assert restored.price == Decimal("19.90")
def test_orjson_needs_a_default_for_models():
event = make_event()
with pytest.raises(TypeError):
orjson.dumps(event)
assert loads(dumps(event), Event) == event
def test_sort_keys_is_field_order_independent():
assert dumps(ZFirst(z=1, a=2), sort_keys=True) == dumps(AFirst(a=2, z=1), sort_keys=True)
assert dumps(ZFirst(z=1, a=2), sort_keys=True) == b'{"a":2,"z":1}'
def test_utc_z_option_makes_naive_datetimes_aware():
event = make_event(at=datetime.datetime(2024, 1, 2, 3, 4, 5))
raw = orjson.loads(dumps(event, utc_z=True))
assert raw["at"] == "2024-01-02T03:04:05Z"
restored = loads(dumps(event, utc_z=True), Event)
assert restored.at.utcoffset() == datetime.timedelta(0)
assert restored != event
def test_secretstr_is_masked_and_roundtrip_is_lossy():
account = Account(user="ada", token="s3cr3t")
assert orjson.loads(dumps(account)) == {"user": "ada", "token": "**********"}
restored = loads(dumps(account), Account)
assert restored.token.get_secret_value() == "**********"
assert restored != account
def test_non_str_keys_are_stringified_for_json():
raw = orjson.loads(dumps(make_event(labels={7: "prod"})))
assert raw["labels"] == {"7": "prod"}
with pytest.raises(TypeError):
orjson.dumps({7: "prod"})
[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
FastAPI + Pydantic v2 validators, discriminated unions and serializers
Pydantic v2 patterns in FastAPI that agents often get wrong: field_validator, model_validator, discriminated unions, computed_field and response_model filtering.
FastAPI background tasks + dependency overrides
FastAPI BackgroundTasks for post-response work, with dependency_overrides to swap services in tests.
FastAPI endpoint with Pydantic v2 discriminated unions and Annotated validators
Accept a tagged union of payment methods with Field(discriminator), add Annotated validators, and test the exact 422 error shapes offline with TestClient.
feedparser XML fixtures into pydantic v2 models
Convert RSS and Atom feeds parsed by feedparser, including struct_time dates and tags, into validated pydantic v2 models with offline XML fixtures.