Skip to content

key ordering for UCP map registries #525

Description

@igrigorik

UCP represents extensible registries as JSON objects keyed by reverse-DNS identifiers. One limitation of this contract is that JSON object member order is not guaranteed, and we've run into cases where we want to declare the preferred traversal order of their keys — e.g. payment handlers, identity providers, etc.

{
  "payment_handlers": {
    "com.google.pay": [{"id": "gpay", "version": "2026-01-11"}],
    "dev.shopify.shop_pay": [{"id": "shop_pay", "version": "2026-01-11"}]
  },
  "providers": {
    "app.example.login": [{"type": "oauth2", ... }],
    "com.google": [{"type": "oauth2", ...}]
  }
}

Note: this shape is intentional. Reverse-DNS keys provide decentralized namespace governance, JSON object keys provide uniqueness by construction, and schemas can enforce key shape with propertyNames / patternProperties. However, JSON object member order is not semantically reliable. Canonicalization, signing, intermediaries, and ordinary JSON tooling may reorder object keys.


Proposal

Introduce a reserved local companion object named $order.

{
  "providers": {
    "app.example.login": [{"type": "oauth2"}],
    "com.google": [{"type": "oauth2"}]
  },
  "$order": {
    "providers": ["app.example.login", "com.google"]
  }
}

$order is an object scoped to its containing object. Each property of $order names a sibling map-valued field. The value is an array of keys from that sibling map and defines ordered traversal of those keys.

$order defines key order only. It does not, by itself, define display priority, selection priority, trust level, negotiation preference, endorsement, or fallback behavior. The referenced field’s specification must state what ordered traversal means in that context.

Normative rules

For a sibling map field <field> and companion array $order.<field>:

  1. Producers MUST NOT rely on JSON object member order for UCP map-valued registries.
  2. $order.<field> contains keys from <field> in preferred traversal order.
  3. The order array MAY be partial. Listed keys are traversed first, in array order.
  4. Unlisted map keys remain valid and available. They are appended after listed keys using the field-defined fallback, or lexicographic order if no fallback is defined.
  5. The order array is not a whitelist. Omission from $order.<field> MUST NOT mean removal, ineligibility, or reduced support.
  6. Producers SHOULD only list keys present in the sibling map. Robust consumers SHOULD ignore keys that are not present.
  7. Duplicate keys are invalid. Robust consumers SHOULD honor the first occurrence and ignore later duplicates.
  8. If $order or $order.<field> is absent, no order is declared. Consumers MUST NOT use object member order as fallback.
  9. A parent field’s specification MUST state what the ordered traversal is used for.

Examples

Identity providers

If identity provider ordering is later desired, the shape can order provider namespaces without changing the current providers map:

{
  "config": {
    "providers": {
      "app.example.login": [
        {"type": "oauth2", "auth_url": "https://accounts.example-login.app/"},
        {"type": "wallet"}
      ],
      "com.google": [
        {"type": "oauth2", "auth_url": "https://accounts.google.com/"}
      ]
    },
    "$order": {
      "providers": ["app.example.login", "com.google"]
    }
  }
}

This orders provider buckets. It does not define mechanism selection order inside app.example.login; that remains local to the providers["app.example.login"] array and the identity-linking spec.

Payment handlers

If the desired question is “which payment handler / wallet namespace should be promoted first?” then the order belongs to payment_handlers keys:

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

This does not rank concrete payment instruments such as “card” vs “bank”, that order can be derived directly from the instruments array: JSON arrays are ordered sequences; JSON canonicalization preserves array element order even while sorting object keys.

Schema sketch

Each schema declares $order entries for the sibling maps that support ordering:

{
  "type": "object",
  "properties": {
    "providers": {
      "type": "object",
      "propertyNames": {"$ref": "types/reverse_domain_name.json"},
      "additionalProperties": {
        "type": "array",
        "items": {"$ref": "#/$defs/provider"}
      }
    },
    "$order": {
      "type": "object",
      "properties": {
        "providers": {
          "type": "array",
          "items": {"$ref": "types/reverse_domain_name.json"},
          "uniqueItems": true
        }
      },
      "additionalProperties": false
    }
  }
}

Standard JSON Schema can validate the shape, item syntax, and uniqueness. It generally cannot validate “every ordered key is present in the sibling map” without custom validation. That rule should live in UCP conformance tooling and tolerant consumer behavior.

Compatibility and trade-offs

This is additive. Existing map values remain unchanged, existing consumers that ignore unknown properties can continue to read the map as is and the registry map itself remains pure and homogeneous.

Costs:

  • reserves $order as structural UCP metadata in object scopes that use it;
  • requires prose/custom validation for cross-reference integrity;
  • introduces a new $-prefixed instance-field convention;
  • parent field specs must be disciplined about semantics.

Alternatives

This proposal is intentionally narrow. It orders reverse-DNS map buckets without changing the existing map value shape. Other ordering shapes remain useful, but solve different problems or break compatibility.

Why not display_order?

display_order is not wrong; it solves a different level of the model. PR #176 explored adding display_order to available_instruments[] entries:

{
  "payment_handlers": {
    "com.google.pay": [
      {
        "id": "gpay",
        "available_instruments": [
          {"type": "card", "display_order": 0}
        ]
      }
    ],
    "com.example.tokenizer": [
      {
        "id": "tokenizer",
        "available_instruments": [
          {"type": "card", "display_order": 1},
          {"type": "bank", "display_order": 2}
        ]
      }
    ]
  }
}

First, because instruments is an array, display_order is unnecessary. Also, it does not / cannot solve order of the parent map. Further, if we can solve order of the parent map, then it's also possible to derive relative ordering of cross-handler instruments based on parent rank and instrument rank within each handler.

Finally, display_order cannot be added as a peer inside a reverse-DNS map without breaking the map invariant. Here display_order is not a reverse-DNS key, not a payment handler entry, and would break existing clients:

{
  "payment_handlers": {
    "display_order": 0,
    "com.google.pay": [...]
  }
}

Other shapes

Option Works for Why not sufficient here
Embedded rank (display_order: 0) A concrete value object or child entry that is itself being ordered Cannot attach cleanly to a parent map bucket when the map value is an array.
Array of keyed entries Greenfield ordered registries Cleanest standard pattern, but breaking for existing UCP maps — see above.
In-map reserved order key Locality inside the map Breaks the invariant that every map property is an entry; old validators reject it; typed maps become unions.
Per-field sibling (providers_order) Backward-compatible key order Safe, but creates per-field naming sprawl.

$order is the best compromise I've found so far: it preserves the existing map shape, keeps maps homogeneous, avoids dotted fixed property names, groups ordering metadata for multiple sibling maps, and visibly marks the data as structural metadata. Would love to hear alt proposals and ideas.

p.s. if we go down this round we can also later formalize this for UCP-aware tooling with a custom JSON Schema vocabulary, e.g. a schema annotation such as ucp_orderable_map. That annotation could tell UCP tooling that $order.providers orders the keys of providers, enabling linting and helper generation. This is useful but not in the critical path. $order is the wire convention; a custom vocabulary would be tooling metadata layered on top.

Metadata

Metadata

Assignees

Labels

TC reviewReady for TC review

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions