Skip to content

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

Description

@Monarch33

Title

[Bug]: An OpenPaymentMandate carrying an AgentRecurrence constraint cannot be signed — TypeError: Object of type Frequency is not JSON serializable

Body

Where

common.delegate_claims_from_model in code/sdk/python/ap2/sdk/sdjwt/common.py, commit e1ea56d, interacting with the third-party sd_jwt library's disclosure hashing.

What's wrong

delegate_claims_from_model serializes a Pydantic payload with:

return payload.model_dump(by_alias=True, exclude_none=True)

This is Pydantic's Python-mode dump (the default), which leaves enum-typed fields as the enum member itself rather than its underlying value — that conversion is what model_dump(mode="json") does instead, and mode="json" is not used here. The resulting dict — still containing a raw Frequency enum instance wherever an AgentRecurrence.frequency field is set — is later handed to the third-party sd_jwt library's disclosure-hashing code, which calls plain json.dumps() on it. Frequency (code/sdk/python/ap2/sdk/generated/...) is a plain Enum, not a str-mixed enum, so json.dumps has no way to serialize it and raises.

Reproduction

from ap2.sdk.mandate import MandateClient
from ap2.sdk.generated.open_payment_mandate import OpenPaymentMandate, AgentRecurrence
from ap2.sdk.generated.types.recurrence import Frequency

MandateClient().create(
    payloads=[
        OpenPaymentMandate(
            constraints=[
                AgentRecurrence(frequency=Frequency.ON_DEMAND, max_occurrences=1)
            ],
            cnf=...,
            iat=...,
            exp=...,
        )
    ],
    issuer_key=issuer_jwk,
)
# TypeError: Object of type Frequency is not JSON serializable

Impact

Unlike a verification-time bug, this fails closed — the mandate simply cannot be signed, so no incorrectly-accepted mandate results from it. But it means AgentRecurrence, one of the documented constraint types, cannot currently be exercised end-to-end through the SDK at all: no caller can construct a valid signed OpenPaymentMandate carrying this constraint using the public create() API. This also means the constraint's own evaluator (AgentRecurrenceEvaluator) cannot be tested against a real signed chain by downstream integrators — only against synthetic/hand-built payloads.

Suggested fix

Use payload.model_dump(by_alias=True, exclude_none=True, mode="json") in delegate_claims_from_model so enum fields (and any other JSON-incompatible Python-mode types) are serialized to their JSON-safe representation before being handed to the disclosure-hashing code.

Note

This is unrelated to #297, which concerns AgentRecurrenceEvaluator's frequency/period semantics being underspecified (a design/spec gap in what the constraint enforces), not the constraint failing to serialize at signing time at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions