Skip to content

add path targeting to Request Constraints - #744

Merged
igrigorik merged 2 commits into
feat/request-constraintsfrom
feat/request-constraints-path
Aug 20, 2026
Merged

add path targeting to Request Constraints#744
igrigorik merged 2 commits into
feat/request-constraintsfrom
feat/request-constraints-path

Conversation

@igrigorik

Copy link
Copy Markdown
Contributor

#655 defines a reusable closed Constraint Expression grammar, but the response location alone does not always identify the request data to validate. For example, a constraint attached to a response Line Item could mean the item at the same array position, the item with the same identity, or some other request object. The ambiguity becomes unavoidable when arrays reorder or when response and request shapes differ, such as an available payment instrument constraining a submitted instrument.


This PR adds an optional, outer-only path to Request Constraints. An explicit path is a complete RFC 9535 JSONPath query evaluated against the next logical UCP request to the same resource. When omitted, the effective path is derived from the RFC 9535 Normalized Path of the structured response object whose ucp member contains the constraint.

The omission behavior preserves concise ambient authoring. A constraint on the response root derives $; a constraint on a nested object derives that object's positional path. That positional correspondence is deterministic but does not imply stable identity. Authors use an explicit query when a rule must survive reordering, select multiple objects, or bridge different response and request shapes.

A path may select zero, one, or many objects. Zero matches have no effect, while every selected object must satisfy the complete Constraint Expression. Overlapping constraints compose conjunctively without precedence or override, and the Business must ensure potentially overlapping expressions are jointly satisfiable. These checks narrow the resolved request schema; they cannot make an otherwise invalid request valid.

Binding shape

The chosen wire shape keeps path beside the root members of the expression:

{
  "path": "$['line_items'][?@['id'] == 'line_123']",
  "properties": {
    "quantity": {"const": 100}
  }
}

One considered alternative was separating path from constraint expression:

{
  "path": "$['line_items'][?@['id'] == 'line_123']",
  "expression": {
    "properties": {
      "quantity": {"const": 100}
    }
  }
}

The benefit of the latter is that keeps expression strict subset of JSONSchema. The cost is permanent wire noise in the common case. Most constraints use ambient placement and omit path, so even a simple local requirement would need an otherwise meaningless wrapper:

{
  "expression": {
    "required": ["billing_address"]
  }
}

The selected shape optimizes for the authoring and wire surface rather than schema-factoring purity. constraint_expression.json remains direction-neutral and contains no path; request_constraints.json is the closed binding that admits path only at its root and re-exposes the root Object Constraint members. Recursive constraints continue through the pure expression schema, so nested path members remain invalid. The trade-off is that future changes to the root Object Constraint must also be reflected in request_constraints.json.

This preserves the core invariant that one binding carries one schema. It avoids introducing a list of independently targeted rules, repeated navigation, target precedence, or another orchestration model alongside JSON Schema.

   Request Constraints described what to validate but did not define how a nested
   response occurrence corresponded to request data. Leaving that mapping to each
   domain made array positions fragile and could not express cross-shape bindings,
   such as an available payment instrument constraining a submitted instrument.

   Bind each value to request data with an optional, outer-only `path`. An explicit
   path is a complete RFC 9535 JSONPath query evaluated against the next request;
   when omitted, the path is derived from the Normalized Path of the structured
   response object containing the constraint. This preserves concise ambient
   authoring while supporting stable-identity queries and multi-object selection.

   Validate every selected object against the complete closed Constraint Expression
   in conjunction with the resolved request schema. Zero matches have no effect,
   all matches must pass, and overlapping expressions compose without precedence.
   Businesses must ensure potentially overlapping expressions are jointly
   satisfiable.

   Keep Business evaluation authoritative while making Platform preflight optional
   and advisory. A Platform may evaluate a supported subset, but must process each
   chosen value completely and skip malformed, unsupported, non-object, or
   resource-limited values rather than partially interpreting them. Existing
   operation outcomes and messages continue to govern runtime failures.

   Each authoritative resource response supplies the advertised constraints for
   the next request to that resource; the following response replaces that set,
   omission clears it, and sets are never merged by path. Require explicit payment
   instrument paths to bridge response and request shapes by matching handler and
   instrument identity.
@igrigorik igrigorik added this to the 2026-08-24 milestone Aug 18, 2026
@igrigorik igrigorik self-assigned this Aug 18, 2026
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 18, 2026
@carolinerg1 carolinerg1 added status:under-review gov:needs-tc-review and removed status:needs-triage Signal that the PR is ready for human triage labels Aug 19, 2026

@raginpirate raginpirate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the deep thinking here Ilya. Approving, with a note from our discussion in the deep dive earlier.

