Skip to content

feat: reserve the ucp protocol namespace, add map_order for registry ordering - #657

Open
igrigorik wants to merge 11 commits into
mainfrom
feat/map-order
Open

feat: reserve the ucp protocol namespace, add map_order for registry ordering#657
igrigorik wants to merge 11 commits into
mainfrom
feat/map-order

Conversation

@igrigorik

Copy link
Copy Markdown
Contributor

UCP represents extensible registries as JSON objects keyed by reverse-DNS names — payment_handlers, services, capabilities. That shape is deliberate (decentralized governance, uniqueness by construction, propertyNames enforcement), but JSON object member order is not a protocol contract: RFC 8785 canonicalization, which UCP signing relies on, sorts object member names. There is currently no reliable way for a Business to declare preferred traversal order for registry keys — the concrete driver is merchant-preferred payment-handler presentation order (#170; design discussion in #525).

Proposed model

  • The member name ucp is reserved at every object scope as the protocol namespace. The top-level envelope that profiles and responses already carry is not a special wrapper — it is the root manifestation of this reservation: a reserved member of the root object, grammatically identical to any nested scope carrying protocol annotations. Placement carries no semantics.
  • The namespace is ambient and its vocabulary is schema-registered. Any object scope MAY carry a ucp member; domain schemas never declare it — the member is part of the UCP document grammar, like the name reservation itself. Its contents are defined exclusively by the vocabulary registered in ucp.json#/$defs/members. Consumers process the members they recognize and MUST ignore the rest.
  • Admission rule: a member enters the vocabulary only if it is safe to ignore — a non-processing consumer loses only that member's benefit, never correctness. No structural member can ever be load-bearing. One exception is an object closed with additionalProperties: false does not get an exemption from the grammar — it MUST declare the optional ucp property explicitly.

Because registration is central, future members work at every scope the moment they land: #655's request constraints rebase onto this vocabulary as one property registration, immediately usable on any leaf object (e.g. per-line-item quantity constraints).

First member: map_order

map_order declares preferred key-traversal order for sibling map-valued fields, carried in an array value because JCS preserves array element order while sorting object keys. Partial lists are valid; unlisted keys remain valid and follow via field-defined fallback or lexicographic order; the list is not an allowlist; absent means no order is declared, and consumers MUST NOT fall back to member order.

At the root envelope, it orders the registries directly beside it:

{
  "ucp": {
    "version": "2026-01-11",
    "payment_handlers": {
      "com.google.pay": [{ "id": "gpay", "version": "2026-01-11" }],
      "dev.shopify.shop_pay": [{ "id": "shop_pay", "version": "2026-01-11" }]
    },
    "map_order": {
      "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"]
    }
  }
}

At any deeper scope, the same member rides the ambient ucp member — no schema change to the host object. Ordering an identity-provider registry inside a capability config:

{
  "config": {
    "providers": {
      "app.example.login": [{ "type": "oauth2" }],
      "com.google": [{ "type": "oauth2" }]
    },
    "ucp": {
      "map_order": { "providers": ["app.example.login", "com.google"] }
    }
  }
}

For payment_handlers, the declared order is the Business's presentation preference (#170): suggestive — the Platform SHOULD consider it and MAY apply its own ordering.

