Skip to content

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

Open
igrigorik wants to merge 1 commit into
mainfrom
feat/map-order
Open

feat: reserve the ucp protocol namespace, add map_order for registry ordering#657
igrigorik wants to merge 1 commit 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.

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.

3 participants