cattrs preconfigured orjson converter for attrs datetimes: working versions (tested 2026-10-16)
An attrs model with timezone-aware datetime fields serialized through cattrs' preconfigured orjson converter, showing native RFC 3339 handling and the hooks you still need to add.
passed5 passed · 0 failed2026-10-16 09:57 UTC- Status
- passed
- Last passed
- 2026-10-16 09:57 UTC
- Tests
- 5 / 5
- 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("cattrs-preconf-orjson-converter") (install), or see them on GitHub.
attrs==26.1.0 cattrs==26.2.1 orjson==3.13.0 pytest==9.1.1
from datetime import datetime
import attrs
from cattrs.preconf.orjson import make_converter
@attrs.define(frozen=True, slots=True)
class Session:
id: int
label: str
started_at: datetime
expires_at: datetime
tags: list[str] = attrs.field(factory=list)
# the orjson preconf passes datetimes straight through to orjson and reads
# them back with datetime.fromisoformat, so RFC 3339 survives the round-trip
converter = make_converter()
def dump(session: Session) -> bytes:
return converter.dumps(session)
def load(data: bytes | str) -> Session:
return converter.loads(data, Session)
import json
from datetime import datetime, timedelta, time, timezone
import attrs
import cattrs
import orjson
import pytest
from cattrs.preconf.orjson import make_converter
from converter import Session, converter, dump, load
def make_session() -> Session:
return Session(
id=7,
label="login",
started_at=datetime(2026, 1, 2, 3, 4, 5, 123456, tzinfo=timezone.utc),
expires_at=datetime(
2026, 1, 2, 4, 4, 5, 123456, tzinfo=timezone(timedelta(hours=5, minutes=30))
),
tags=["web", "mfa"],
)
def test_roundtrip_preserves_aware_datetimes_and_microseconds():
session = make_session()
payload = dump(session)
assert isinstance(payload, bytes)
assert load(payload) == session
assert load(payload).started_at.microsecond == 123456
def test_naive_datetimes_are_written_without_an_offset():
session = Session(
id=1,
label="x",
started_at=datetime(2026, 1, 2, 3, 4, 5),
expires_at=datetime(2026, 1, 2, 3, 4, 5),
)
assert orjson.loads(dump(session))["started_at"] == "2026-01-02T03:04:05"
assert load(dump(session)).started_at.tzinfo is None
def test_unstructure_keeps_datetime_objects_for_orjson():
unstructured = converter.unstructure(make_session())
assert isinstance(unstructured["started_at"], datetime)
with pytest.raises(TypeError):
json.dumps(unstructured) # stdlib json has no datetime encoder here
assert isinstance(converter.dumps(make_session()), bytes)
def test_time_values_dump_but_have_no_structure_hook():
local = make_converter()
@attrs.define
class Slot:
at: time
payload = local.dumps(Slot(time(3, 4, 5)))
assert payload == b'{"at":"03:04:05"}'
with pytest.raises(cattrs.errors.StructureHandlerNotFoundError):
local.loads(payload, Slot)
local.register_structure_hook(time, lambda value, _: time.fromisoformat(value))
assert local.loads(payload, Slot) == Slot(time(3, 4, 5))
def test_orjson_options_pass_through_to_dumps_only():
session = make_session()
payload = converter.dumps(session, option=orjson.OPT_INDENT_2)
assert payload.startswith(b"{\n")
assert load(payload) == session
[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
attrs classes + cattrs JSON dict round-trip
Nested attrs classes with enums, datetimes and defaults, structured and unstructured to JSON dicts with cattrs' preconfigured json converter.
attrs models with cattrs structuring, custom datetime hooks and renaming
Define attrs classes with validators and converters, then use a cattrs Converter with custom datetime hooks and wire-name renaming, roundtrip through unstructure/structure, and inspect ClassValidationError details. Tested offline.
orjson + Pydantic v2 model round trip
Serialize Pydantic v2 models to bytes with orjson through a default fallback and option flags, then validate them back offline with TypeAdapter.
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.