fix!: discriminate destinations at the method level - #689
Merged
igrigorik merged 2 commits intoAug 6, 2026
Merged
Conversation
…ly in requests A fulfillment method's type selects the shape of its entire subtree, destinations included: a shipping method has shipping-address destinations, a pickup method has business-location destinations, and extension-defined method types define their own. Polymorphism is resolved at the parent, so request destinations need no per-object discriminator. - fulfillment_method branches per method type; the generic fulfillment_destination union is no longer referenced by schemas (kept for response documentation). - Destination type is required in responses, optional in requests. - destinations under pickup is response-only (ucp_request omit inside the pickup branch): under strict resolution the Platform cannot write business locations. selected_destination_id is the sole selection channel and accepts any Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated (#589 handoff). - dependentRequired: a request that writes destinations[] must carry the method's type. - Removed explicit additionalProperties:true from fulfillment_method (behavior-neutral in open validation; lets strict sealing work). - Existing request wire shapes are unchanged; responses gain the required type field. Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247
Fulfillment responses always self-describe with a required destination type, while Platform requests follow the enclosing method's authorship contract. Clarify that untyped destinations under the well-known shipping method default to Shipping Destination, pickup destinations are Business-authored and selected through selected_destination_id, and other method types define their own request shape and Platform writability. Remove the unsupported suggestion that an alternate destination type can be selected under core shipping. An ID-only saved or provider-held address remains a Shipping Destination; provider provenance or additional fields require a negotiated extension contract.
igrigorik
marked this pull request as ready for review
August 5, 2026 21:52
igrigorik
merged commit Aug 6, 2026
455ab32
into
feat/typed-fulfillment-destinations
4 of 5 checks passed
4 tasks
1 task
igrigorik
added a commit
that referenced
this pull request
Aug 18, 2026
* fix!: require destination type Fulfillment originally paired a closed shipping/pickup method enum with an untagged oneOf between Shipping Destination and Retail Location. Humans could infer the intended shape from the enclosing method, but the schema did not condition destinations[] on method.type. Every destination was validated against both branches. The branches are not structurally disjoint. Shipping Destination is open and requires only id in responses, so every Retail Location also matches it. Strict oneOf validation rejects intended pickup destinations. Request resolution has the same defect: it omits the stable Location id and requires Business-owned name/address fields, which the open Shipping branch also accepts. PR #507 opened the method-type vocabulary but left this inherited destination debt unchanged. PR #589 exposes it again and adds a second boundary problem: using the full Location Search/Lookup entity in Checkout would couple Fulfillment to a separately negotiated capability's schema and lifecycle. Checkout and Location Lookup need different projections: - Checkout request: stable type-plus-id Location reference - Checkout response: bounded id/name/address rendering summary - Location Search/Lookup: richer discovery entity with geo, hours, amenities, service areas, and future Location fields Platforms must be able to negotiate and render Checkout without supporting or invoking Location Lookup. This change: - Introduces a bounded Location Summary with stable Business-scoped id, Buyer-facing name, and optional address. PR #589 can compose its richer Location entity on top without leaking discovery fields into Checkout. - Replaces structural destination inference with a required, open type discriminator. Well-known values are shipping_address and business_location; negotiated extensions may define additional values. - Preserves flat Platform-owned shipping-address fields. Shipping requests keep destination id optional; Business responses continue assigning the id. - Changes Business Location requests to type-plus-id references. The Business owns the Location name/address and returns those facts in the response summary. - Keeps Catalog's scalar Location id and selected_destination_id semantics. Location recognition is method-scoped and does not reserve inventory or guarantee eligibility; the Business revalidates current terms through normal Fulfillment responses and messages. - Generalizes the active pickup destination from Retail Location to Business Location Destination. Retail stores remain supported as Business Locations, alongside other Business-scoped places such as lockers and partner pickup points. - Updates all destination examples and documents the new authority, identity, and extension boundaries. BREAKING CHANGE: every active Fulfillment Destination now requires type. Shipping-address producers must add type: shipping_address. Retail Location destinations migrate to type: business_location; requests send the stable Location id instead of name/address. * fix!: discriminate destinations at the method level (#689) * fix!: discriminate destinations at the method level; type response-only in requests A fulfillment method's type selects the shape of its entire subtree, destinations included: a shipping method has shipping-address destinations, a pickup method has business-location destinations, and extension-defined method types define their own. Polymorphism is resolved at the parent, so request destinations need no per-object discriminator. - fulfillment_method branches per method type; the generic fulfillment_destination union is no longer referenced by schemas (kept for response documentation). - Destination type is required in responses, optional in requests. - destinations under pickup is response-only (ucp_request omit inside the pickup branch): under strict resolution the Platform cannot write business locations. selected_destination_id is the sole selection channel and accepts any Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated (#589 handoff). - dependentRequired: a request that writes destinations[] must carry the method's type. - Removed explicit additionalProperties:true from fulfillment_method (behavior-neutral in open validation; lets strict sealing work). - Existing request wire shapes are unchanged; responses gain the required type field. Assisted-By: devx/296664b9-53b6-409a-989a-ace9d3348247 * clarify directional destination typing Fulfillment responses always self-describe with a required destination type, while Platform requests follow the enclosing method's authorship contract. Clarify that untyped destinations under the well-known shipping method default to Shipping Destination, pickup destinations are Business-authored and selected through selected_destination_id, and other method types define their own request shape and Platform writability. Remove the unsupported suggestion that an alternate destination type can be selected under core shipping. An ID-only saved or provider-held address remains a Shipping Destination; provider provenance or additional fields require a negotiated extension contract. --------- Co-authored-by: Ilya Grigorik <ilya@grigorik.com> * fix eof * restore generic destination response Moving destinations entirely into method-specific conditionals orphaned the Fulfillment Destination schema and removed destinations from generic method models and generated documentation. It also left extension-defined method responses without the shared type/id destination contract. Restore response-only destinations on the base Fulfillment Method using the generic Fulfillment Destination schema. Known method branches refine that base: shipping re-enables Platform-writable request destinations, while pickup destinations remain response-only. This keeps extension-defined method responses typed, restores destinations to generated models and field tables, and makes the generic schema normative instead of documentation-only. * style(schema): remove redundant destination optionality text * clarify default destination selection The Curbside review exposed two gaps in how the specification describes the existing destination contract. The schema already gives every method a safe default—destinations are Business-authored and response-only, the Platform selects by ID, and returned destinations self-describe through type and id—but the prose described non-Shipping/Pickup methods as undefined. It also gave IDs learned through Catalog a stronger continuity guarantee than IDs learned from Location or an earlier Checkout. Clarify that default and make accepted selection source-agnostic. When the Business accepts a non-null selected_destination_id, it returns the same value with exactly one matching typed destination, revalidates availability and terms, and never silently substitutes another destination. If an Update cannot be accepted, the Business leaves the current Checkout unchanged and returns a recoverable Message identifying the attempted selection. Remove the stale mental-model line that placed Curbside beneath Pickup; Curbside remains a peer method and already inherits the default contract without a new schema branch. No schema change is required because fulfillment_method and fulfillment_destination already express this behavior. Separately, we *have* replicated well-known values across too many places, but I'm reserving that for a separate and followup commit. * clarify destination authorship Describe method and destination types with human-facing examples, state when the Platform writes destination inputs, and use the protocol's "omitted" terminology. Explain shipping destinations as either inline addresses or saved-address references resolvable by the Business or a trusted Credential Provider, and clarify the Checkout request and response flow. --------- Co-authored-by: Lee Richmond <richmolj@gmail.com>
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.
Counter-proposal for the request side of #688; response-side typing is unchanged. Context: Slack thread.
Problem
#688 discriminates destinations per-object in both directions. Requiring
typeon request destinations invalidates every deployed Platform integration — requests that validate today fail the moment the requirement lands. Response-sidetypeis additive: producers add a field; consumers ignore unknown fields under the open-world model. The two directions carry incomparable migration costs.The per-object tag also leaves the agreed authority boundary unenforced.
{ "type": "business_location", "id": ... }remains a schema-valid Platform-written destination, so a written destination andselected_destination_idcan select different locations with no precedence rule — the conflict #688 sets out to remove.Solution
Move the discriminator up one level: a fulfillment method's
typeselects the shape of its entire subtree, destinations included. Ashippingmethod has Shipping Destinations; apickupmethod has Business Location Destinations; extension-defined method types define their own. Polymorphism resolves at the parent, so request destinations need no per-object tag, and the request wire format Platforms send today remains valid unchanged.Plain draft-2020
if/then+$ref— no direction- or context-specific request schemas.Design decisions:
ucp_request: "omit"inside the pickup branch removesdestinationsfrom the request projection; under strict resolution a Platform-written location is rejected as unevaluated.selected_destination_idis the sole selection channel and accepts any stable, Business-scoped Location ID the Business recognizes for the method, including IDs not yet enumerated indestinations[]— the feat!: Introduce Location Search + Lookup capabilities #589 handoff. One channel, one authority.dependentRequired: {"destinations": ["type"]}. Methodtypeis update-optional (target byid), so a request that writesdestinations[]must carry the methodtypefor branch dispatch. Updates touching onlyselected_destination_idorselected_option_idare unaffected.typeand remains self-describing.typestays optional in requests for reference disambiguation (e.g., anid-only destination naming one of several address sources).Before / After — strict request validation
destinations[]destinations[]withoutmethod.typedependentRequired)Resolver change
ucp-schema --strictseals item schemas referenced from base properties but does not descend intoif/then/else; branch item schemas are never sealed. One match arm inclose_additional_properties_inner(resolver.rs):Verified against a patched build: the full matrix above holds on unmodified resolver output; resolver unit tests pass (153/153). Diagnostic trade-off: a failed
thencontributes no annotations, so some rejections surface as a root-level unevaluated-destinationserror rather than an item-level message — verdicts unchanged, messages less precise.Trade-offs
schema_fieldsmacro does not collect branch-scoped properties, sodestinationsno longer renders in the Fulfillment Method field table (covered in the Destinations section). Either the macro learnsthen.properties, or destinations are documented per method type.fulfillment_destination.jsonis no longer schema-referenced; retained as the response-side documentation entity, or foldable into docs.