Skip to content

fix(sdjwt): serialize pydantic models in json mode to support enum claims in mandates - #355

Open
harkanwalbedi wants to merge 2 commits into
google-agentic-commerce:mainfrom
harkanwalbedi:fix/agent-recurrence-sdjwt-serialization
Open

fix(sdjwt): serialize pydantic models in json mode to support enum claims in mandates#355
harkanwalbedi wants to merge 2 commits into
google-agentic-commerce:mainfrom
harkanwalbedi:fix/agent-recurrence-sdjwt-serialization

Conversation

@harkanwalbedi

Copy link
Copy Markdown

Description

An OpenPaymentMandate carrying an AgentRecurrence constraint can't be signed. MandateClient.create() throws:

TypeError: Object of type Frequency is not JSON serializable

What's happening

delegate_claims_from_model() was using a plain model_dump(), which leaves enums as Enum members rather than their values. Nothing in our own code minds that, but the claims then get handed to the sd_jwt library, which hashes every disclosure with stdlib json.dumps() — and that has no idea what a Frequency is:

ap2/sdk/mandate.py:168            MandateClient.create
ap2/sdk/sdjwt/sd_jwt.py:53        sd_jwt.create
ap2/sdk/sdjwt/common.py:281       common.issue_sd_jwt
sd_jwt/issuer.py:106              _create_sd_claims_list
sd_jwt/disclosure.py:24           Disclosure._hash
json/encoder.py                   -> TypeError

The fix

-    return payload.model_dump(by_alias=True, exclude_none=True)
+    return payload.model_dump(by_alias=True, exclude_none=True, mode='json')

delegate_claims_from_model() is the one spot where Pydantic models cross into the SD-JWT layer — both sd_jwt.py and kb_sd_jwt.py route through it — so fixing it there covers both paths rather than patching each call site.

A note on the scope of mode='json'

This flag does more than enums, so I checked what else it touches in the generated types. Two things: AnyUrl and AwareDatetime. All three were already broken:

Field type Before Survives json.dumps? After
Enum Frequency.DAILY no 'DAILY'
AnyUrl AnyUrl('https://example.com/x') no 'https://example.com/x'
AwareDatetime datetime(..., tzinfo=...) no '2026-09-11T20:00:00-07:00'

So this can't change any claim that previously signed successfully — none of these ever made it through json.dumps() in the first place. It just happens to fix the URL and timestamp cases along the way.

One thing I specifically verified: AnyUrl normalization (Example.COM:443example.com) happens at validation time, not at dump time, so signed values don't shift.

Tests

Added test_open_payment_mandate_agent_recurrence_can_be_signed. It signs a mandate containing AgentRecurrence and verifies the round trip, asserting the enum comes back as a real Frequency.DAILY and not a bare string. Fails on main with the exact error from the issue, passes with this change.

  • agent_recurrence_tests.py — 8/8 passing
  • Full SDK suite — 187 passing
  • ruff check clean against the repo's .ruff.toml

One heads-up: kb_sd_jwt_intermediate_tests.py::test_verify_rejects_aud_mismatch and ::test_verify_rejects_nonce_mismatch fail, but they fail on main as well — I stashed this change and re-ran to confirm. They're key-binding checks, unrelated to serialization. Happy to file a separate issue if it's not already known.

Fixes #354 🦕

@harkanwalbedi
harkanwalbedi requested a review from a team as a code owner September 11, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: An OpenPaymentMandate carrying AgentRecurrence cannot be signed (TypeError: Frequency not JSON serializable)

1 participant