Another solution is to put checkout.ucp.request_constraints on the response root as a single property, which carries an array of constraints and their paths in one place. As you mentioned, an implementer could even choose to do this.

In a vacuum, I think thats the simple solution, as it avoids having to walk the tree to assemble all the fragments you'll evaluate to restrict your json schema. However, I'll concede that it is very nice to keep the constraints authored in a semantically-relevant place in the schema; something I care deeply about for our payment requests 😄

Lets land this thing!

Comment thread source/schemas/common/types/request_constraints.json Outdated
@gsmith85
gsmith85 self-requested a review August 20, 2026 03:43

@gsmith85 gsmith85 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for driving this through, @igrigorik! This looks great and aligns with what we discussed in the deep dive.

Adding RFC 9535 path targeting gives us the precision needed for array reordering and payment shape bridging while keeping the common ambient case clean and flat. LGTM! 🚀

Co-authored-by: Daniel Wyckoff <daniel.wyckoff@shopify.com>
@igrigorik
igrigorik merged commit e5de5ca into feat/request-constraints Aug 20, 2026
2 of 4 checks passed
@igrigorik
igrigorik deleted the feat/request-constraints-path branch August 20, 2026 05:06

@jamesandersen jamesandersen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit late to the party here ... but agree this is a good catch and addition @igrigorik - thanks!

Comment thread docs/specification/overview.md
igrigorik added a commit that referenced this pull request Aug 20, 2026
* add response-carried request constraints

   Static request schemas describe the inputs a Business accepts generally, but
   cannot express narrower requirements for specific transaction. Platforms
   therefore cannot know before submission that a negotiated quantity is fixed
   or that an otherwise optional field is required.

   Add response-only `$requestConstraints` as a bounded Draft 2020-12 fragment
   that a Business emits and enforces against the corresponding later request
   representation. A Platform may use the fragment when forming or validating
   input, or ignore it and rely on the Business's existing validation errors.

   Keep the structural member ambient rather than adding an ordinary property to
   every carrier schema. Each adopting contract defines correspondence and
   lifecycle, while the shared type closes the vocabulary to `required`,
   `properties`, `enum`, and `const`, with optional display text and `$comment`.

   Examples demonstrate a Cart Line Item fixed at quantity 100, preservation of
   the company-scoped `ACME-X7Q9-L2M4` discount code, and a submitted card
   instrument that requires `billing_address`.

* register request constraints in the `ucp` namespace

   Static request schemas cannot express transaction-specific narrowing, forcing
   Platforms to discover negotiated requirements only after submission fails.

   Register response-only `ucp.request_constraints` in the central protocol
   vocabulary and define a closed Draft 2020-12 fragment for object presence and
   value constraints. Attach constraints through the containing `ucp` scope so
   host schemas do not need carrier-specific declarations.

   Keep Platform processing optional and Business validation authoritative,
   preserving the namespace requirement that registered members remain safe to
   ignore.

* simplify request constraints validation & preflight

   The TC discussion sharpened the intended split: Request Constraints narrow
   the resolved request schema, Business enforcement is authoritative, and
   Platform preflight is an optional optimization rather than a second
   conformance boundary.

   Define effective request validation as the conjunction of the resolved request
   schema and emitted constraints. Require Businesses to emit conforming
   constraints and enforce every rule, while allowing Platforms to evaluate any
   supported subset after validating the member's closed shape. A successful
   preflight can therefore avoid doomed requests without claiming that every
   emitted constraint has passed.

   Keep the wire language focused on required, properties, enum, and const by
   removing display annotations and $comment. Clarify that constraint objects are
   embedded JSON Schema rather than ambient UCP objects, and make each operation
   responsible for scope and lifecycle.

