From e49c4da2587909e73940327b74377f2e14a3c7c4 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Wed, 22 Jul 2026 00:10:12 -0400 Subject: [PATCH 1/2] split instrument availability into constraints and options axes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior shape put everything an instrument declares — presence, per-field constraints, accepted card brands, and credential families — into one `constraints` bag. Prototyping a resolver against that shape showed the cost: to enforce field requirements a consumer must first classify every key by resolving its concrete schema (is this a field constraint? a value menu? a typed family?). That classification is the whole problem — it needs the target schema, defeats "compile from data alone," and produced real bugs in the prototype (per-key allOf composition, and a `required` name collision). Partition the bag into two sibling axes: - `constraints` — field requirements over the instrument's OWN fields (an Object Constraint: `required` plus one direct key per constrained field). Compiles to a JSON Schema overlay (`required` + `properties`) a standard validator runs. - `options` — accepted value menus (`brands`) and typed families (`credentials`), resolved by data lookup and never part of the overlay. With the menus and families moved to `options`, `constraints` holds nothing but `required` and field constraints — and `object_constraint` now enforces exactly that: every non-`required` key is a nested Object Constraint or a Value Constraint (recursing via `$ref "#"`). Compiling the overlay is therefore a pure data walk — no schema resolution, no classification. That is the win: the resolver is clean by construction, not by careful bookkeeping. Proven in the ucp-schema resolver prototype (~20-line compile; the compiled overlay enforces presence and allowed values against real instances). Trade-offs: - `constraints` is deliberately not a literal JSON Schema: fields are direct keys, so a consumer compiles it (take `required`, lift field keys under `properties`). A `properties`-keyed sub-map would make it literal but collide the JSON Schema `properties` keyword with the data key when a schema narrows a nested field. The compile is trivial, and a resolver runs regardless. - `required` is reserved inside a constraint object, so a field literally named `required` can be listed as present but cannot carry a nested constraint. Rare, and documented. - The shape still does not enforce name resolution (does the named field exist on the target?). Unchanged from before; a lint pass can add it later — the partition makes it tractable, since every non-`required` key is unambiguously a field name. Migration: the stricter `object_constraint` (every non-`required` key must be a field constraint) surfaced five stale example docs still using `constraints: {brands}`; migrated them to `options: {brands}`. schema-authoring and payment-handler-guide are rewritten to the two-axis model, and the availability wire example is now validated by the example harness instead of skipped. --- docs/documentation/schema-authoring.md | 118 +++++++++--------- docs/specification/checkout-mcp.md | 2 +- .../examples/encrypted-credential-handler.md | 6 +- .../platform-tokenizer-payment-handler.md | 6 +- .../processor-tokenizer-payment-handler.md | 6 +- docs/specification/overview.md | 6 +- docs/specification/payment-handler-guide.md | 40 +++--- .../types/available_payment_instrument.json | 17 +-- .../types/card_payment_instrument.json | 11 +- .../shopping/types/object_constraint.json | 10 +- 10 files changed, 117 insertions(+), 105 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 799357524..366b09064 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -422,58 +422,59 @@ typos in core metadata like the `ucp` block). ### Constraint Objects -UCP uses `constraints` objects when a declaration or response needs to narrow -what is acceptable without changing the base payload schema. Constraints are sparse runtime requirements on an already-typed target. -UCP defines three composable primitive constraints: +UCP uses constraint objects when a declaration or response needs to narrow what +is acceptable without changing the base payload schema. Constraints are sparse +runtime requirements on an already-typed target, declared along **two axes**: -| Primitive | Target | Built-in vocabulary | -| :-------- | :----- | :------------------ | -| [`ObjectConstraint`](site:{{ ucp_version }}/schemas/shopping/types/object_constraint.json) | Object | `required` | -| [`ValueConstraint`](site:{{ ucp_version }}/schemas/shopping/types/value_constraint.json) | Property value | `enum`, `const` | -| [`TypeConstraint`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json) | Typed branch | `type`, optional `constraints` | - -A concrete Object Constraint **MUST** extend `object_constraint.json` and define -every supported key. The key's schema determines its meaning: +- **`constraints`** — field requirements over the target's OWN fields, as an + Object Constraint: `required` (presence) plus one key per constrained field. + A consumer compiles it to a JSON Schema overlay (`required` + `properties`) a + standard validator can run. +- **`options`** — accepted value menus and typed families the target negotiates: + a uniform map from attribute to acceptable set — a scalar menu (e.g. `brands`) or + a typed family (Type Constraint entries keyed by `type` with per-branch + `constraints`, e.g. `credentials`). Resolved by lookup; not part of the overlay. -- `ObjectConstraint` — recursively constrains a same-named object property. -- `ValueConstraint` — constrains a same-named property's value. -- `TypeConstraint` (or an array of them) — selects a typed branch. -- Any other schema — defines a domain-specific literal operator whose semantics - the owning specification must document. +UCP defines three composable primitives: -Every name in `required` **MUST** be a property of the constrained object. The -Object Constraint stays open so handler and extension schemas can compose with -`allOf`; after negotiation, a key not defined by the concrete constraint schema -is an authoring or compatibility error. - -This example defines all four key forms: +| Primitive | Target | Built-in vocabulary | +| :-------- | :----- | :------------------ | +| [`ObjectConstraint`](site:{{ ucp_version }}/schemas/shopping/types/object_constraint.json) | An object's fields | `required` + a key per constrained field | +| [`ValueConstraint`](site:{{ ucp_version }}/schemas/shopping/types/value_constraint.json) | A property's value | `enum`, `const` | +| [`TypeConstraint`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json) | A typed branch | `type`, optional `constraints` | + +Inside an Object Constraint, `required` is reserved (the presence list); every +other key names a target field and carries that field's nested constraint — an +Object Constraint (recurse) or a Value Constraint. The two are distinguishable +**from the data alone** — `enum`/`const` is a Value Constraint, otherwise it's an +Object Constraint — so a consumer compiles the overlay without resolving the +concrete schema. Field constraints are **open by default**: they live in the wire +data and validate against the open Object Constraint, so most handlers only narrow +`options`. Every name in `required` **MUST** be a property of the target. + +Concrete availability schema (a handler narrowing the card branch). Narrowing a +field constraint takes a single `properties` keyword — rarely needed, since field +constraints are open: ```json { "$defs": { - "constraint": { + "available_card": { "allOf": [ - { "$ref": "object_constraint.json" }, + { "$ref": "available_payment_instrument.json" }, { "properties": { - "billing_address": { - "allOf": [ - { "$ref": "object_constraint.json" }, - { - "properties": { - "address_country": { "$ref": "value_constraint.json" } - } - } - ] - }, - "credentials": { - "type": "array", - "items": { "$ref": "type_constraint.json" } + "type": { "const": "card" }, + "constraints": { + "properties": { + "billing_address": { "$ref": "object_constraint.json" } + } }, - "brands": { - "type": "array", - "items": { "type": "string" } + "options": { + "properties": { + "brands": { "type": "array", "items": { "type": "string" } } + } } } } @@ -483,29 +484,34 @@ This example defines all four key forms: } ``` -Its wire value remains local to the constrained fields: +Its wire value keeps field requirements and accepted options in separate axes, +and field constraints nest as plain direct keys: - + ```json { - "required": ["billing_address"], - "billing_address": { - "required": ["address_country"], - "address_country": { "enum": ["US", "CA"] } + "type": "card", + "constraints": { + "required": ["billing_address"], + "billing_address": { + "required": ["address_country"], + "address_country": { "enum": ["US", "CA"] } + } }, - "credentials": [{ "type": "token" }], - "brands": ["visa", "mastercard"] + "options": { + "brands": ["visa", "mastercard"], + "credentials": [{ "type": "token" }] + } } ``` -`ValueConstraint` is deliberately closed: unsupported assertions cannot be -ignored safely. `ObjectConstraint` and `TypeConstraint.type` are extension -points. To type-check a declaration, resolve its target and concrete constraint -schemas, validate each key and value, and check `required` names against the -target. Object and Value Constraints can compile into a JSON Schema overlay; -literal domain operators are only type-checked, and their owner enforces their -meaning. Declared constraints are an upfront minimum; dynamic requirements still -use recoverable errors and [`message_error.path`](site:{{ ucp_version }}/schemas/common/types/message_error.json). +`ValueConstraint` is deliberately closed: unsupported assertions cannot be ignored +safely. `ObjectConstraint` and `TypeConstraint.type` are extension points. +`constraints` compiles into a JSON Schema overlay (presence + allowed values) that +a standard validator runs; `options` is resolved by lookup — scalar menus by +membership, typed families by dispatch on the submitted `type`. Declared constraints +are an upfront minimum; dynamic requirements still use recoverable errors and +[`message_error.path`](site:{{ ucp_version }}/schemas/common/types/message_error.json). ### Property-Count Constraints (`minProperties` / `maxProperties`) diff --git a/docs/specification/checkout-mcp.md b/docs/specification/checkout-mcp.md index 303e26051..a205fc3cd 100644 --- a/docs/specification/checkout-mcp.md +++ b/docs/specification/checkout-mcp.md @@ -67,7 +67,7 @@ Businesses advertise MCP transport availability through their UCP profile at "spec": "https://example.vendor.com/specs/delegate-payment", "schema": "https://example.vendor.com/schemas/delegate-payment-config.json", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard"]}} + {"type": "card", "options": {"brands": ["visa", "mastercard"]}} ], "config": {} } diff --git a/docs/specification/examples/encrypted-credential-handler.md b/docs/specification/examples/encrypted-credential-handler.md index 58779ce83..99c37c765 100644 --- a/docs/specification/examples/encrypted-credential-handler.md +++ b/docs/specification/examples/encrypted-credential-handler.md @@ -156,7 +156,7 @@ have their own compliance requirements. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -194,7 +194,7 @@ The response config includes information about the encryption used. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -268,7 +268,7 @@ registry using `platform_config`. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex", "discover"] } } diff --git a/docs/specification/examples/platform-tokenizer-payment-handler.md b/docs/specification/examples/platform-tokenizer-payment-handler.md index bf4962f2b..87e576382 100644 --- a/docs/specification/examples/platform-tokenizer-payment-handler.md +++ b/docs/specification/examples/platform-tokenizer-payment-handler.md @@ -207,7 +207,7 @@ credential type (e.g., PCI DSS for cards). "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -243,7 +243,7 @@ The response config includes runtime token lifecycle information. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -341,7 +341,7 @@ registry using `platform_config`. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex", "discover"] } } diff --git a/docs/specification/examples/processor-tokenizer-payment-handler.md b/docs/specification/examples/processor-tokenizer-payment-handler.md index deac8e020..7fc1b0ddd 100644 --- a/docs/specification/examples/processor-tokenizer-payment-handler.md +++ b/docs/specification/examples/processor-tokenizer-payment-handler.md @@ -122,7 +122,7 @@ The handler's specification (referenced via the `spec` field) documents the "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex"] } } @@ -157,7 +157,7 @@ The response config includes runtime information about what's available for this "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex"] } } @@ -205,7 +205,7 @@ business's configuration. "id": "processor_tokenizer", "version": "{{ ucp_version }}", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard", "amex"]}} + {"type": "card", "options": {"brands": ["visa", "mastercard", "amex"]}} ], "config": { "environment": "production", diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 452e7a5b7..2762c4fec 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -565,7 +565,7 @@ Businesses publish their profile at `/.well-known/ucp`. An example: "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex"] } } @@ -727,7 +727,7 @@ example: "spec": "https://example.com/specs/payments/processor_tokenizer-payment", "schema": "https://example.com/schemas/payments/delegate-payment.json", "available_instruments": [ - {"type": "card", "constraints": {"brands": ["visa", "mastercard"]}} + {"type": "card", "options": {"brands": ["visa", "mastercard"]}} ] } ] @@ -1724,7 +1724,7 @@ request a challenge. "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index a7ccdf385..d45cbf1e0 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -229,7 +229,7 @@ and typically includes different configuration: "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -253,7 +253,7 @@ and typically includes different configuration: "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard", "amex", "discover"] } } @@ -275,7 +275,7 @@ and typically includes different configuration: "available_instruments": [ { "type": "card", - "constraints": { + "options": { "brands": ["visa", "mastercard"] } } @@ -319,28 +319,26 @@ authoritative value returned in the `response_schema`. | Source | `available_instruments` | | :----- | :---------------------- | -| Platform profile | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex", "discover"]}}]` | -| Business profile | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex"]}}]` | -| **Response (resolved)** | `[{type: "card", constraints: {brands: ["visa", "mastercard", "amex"]}}]` | +| Platform profile | `[{type: "card", options: {brands: ["visa", "mastercard", "amex", "discover"]}}]` | +| Business profile | `[{type: "card", options: {brands: ["visa", "mastercard", "amex"]}}]` | +| **Response (resolved)** | `[{type: "card", options: {brands: ["visa", "mastercard", "amex"]}}]` | In this example, the business's PSP is not configured for Discover, so Discover is excluded from the response even though the platform supports it. #### Constraint Semantics -Within each available-instrument Type Constraint, `constraints` is an -[`ObjectConstraint`](site:schemas/shopping/types/object_constraint.json) on the -selected instrument. The base availability schema defines: +Within each available-instrument entry, requirements are declared along **two +axes**: -| Key | Constraint type | Meaning | -| :-- | :-------------- | :------ | -| `required` | Object | Instrument properties required in this context. | -| `billing_address` | Object | Nested requirements on the billing address. | -| `credentials` | Type | Accepted credential branches and their requirements. | +| Axis | Type | Meaning | +| :--- | :--- | :------ | +| `constraints` | [`ObjectConstraint`](site:schemas/shopping/types/object_constraint.json) | Field requirements on the instrument's OWN fields: `required` (presence) plus a key per constrained field carrying its nested requirement / allowed values. Compiles to a JSON Schema overlay. | +| `options` | map | Accepted value menus and typed families: `brands` (a scalar menu of accepted networks) and `credentials` (typed branches with per-branch requirements). Resolved by lookup. | -Concrete schemas add their own keys. The card availability schema adds `brands`, -a literal list of accepted networks. Use field constraints instead of -handler-specific booleans for modeled data. +Express field requirements as `constraints` (an AVS postal-code requirement is +`constraints.billing_address.required: ["postal_code"]`), and accepted choices as +`options` — not handler-specific booleans or bespoke keys. ```json @@ -354,9 +352,11 @@ handler-specific booleans for modeled data. "required": ["billing_address"], "billing_address": { "required": ["postal_code", "address_country"] - }, - "credentials": [{ "type": "token" }], - "brands": ["visa", "mastercard"] + } + }, + "options": { + "brands": ["visa", "mastercard"], + "credentials": [{ "type": "token" }] } } ] diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index cf50579c0..97599ecf8 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -2,27 +2,28 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/available_payment_instrument.json", "title": "Available Payment Instrument", - "description": "An instrument type available from a payment handler with optional constraints.", + "description": "An instrument type a handler accepts, declared along two axes. `constraints` is an Object Constraint over the instrument's OWN fields (a JSON Schema overlay: presence + allowed values). `options` is the set of accepted value menus and typed families this instrument negotiates (resolved by lookup, not compiled into the overlay).", "allOf": [ { "$ref": "type_constraint.json" }, { "type": "object", "properties": { - "constraints": { + "options": { "type": "object", "properties": { - "billing_address": { - "$ref": "object_constraint.json", - "description": "Local constraint on the instrument's `billing_address` field." - }, "credentials": { "type": "array", "items": { "$ref": "type_constraint.json" }, "uniqueItems": true, - "description": "Credential specific constraints accepted for the payment instrument branch selected by `type`. Concrete instrument schemas can narrow known credential entries while preserving handler-extended credential entries." + "description": "Accepted credential families — each a Typed Constraint keyed by `type` with per-branch `constraints`. Applied by data lookup on the submitted credential's `type`; unknown types are handler/extension branches." } }, - "description": "Constraints on this instrument type. Base payment instrument constraints can require and describe `billing_address` and `credentials`; concrete instrument schemas SHOULD add instrument-specific constraint keys while remaining open to extensions." + "additionalProperties": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "description": "Accepted options this instrument negotiates: `credentials` (a typed family) plus scalar value menus keyed by attribute (e.g. `brands`). A uniform map read to OFFER options; not part of the field-constraint overlay." } } } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index 1e40ae40b..cd7d179c3 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -6,14 +6,14 @@ "$defs": { "available_card_payment_instrument": { "title": "Available Card Payment Instrument", - "description": "Declares card instrument availability with card-specific constraints.", + "description": "Declares card instrument availability. Field requirements go under `constraints` (e.g. `billing_address`); accepted menus and typed families go under `options` (`brands`, `credentials`).", "allOf": [ { "$ref": "available_payment_instrument.json" }, { "type": "object", "properties": { "type": { "const": "card" }, - "constraints": { + "options": { "type": "object", "properties": { "brands": { @@ -21,16 +21,15 @@ "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, - "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])." + "description": "Accepted card brands (e.g., ['visa', 'mastercard', 'amex'])." }, "credentials": { + "type": "array", "items": { "if": { "type": "object", "required": ["type"], - "properties": { - "type": { "const": "token" } - } + "properties": { "type": { "const": "token" } } }, "then": { "$ref": "token_credential.json#/$defs/constraint" } } diff --git a/source/schemas/shopping/types/object_constraint.json b/source/schemas/shopping/types/object_constraint.json index 4278792b8..12750d7fc 100644 --- a/source/schemas/shopping/types/object_constraint.json +++ b/source/schemas/shopping/types/object_constraint.json @@ -2,14 +2,20 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/object_constraint.json", "title": "Object Constraint", - "description": "Constraint applied to a local object target. `required` names target properties that MUST be present in an acceptable instance. Additional keys are nested object constraints, value constraints, typed constraints, or domain-specific operators defined by the concrete constraint schema.", + "description": "Field requirements over a local object target. `required` names properties that MUST be present; every other key is the name of a target property whose nested constraint follows — an Object Constraint (recurse into a sub-object) or a Value Constraint (restrict a value). A consumer compiles it to a JSON Schema overlay by taking `required` and moving each field key under `properties`. Stays open so extensions can add fields. `required` is reserved.", "type": "object", "properties": { "required": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, - "description": "Names of properties on the local constrained object that MUST be present in an acceptable instance. Evaluated with the same semantics as JSON Schema `required`. The base accepts any string so extension schemas can add fields without changing this schema." + "description": "Names of properties on the target object that MUST be present. Same semantics as JSON Schema `required`. Open string list so extension schemas can add fields." } + }, + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "value_constraint.json" } + ] } } From 387eb336acff653215457f2f529a9a26683a6e87 Mon Sep 17 00:00:00 2001 From: Ilya Grigorik Date: Thu, 23 Jul 2026 08:04:51 -0400 Subject: [PATCH 2/2] clarify options-vs-required; drop "menu" jargon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Answer whether `options` keys belong in `required` (they do not): add one line to the `options` bullet stating the split — `required` is what the buyer must send (presence of submitted fields), while `options` attributes like `brands` name values the seller accepts, not fields the buyer sends. - Generalize the cross-domain Constraint Objects prose: "most handlers" -> "most schemas", and mark the card example as "a payment handler", so payment framing does not leak into the general primitive docs. - Drop "menu" throughout (options bullet, handler-guide table, schema descriptions) in favor of the precise terms already in use — "acceptable set" / "scalar list" / "accepted values" — so `options` introduces no undefined concept. --- docs/documentation/schema-authoring.md | 13 ++++++++----- docs/specification/payment-handler-guide.md | 2 +- .../types/available_payment_instrument.json | 4 ++-- .../shopping/types/card_payment_instrument.json | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 366b09064..41e00168f 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -430,10 +430,13 @@ runtime requirements on an already-typed target, declared along **two axes**: Object Constraint: `required` (presence) plus one key per constrained field. A consumer compiles it to a JSON Schema overlay (`required` + `properties`) a standard validator can run. -- **`options`** — accepted value menus and typed families the target negotiates: - a uniform map from attribute to acceptable set — a scalar menu (e.g. `brands`) or +- **`options`** — accepted values and typed families the target negotiates: + a uniform map from attribute to acceptable set — a scalar list (e.g. `brands`) or a typed family (Type Constraint entries keyed by `type` with per-branch `constraints`, e.g. `credentials`). Resolved by lookup; not part of the overlay. + `required` names fields the buyer must **submit** (presence); it never lists + `options` attributes like `brands`, which name values the seller **accepts**, + not fields the buyer sends. UCP defines three composable primitives: @@ -449,10 +452,10 @@ Object Constraint (recurse) or a Value Constraint. The two are distinguishable **from the data alone** — `enum`/`const` is a Value Constraint, otherwise it's an Object Constraint — so a consumer compiles the overlay without resolving the concrete schema. Field constraints are **open by default**: they live in the wire -data and validate against the open Object Constraint, so most handlers only narrow +data and validate against the open Object Constraint, so most schemas only narrow `options`. Every name in `required` **MUST** be a property of the target. -Concrete availability schema (a handler narrowing the card branch). Narrowing a +Concrete availability schema (a payment handler narrowing the card branch). Narrowing a field constraint takes a single `properties` keyword — rarely needed, since field constraints are open: @@ -508,7 +511,7 @@ and field constraints nest as plain direct keys: `ValueConstraint` is deliberately closed: unsupported assertions cannot be ignored safely. `ObjectConstraint` and `TypeConstraint.type` are extension points. `constraints` compiles into a JSON Schema overlay (presence + allowed values) that -a standard validator runs; `options` is resolved by lookup — scalar menus by +a standard validator runs; `options` is resolved by lookup — scalar lists by membership, typed families by dispatch on the submitted `type`. Declared constraints are an upfront minimum; dynamic requirements still use recoverable errors and [`message_error.path`](site:{{ ucp_version }}/schemas/common/types/message_error.json). diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index d45cbf1e0..711b899b9 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -334,7 +334,7 @@ axes**: | Axis | Type | Meaning | | :--- | :--- | :------ | | `constraints` | [`ObjectConstraint`](site:schemas/shopping/types/object_constraint.json) | Field requirements on the instrument's OWN fields: `required` (presence) plus a key per constrained field carrying its nested requirement / allowed values. Compiles to a JSON Schema overlay. | -| `options` | map | Accepted value menus and typed families: `brands` (a scalar menu of accepted networks) and `credentials` (typed branches with per-branch requirements). Resolved by lookup. | +| `options` | map | Accepted values and typed families: `brands` (a scalar list of accepted networks) and `credentials` (typed branches with per-branch requirements). Resolved by lookup. | Express field requirements as `constraints` (an AVS postal-code requirement is `constraints.billing_address.required: ["postal_code"]`), and accepted choices as diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index 97599ecf8..b2b4db640 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/available_payment_instrument.json", "title": "Available Payment Instrument", - "description": "An instrument type a handler accepts, declared along two axes. `constraints` is an Object Constraint over the instrument's OWN fields (a JSON Schema overlay: presence + allowed values). `options` is the set of accepted value menus and typed families this instrument negotiates (resolved by lookup, not compiled into the overlay).", + "description": "An instrument type a handler accepts, declared along two axes. `constraints` is an Object Constraint over the instrument's OWN fields (a JSON Schema overlay: presence + allowed values). `options` is the set of accepted values and typed families this instrument negotiates (resolved by lookup, not compiled into the overlay).", "allOf": [ { "$ref": "type_constraint.json" }, { @@ -23,7 +23,7 @@ "items": { "type": "string" }, "uniqueItems": true }, - "description": "Accepted options this instrument negotiates: `credentials` (a typed family) plus scalar value menus keyed by attribute (e.g. `brands`). A uniform map read to OFFER options; not part of the field-constraint overlay." + "description": "Accepted options this instrument negotiates: `credentials` (a typed family) plus scalar value lists keyed by attribute (e.g. `brands`). A uniform map read to OFFER options; not part of the field-constraint overlay." } } } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index cd7d179c3..2c9a6b85d 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -6,7 +6,7 @@ "$defs": { "available_card_payment_instrument": { "title": "Available Card Payment Instrument", - "description": "Declares card instrument availability. Field requirements go under `constraints` (e.g. `billing_address`); accepted menus and typed families go under `options` (`brands`, `credentials`).", + "description": "Declares card instrument availability. Field requirements go under `constraints` (e.g. `billing_address`); accepted values and typed families go under `options` (`brands`, `credentials`).", "allOf": [ { "$ref": "available_payment_instrument.json" }, {