Skip to content

Order responses serialize unset optional fields as null, failing validation against the official ucp-schema #223

Description

@vishkaty

The Flower Shop order responses do not validate against the official spec schemas for the version they implement (2026-04-08, and unchanged under 2026-08-25): unset optional fields are serialized as null, where the schema types them as non-nullable.

This is the same defect #115/#117 fixed for checkout responses; the order route has the identical gap in three places.

Reproduction: fresh clone at main (00333a8), ucp-sdk pinned to 0.4.6 (a fresh uv sync currently resolves 0.5.0 and the server fails to import, tracked separately in #221/#222). Boot the server, run simple_happy_path_client.py to completion, then GET /orders/{id}. Validated the response with the official ucp-schema validator (root source/schemas/shopping/order.json, --op read --response) and independently with a Draft 2020-12 jsonschema referee over the full vendored schema set, against both the 2026-04-08 and 2026-08-25 spec corpora. All four combinations agree on the same 34 violating paths, faults of the form null is not of type ... (the capability extends fails its oneOf instead):

/label: null is not of type "string"
/line_items/0/item/image_url: null is not of type "string"
/line_items/0/quantity/original: null is not of type "integer"
/line_items/0/totals/0/display_text: null is not of type "string"
/line_items/0/parent_id: null is not of type "string"
/fulfillment/expectations/0/destination/extended_address: null is not of type "string"
/fulfillment/expectations/0/fulfillable_on: null is not of type "string"
/adjustments: null is not of type "array"
/totals/0/display_text: null is not of type "string"
/totals/0/lines: null is not of type "array"
/messages: null is not of type "array"
/attribution: null is not of type "object"
/ucp/services: null is not of type "object"
/ucp/payment_handlers: null is not of type "object"
/ucp/capabilities/dev.ucp.shopping.checkout/0/spec: null is not of type "string"
/ucp/capabilities/dev.ucp.shopping.checkout/0/extends: null is not valid under any of the schemas listed in the 'oneOf' keyword

...and 18 more of the same shape; the full list is in the linked PR.

Mechanism: three independent write sites model_dump an Order without exclude_none, so every unset optional field round-trips through storage (and back out) as an explicit null:

  • services/checkout_service.py:885, inside complete_checkout, the initial persist when an order is created.
  • routes/order.py:89, the PUT /orders/{id} update handler.
  • routes/ucp_implementation.py:321, order_event_webhook, the inbound receiver a partner posts order events to.

GET /orders/{id} and PUT /orders/{id} both read/write the same stored row, and the outbound order-event webhook the server sends (_notify_webhook in checkout_service.py) delivers that same stored row as its body, so all three surfaces carry the defect.

Why CI does not catch it: every existing order-touching test in integration_test.py (test_shipping_event_matches_order_schema, test_webhook_delivers_the_bare_order_as_body) validates with Order.model_validate(...). The generated pydantic model types every one of these fields Optional[...] = None, so it happily accepts a null the wire schema forbids. Pydantic model validation and JSON Schema validation disagree here in exactly the way the #115 issue body flagged for checkout.

Class sweep: checked every other order-adjacent response for the same pattern (checkout, cart, the order-event webhook body). Checkout is clean (fixed by #117). Cart is clean (built after #117, over its idiom, in #159). The webhook body is fixed by the same three sites above, since it reads the same storage. Table with both-validator results is in the linked PR.

Node twin: the Node reference server does not have this defect. src/api/order.ts returns the stored object via c.json(); the object is assembled in checkout.ts by copying fields straight through (e.g. parent_id: li.parent_id), and an unset optional field there is JS undefined, which JSON.stringify (and the Hono c.json) omits rather than serializing as null. Verified with an in-process probe driving the same create-complete-GET-PUT flow: 0 nulls in either the GET or PUT order body, both validated clean against both spec corpora. No fix needed on the Node side.

Expected: an unset optional field should be omitted from the response, the same as checkout and cart already do.

Fix: PR with exclude_none=True on all three sites, following the #117 idiom exactly, plus tests pinning the no-null invariant on all three surfaces.

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