* extract reusable constraint expression grammar

   Request Constraints currently combines two independent concerns: a closed
   predicate language and a binding that applies that language to subsequent
   request data. Keeping both in request_constraints.json makes the grammar
   appear request-specific and prevents other contracts from referencing it
   without inheriting that name and semantic scope.

   Extract the Object and Value Constraint grammar into the direction-agnostic
   constraint_expression.json schema. Keep request_constraints.json as the
   binding to subsequent UCP request data. Each containing UCP property owns
   its request and response applicability; the shared grammar has no direction
   of its own.

   A domain reuses the grammar by defining a target model and a constraint
   binding for that target. For example, payment could define:

     PaymentAcceptanceFacts {
       brand,
       is_po_box,
       credential_type
     }

   Payment Acceptance Constraints would reference Constraint Expression and
   state that emitted expressions are evaluated against PaymentAcceptanceFacts:

     {
       "$id": "https://example.com/payment_acceptance_constraints.json",
       "title": "Payment Acceptance Constraints",
       "$ref":
         "https://ucp.dev/schemas/common/types/constraint_expression.json"
     }

   A payment handler could then expose that binding through its own carrier:

     "acceptance_constraints": {
       "$ref": "payment_acceptance_constraints.json",
       "ucp_request": "omit"
     }

   An emitted value could constrain the derived facts using only the shared
   operations:

     {
       "required": ["brand"],
       "properties": {
         "brand": {
           "enum": ["visa", "mastercard"]
         },
         "is_po_box": {
           "const": false
         }
       }
     }

   The payment contract remains responsible for deriving
   PaymentAcceptanceFacts and defining authority and lifecycle. The binding
   schema validates that the emitted expression uses the shared grammar, and
   the emitted expression is then evaluated against the derived facts.

   Constraint Expression admits two recursive positions. Object Constraints
   use required and properties to constrain field presence and nested objects.
   Value Constraints use enum and const to constrain JSON values. Target field
   names remain domain-defined while the executable operation vocabulary stays
   closed.

   This boundary gives Platforms one coherent evaluator contract. N domains
   can bind the grammar to their own target models, and M Platforms can
   implement the grammar once, producing an N + M integration model. Allowing
   each handler to define executable keywords would instead require bespoke
   semantics for every Platform x handler pair and trend toward N x M
   integrations.

   The refactor does not add payment acceptance facts or another domain
   binding. It establishes the reusable grammar those contracts can adopt
   later. The existing Request Constraints wire language, response-only
   applicability, validation behavior, and Business enforcement remain
   unchanged.

* inline object constraint at expression root

   Model Constraint Expression directly as its root Object Constraint instead
   of delegating through the repository's only top-level local reference.
   Preserve recursive Object and Value Constraint semantics while remaining
   compatible with the existing schema reference renderer.

* clarify Request Constraints authoring and outcomes

   Replace overloaded grammar and host terminology with actionable guidance on
   where expression syntax and request-binding changes belong.

   Distinguish stable, proactive preflight from runtime outcomes reported through
   messages. Advise Businesses not to advertise volatile constraints unless they
   can continue enforcing them, and describe quantity locks as exact sale-basis
   steps rather than lot semantics.

* add path targeting to Request Constraints (#744)

* add path targeting to Request Constraints

   Request Constraints described what to validate but did not define how a nested
   response occurrence corresponded to request data. Leaving that mapping to each
   domain made array positions fragile and could not express cross-shape bindings,
   such as an available payment instrument constraining a submitted instrument.

   Bind each value to request data with an optional, outer-only `path`. An explicit
   path is a complete RFC 9535 JSONPath query evaluated against the next request;
   when omitted, the path is derived from the Normalized Path of the structured
   response object containing the constraint. This preserves concise ambient
   authoring while supporting stable-identity queries and multi-object selection.

   Validate every selected object against the complete closed Constraint Expression
   in conjunction with the resolved request schema. Zero matches have no effect,
   all matches must pass, and overlapping expressions compose without precedence.
   Businesses must ensure potentially overlapping expressions are jointly
   satisfiable.

   Keep Business evaluation authoritative while making Platform preflight optional
   and advisory. A Platform may evaluate a supported subset, but must process each
   chosen value completely and skip malformed, unsupported, non-object, or
   resource-limited values rather than partially interpreting them. Existing
   operation outcomes and messages continue to govern runtime failures.

   Each authoritative resource response supplies the advertised constraints for
   the next request to that resource; the following response replaces that set,
   omission clears it, and sets are never merged by path. Require explicit payment
   instrument paths to bridge response and request shapes by matching handler and
   instrument identity.

* Update source/schemas/common/types/request_constraints.json

Co-authored-by: Daniel Wyckoff <daniel.wyckoff@shopify.com>

* fix malformed Request Constraints schema

* clarify Request Constraints path resolution

* restore explicit Request Constraints root members

The path-targeting change referenced Constraint Expression's `required`
property directly, and a follow-up applied the same factoring to `properties`.
Although both references are valid Draft 2020-12, the documentation renderer
turns both anonymous targets into nonexistent anchors. The `properties` target
also contains relative nested references whose source-resource scope the
released bundler does not preserve.

Spell out `required` and `properties` at the Request Constraints root while
continuing to reference the shared grammar for nested Object and Value
Constraints. This preserves the wire contract and keeps the PR compatible with
today's toolchain.

The property-level references can replace these duplicated root shapes after
resource-aware fragment bundling and anonymous-target documentation rendering
are both supported and released.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants