split instrument availability into constraints and options axes#626
Open
igrigorik wants to merge 1 commit into
Open
split instrument availability into constraints and options axes#626igrigorik wants to merge 1 commit into
constraints and options axes#626igrigorik wants to merge 1 commit into
Conversation
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.
igrigorik
marked this pull request as ready for review
July 22, 2026 04:14
| } | ||
| "properties": { "type": { "const": "token" } } | ||
| }, | ||
| "then": { "$ref": "token_credential.json#/$defs/constraint" } |
Contributor
There was a problem hiding this comment.
Not introduced here ... but unless I'm missing something, I don't see a token_credential.json#/$defs/constraint?
| **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 |
Contributor
There was a problem hiding this comment.
nit: inclusion in schema-authoring.md implies use across UCP broadly so "handlers" could be slightly confusing here.
| 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. |
Contributor
There was a problem hiding this comment.
Should we clarify whether the required should include the keys from options? They are presumably required (with more than just presence)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current #424 shape put everything an instrument declares — presence, per-field
constraints, accepted card brands, and credential families — into one
constraintsbag. 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
requiredname collision).Partition the bag into two sibling axes:
constraints— field requirements over the instrument's OWN fields (anObject Constraint:
requiredplus one direct key per constrained field).Compiles to a JSON Schema overlay (
required+properties) a standardvalidator 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,constraintsholds nothing butrequiredand field constraints — andobject_constraintnow enforces exactlythat: every non-
requiredkey is a nested Object Constraint or a ValueConstraint (recursing via
$ref "#"). Compiling the overlay is therefore apure 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:
constraintsis deliberately not a literal JSON Schema: fields are directkeys, so a consumer compiles it (take
required, lift field keys underproperties). Aproperties-keyed sub-map would make it literal but collidethe JSON Schema
propertieskeyword with the data key when a schema narrows anested field. The compile is trivial, and a resolver runs regardless.
requiredis reserved inside a constraint object, so a field literally namedrequiredcan be listed as present but cannot carry a nested constraint.Rare, and documented.
on the target?). Unchanged from before; a lint pass can add it later — the
partition makes it tractable, since every non-
requiredkey is unambiguouslya field name.