Why this shape / alternatives considered in #525:

  • display_order embedded rank (feat: add optional display_order to payment handlers #176): cannot attach to a map bucket whose value is an array; unnecessary where arrays already self-order.
  • Reserved key inside the map: breaks propertyNames — a breaking change for every shipped validator; typed maps become unions.
  • Per-field siblings (payment_handlers_order): open-ended reserved-name sprawl; _order suffix collides with the commerce Order noun.
  • $-prefixed member (original key ordering for UCP map registries #525 proposal): empirically taxed in codegen — $ is an illegal identifier in Python/Go/Java/C#/Rust, and Pydantic's asymmetric alias defaults silently emit non-conforming payloads with stock datamodel-code-generator. ucp is a clean identifier in every ecosystem, and the name is already de facto reserved (zero domain usages in the corpus).
  • Array-of-keyed-entries registries: cleanest greenfield shape, breaking for every shipped UCP map.
  • Per-scope opt-in declaration: wastes the universal reservation and pollutes every host schema; rejected in favor of the ambient grammar plus central registration.

Compatibility

Additive. The envelope is open today; existing consumers ignore map_order. No shipped registry is reshaped; registry maps stay pure and homogeneous (propertyNames already rejects ucp and map_order as registry keys — verified). Consumers that ignore the member lose nothing: order was never reliable before.


Checklist

  • Core Protocol: Changes to the base communication layer, global context, or breaking refactors.
  • Documentation: Updates to README, or documentations regarding schema or capabilities.
  • I have followed the Contributing Guide
  • I have updated the documentation (if applicable).
  • My changes pass all local linting and formatting checks.

UCP registries are reverse-DNS keyed JSON maps, and JSON object member
order is not a protocol contract: RFC 8785 (JCS) canonicalization, used
by UCP signing, sorts object member names. Businesses have no reliable
way to declare preferred traversal order for registry keys, e.g. payment
handler presentation order (#170, design discussion in #525).

The model: the member name `ucp` is reserved at every object scope as
the protocol namespace. The top-level envelope is the root manifestation
of that reservation, not a special wrapper. The namespace is ambient:
any object scope MAY carry a `ucp` member, domain schemas never declare
it (it is document grammar, like the reservation itself), and its
contents are defined exclusively by the vocabulary registered in
ucp.json#/$defs/members. Consumers process the members they recognize
and ignore the rest; a member is admitted to the vocabulary only if it
is safe to ignore, so no member can be load-bearing for correctness.
One exception: an object closed with additionalProperties:false must
declare the optional `ucp` property explicitly. Future members register
once and work at every scope immediately -- request constraints (#655)
can rebase onto this vocabulary.

The first member, map_order, declares key-traversal order for sibling
map-valued fields, carried in an array because JCS preserves array
element order. Partial lists are valid, unlisted keys remain valid and
follow, and the list is not an allowlist. At the root envelope it orders
the registries beside it:

  "ucp": {
    "payment_handlers": {
      "com.google.pay": [ ... ],
      "dev.shopify.shop_pay": [ ... ]
    },
    "map_order": {
      "payment_handlers": ["dev.shopify.shop_pay", "com.google.pay"]
    }
  }

At any deeper scope the same member rides the ambient `ucp` member, with
no schema change to the host object -- e.g. ordering an identity
provider registry inside a capability config:

  "config": {
    "providers": {
      "app.example.login": [ ... ],
      "com.google": [ ... ]
    },
    "ucp": {
      "map_order": { "providers": ["app.example.login", "com.google"] }
    }
  }

Refs #525, #170
@igrigorik igrigorik added this to the Working Draft milestone Jul 31, 2026
@igrigorik
igrigorik requested a review from amithanda July 31, 2026 18:19
@igrigorik igrigorik self-assigned this Jul 31, 2026
@igrigorik igrigorik added the TC review Ready for TC review label Jul 31, 2026
the context of the cart (e.g., removing "Buy Now Pay Later" for subscription
items, or filtering regional methods based on shipping address).

**Presentation Order:** Businesses **MAY** declare a preferred presentation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a tricky as per the comments on #170 and #176 citing that available instruments within handlers are really the presentation grain that a business may be most interested in ordering and map_order doesn't cover that (in current form). However, in practice this may still address a large number of use cases (where payment handlers are advertising more than one available instrument or where businesses are content to order at the handler level) riding on a broadly usable ordering mechanism.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed an update that clarifies this: 6391c451 — within a handler, the business's available_instruments array order carries the instrument presentation preference, so combined with map_order.payment_handlers the full presentation order can be derived (covers the #176 example: order the handlers, tokenizer advertises [card, bank]).

To be fair, this doesn't cover every case: a business can rank handlers, and rank instruments within each handler, but it can't mix them freely across handlers — e.g. card via gpay first, then bank via tokenizer, then back to paypal via gpay. I'd suggest we leave that as is though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - it was the mixing case that I had in mind ... but - agreed - not a critical / blocking use case.

Handler-level presentation preference is carried by
map_order.payment_handlers; review on #657 noted the instrument grain
was uncovered. The order of the business's advertised
available_instruments array now carries the same suggestive
within-handler preference, earliest first: platforms SHOULD consider it
and MAY apply their own ordering, and the buyer-side context.payment[]
preference remains distinct with the platform arbitrating. Completes
handler-major presentation ordering at both grains.
@jamesandersen

Copy link
Copy Markdown
Contributor

Two comments coming to mind:

  1. An ambient ucp that domain schemas never declare is invisible to schema-driven codegen (untyped escape hatch to read it, and non-uniform given the closed-object exception), and its contents fall outside ordinary instance validation. More importantly, the tolerant-reader mandate isn't free at every scope — e.g. Java's Jackson fails on unknown by default; Go/Pydantic drop it (round-trip/re-sign loss). Given $ was rejected partly on codegen grounds, this trades one codegen tax for another.

  2. Does the reserved ucp member have defined meaning on the request side, or only responses? The container looks request/response-agnostic, and gating is per-member via ucp_request: omit — which feat: add response-carried request constraints #655 uses for request_constraints, but map_order isn't marked, so it's schema-valid on a request with no defined semantics. Since this is the foundational PR, worth stating the general rule here: either per-member applicability (with map_order marked/ignored on requests) or response/profile-only for the whole container?

Comment thread docs/specification/overview.md Outdated
Comment thread docs/specification/overview.md Outdated
Comment thread source/schemas/ucp.json
Comment thread docs/specification/overview.md Outdated
Comment thread docs/specification/overview.md Outdated
Comment thread docs/specification/overview.md Outdated
   The ambient `ucp` reservation applies to structured objects whose
   members are schema-defined fields. Dictionary keys are application data,
   so reserving `ucp` there conflicts with schemas such as `attribution`,
   where all values are strings.

   Limit the reservation to structured object scopes and treat `ucp` as
   ordinary data in dictionary containers. Keep structured objects used as
   dictionary values eligible. Narrow the closed-object authoring rule to
   the same boundary and align the central vocabulary description,
   overview, and glossary.
   The existing idempotence rule prohibited any nested `ucp` member, which
   also excluded valid protocol namespaces on structured objects located
   beneath the root envelope, such as a capability's `config`.

   Limit the prohibition to a direct `ucp.ucp` child while keeping deeper
   structured objects eligible for their own protocol namespace.
   UCP schemas are open by default, so ordinary JSON Schema validation
   accepts ambient `ucp` at eligible structured scopes without applying
   vocabulary rules. Recommend that UCP-aware tooling recognize and apply
   the central vocabulary.

   Require UCP-aware strict validators to continue allowing and validating
   ambient `ucp` while rejecting other unknown domain fields. Note that
   non-UCP-aware strict validators may reject it; this is an accepted
   compatibility limit.
   AP2 merchant authorization covers the JCS-canonicalized checkout
   without `ap2`, rather than the raw HTTP body bytes. Projecting the
   checkout through schema-recognized fields can therefore change the
   signed logical content.

   Clarify that verification and mandate construction use the complete
   checkout JSON and that removing any covered member invalidates the
   signature.
   Mark both `map_order` properties with `ucp_request: "omit"`. Document
   per-member applicability and require repeated exposures to carry the
   same annotation. Request resolution now removes `map_order` while
   retaining the containing `ucp` envelope where its schema requires it.
   Nested `map_order` lives inside `ucp` while its target map sits on
   the object annotated by `ucp`. At the root, registries and `map_order`
   instead share the root envelope. Calling both relationships sibling
   fields left root domain maps ambiguous.

   Define nested lookup on the object containing `ucp` and root lookup
   inside the root envelope. Leave root domain fields such as `actions`
   out of scope and align the schema descriptions and processing rules.
   `uniqueItems` allowed a repeated key in advisory ordering metadata to
   invalidate an entire profile or response, even though deterministic
   recovery is available.

   Allow repeated keys at the schema layer while prohibiting producers from
   emitting them. Require consumers to keep the containing document valid,
   honor the first occurrence, and ignore later repetitions.
   Define UCP-aware schema resolution as the boundary that applies the central
   namespace vocabulary at eligible structured scopes and emits ordinary JSON
   Schema for standard validators and code generators. Keep the authoring guide
   focused on source-schema rules rather than repeating the resolution contract.
igrigorik added a commit to Universal-Commerce-Protocol/ucp-schema that referenced this pull request Aug 7, 2026
   Implement the reference schema-resolution behavior for
   Universal-Commerce-Protocol/ucp#657.

   Add a UCP-aware resolution path that applies the central namespace vocabulary
   at eligible structured object scopes while excluding dictionaries. Preserve
   namespace openness and directional applicability, reject direct `ucp.ucp`,
   and emit ordinary JSON Schema for existing validators and code generators.

   Keep helper references valid through nested resource IDs and selected
   definitions, extend strict traversal across child-instance applicators, and
   cover directionality, composition, dictionary values, forward compatibility,
   and collision handling.
@igrigorik

Copy link
Copy Markdown
Contributor Author

@jamesandersen put together ucp-schema#48 as ref implementation: it boils down to a preprocessor pass that materializes ucp properties at eligible objects. The resulting output is ordinary JSON Schema, so downstream validation and codegen work unchanged. Overall, I agree that this adds another UCP-specific requirement, but we already have the preprocessor requirement for resolving ucp request/response annotations, so it's not a net new ask.

@amithanda great feedback, ty! Addressed in followup commits -- ptal.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants