From b434e15d94d5d2a47ab1aee716df5deebc5cea6e Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Mon, 6 Jul 2026 01:47:14 -0400 Subject: [PATCH 1/3] introduce constraints; apply them to addresses, payment instruments --- docs/documentation/schema-authoring.md | 59 ++++++++++ docs/specification/payment-handler-guide.md | 109 +++++++++++++++--- .../shopping/types/address_constraint.json | 35 ++++++ .../types/available_payment_instrument.json | 24 +++- .../types/card_payment_instrument.json | 34 +++++- source/schemas/shopping/types/constraint.json | 16 +++ .../shopping/types/token_credential.json | 17 ++- .../shopping/types/type_constraint.json | 18 +++ 8 files changed, 288 insertions(+), 24 deletions(-) create mode 100644 source/schemas/shopping/types/address_constraint.json create mode 100644 source/schemas/shopping/types/constraint.json create mode 100644 source/schemas/shopping/types/type_constraint.json diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index cf16a6026..77e85e664 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -420,6 +420,65 @@ 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. A constraint is a +runtime requirement attached to a concrete context, not a global schema change. + +All new constraint objects **SHOULD** extend +[`constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/constraint.json): + +- `required_fields` names fields on the constrained object that must be present + in an acceptable instance for this context. +- Domain-specific constraint keys remain sibling properties. For example, the + base payment instrument can require `billing_address` and add a nested + `billing_address` constraint describing which address fields are needed. +- Concrete schemas **SHOULD** narrow `required_fields` to the known property names + of the constrained schema when machine validation is valuable, while leaving + the object open to future domain-specific keys. + +Prefer field-level constraints over ad-hoc booleans. For example, prefer +`required_fields: ["billing_address"]` plus a nested address constraint over +new booleans such as `requires_billing_address` or +`requires_billing_postal_code`. + + +```json +{ + "type": "card", + "constraints": { + "brands": ["visa", "mastercard"], + "required_fields": ["billing_address"], + "billing_address": { + "required_fields": ["postal_code", "address_country"] + } + } +} +``` + +For arrays of constraints over a typed family, extend +[`type_constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json). Each +known branch should publish a `$defs/constraint` entry with a `type` discriminator +and, when applicable, a narrowed `constraints` body. The parent schema can then +validate UCP-defined branches while still leaving an extension point for +handler-specific branches. + + +```json +{ + "credentials": [ + { "type": "token" }, + { + "type": "com.example.wallet_token", + "constraints": { + "required_fields": ["assurance_level"] + } + } + ] +} +``` + ### 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..3eb93be58 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -312,6 +312,59 @@ 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 + +`available_instruments[].constraints` describes what an acceptable instrument +must satisfy for a handler declaration or resolved checkout response. Constraint +objects extend [`Constraint`](site:schemas/shopping/types/constraint.json): + +| Constraint key | Meaning | +| :------------- | :------ | +| `required_fields` | Field names from the constrained object that must be present in this context. | +| Domain-specific keys | Additional constraints defined by the concrete instrument or handler schema. | + +Base payment instruments define these common constraints: + +| Key | Description | +| :-- | :---------- | +| `required_fields` | Base payment instrument fields required by this handler. The base schema currently narrows this to `billing_address`. | +| `billing_address` | Nested [`AddressConstraint`](site:schemas/shopping/types/address_constraint.json) for required billing address fields. | + +Card instruments inherit those base constraints and add card-specific constraints: + +| Key | Description | +| :-- | :---------- | +| `brands` | Accepted card network names, such as `visa`, `mastercard`, or `amex`. | +| `credentials` | Accepted credential families and credential-specific constraints. Entries are typed constraints; UCP-defined entries are machine-validated and handler schemas can add their own entries. | + +Use field-level constraints instead of handler-specific booleans when the +requirement is about data that is already modeled by a schema. For example, an +AVS postal-code requirement is expressed as a billing address constraint rather +than a new `requires_billing_postal_code` flag. + + +```json +{ + "id": "processor_tokenizer_1234", + "version": "{{ ucp_version }}", + "available_instruments": [ + { + "type": "card", + "constraints": { + "brands": ["visa", "mastercard"], + "required_fields": ["billing_address"], + "billing_address": { + "required_fields": ["postal_code", "address_country"] + }, + "credentials": [ + { "type": "token" } + ] + } + } + ] +} +``` + --- #### Defining the Schema @@ -511,16 +564,20 @@ multiple instrument types for different payment flows. 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. +defines `available_card_payment_instrument` with card-specific constraints such +as `brands` and accepted `credentials`. Base payment-instrument constraints such +as `billing_address` also apply. -| 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 | Constraints | +| :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- | +| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, `required_fields`, and `billing_address` constraints | +| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `brands` and `credentials` | 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"]`). +constraints are meaningful (e.g., `brands` for cards), and +**platforms/businesses** use this to advertise what they support (e.g., +`["visa", "mastercard"]`). **Example `types/tokenizer_instrument.json`**: @@ -543,14 +600,19 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "properties": { "type": { "const": "tokenizer_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/constraint.json" }, + { + "type": "object", + "properties": { + "tokenization_types": { + "type": "array", + "items": { "type": "string" }, + "description": "Supported tokenization types (e.g., ['network_token', 'merchant_token'])." + } + } } - } + ] } } } @@ -625,7 +687,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 +721,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/address_constraint.json b/source/schemas/shopping/types/address_constraint.json new file mode 100644 index 000000000..1cfccf559 --- /dev/null +++ b/source/schemas/shopping/types/address_constraint.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/address_constraint.json", + "title": "Address Constraint", + "description": "Constraint object for a postal address. `required_fields` is narrowed to postal address property names for default machine validation of the current schema shape.", + "allOf": [ + { "$ref": "constraint.json" }, + { + "type": "object", + "properties": { + "required_fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "street_address", + "extended_address", + "address_locality", + "address_region", + "address_country", + "postal_code", + "first_name", + "last_name", + "phone_number" + ] + }, + "uniqueItems": true + } + } + } + ], + "examples": [ + { "required_fields": ["postal_code", "address_country"] } + ] +} diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index cf847b355..a57080aae 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -11,9 +11,27 @@ "description": "The instrument type identifier (e.g., 'card', 'gift_card'). References an instrument schema's type constant." }, "constraints": { - "type": "object", - "additionalProperties": true, - "description": "Constraints on this instrument type. Structure depends on instrument type and active capabilities.", + "allOf": [ + { "$ref": "constraint.json" }, + { + "type": "object", + "properties": { + "required_fields": { + "type": "array", + "items": { + "type": "string", + "enum": ["billing_address"] + }, + "uniqueItems": true + }, + "billing_address": { + "$ref": "address_constraint.json", + "description": "Constraint on the instrument's `billing_address` field. The address itself is required only when `billing_address` appears in this object's `required_fields`." + } + } + } + ], + "description": "Constraints on this instrument type. Base payment instrument constraints can require and describe `billing_address`; concrete instrument schemas SHOULD add instrument-specific constraint keys while remaining open to extensions.", "minProperties": 1 } } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index 2704308bc..864a1998e 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,32 @@ "minItems": 1, "uniqueItems": true, "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])." + }, + "credentials": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "token_credential.json#/$defs/constraint" }, + { + "title": "Extension Credential Constraint", + "description": "Extension point for handler-specific credential constraint entries. UCP-defined credential types are excluded here so their concrete constraint schemas remain machine-validated.", + "allOf": [ + { "$ref": "type_constraint.json" }, + { + "not": { + "required": ["type"], + "properties": { + "type": { "const": "token" } + } + } + } + ] + } + ] + }, + "uniqueItems": true, + "minItems": 1, + "description": "Credential types and credential-specific constraints accepted for this instrument. UCP-defined credential entries are machine-validated; handler-specific schemas can add their own entries." } } } @@ -33,9 +57,7 @@ } }, "allOf": [ - { - "$ref": "payment_instrument.json" - }, + { "$ref": "payment_instrument.json" }, { "type": "object", "required": ["type"], diff --git a/source/schemas/shopping/types/constraint.json b/source/schemas/shopping/types/constraint.json new file mode 100644 index 000000000..2d96d90b9 --- /dev/null +++ b/source/schemas/shopping/types/constraint.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/constraint.json", + "title": "Constraint", + "description": "The base shape of a constraint object. `required_fields` names properties of the constrained object that MUST be present in an acceptable instance. Additional properties are domain-specific constraint keys defined by concrete constraint schemas.", + "type": "object", + "properties": { + "required_fields": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "minItems": 1, + "description": "Names of properties on the constrained object that MUST be present in an acceptable instance. Concrete constraint schemas may narrow these to known property names when they want default machine validation for the current schema version." + } + } +} 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..05caa9fe8 --- /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": "Base shape for a constraint entry that selects one branch of a typed or discriminated family. The `type` value identifies the selected branch, and `constraints` applies to that branch's schema. Domain-specific schemas extend this base to document what the `type` refers to and to narrow `constraints` to the selected branch's constraint schema.", + "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": "constraint.json", + "description": "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 with only its schema-defined required fields." + } + } +} From 94958570ae457bad49c06478ad05ea3045648a93 Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Fri, 10 Jul 2026 02:08:05 -0400 Subject: [PATCH 2/3] simplify constraints proposal: remove enums, keep extensibility open --- docs/documentation/schema-authoring.md | 12 ++-- docs/specification/payment-handler-guide.md | 71 +++++++++++++------ .../shopping/types/address_constraint.json | 35 --------- .../types/available_payment_instrument.json | 57 ++++++++------- .../types/card_payment_instrument.json | 30 +++----- source/schemas/shopping/types/constraint.json | 4 +- 6 files changed, 97 insertions(+), 112 deletions(-) delete mode 100644 source/schemas/shopping/types/address_constraint.json diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 77e85e664..d4e337f11 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -429,13 +429,17 @@ runtime requirement attached to a concrete context, not a global schema change. All new constraint objects **SHOULD** extend [`constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/constraint.json): -- `required_fields` names fields on the constrained object that must be present - in an acceptable instance for this context. +- `required_fields` names fields on the local constrained object that must be + present in an acceptable instance for this context. +- The schema property that embeds the constraint defines the target object. For + example, `available_instruments[].constraints` targets the payment instrument, + while `constraints.billing_address` targets that instrument's billing address. - Domain-specific constraint keys remain sibling properties. For example, the base payment instrument can require `billing_address` and add a nested `billing_address` constraint describing which address fields are needed. -- Concrete schemas **SHOULD** narrow `required_fields` to the known property names - of the constrained schema when machine validation is valuable, while leaving +- Reusable base schemas **SHOULD NOT** close `required_fields` with enums when + the target object can be extended. Concrete resolved schemas MAY narrow + `required_fields` when they own the complete target vocabulary, while leaving the object open to future domain-specific keys. Prefer field-level constraints over ad-hoc booleans. For example, prefer diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index 3eb93be58..a720b026f 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -327,15 +327,16 @@ Base payment instruments define these common constraints: | Key | Description | | :-- | :---------- | -| `required_fields` | Base payment instrument fields required by this handler. The base schema currently narrows this to `billing_address`. | -| `billing_address` | Nested [`AddressConstraint`](site:schemas/shopping/types/address_constraint.json) for required billing address fields. | +| `required_fields` | Payment instrument fields required by this handler. The base schema intentionally keeps this list open for handler-specific instrument extensions; `billing_address` is the standard base field constrained here. | +| `billing_address` | Nested local [`Constraint`](site:schemas/shopping/types/constraint.json) whose `required_fields` values name billing-address fields. | +| `credentials` | Accepted credential families and credential-specific constraints. Entries are typed constraints; concrete instrument schemas can narrow known entries while still allowing handler-specific entries. | Card instruments inherit those base constraints and add card-specific constraints: | Key | Description | | :-- | :---------- | | `brands` | Accepted card network names, such as `visa`, `mastercard`, or `amex`. | -| `credentials` | Accepted credential families and credential-specific constraints. Entries are typed constraints; UCP-defined entries are machine-validated and handler schemas can add their own entries. | +| `credentials` | Refines the base typed credential list with UCP-defined card credential entries while preserving extension credential entries. | Use field-level constraints instead of handler-specific booleans when the requirement is about data that is already modeled by a schema. For example, an @@ -375,6 +376,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:** @@ -404,6 +406,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.", @@ -561,23 +571,31 @@ 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, +[`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) +is the payment-instrument application of the generic +[`TypeConstraint`](site:schemas/shopping/types/type_constraint.json) pattern: the +`type` value selects the payment instrument branch, and `constraints` applies to +that selected branch. Each instrument schema defines its own `available_*` +variant in `$defs` that specializes this typed entry. For example, [`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) -defines `available_card_payment_instrument` with card-specific constraints such -as `brands` and accepted `credentials`. Base payment-instrument constraints such -as `billing_address` also apply. - -| Schema | Constraints | -| :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- | -| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, `required_fields`, and `billing_address` constraints | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `brands` and `credentials` | - -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 +defines `available_card_payment_instrument` as the `card` branch with +card-specific constraints such as `brands` and card credential refinements. Base +payment-instrument constraints such as `billing_address` and `credentials` also +apply. + +| Schema | Constraints | +| :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | +| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base typed entry: type, open `required_fields`, `billing_address`, and `credentials` constraints | +| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Card branch: `type: "card"`, `brands`, and card credential refinements | + +Handlers reference these instrument-defined schemas from +`$defs.{handler_name}.available_payment_instrument` when they need machine +validation for availability declarations. 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"]`). +`["visa", "mastercard"]`). Handlers that do not add availability-specific +constraint keys can omit this entry and rely on their `payment_instrument` schema +to define the instruments they support. **Example `types/tokenizer_instrument.json`**: @@ -598,7 +616,7 @@ constraints are meaningful (e.g., `brands` for cards), and { "type": "object", "properties": { - "type": { "const": "tokenizer_card" }, + "type": { "const": "card" }, "constraints": { "allOf": [ { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/constraint.json" }, @@ -626,7 +644,7 @@ constraints are meaningful (e.g., `brands` for cards), and "type": "object", "required": ["type"], "properties": { - "type": { "const": "tokenizer_card" }, + "type": { "const": "card" }, "credential": { "oneOf": [ { "$ref": "tokenizer_token.json" }, @@ -650,6 +668,19 @@ constraints are meaningful (e.g., `brands` for cards), and "$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" } ], diff --git a/source/schemas/shopping/types/address_constraint.json b/source/schemas/shopping/types/address_constraint.json deleted file mode 100644 index 1cfccf559..000000000 --- a/source/schemas/shopping/types/address_constraint.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/address_constraint.json", - "title": "Address Constraint", - "description": "Constraint object for a postal address. `required_fields` is narrowed to postal address property names for default machine validation of the current schema shape.", - "allOf": [ - { "$ref": "constraint.json" }, - { - "type": "object", - "properties": { - "required_fields": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "street_address", - "extended_address", - "address_locality", - "address_region", - "address_country", - "postal_code", - "first_name", - "last_name", - "phone_number" - ] - }, - "uniqueItems": true - } - } - } - ], - "examples": [ - { "required_fields": ["postal_code", "address_country"] } - ] -} diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index a57080aae..63c50feb1 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -3,36 +3,35 @@ "$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": "constraint.json" }, - { - "type": "object", - "properties": { - "required_fields": { - "type": "array", - "items": { - "type": "string", - "enum": ["billing_address"] - }, - "uniqueItems": true - }, - "billing_address": { - "$ref": "address_constraint.json", - "description": "Constraint on the instrument's `billing_address` field. The address itself is required only when `billing_address` appears in this object's `required_fields`." + "allOf": [ + { "$ref": "type_constraint.json" }, + { + "type": "object", + "properties": { + "constraints": { + "allOf": [ + { "$ref": "constraint.json" }, + { + "type": "object", + "properties": { + "billing_address": { + "$ref": "constraint.json", + "description": "Local constraint on the instrument's `billing_address` field." + }, + "credentials": { + "type": "array", + "items": { "$ref": "type_constraint.json" }, + "uniqueItems": true, + "minItems": 1, + "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.", + "minProperties": 1 } - ], - "description": "Constraints on this instrument type. Base payment instrument constraints can require and describe `billing_address`; concrete instrument schemas SHOULD add instrument-specific constraint keys while remaining open to extensions.", - "minProperties": 1 + } } - } + ] } diff --git a/source/schemas/shopping/types/card_payment_instrument.json b/source/schemas/shopping/types/card_payment_instrument.json index 864a1998e..1e40ae40b 100644 --- a/source/schemas/shopping/types/card_payment_instrument.json +++ b/source/schemas/shopping/types/card_payment_instrument.json @@ -24,30 +24,16 @@ "description": "Limit to specific card brands (e.g., ['visa', 'mastercard', 'amex'])." }, "credentials": { - "type": "array", "items": { - "oneOf": [ - { "$ref": "token_credential.json#/$defs/constraint" }, - { - "title": "Extension Credential Constraint", - "description": "Extension point for handler-specific credential constraint entries. UCP-defined credential types are excluded here so their concrete constraint schemas remain machine-validated.", - "allOf": [ - { "$ref": "type_constraint.json" }, - { - "not": { - "required": ["type"], - "properties": { - "type": { "const": "token" } - } - } - } - ] + "if": { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "token" } } - ] - }, - "uniqueItems": true, - "minItems": 1, - "description": "Credential types and credential-specific constraints accepted for this instrument. UCP-defined credential entries are machine-validated; handler-specific schemas can add their own entries." + }, + "then": { "$ref": "token_credential.json#/$defs/constraint" } + } } } } diff --git a/source/schemas/shopping/types/constraint.json b/source/schemas/shopping/types/constraint.json index 2d96d90b9..7727d2bc9 100644 --- a/source/schemas/shopping/types/constraint.json +++ b/source/schemas/shopping/types/constraint.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/constraint.json", "title": "Constraint", - "description": "The base shape of a constraint object. `required_fields` names properties of the constrained object that MUST be present in an acceptable instance. Additional properties are domain-specific constraint keys defined by concrete constraint schemas.", + "description": "Reusable local mixin for a constraint object. The schema property that embeds this mixin defines the constrained target object. `required_fields` names properties of that local target MUST be present in an acceptable instance. Additional properties are domain-specific constraint keys defined by concrete constraint schemas.", "type": "object", "properties": { "required_fields": { @@ -10,7 +10,7 @@ "items": { "type": "string" }, "uniqueItems": true, "minItems": 1, - "description": "Names of properties on the constrained object that MUST be present in an acceptable instance. Concrete constraint schemas may narrow these to known property names when they want default machine validation for the current schema version." + "description": "Names of properties on the local constrained object that MUST be present in an acceptable instance. The base mixin intentionally accepts any string so extension schemas can add fields without changing this schema; concrete resolved schemas may narrow this list when they own the complete target vocabulary." } } } From 3bd8c2429ece44be35445ce0c4c9b98755b7b049 Mon Sep 17 00:00:00 2001 From: Daniel Wyckoff Date: Mon, 20 Jul 2026 21:33:34 -0400 Subject: [PATCH 3/3] unify more with base json schema shapes --- docs/documentation/schema-authoring.md | 112 ++++++++++------- docs/specification/payment-handler-guide.md | 114 ++++++++---------- .../types/available_payment_instrument.json | 33 ++--- source/schemas/shopping/types/constraint.json | 16 --- .../shopping/types/object_constraint.json | 15 +++ .../shopping/types/type_constraint.json | 6 +- .../shopping/types/value_constraint.json | 21 ++++ 7 files changed, 172 insertions(+), 145 deletions(-) delete mode 100644 source/schemas/shopping/types/constraint.json create mode 100644 source/schemas/shopping/types/object_constraint.json create mode 100644 source/schemas/shopping/types/value_constraint.json diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index d4e337f11..799357524 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -423,66 +423,90 @@ 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. A constraint is a -runtime requirement attached to a concrete context, not a global schema change. - -All new constraint objects **SHOULD** extend -[`constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/constraint.json): - -- `required_fields` names fields on the local constrained object that must be - present in an acceptable instance for this context. -- The schema property that embeds the constraint defines the target object. For - example, `available_instruments[].constraints` targets the payment instrument, - while `constraints.billing_address` targets that instrument's billing address. -- Domain-specific constraint keys remain sibling properties. For example, the - base payment instrument can require `billing_address` and add a nested - `billing_address` constraint describing which address fields are needed. -- Reusable base schemas **SHOULD NOT** close `required_fields` with enums when - the target object can be extended. Concrete resolved schemas MAY narrow - `required_fields` when they own the complete target vocabulary, while leaving - the object open to future domain-specific keys. - -Prefer field-level constraints over ad-hoc booleans. For example, prefer -`required_fields: ["billing_address"]` plus a nested address constraint over -new booleans such as `requires_billing_address` or -`requires_billing_postal_code`. +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 { - "type": "card", - "constraints": { - "brands": ["visa", "mastercard"], - "required_fields": ["billing_address"], - "billing_address": { - "required_fields": ["postal_code", "address_country"] + "$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" } + } + } + } + ] } } } ``` -For arrays of constraints over a typed family, extend -[`type_constraint.json`](site:{{ ucp_version }}/schemas/shopping/types/type_constraint.json). Each -known branch should publish a `$defs/constraint` entry with a `type` discriminator -and, when applicable, a narrowed `constraints` body. The parent schema can then -validate UCP-defined branches while still leaving an extension point for -handler-specific branches. +Its wire value remains local to the constrained fields: ```json { - "credentials": [ - { "type": "token" }, - { - "type": "com.example.wallet_token", - "constraints": { - "required_fields": ["assurance_level"] - } - } - ] + "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 a720b026f..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 @@ -314,34 +328,19 @@ is excluded from the response even though the platform supports it. #### Constraint Semantics -`available_instruments[].constraints` describes what an acceptable instrument -must satisfy for a handler declaration or resolved checkout response. Constraint -objects extend [`Constraint`](site:schemas/shopping/types/constraint.json): - -| Constraint key | Meaning | -| :------------- | :------ | -| `required_fields` | Field names from the constrained object that must be present in this context. | -| Domain-specific keys | Additional constraints defined by the concrete instrument or handler schema. | - -Base payment instruments define these common constraints: +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 | Description | -| :-- | :---------- | -| `required_fields` | Payment instrument fields required by this handler. The base schema intentionally keeps this list open for handler-specific instrument extensions; `billing_address` is the standard base field constrained here. | -| `billing_address` | Nested local [`Constraint`](site:schemas/shopping/types/constraint.json) whose `required_fields` values name billing-address fields. | -| `credentials` | Accepted credential families and credential-specific constraints. Entries are typed constraints; concrete instrument schemas can narrow known entries while still allowing handler-specific entries. | +| 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. | -Card instruments inherit those base constraints and add card-specific constraints: - -| Key | Description | -| :-- | :---------- | -| `brands` | Accepted card network names, such as `visa`, `mastercard`, or `amex`. | -| `credentials` | Refines the base typed credential list with UCP-defined card credential entries while preserving extension credential entries. | - -Use field-level constraints instead of handler-specific booleans when the -requirement is about data that is already modeled by a schema. For example, an -AVS postal-code requirement is expressed as a billing address constraint rather -than a new `requires_billing_postal_code` flag. +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 @@ -352,20 +351,22 @@ than a new `requires_billing_postal_code` flag. { "type": "card", "constraints": { - "brands": ["visa", "mastercard"], - "required_fields": ["billing_address"], + "required": ["billing_address"], "billing_address": { - "required_fields": ["postal_code", "address_country"] + "required": ["postal_code", "address_country"] }, - "credentials": [ - { "type": "token" } - ] + "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 @@ -572,30 +573,19 @@ multiple instrument types for different payment flows. **Available Instrument Schemas:** [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) -is the payment-instrument application of the generic -[`TypeConstraint`](site:schemas/shopping/types/type_constraint.json) pattern: the -`type` value selects the payment instrument branch, and `constraints` applies to -that selected branch. Each instrument schema defines its own `available_*` -variant in `$defs` that specializes this typed entry. For example, -[`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) -defines `available_card_payment_instrument` as the `card` branch with -card-specific constraints such as `brands` and card credential refinements. Base -payment-instrument constraints such as `billing_address` and `credentials` also -apply. - -| Schema | Constraints | -| :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | -| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base typed entry: type, open `required_fields`, `billing_address`, and `credentials` constraints | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Card branch: `type: "card"`, `brands`, and card credential refinements | - -Handlers reference these instrument-defined schemas from -`$defs.{handler_name}.available_payment_instrument` when they need machine -validation for availability declarations. 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"]`). Handlers that do not add availability-specific -constraint keys can omit this entry and rely on their `payment_instrument` schema -to define the instruments they support. +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 | 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 | + +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`**: @@ -619,7 +609,7 @@ to define the instruments they support. "type": { "const": "card" }, "constraints": { "allOf": [ - { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/constraint.json" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/object_constraint.json" }, { "type": "object", "properties": { diff --git a/source/schemas/shopping/types/available_payment_instrument.json b/source/schemas/shopping/types/available_payment_instrument.json index 63c50feb1..cf50579c0 100644 --- a/source/schemas/shopping/types/available_payment_instrument.json +++ b/source/schemas/shopping/types/available_payment_instrument.json @@ -9,27 +9,20 @@ "type": "object", "properties": { "constraints": { - "allOf": [ - { "$ref": "constraint.json" }, - { - "type": "object", - "properties": { - "billing_address": { - "$ref": "constraint.json", - "description": "Local constraint on the instrument's `billing_address` field." - }, - "credentials": { - "type": "array", - "items": { "$ref": "type_constraint.json" }, - "uniqueItems": true, - "minItems": 1, - "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." - } - } + "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.", - "minProperties": 1 + }, + "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/constraint.json b/source/schemas/shopping/types/constraint.json deleted file mode 100644 index 7727d2bc9..000000000 --- a/source/schemas/shopping/types/constraint.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/types/constraint.json", - "title": "Constraint", - "description": "Reusable local mixin for a constraint object. The schema property that embeds this mixin defines the constrained target object. `required_fields` names properties of that local target MUST be present in an acceptable instance. Additional properties are domain-specific constraint keys defined by concrete constraint schemas.", - "type": "object", - "properties": { - "required_fields": { - "type": "array", - "items": { "type": "string" }, - "uniqueItems": true, - "minItems": 1, - "description": "Names of properties on the local constrained object that MUST be present in an acceptable instance. The base mixin intentionally accepts any string so extension schemas can add fields without changing this schema; concrete resolved schemas may narrow this list when they own the complete target vocabulary." - } - } -} 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/type_constraint.json b/source/schemas/shopping/types/type_constraint.json index 05caa9fe8..c8eac0327 100644 --- a/source/schemas/shopping/types/type_constraint.json +++ b/source/schemas/shopping/types/type_constraint.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/type_constraint.json", "title": "Type Constraint", - "description": "Base shape for a constraint entry that selects one branch of a typed or discriminated family. The `type` value identifies the selected branch, and `constraints` applies to that branch's schema. Domain-specific schemas extend this base to document what the `type` refers to and to narrow `constraints` to the selected branch's constraint schema.", + "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": { @@ -11,8 +11,8 @@ "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": "constraint.json", - "description": "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 with only its schema-defined required fields." + "$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"] } + ] +}