ProvenPin Install

attrs classes + cattrs JSON dict round-trip: working versions (tested 2026-10-15)

Nested attrs classes with enums, datetimes and defaults, structured and unstructured to JSON dicts with cattrs' preconfigured json converter.

passed5 passed · 0 failed2026-10-15 15:57 UTC
Status
passed
Last passed
2026-10-15 15:57 UTC
Tests
5 / 5
Language
Python
Runtime
Python 3.13.16
pinned versions3
attrs@26.1.0cattrs@26.2.1pytest@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 38764699834

The code

Exactly the files that passed. Your agent can pull them with get_recipe("attrs-cattrs-json-roundtrip") (install), or see them on GitHub.

requirements.txt
attrs==26.1.0
cattrs==26.2.1
pytest==9.1.1
models.py
from datetime import datetime
from enum import Enum

import attrs
from cattrs.preconf.json import make_converter

class Role(Enum):
    ADMIN = "admin"
    USER = "user"

@attrs.define(frozen=True, slots=True)
class Address:
    city: str
    zip: str = "00000"

@attrs.define(slots=True)
class User:
    id: int
    name: str
    role: Role = Role.USER
    created_at: datetime | None = None
    address: Address | None = None
    tags: list[str] = attrs.field(factory=list)

# preconf json adds ISO datetime hooks on top of the stdlib json support
converter = make_converter()

def to_json(user: User) -> str:
    return converter.dumps(user)

def from_json(data: str) -> User:
    return converter.loads(data, User)
tests/test_roundtrip.py
import json
from datetime import datetime, timezone

import cattrs
import pytest
from cattrs.gen import make_dict_unstructure_fn
from cattrs.preconf.json import make_converter

from models import Address, Role, User, converter, from_json, to_json

def make_user() -> User:
    return User(
        id=1,
        name="Ada",
        role=Role.ADMIN,
        created_at=datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
        address=Address("London", "N1"),
        tags=["math", "code"],
    )

def test_full_roundtrip_preserves_values():
    user = make_user()
    assert from_json(to_json(user)) == user

def test_bare_converter_output_is_not_json_serializable():
    # a plain Converter keeps datetime objects, so json.dumps fails
    plain = cattrs.Converter()
    with pytest.raises(TypeError):
        json.dumps(plain.unstructure(make_user()))

def test_enum_uses_its_value_by_default():
    assert converter.unstructure(Role.ADMIN) == "admin"
    assert converter.structure("admin", Role) is Role.ADMIN

def test_unknown_keys_are_ignored_unless_forbidden():
    data = {"id": 1, "name": "Ada", "typo": "oops"}
    assert not hasattr(converter.structure(data, User), "typo")
    strict = make_converter(forbid_extra_keys=True)
    with pytest.raises(cattrs.errors.ClassValidationError):
        strict.structure(data, User)

def test_defaults_can_be_omitted_on_the_way_out():
    lean = make_dict_unstructure_fn(User, converter, _cattrs_omit_if_default=True)
    user = User(id=1, name="Ada")
    assert lean(user) == {"id": 1, "name": "Ada"}
    # defaults are restored when the lean payload is structured back
    assert converter.structure(lean(user), User) == user
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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.