diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index cf16a6026..799357524 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -420,6 +420,93 @@ 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: + +| 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: + +- `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. + +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: + + +```json +{ + "$defs": { + "constraint": { + "allOf": [ + { "$ref": "object_constraint.json" }, + { + "properties": { + "billing_address": { + "allOf": [ + { "$ref": "object_constraint.json" }, + { + "properties": { + "address_country": { "$ref": "value_constraint.json" } + } + } + ] + }, + "credentials": { + "type": "array", + "items": { "$ref": "type_constraint.json" } + }, + "brands": { + "type": "array", + "items": { "type": "string" } + } + } + } + ] + } + } +} +``` + +Its wire value remains local to the constrained fields: + + +```json +{ + "required": ["billing_address"], + "billing_address": { + "required": ["address_country"], + "address_country": { "enum": ["US", "CA"] } + }, + "credentials": [{ "type": "token" }], + "brands": ["visa", "mastercard"] +} +``` + +`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). + ### Property-Count Constraints (`minProperties` / `maxProperties`) By default, UCP schemas do not set `minProperties` or `maxProperties` on diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index fa79966bb..a7ccdf385 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -186,11 +186,22 @@ and desired configuration. } ``` -**`available_instruments`** is optional. When absent, the handler places no -restrictions on instrument types or constraints — it supports the full set of -instrument types defined by its handler schema. When present, it narrows the -advertised types and/or applies additional constraints (e.g., limiting card -brands to `["visa", "mastercard"]`). +**`available_instruments`** is an array of +[`TypeConstraint`](site:schemas/shopping/types/type_constraint.json) entries over +the handler's payment-instrument family. Each entry selects an instrument branch +with `type` and applies an Object Constraint to acceptable instances: + +```text +available_instruments[] Type Constraint +├── type selects an instrument schema +└── constraints Object Constraint on that instrument + ├── required requires instrument properties + ├── billing_address constrains a nested object + └── credentials[] selects and constrains credential types +``` + +When omitted, the declaration does not narrow the handler's instruments. When +present, only the listed types are available and each entry's constraints apply. --- @@ -296,6 +307,9 @@ authoritative value returned in the `response_schema`. - Its own `business_schema` declaration (what the merchant is actually set up to accept) - Cart/checkout context (e.g., certain item types may restrict eligible methods) + The business matches Type Constraints by `type` and resolves their Object + Constraints according to the negotiated handler schema. + 3. **Response is authoritative** — the `available_instruments` in the `response_schema` reflects the business's resolved selection for this specific checkout. Platforms **MUST** treat it as authoritative and **MUST NOT** attempt @@ -312,6 +326,47 @@ authoritative value returned in the `response_schema`. 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: + +| 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. | + +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. + + +```json +{ + "id": "processor_tokenizer_1234", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card", + "constraints": { + "required": ["billing_address"], + "billing_address": { + "required": ["postal_code", "address_country"] + }, + "credentials": [{ "type": "token" }], + "brands": ["visa", "mastercard"] + } + } + ] +} +``` + +See [Constraint Objects](../documentation/schema-authoring.md#constraint-objects) +for composition rules. Declared constraints are the upfront minimum; dynamic +requirements still use recoverable errors. + --- #### Defining the Schema @@ -322,6 +377,7 @@ Authors typically define each shape in its own file and reference them: - **Config** — Configuration for platform/business declarations and runtime responses - **Instrument** — The payment instrument structure returned to platforms - **Credential** — The credential structure within instruments +- **Available instrument** — Optional availability declaration item, used only when the handler has typed availability constraints to validate **Example Handler Schema:** @@ -351,6 +407,14 @@ Authors typically define each shape in its own file and reference them: { "$ref": "#/$defs/tokenizer_alt_instrument" } ] }, + "available_payment_instrument": { + "title": "Available Tokenizer Instrument", + "description": "Optional: validates available_instruments[] because this handler defines typed availability constraints.", + "oneOf": [ + { "$ref": "types/tokenizer_instrument.json#/$defs/available_tokenizer_card" }, + { "$ref": "types/tokenizer_alt_instrument.json#/$defs/available_tokenizer_alt" } + ] + }, "platform_schema": { "title": "Tokenizer (Platform)", "description": "Platform-level handler configuration for discovery.", @@ -508,19 +572,20 @@ multiple instrument types for different payment flows. **Available Instrument Schemas:** -Each instrument schema defines its own `available_*` variant in `$defs` that -specifies what constraints are valid for that instrument type. For example, -[`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) -defines `available_card_payment_instrument` with a `brands` constraint. +[`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) +is the reusable Type Constraint over payment instruments. Instrument schemas can +publish an `available_*` definition that specializes a branch and its Object +Constraint; the card definition adds `brands` and credential refinements. -| Schema | Constraints | -| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | -| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, constraints (open object) | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brands` for card networks | +| Schema | Constraint shape | +| :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | +| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Open type, `required`, address, and credential keys | +| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | `type: "card"`, `brands`, and credential refinements | -Handlers reference these instrument-defined schemas when declaring -`available_instruments`. The **instrument schema authors** define what -constraints are meaningful (e.g., `brands` for cards), and **platforms/businesses** use this to advertise what they support (e.g., `["visa", "mastercard"]`). +The base Payment Handler intentionally remains open: an instrument `type` does +not globally select a schema. After negotiation, consumers use +`$defs.{handler_name}.available_payment_instrument` from the handler schema. +Handlers that add no availability-specific keys can omit this definition. **Example `types/tokenizer_instrument.json`**: @@ -541,16 +606,21 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse { "type": "object", "properties": { - "type": { "const": "tokenizer_card" }, + "type": { "const": "card" }, "constraints": { - "type": "object", - "properties": { - "tokenization_types": { - "type": "array", - "items": { "type": "string" }, - "description": "Supported tokenization types (e.g., ['network_token', 'merchant_token'])." + "allOf": [ + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/object_constraint.json" }, + { + "type": "object", + "properties": { + "tokenization_types": { + "type": "array", + "items": { "type": "string" }, + "description": "Supported tokenization types (e.g., ['network_token', 'merchant_token'])." + } + } } - } + ] } } } @@ -564,7 +634,7 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "type": "object", "required": ["type"], "properties": { - "type": { "const": "tokenizer_card" }, + "type": { "const": "card" }, "credential": { "oneOf": [ { "$ref": "tokenizer_token.json" }, @@ -588,6 +658,19 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "$id": "https://example.com/ucp/handlers/tokenizer/types/tokenizer_alt_instrument.json", "title": "Tokenizer Alt Instrument", "description": "Alternative payment instrument for com.example.tokenizer.", + "$defs": { + "available_tokenizer_alt": { + "allOf": [ + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/available_payment_instrument.json" }, + { + "type": "object", + "properties": { + "type": { "const": "tokenizer_alt" } + } + } + ] + } + }, "allOf": [ { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/payment_instrument.json" } ], @@ -625,7 +708,11 @@ extend these schemas to include handler-specific credential context. Handlers **MAY** define multiple credential types for different instrument flows. The specification **MUST** define which credential types are accepted by the -handler. +handler. Credential schemas that can appear in +`available_instruments[].constraints.credentials[]` **SHOULD** also define a +`$defs.constraint` typed constraint entry. That entry lets handler declarations +say both "this credential family is accepted" and, when needed, which optional +credential fields are required in the current context. **Important:** If using token credentials, the schema **MUST** include an expiration field (`expiry`, `ttl`, or similar) to ensure platforms know when to @@ -655,6 +742,21 @@ refresh credentials. "format": "date-time", "description": "Token expiration. Platforms must refresh before this time." } + }, + "$defs": { + "constraint": { + "title": "Tokenizer Card Token Constraint", + "description": "Typed constraint entry for tokenizer card token credentials.", + "allOf": [ + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/type_constraint.json" }, + { + "type": "object", + "properties": { + "type": { "const": "tokenizer_card_token" } + } + } + ] + } } } ``` diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index cf847b355..cf50579c0 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -3,18 +3,28 @@ "$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.", - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "description": "The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant." - }, - "constraints": { + "allOf": [ + { "$ref": "type_constraint.json" }, + { "type": "object", - "additionalProperties": true, - "description": "Constraints on this instrument type. Structure depends on instrument type and active capabilities.", - "minProperties": 1 + "properties": { + "constraints": { + "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": "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." + } + } } - } + ] } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index 2704308bc..1e40ae40b 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -12,9 +12,7 @@ { "type": "object", "properties": { - "type": { - "const": "card" - }, + "type": { "const": "card" }, "constraints": { "type": "object", "properties": { @@ -24,6 +22,18 @@ "minItems": 1, "uniqueItems": true, "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])." + }, + "credentials": { + "items": { + "if": { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "token" } + } + }, + "then": { "$ref": "token_credential.json#/$defs/constraint" } + } } } } @@ -33,9 +43,7 @@ } }, "allOf": [ - { - "$ref": "payment_instrument.json" - }, + { "$ref": "payment_instrument.json" }, { "type": "object", "required": ["type"], diff --git a/source/schemas/shopping/types/object_constraint.json b/source/schemas/shopping/types/object_constraint.json new file mode 100644 index 000000000..4278792b8 --- /dev/null +++ b/source/schemas/shopping/types/object_constraint.json @@ -0,0 +1,15 @@ +{ + "$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.", + "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." + } + } +} diff --git a/source/schemas/shopping/types/token_credential.json b/source/schemas/shopping/types/token_credential.json index aa0cde381..2bdca570c 100644 --- a/source/schemas/shopping/types/token_credential.json +++ b/source/schemas/shopping/types/token_credential.json @@ -22,5 +22,20 @@ } } } - ] + ], + "$defs": { + "constraint": { + "title": "Token Credential Constraint", + "description": "Typed constraint entry indicating support for generic token credentials. Handler-specific token credential schemas can extend this entry and narrow `constraints` to their own fields.", + "allOf": [ + { "$ref": "type_constraint.json" }, + { + "type": "object", + "properties": { + "type": { "const": "token" } + } + } + ] + } + } } diff --git a/source/schemas/shopping/types/type_constraint.json b/source/schemas/shopping/types/type_constraint.json new file mode 100644 index 000000000..c8eac0327 --- /dev/null +++ b/source/schemas/shopping/types/type_constraint.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/type_constraint.json", + "title": "Type Constraint", + "description": "Constraint entry that selects one branch of a typed or discriminated family. The `type` value identifies the selected branch, and `constraints` applies an Object Constraint to that branch. Domain-specific schemas extend this base to document the discriminator and narrow the selected branch's constraints.", + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "The discriminator value for the constrained branch. Concrete schemas typically narrow this with `const`, `enum`, or another type-family constraint. Unknown values are reserved for extension schemas that document their own constraints." + }, + "constraints": { + "$ref": "object_constraint.json", + "description": "Object constraints applied to the branch selected by `type`. Concrete typed-constraint schemas should narrow this to the selected branch's constraint schema. Omit when accepting the branch without additional context-specific requirements." + } + } +} diff --git a/source/schemas/shopping/types/value_constraint.json b/source/schemas/shopping/types/value_constraint.json new file mode 100644 index 000000000..5f083fee1 --- /dev/null +++ b/source/schemas/shopping/types/value_constraint.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/value_constraint.json", + "title": "Value Constraint", + "description": "Bounded JSON Schema fragment applied to a local target value.", + "type": "object", + "properties": { + "enum": { + "type": "array", + "uniqueItems": true, + "description": "Allowed values for the local target. Evaluated with the same semantics as JSON Schema `enum`." + }, + "const": { + "description": "The required value for the local target. Evaluated with the same semantics as JSON Schema `const`." + } + }, + "anyOf": [ + { "required": ["enum"] }, + { "required": ["const"] } + ] +}