From b4d78116bf24ca5c0ff422de7bf4fadfdc0eebdf Mon Sep 17 00:00:00 2001 From: Sakina Roufid Date: Sat, 3 Oct 2026 16:50:38 +0000 Subject: [PATCH 1/2] fix: keep additional properties on open schemas quicktype emits every object as a bare z.object, which strips unknown keys on parse, so objects the spec declares open (additionalProperties: true) lost data. A complete checkout request dropped payment.instruments[].credential.token, and checkouts and carts parsed with the base schemas dropped their extension fields. A new generation step, scripts/retain-additional-properties.mjs, appends .catchall(z.any()) to a generated object when every source schema with its property set is open, following $ref and allOf. It looks at the authored, projected and discovery trees together, because one generated object can stand for schemas from more than one of them. Objects that also stand for a closed schema, or for one that does not declare itself open, are left as generated, and the step reports them. 71 objects gain .catchall(z.any()). Six are left as generated: A2ASchema, PriceClassSchema, PriceRangeSchema, ConstraintsElementSchema, FulfillmentCreateRequestSchema and FulfillmentUpdateRequestSchema. The pretest step now compiles extensions.ts as well, so the tests can check that the Extended* schemas keep unknown keys. --- generate_models.sh | 13 + package.json | 2 +- scripts/retain-additional-properties.mjs | 423 +++++ src/spec_generated.ts | 1855 ++++++++++++---------- tests/additional-properties.test.js | 374 +++++ 5 files changed, 1822 insertions(+), 845 deletions(-) create mode 100644 scripts/retain-additional-properties.mjs create mode 100644 tests/additional-properties.test.js diff --git a/generate_models.sh b/generate_models.sh index 6acacce..8532131 100755 --- a/generate_models.sh +++ b/generate_models.sh @@ -300,6 +300,19 @@ if [[ -d "$SPEC_DIR/discovery" ]]; then node scripts/inject-schema-constraints.mjs "$SPEC_DIR/discovery" src/spec_generated.ts fi +# Keep the unknown keys an open object (`additionalProperties: true`) admits; +# a bare z.object strips them on parse. Judged over every tree the models are +# generated from in one pass, because one generated object can stand for +# sources in more than one of them (scripts/retain-additional-properties.mjs). +RETAIN_SCHEMA_DIRS=("$RAW_CONSTRAINT_SCHEMA_DIR") +if [[ -n "$PROJECTED_CONSTRAINT_SCHEMA_DIR" ]]; then + RETAIN_SCHEMA_DIRS+=("$PROJECTED_CONSTRAINT_SCHEMA_DIR") +fi +if [[ -d "$SPEC_DIR/discovery" ]]; then + RETAIN_SCHEMA_DIRS+=("$SPEC_DIR/discovery") +fi +node scripts/retain-additional-properties.mjs src/spec_generated.ts "${RETAIN_SCHEMA_DIRS[@]}" + # Format the generated output to match the repo's Prettier config (the checked-in # file is formatted with .prettierrc; without this step regenerated output drifts # by indentation and formatting noise). diff --git a/package.json b/package.json index 5ccf187..f4faac6 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "build:types": "tsc -p tsconfig.types.json", "build:noEmit": "tsc --noEmit", "generate": "./generate_models.sh", - "pretest": "tsc src/spec_generated.ts --outDir tests/.dist --module commonjs --moduleResolution node --target ES2020 --strict --esModuleInterop --skipLibCheck", + "pretest": "tsc src/spec_generated.ts src/extensions.ts --outDir tests/.dist --module commonjs --moduleResolution node --target ES2020 --strict --esModuleInterop --skipLibCheck", "test": "node --test \"tests/**/*.test.js\"", "prepublishOnly": "npm run build" }, diff --git a/scripts/retain-additional-properties.mjs b/scripts/retain-additional-properties.mjs new file mode 100644 index 0000000..3f655be --- /dev/null +++ b/scripts/retain-additional-properties.mjs @@ -0,0 +1,423 @@ +// Copyright 2026 UCP Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Keep the unknown keys that an open object schema admits. + * + * quicktype renders every object as a bare `z.object({...})`, and a bare + * z.object strips unknown keys on parse. An object the spec declares open + * (`additionalProperties: true`) therefore silently lost data: a complete + * checkout request dropped `payment.instruments[].credential.token` (the + * instrument credential is the open payment_credential.json base), and a + * checkout parsed with the base schema dropped every extension field + * (`fulfillment`, `discounts`, ...). This splices `.catchall(z.any())` directly + * after each such `z.object(...)` call, ahead of any refinement the constraint + * injector chained there (a refined schema no longer has `.catchall`), the same + * retention inject-schema-constraints.mjs renders alongside `propertyNames` and + * `minProperties`. + * + * Matching follows the constraint injector: every object schema is indexed by + * its sorted property-name set, and a generated `z.object` whose set matches + * is judged by every source occurrence of that set. Each occurrence is "open" + * (`true` on it or on a schema it composes via `$ref`/`allOf`), "closed" + * (`false` anywhere in that composition), "schema" (a schema-valued keyword) + * or "default" (no keyword). JSON Schema treats an absent keyword as open too; + * this step acts on the explicit keyword only, so an object that says neither + * keeps its current behavior. + * + * Only a set whose occurrences are ALL open is retained. quicktype merges + * structurally identical objects, so one generated schema can stand for + * several sources, and an unrelated object can share a property set by + * coincidence; either way, extra keys are kept only when every source admits + * them. A set that is also closed somewhere, or also belongs to a source that + * never declared itself open, is left as generated: `{methods}` is both the + * open catalog fulfillment and the checkout fulfillment request, which is not, + * and the generated object is the latter. + * + * The decision is taken over every tree the models are generated from at once + * (the authored tree, the request/response projection and the discovery + * shapes), because a generated object can stand for sources in more than one + * of them. Composition branches (`allOf`/`anyOf`/`oneOf` members) are not + * occurrences: a branch only ever applies together with the schema that + * composes it, so its own keyword says nothing about the composed object. + * + * Runs from generate_models.sh after inject-schema-constraints.mjs. + * Idempotent: an object whose chain already carries `.catchall`, + * `.passthrough`, `.strict` or `.strip` is left alone. + * + * Usage: + * node scripts/retain-additional-properties.mjs ... + */ + +import fs from "node:fs"; +import path from "node:path"; +import ts from "typescript"; + +const [, , targetArg, ...schemaDirArgs] = process.argv; + +if (!targetArg || !schemaDirArgs.length) { + console.error( + "Usage: node scripts/retain-additional-properties.mjs ..." + ); + process.exit(1); +} + +const targetPath = path.resolve(targetArg); + +// --- JSON Schema loading + $ref resolution --------------------------------- + +const documentCache = new Map(); + +function loadDocument(file) { + const absolute = path.resolve(file); + if (documentCache.has(absolute)) { + return documentCache.get(absolute); + } + const parsed = JSON.parse(fs.readFileSync(absolute, "utf8")); + documentCache.set(absolute, parsed); + return parsed; +} + +function resolvePointer(document, pointer) { + let node = document; + for (const rawSegment of pointer.split("/").filter(Boolean)) { + const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~"); + node = node?.[segment]; + } + return node; +} + +function resolveRef(ref, baseFile) { + const hashIndex = ref.indexOf("#"); + const filePart = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref; + const pointer = hashIndex >= 0 ? ref.slice(hashIndex + 1) : ""; + const targetFile = filePart + ? path.resolve(path.dirname(baseFile), filePart) + : baseFile; + const document = loadDocument(targetFile); + const node = pointer ? resolvePointer(document, pointer) : document; + return { node, file: targetFile }; +} + +/** Property names of an object schema, own plus those composed via allOf. */ +function resolvePropertyNames(node, file, seen = new Set(), depth = 0) { + if (!node || typeof node !== "object" || depth > 32) { + return new Set(); + } + if (typeof node.$ref === "string") { + const key = `${file}|${node.$ref}`; + if (seen.has(key)) { + return new Set(); + } + seen.add(key); + const resolved = resolveRef(node.$ref, file); + return resolvePropertyNames(resolved.node, resolved.file, seen, depth + 1); + } + const names = new Set(); + if (Array.isArray(node.allOf)) { + for (const sub of node.allOf) { + for (const name of resolvePropertyNames( + sub, + file, + new Set(seen), + depth + 1 + )) { + names.add(name); + } + } + } + if (node.properties && typeof node.properties === "object") { + for (const name of Object.keys(node.properties)) { + names.add(name); + } + } + return names; +} + +/** + * Every `additionalProperties` value that applies to an object instance: the + * node's own plus those of the schemas it composes via `$ref` and `allOf`. + * token_credential.json declares no keyword of its own but composes + * payment_credential.json, which is open, so a token credential is open too. + */ +function collectAdditionalProperties(node, file, seen = new Set(), depth = 0) { + if (!node || typeof node !== "object" || depth > 32) { + return []; + } + if (typeof node.$ref === "string") { + const key = `${file}|${node.$ref}`; + if (seen.has(key)) { + return []; + } + seen.add(key); + const resolved = resolveRef(node.$ref, file); + return collectAdditionalProperties( + resolved.node, + resolved.file, + seen, + depth + 1 + ); + } + const values = []; + if (Array.isArray(node.allOf)) { + for (const sub of node.allOf) { + values.push( + ...collectAdditionalProperties(sub, file, new Set(seen), depth + 1) + ); + } + } + if ("additionalProperties" in node) { + values.push(node.additionalProperties); + } + return values; +} + +function disposition(node, file) { + const values = collectAdditionalProperties(node, file); + if (values.includes(false)) return "closed"; + if (values.some((value) => value && typeof value === "object")) { + return "schema"; + } + if (values.includes(true)) return "open"; + return "default"; +} + +// --- Index every object occurrence by property set -------------------------- + +// setKey -> Set(disposition) +const dispositionIndex = new Map(); + +const SCHEMA_KEYWORDS = [ + "$ref", + "type", + "properties", + "items", + "allOf", + "anyOf", + "oneOf", +]; + +function walkSchema(node, file, seen = new Set(), depth = 0, branch = false) { + if (!node || typeof node !== "object" || depth > 64) { + return; + } + // The projected tree does not carry every authored file (capability.json, + // for one), so a ref inside a projected capability declaration can dangle. + // Such a node records nothing, and the rest of its file is still walked. + if (typeof node.$ref === "string") { + const key = `${file}|${node.$ref}`; + if (seen.has(key)) { + return; + } + seen.add(key); + let resolved; + try { + resolved = resolveRef(node.$ref, file); + } catch { + return; + } + walkSchema(resolved.node, resolved.file, seen, depth + 1, branch); + return; + } + if (!branch) { + try { + const names = resolvePropertyNames(node, file); + if (names.size) { + const setKey = [...names].sort().join(","); + const value = disposition(node, file); + if (!dispositionIndex.has(setKey)) { + dispositionIndex.set(setKey, new Set()); + } + dispositionIndex.get(setKey).add(value); + } + } catch { + // A composed schema that does not resolve cannot be judged. + } + } + if (node.properties && typeof node.properties === "object") { + for (const child of Object.values(node.properties)) { + walkSchema(child, file, new Set(seen), depth + 1); + } + } + for (const key of ["items", "additionalProperties"]) { + if (node[key] && typeof node[key] === "object") { + walkSchema(node[key], file, new Set(seen), depth + 1); + } + } + for (const key of ["allOf", "anyOf", "oneOf"]) { + if (Array.isArray(node[key])) { + for (const child of node[key]) { + walkSchema(child, file, new Set(seen), depth + 1, true); + } + } + } + if (node.$defs && typeof node.$defs === "object") { + for (const child of Object.values(node.$defs)) { + walkSchema(child, file, new Set(seen), depth + 1); + // A capability declares its platform/business/response schema roles + // under a `$defs` key equal to its reverse domain name. That entry holds + // schemas rather than being one, so walk its members. + if ( + child && + typeof child === "object" && + !SCHEMA_KEYWORDS.some((keyword) => keyword in child) + ) { + for (const role of Object.values(child)) { + walkSchema(role, file, new Set(seen), depth + 1); + } + } + } + } +} + +function collectSchemaFiles(directory) { + const out = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const full = path.join(directory, entry.name); + if (entry.isDirectory()) { + out.push(...collectSchemaFiles(full)); + } else if (entry.name.endsWith(".json")) { + out.push(full); + } + } + return out; +} + +for (const schemaDir of schemaDirArgs) { + for (const file of collectSchemaFiles(path.resolve(schemaDir))) { + try { + walkSchema(loadDocument(file), file); + } catch { + // Ignore unreadable / non-schema JSON files. + } + } +} + +// --- Splice `.catchall(z.any())` onto the open generated objects ------------- + +const sourceText = fs.readFileSync(targetPath, "utf8"); +const sourceFile = ts.createSourceFile( + targetPath, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS +); + +const UNKNOWN_KEY_METHODS = new Set([ + "catchall", + "passthrough", + "strict", + "strip", +]); + +/** + * Whether the method chain wrapping a `z.object({...})` call already decides + * what happens to unknown keys. + */ +function unknownKeysDecided(objectCall) { + let outer = objectCall; + while ( + outer.parent && + ts.isPropertyAccessExpression(outer.parent) && + outer.parent.expression === outer && + outer.parent.parent && + ts.isCallExpression(outer.parent.parent) + ) { + if (UNKNOWN_KEY_METHODS.has(outer.parent.name.text)) { + return true; + } + outer = outer.parent.parent; + } + return false; +} + +function objectLiteralPropertySet(objectLiteral) { + const names = []; + for (const prop of objectLiteral.properties) { + if ( + ts.isPropertyAssignment(prop) && + (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) + ) { + names.push(prop.name.text); + } else { + return null; // spreads / computed / shorthand -> bail out + } + } + return names; +} + +function owningDeclaration(node) { + let current = node; + while (current && !ts.isVariableDeclaration(current)) { + current = current.parent; + } + return current && ts.isIdentifier(current.name) ? current.name.text : "?"; +} + +const edits = []; +const report = { retained: 0, alreadyDecided: 0, leftAsIs: [] }; + +function visit(node) { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + node.expression.expression.text === "z" && + node.expression.name.text === "object" && + node.arguments.length === 1 && + ts.isObjectLiteralExpression(node.arguments[0]) + ) { + const names = objectLiteralPropertySet(node.arguments[0]); + const dispositions = names + ? dispositionIndex.get([...names].sort().join(",")) + : undefined; + if (dispositions?.has("open")) { + if (dispositions.size > 1) { + report.leftAsIs.push( + `${owningDeclaration(node)} (${[...dispositions].sort().join("+")})` + ); + } else if (unknownKeysDecided(node)) { + report.alreadyDecided += 1; + } else { + edits.push({ pos: node.getEnd(), text: ".catchall(z.any())" }); + report.retained += 1; + } + } + } + ts.forEachChild(node, visit); +} + +visit(sourceFile); + +edits.sort((a, b) => b.pos - a.pos); +let output = sourceText; +for (const edit of edits) { + output = output.slice(0, edit.pos) + edit.text + output.slice(edit.pos); +} + +fs.writeFileSync(targetPath, output); + +process.stdout.write( + `retain-additional-properties: ${report.retained} open object schema(s) ` + + `retain unknown keys; ${report.alreadyDecided} already decided.\n` +); +if (report.leftAsIs.length) { + process.stdout.write( + `retain-additional-properties: ${report.leftAsIs.length} object ` + + `schema(s) left as generated (a source of the same shape is not open): ` + + report.leftAsIs.join(", ") + + "\n" + ); +} diff --git a/src/spec_generated.ts b/src/spec_generated.ts index febd827..355b601 100644 --- a/src/spec_generated.ts +++ b/src/spec_generated.ts @@ -355,12 +355,14 @@ export type Mcp = SchemaEndpoint; export const RestSchema = SchemaEndpointSchema; export type Rest = SchemaEndpoint; -export const BuyerSchema = z.object({ - email: z.string().optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - phone_number: z.string().optional(), -}); +export const BuyerSchema = z + .object({ + email: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + }) + .catchall(z.any()); export type Buyer = z.infer; export const BuyerClassSchema = BuyerSchema; export type BuyerClass = Buyer; @@ -402,9 +404,11 @@ export type PostalAddress = z.infer; export const BillingAddressClassSchema = PostalAddressSchema; export type BillingAddressClass = PostalAddress; -export const CredentialClassSchema = z.object({ - type: z.string(), -}); +export const CredentialClassSchema = z + .object({ + type: z.string(), + }) + .catchall(z.any()); export type CredentialClass = z.infer; export const IdentityProviderSchema = CredentialClassSchema; export type IdentityProvider = CredentialClass; @@ -446,10 +450,12 @@ export type ItemUpdateRequest = z.infer; export const ItemCreateRequestSchema = ItemUpdateRequestSchema; export type ItemCreateRequest = ItemUpdateRequest; -export const ActionElementSchema = z.object({ - config: z.record(z.string(), z.any()).optional(), - id: z.string().min(1), -}); +export const ActionElementSchema = z + .object({ + config: z.record(z.string(), z.any()).optional(), + id: z.string().min(1), + }) + .catchall(z.any()); export type ActionElement = z.infer; export const PurpleMeasureSchema = z.object({ @@ -768,14 +774,16 @@ export type LineItemQuantity = z.infer; export const OrderLineItemQuantitySchema = LineItemQuantitySchema; export type OrderLineItemQuantity = LineItemQuantity; -export const PaymentInstrumentSchema = z.object({ - billing_address: BillingAddressClassSchema.optional(), - credential: CredentialClassSchema.optional(), - display: z.record(z.string(), z.any()).optional(), - handler_id: z.string(), - id: z.string(), - type: z.string(), -}); +export const PaymentInstrumentSchema = z + .object({ + billing_address: BillingAddressClassSchema.optional(), + credential: CredentialClassSchema.optional(), + display: z.record(z.string(), z.any()).optional(), + handler_id: z.string(), + id: z.string(), + type: z.string(), + }) + .catchall(z.any()); export type PaymentInstrument = z.infer; export const SegmentValueSchema = z.object({ @@ -831,27 +839,33 @@ export const ShippingDestinationUpdateRequestSchema = ShippingDestinationCreateRequestSchema; export type ShippingDestinationUpdateRequest = ShippingDestinationCreateRequest; -export const FulfillmentGroupCreateRequestSchema = z.object({ - selected_option_id: z.union([z.null(), z.string()]).optional(), -}); +export const FulfillmentGroupCreateRequestSchema = z + .object({ + selected_option_id: z.union([z.null(), z.string()]).optional(), + }) + .catchall(z.any()); export type FulfillmentGroupCreateRequest = z.infer< typeof FulfillmentGroupCreateRequestSchema >; -export const FulfillmentGroupUpdateRequestSchema = z.object({ - id: z.string(), - selected_option_id: z.union([z.null(), z.string()]).optional(), -}); +export const FulfillmentGroupUpdateRequestSchema = z + .object({ + id: z.string(), + selected_option_id: z.union([z.null(), z.string()]).optional(), + }) + .catchall(z.any()); export type FulfillmentGroupUpdateRequest = z.infer< typeof FulfillmentGroupUpdateRequestSchema >; -export const FulfillmentAvailableMethodResponseSchema = z.object({ - description: z.string().optional(), - fulfillable_on: z.union([z.null(), z.string()]).optional(), - line_item_ids: z.array(z.string()), - type: z.string(), -}); +export const FulfillmentAvailableMethodResponseSchema = z + .object({ + description: z.string().optional(), + fulfillable_on: z.union([z.null(), z.string()]).optional(), + line_item_ids: z.array(z.string()), + type: z.string(), + }) + .catchall(z.any()); export type FulfillmentAvailableMethodResponse = z.infer< typeof FulfillmentAvailableMethodResponseSchema >; @@ -873,15 +887,17 @@ export const BindingSchema = z.object({ }); export type Binding = FulfillmentDestinationResponse; -export const FulfillmentOptionResponseSchema = z.object({ - description: DescriptionClassSchema.optional(), - id: z.string(), - title: z.string(), - carrier: z.string().optional(), - earliest_fulfillment_time: z.string().datetime({ offset: true }).optional(), - latest_fulfillment_time: z.string().datetime({ offset: true }).optional(), - totals: z.array(LineItemResponseTotalSchema), -}); +export const FulfillmentOptionResponseSchema = z + .object({ + description: DescriptionClassSchema.optional(), + id: z.string(), + title: z.string(), + carrier: z.string().optional(), + earliest_fulfillment_time: z.string().datetime({ offset: true }).optional(), + latest_fulfillment_time: z.string().datetime({ offset: true }).optional(), + totals: z.array(LineItemResponseTotalSchema), + }) + .catchall(z.any()); export type FulfillmentOptionResponse = z.infer< typeof FulfillmentOptionResponseSchema >; @@ -894,16 +910,18 @@ export type PriceClass = z.infer; export const PriceFilterSchema = PriceClassSchema; export type PriceFilter = PriceClass; -export const PolicySchema = z.object({ - applies_to: z.array(z.string()).optional(), - description: DescriptionClassSchema, - type: z - .string() - .regex( - /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/ - ), - url: z.string().url().optional(), -}); +export const PolicySchema = z + .object({ + applies_to: z.array(z.string()).optional(), + description: DescriptionClassSchema, + type: z + .string() + .regex( + /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/ + ), + url: z.string().url().optional(), + }) + .catchall(z.any()); export type Policy = z.infer; export const PolicyElementSchema = PolicySchema; export type PolicyElement = Policy; @@ -1120,18 +1138,22 @@ export const HoursSchema = z.object({ }); export type Hours = z.infer; -export const AddressSchema = z.object({ - address_country: z.string().optional(), - address_region: z.string().optional(), - postal_code: z.string().optional(), -}); +export const AddressSchema = z + .object({ + address_country: z.string().optional(), + address_region: z.string().optional(), + postal_code: z.string().optional(), + }) + .catchall(z.any()); export type Address = z.infer; export const LocalitySchema = AddressSchema; export type Locality = Address; -export const AmenitySchema = z.object({ - description: z.string(), -}); +export const AmenitySchema = z + .object({ + description: z.string(), + }) + .catchall(z.any()); export type Amenity = z.infer; export const ExceptionHourElementSchema = z @@ -1207,27 +1229,32 @@ export const InputSchema = z.object({ }); export type Input = z.infer; -export const LocationSchema = z.object({ - address: BillingAddressClassSchema.optional(), - id: z.string(), - name: z.string(), - amenities: z - .record(z.string(), AmenitySchema) - .refine( - (value) => - Object.keys(value).every((key) => - /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test( - key - ) - ), - { message: "Record keys must match the required pattern (propertyNames)" } - ) - .optional(), - exception_hours: z.array(ExceptionHourElementSchema).optional(), - geo: GeoClassSchema.optional(), - hours: z.array(DailyHourElementSchema).optional(), - timezone: z.string().optional(), -}); +export const LocationSchema = z + .object({ + address: BillingAddressClassSchema.optional(), + id: z.string(), + name: z.string(), + amenities: z + .record(z.string(), AmenitySchema) + .refine( + (value) => + Object.keys(value).every((key) => + /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test( + key + ) + ), + { + message: + "Record keys must match the required pattern (propertyNames)", + } + ) + .optional(), + exception_hours: z.array(ExceptionHourElementSchema).optional(), + geo: GeoClassSchema.optional(), + hours: z.array(DailyHourElementSchema).optional(), + timezone: z.string().optional(), + }) + .catchall(z.any()); export type Location = z.infer; export const BreakdownElementSchema = z.object({ @@ -1261,13 +1288,15 @@ export const BenefitElementSchema = z.object({ }); export type BenefitElement = z.infer; -export const ScheduleElementSchema = z.object({ - amount: z.number().int().gte(0).lte(9007199254740991), - description: DescriptionClassSchema, - due_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - type: z.string(), -}); +export const ScheduleElementSchema = z + .object({ + amount: z.number().int().gte(0).lte(9007199254740991), + description: DescriptionClassSchema, + due_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + type: z.string(), + }) + .catchall(z.any()); export type ScheduleElement = z.infer; export const PaymentScheduleSchema = ScheduleElementSchema; export type PaymentSchedule = ScheduleElement; @@ -1283,32 +1312,38 @@ export type AllowedCombinationElement = PurpleInstrumentGroup; export const InstrumentGroupSchema = PurpleInstrumentGroupSchema; export type InstrumentGroup = PurpleInstrumentGroup; -export const PaymentInstrumentSplitPaymentsSchema = z.object({ - billing_address: BillingAddressClassSchema.optional(), - credential: CredentialClassSchema.optional(), - display: z.record(z.string(), z.any()).optional(), - handler_id: z.string(), - id: z.string(), - type: z.string(), - amount: z.number().int().gte(0).lte(9007199254740991).optional(), -}); +export const PaymentInstrumentSplitPaymentsSchema = z + .object({ + billing_address: BillingAddressClassSchema.optional(), + credential: CredentialClassSchema.optional(), + display: z.record(z.string(), z.any()).optional(), + handler_id: z.string(), + id: z.string(), + type: z.string(), + amount: z.number().int().gte(0).lte(9007199254740991).optional(), + }) + .catchall(z.any()); export type PaymentInstrumentSplitPayments = z.infer< typeof PaymentInstrumentSplitPaymentsSchema >; -export const ExtensionElementSchema = z.object({ - description: z.string().optional(), - params: z.record(z.string(), z.any()).optional(), - uri: z.string().url(), -}); +export const ExtensionElementSchema = z + .object({ + description: z.string().optional(), + params: z.record(z.string(), z.any()).optional(), + uri: z.string().url(), + }) + .catchall(z.any()); export type ExtensionElement = z.infer; -export const PartElementSchema = z.object({ - data: z.record(z.string(), z.any()).optional(), - kind: z.string().optional(), - text: z.string().optional(), - type: z.string().optional(), -}); +export const PartElementSchema = z + .object({ + data: z.record(z.string(), z.any()).optional(), + kind: z.string().optional(), + text: z.string().optional(), + type: z.string().optional(), + }) + .catchall(z.any()); export type PartElement = z.infer; export const EmbeddedTransportConfigSchema = z.object({ @@ -1319,11 +1354,13 @@ export type EmbeddedTransportConfig = z.infer< typeof EmbeddedTransportConfigSchema >; -export const ErrorClassSchema = z.object({ - code: z.number().int(), - data: z.any().optional(), - message: z.string(), -}); +export const ErrorClassSchema = z + .object({ + code: z.number().int(), + data: z.any().optional(), + message: z.string(), + }) + .catchall(z.any()); export type ErrorClass = z.infer; export const JsonRpc20EnvelopeSchema = z.object({ @@ -1336,15 +1373,19 @@ export const JsonRpc20EnvelopeSchema = z.object({ }); export type JsonRpc20Envelope = z.infer; -export const UcpAgentSchema = z.object({ - profile: z.string(), -}); +export const UcpAgentSchema = z + .object({ + profile: z.string(), + }) + .catchall(z.any()); export type UcpAgent = z.infer; -export const McpToolCallSchema = z.object({ - text: z.string().optional(), - type: z.string(), -}); +export const McpToolCallSchema = z + .object({ + text: z.string().optional(), + type: z.string(), + }) + .catchall(z.any()); export type McpToolCall = z.infer; export const EcKeysCarryCrvXYSchema = z @@ -1357,6 +1398,7 @@ export const EcKeysCarryCrvXYSchema = z x: z.string().optional(), y: z.string().optional(), }) + .catchall(z.any()) .superRefine((value, ctx) => { for (const rule of [ { @@ -1452,9 +1494,11 @@ export type AdjustmentLineItemClass = z.infer< export const AdjustmentLineItemSchema = AdjustmentLineItemClassSchema; export type AdjustmentLineItem = AdjustmentLineItemClass; -export const MultiDestinationSchema = z.object({ - method: z.string(), -}); +export const MultiDestinationSchema = z + .object({ + method: z.string(), + }) + .catchall(z.any()); export type MultiDestination = z.infer; export const DetailOptionValueSchema = z.object({ @@ -1477,30 +1521,36 @@ export const FulfillmentDestinationUpdateRequestSchema = export type FulfillmentDestinationUpdateRequest = FulfillmentDestinationCreateRequest; -export const FulfillmentDestinationFilterSchema = z.object({ - address_country: z.string().optional(), - address_region: z.string().optional(), - postal_code: z.string().optional(), - location: z.string().optional(), -}); +export const FulfillmentDestinationFilterSchema = z + .object({ + address_country: z.string().optional(), + address_region: z.string().optional(), + postal_code: z.string().optional(), + location: z.string().optional(), + }) + .catchall(z.any()); export type FulfillmentDestinationFilter = z.infer< typeof FulfillmentDestinationFilterSchema >; -export const FulfillmentOptionBaseResponseSchema = z.object({ - description: DescriptionClassSchema.optional(), - id: z.string(), - title: z.string(), -}); +export const FulfillmentOptionBaseResponseSchema = z + .object({ + description: DescriptionClassSchema.optional(), + id: z.string(), + title: z.string(), + }) + .catchall(z.any()); export type FulfillmentOptionBaseResponse = z.infer< typeof FulfillmentOptionBaseResponseSchema >; -export const LocationSummarySchema = z.object({ - address: BillingAddressClassSchema.optional(), - id: z.string(), - name: z.string(), -}); +export const LocationSummarySchema = z + .object({ + address: BillingAddressClassSchema.optional(), + id: z.string(), + name: z.string(), + }) + .catchall(z.any()); export type LocationSummary = z.infer; export const BusinessLocationDestinationCreateRequestSchema = LocationSummarySchema; @@ -1509,12 +1559,14 @@ export const BusinessLocationDestinationUpdateRequestSchema = LocationSummarySchema; export type BusinessLocationDestinationUpdateRequest = LocationSummary; -export const BusinessLocationDestinationResponseSchema = z.object({ - address: BillingAddressClassSchema.optional(), - id: z.string(), - name: z.string(), - type: BusinessLocationDestinationResponseTypeSchema, -}); +export const BusinessLocationDestinationResponseSchema = z + .object({ + address: BillingAddressClassSchema.optional(), + id: z.string(), + name: z.string(), + type: BusinessLocationDestinationResponseTypeSchema, + }) + .catchall(z.any()); export type BusinessLocationDestinationResponse = z.infer< typeof BusinessLocationDestinationResponseSchema >; @@ -1551,17 +1603,19 @@ export type ShippingDestinationResponse = z.infer< typeof ShippingDestinationResponseSchema >; -export const CardCredentialSchema = z.object({ - type: CardCredentialTypeSchema, - card_number_type: CardNumberTypeSchema, - cryptogram: z.string().optional(), - cvc: z.string().max(4).optional(), - eci_value: z.string().optional(), - expiry_month: z.number().int().optional(), - expiry_year: z.number().int().optional(), - name: z.string().optional(), - number: z.string().optional(), -}); +export const CardCredentialSchema = z + .object({ + type: CardCredentialTypeSchema, + card_number_type: CardNumberTypeSchema, + cryptogram: z.string().optional(), + cvc: z.string().max(4).optional(), + eci_value: z.string().optional(), + expiry_month: z.number().int().optional(), + expiry_year: z.number().int().optional(), + name: z.string().optional(), + number: z.string().optional(), + }) + .catchall(z.any()); export type CardCredential = z.infer; export const DisplaySchema = z.object({ @@ -1605,28 +1659,32 @@ export const MessageWarningSchema = z.object({ }); export type MessageWarning = z.infer; -export const NetworkTokenCredentialSchema = z.object({ - type: NetworkTokenCredentialTypeSchema, - cryptogram: z.string(), - eci_value: z.string().optional(), - expiry_month: z.number().int().optional(), - expiry_year: z.number().int().optional(), - name: z.string().optional(), - number: z.string(), - token_requestor_id: z.string().optional(), -}); +export const NetworkTokenCredentialSchema = z + .object({ + type: NetworkTokenCredentialTypeSchema, + cryptogram: z.string(), + eci_value: z.string().optional(), + expiry_month: z.number().int().optional(), + expiry_year: z.number().int().optional(), + name: z.string().optional(), + number: z.string(), + token_requestor_id: z.string().optional(), + }) + .catchall(z.any()); export type NetworkTokenCredential = z.infer< typeof NetworkTokenCredentialSchema >; -export const PanCredentialSchema = z.object({ - type: PanCredentialTypeSchema, - cvc: z.string().max(4).optional(), - expiry_month: z.number().int().optional(), - expiry_year: z.number().int().optional(), - name: z.string().optional(), - number: z.string(), -}); +export const PanCredentialSchema = z + .object({ + type: PanCredentialTypeSchema, + cvc: z.string().max(4).optional(), + expiry_month: z.number().int().optional(), + expiry_year: z.number().int().optional(), + name: z.string().optional(), + number: z.string(), + }) + .catchall(z.any()); export type PanCredential = z.infer; export const PaymentIdentitySchema = z.object({ @@ -1725,38 +1783,42 @@ export const UcpServiceSchema = z.object({ }); export type UcpService = z.infer; -export const ContextSchema = z.object({ - address_country: z.string().optional(), - address_region: z.string().optional(), - postal_code: z.string().optional(), - currency: z.string().optional(), - eligibility: z - .array(z.string()) - .refine( - (items) => - new Set(items.map((item) => JSON.stringify(item))).size === - items.length, - { message: "Array items must be unique (uniqueItems)" } - ) - .optional(), - intent: z.string().optional(), - language: z.string().optional(), - location: z.string().optional(), - payment: z.array(PurplePaymentSchema).optional(), -}); +export const ContextSchema = z + .object({ + address_country: z.string().optional(), + address_region: z.string().optional(), + postal_code: z.string().optional(), + currency: z.string().optional(), + eligibility: z + .array(z.string()) + .refine( + (items) => + new Set(items.map((item) => JSON.stringify(item))).size === + items.length, + { message: "Array items must be unique (uniqueItems)" } + ) + .optional(), + intent: z.string().optional(), + language: z.string().optional(), + location: z.string().optional(), + payment: z.array(PurplePaymentSchema).optional(), + }) + .catchall(z.any()); export type Context = z.infer; export const ContextClassSchema = ContextSchema; export type ContextClass = Context; -export const SelectedPaymentInstrumentSchema = z.object({ - billing_address: BillingAddressClassSchema.optional(), - credential: CredentialClassSchema.optional(), - display: z.record(z.string(), z.any()).optional(), - handler_id: z.string(), - id: z.string(), - type: z.string(), - selected: z.boolean().optional(), -}); +export const SelectedPaymentInstrumentSchema = z + .object({ + billing_address: BillingAddressClassSchema.optional(), + credential: CredentialClassSchema.optional(), + display: z.record(z.string(), z.any()).optional(), + handler_id: z.string(), + id: z.string(), + type: z.string(), + selected: z.boolean().optional(), + }) + .catchall(z.any()); export type SelectedPaymentInstrument = z.infer< typeof SelectedPaymentInstrumentSchema >; @@ -1995,29 +2057,35 @@ export type FulfillmentMethodUpdateRequest = z.infer< typeof FulfillmentMethodUpdateRequestSchema >; -export const FulfillmentGroupResponseSchema = z.object({ - id: z.string(), - line_item_ids: z.array(z.string()), - options: z.array(FulfillmentOptionResponseSchema).optional(), - selected_option_id: z.union([z.null(), z.string()]).optional(), -}); +export const FulfillmentGroupResponseSchema = z + .object({ + id: z.string(), + line_item_ids: z.array(z.string()), + options: z.array(FulfillmentOptionResponseSchema).optional(), + selected_option_id: z.union([z.null(), z.string()]).optional(), + }) + .catchall(z.any()); export type FulfillmentGroupResponse = z.infer< typeof FulfillmentGroupResponseSchema >; -export const CartUpdateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemUpdateRequestSchema), - signals: SignalsClassSchema.optional(), -}); +export const CartUpdateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemUpdateRequestSchema), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CartUpdateRequest = z.infer; -export const SearchFiltersSchema = z.object({ - categories: z.array(z.string()).optional(), - price: PriceClassSchema.optional(), -}); +export const SearchFiltersSchema = z + .object({ + categories: z.array(z.string()).optional(), + price: PriceClassSchema.optional(), + }) + .catchall(z.any()); export type SearchFilters = z.infer; export const CatalogLookupSchema = z.object({ @@ -2115,26 +2183,30 @@ export type CompleteCheckoutRequestWithAp2 = z.infer< typeof CompleteCheckoutRequestWithAp2Schema >; -export const LocationDistanceSchema = z.object({ - center: GeoClassSchema, - max: z.number().gte(0), -}); +export const LocationDistanceSchema = z + .object({ + center: GeoClassSchema, + max: z.number().gte(0), + }) + .catchall(z.any()); export type LocationDistance = z.infer; -export const LocationFilterSchema = z.object({ - amenities: z.array(z.string()).optional(), - hours: HoursSchema.optional(), - items: z - .array(z.string()) - .min(1) - .refine( - (items) => - new Set(items.map((item) => JSON.stringify(item))).size === - items.length, - { message: "Array items must be unique (uniqueItems)" } - ) - .optional(), -}); +export const LocationFilterSchema = z + .object({ + amenities: z.array(z.string()).optional(), + hours: HoursSchema.optional(), + items: z + .array(z.string()) + .min(1) + .refine( + (items) => + new Set(items.map((item) => JSON.stringify(item))).size === + items.length, + { message: "Array items must be unique (uniqueItems)" } + ) + .optional(), + }) + .catchall(z.any()); export type LocationFilter = z.infer; export const LocationServesSchema = z @@ -2151,28 +2223,33 @@ export const LocationServesSchema = z }); export type LocationServes = z.infer; -export const LocationElementSchema = z.object({ - address: BillingAddressClassSchema.optional(), - id: z.string(), - name: z.string(), - amenities: z - .record(z.string(), AmenitySchema) - .refine( - (value) => - Object.keys(value).every((key) => - /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test( - key - ) - ), - { message: "Record keys must match the required pattern (propertyNames)" } - ) - .optional(), - exception_hours: z.array(ExceptionHourElementSchema).optional(), - geo: GeoClassSchema.optional(), - hours: z.array(DailyHourElementSchema).optional(), - timezone: z.string().optional(), - inputs: z.array(InputSchema).min(1), -}); +export const LocationElementSchema = z + .object({ + address: BillingAddressClassSchema.optional(), + id: z.string(), + name: z.string(), + amenities: z + .record(z.string(), AmenitySchema) + .refine( + (value) => + Object.keys(value).every((key) => + /^[a-z](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)+$/.test( + key + ) + ), + { + message: + "Record keys must match the required pattern (propertyNames)", + } + ) + .optional(), + exception_hours: z.array(ExceptionHourElementSchema).optional(), + geo: GeoClassSchema.optional(), + hours: z.array(DailyHourElementSchema).optional(), + timezone: z.string().optional(), + inputs: z.array(InputSchema).min(1), + }) + .catchall(z.any()); export type LocationElement = z.infer; export const LocationSearchRequestSchema = z.object({ @@ -2186,9 +2263,11 @@ export const LocationSearchRequestSchema = z.object({ }); export type LocationSearchRequest = z.infer; -export const ScopePolicySchema = z.object({ - description: DescriptionSchema.optional(), -}); +export const ScopePolicySchema = z + .object({ + description: DescriptionSchema.optional(), + }) + .catchall(z.any()); export type ScopePolicy = z.infer; export const MembershipRewardSchema = z.object({ @@ -2204,13 +2283,15 @@ export const MembershipTierSchema = z.object({ }); export type MembershipTier = z.infer; -export const PurplePaymentTermSchema = z.object({ - description: DescriptionClassSchema.optional(), - id: z.string(), - schedules: z.array(ScheduleElementSchema).min(1), - title: z.string(), -}); -export type PurplePaymentTerm = z.infer; +export const PurplePaymentTermSchema = z + .object({ + description: DescriptionClassSchema.optional(), + id: z.string(), + schedules: z.array(ScheduleElementSchema).min(1), + title: z.string(), + }) + .catchall(z.any()); +export type PurplePaymentTerm = z.infer; export const OrderPaymentWithAcceptedTermSchema = z.object({ accepted_term: PurplePaymentTermSchema.optional(), @@ -2235,13 +2316,15 @@ export type CheckoutWithSplitPaymentsPayment = z.infer< typeof CheckoutWithSplitPaymentsPaymentSchema >; -export const ResultClassSchema = z.object({ - contextId: z.string(), - kind: KindSchema, - messageId: z.string(), - parts: z.array(PartElementSchema).min(1), - role: RoleSchema, -}); +export const ResultClassSchema = z + .object({ + contextId: z.string(), + kind: KindSchema, + messageId: z.string(), + parts: z.array(PartElementSchema).min(1), + role: RoleSchema, + }) + .catchall(z.any()); export type ResultClass = z.infer; export const EmbeddedProtocolMessageEnvelopeSchema = z.object({ @@ -2256,16 +2339,20 @@ export type EmbeddedProtocolMessageEnvelope = z.infer< typeof EmbeddedProtocolMessageEnvelopeSchema >; -export const MetaSchema = z.object({ - "idempotency-key": z.string().optional(), - "ucp-agent": UcpAgentSchema.optional(), -}); +export const MetaSchema = z + .object({ + "idempotency-key": z.string().optional(), + "ucp-agent": UcpAgentSchema.optional(), + }) + .catchall(z.any()); export type Meta = z.infer; -export const ResultSchema = z.object({ - content: z.array(McpToolCallSchema).optional(), - structuredContent: z.record(z.string(), z.any()), -}); +export const ResultSchema = z + .object({ + content: z.array(McpToolCallSchema).optional(), + structuredContent: z.record(z.string(), z.any()), + }) + .catchall(z.any()); export type Result = z.infer; export const AdjustmentSchema = z.object({ @@ -2298,15 +2385,17 @@ export type BusinessSplitPaymentsConfig = z.infer< typeof BusinessSplitPaymentsConfigSchema >; -export const CardPaymentInstrumentSchema = z.object({ - billing_address: BillingAddressClassSchema.optional(), - credential: CredentialClassSchema.optional(), - display: DisplaySchema.optional(), - handler_id: z.string(), - id: z.string(), - type: CardCredentialTypeSchema, - network: z.string().optional(), -}); +export const CardPaymentInstrumentSchema = z + .object({ + billing_address: BillingAddressClassSchema.optional(), + credential: CredentialClassSchema.optional(), + display: DisplaySchema.optional(), + handler_id: z.string(), + id: z.string(), + type: CardCredentialTypeSchema, + network: z.string().optional(), + }) + .catchall(z.any()); export type CardPaymentInstrument = z.infer; export const PaymentSchema = z.object({ @@ -2352,23 +2441,27 @@ export const LineItemCreateRequestSchema = z.object({ }); export type LineItemCreateRequest = z.infer; -export const CheckoutUpdateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemUpdateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), -}); +export const CheckoutUpdateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CheckoutUpdateRequest = z.infer; export const CheckoutWithCartUpdateRequestSchema = CheckoutUpdateRequestSchema; export type CheckoutWithCartUpdateRequest = CheckoutUpdateRequest; -export const CheckoutCompleteRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - payment: CheckoutCreateRequestPaymentSchema, - signals: SignalsClassSchema.optional(), -}); +export const CheckoutCompleteRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + payment: CheckoutCreateRequestPaymentSchema, + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CheckoutCompleteRequest = z.infer< typeof CheckoutCompleteRequestSchema >; @@ -2403,61 +2496,71 @@ export type LineItemElement = LineItem; export const OrderLineItemSchema = LineItemSchema; export type OrderLineItem = LineItem; -export const BuyerWithConsentCreateRequestSchema = z.object({ - email: z.string().optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - phone_number: z.string().optional(), - consent: z.record(z.string(), ConsentValueSchema).optional(), -}); +export const BuyerWithConsentCreateRequestSchema = z + .object({ + email: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + consent: z.record(z.string(), ConsentValueSchema).optional(), + }) + .catchall(z.any()); export type BuyerWithConsentCreateRequest = z.infer< typeof BuyerWithConsentCreateRequestSchema >; -export const BuyerWithConsentUpdateRequestSchema = z.object({ - email: z.string().optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - phone_number: z.string().optional(), - consent: z.record(z.string(), ConsentClassSchema).optional(), -}); +export const BuyerWithConsentUpdateRequestSchema = z + .object({ + email: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + consent: z.record(z.string(), ConsentClassSchema).optional(), + }) + .catchall(z.any()); export type BuyerWithConsentUpdateRequest = z.infer< typeof BuyerWithConsentUpdateRequestSchema >; -export const BuyerWithConsentResponseSchema = z.object({ - email: z.string().optional(), - first_name: z.string().optional(), - last_name: z.string().optional(), - phone_number: z.string().optional(), - consent: z.record(z.string(), BuyerConsentSchema).optional(), -}); +export const BuyerWithConsentResponseSchema = z + .object({ + email: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + phone_number: z.string().optional(), + consent: z.record(z.string(), BuyerConsentSchema).optional(), + }) + .catchall(z.any()); export type BuyerWithConsentResponse = z.infer< typeof BuyerWithConsentResponseSchema >; -export const CheckoutWithDiscountCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), - discounts: CheckoutWithDiscountCreateRequestDiscountsSchema.optional(), -}); +export const CheckoutWithDiscountCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + discounts: CheckoutWithDiscountCreateRequestDiscountsSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithDiscountCreateRequest = z.infer< typeof CheckoutWithDiscountCreateRequestSchema >; -export const CheckoutWithDiscountUpdateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemUpdateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), - discounts: CheckoutWithDiscountUpdateRequestDiscountsSchema.optional(), -}); +export const CheckoutWithDiscountUpdateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + discounts: CheckoutWithDiscountUpdateRequestDiscountsSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithDiscountUpdateRequest = z.infer< typeof CheckoutWithDiscountUpdateRequestSchema >; @@ -2496,24 +2599,28 @@ export type FulfillmentMethodResponse = z.infer< typeof FulfillmentMethodResponseSchema >; -export const CartCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - signals: SignalsClassSchema.optional(), -}); +export const CartCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CartCreateRequest = z.infer; -export const CheckoutWithCartCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), - cart_id: z.string().optional(), -}); +export const CheckoutWithCartCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + cart_id: z.string().optional(), + }) + .catchall(z.any()); export type CheckoutWithCartCreateRequest = z.infer< typeof CheckoutWithCartCreateRequestSchema >; @@ -2590,16 +2697,20 @@ export const PaymentWithTermsSchema = z.object({ }); export type PaymentWithTerms = z.infer; -export const A2AUcpMessageEnvelopeParamsSchema = z.object({ - message: ResultClassSchema, -}); +export const A2AUcpMessageEnvelopeParamsSchema = z + .object({ + message: ResultClassSchema, + }) + .catchall(z.any()); export type A2AUcpMessageEnvelopeParams = z.infer< typeof A2AUcpMessageEnvelopeParamsSchema >; -export const ArgumentsSchema = z.object({ - meta: MetaSchema.optional(), -}); +export const ArgumentsSchema = z + .object({ + meta: MetaSchema.optional(), + }) + .catchall(z.any()); export type Arguments = z.infer; export const PaymentHandlerResponseSchema = z.object({ @@ -2666,14 +2777,16 @@ export const UcpSchema = z.object({ }); export type Ucp = z.infer; -export const CheckoutCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), -}); +export const CheckoutCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CheckoutCreateRequest = z.infer; export const LineItemResponseSchema = z.object({ @@ -2770,147 +2883,163 @@ export const UcpResponseSchema = z.object({ }); export type UcpResponse = z.infer; -export const CheckoutWithBuyerConsentCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerWithConsentCreateRequestSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), -}); +export const CheckoutWithBuyerConsentCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerWithConsentCreateRequestSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithBuyerConsentCreateRequest = z.infer< typeof CheckoutWithBuyerConsentCreateRequestSchema >; -export const CheckoutWithBuyerConsentUpdateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerWithConsentUpdateRequestSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemUpdateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), -}); +export const CheckoutWithBuyerConsentUpdateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerWithConsentUpdateRequestSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithBuyerConsentUpdateRequest = z.infer< typeof CheckoutWithBuyerConsentUpdateRequestSchema >; -export const CheckoutWithBuyerConsentResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerWithConsentResponseSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithBuyerConsentResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerWithConsentResponseSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, -}); + }), + ucp: UcpCheckoutResponseSchema, + }) + .catchall(z.any()); export type CheckoutWithBuyerConsentResponse = z.infer< typeof CheckoutWithBuyerConsentResponseSchema >; -export const CheckoutWithDiscountResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithDiscountResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, - discounts: CheckoutWithDiscountResponseDiscountsSchema.optional(), -}); + }), + ucp: UcpCheckoutResponseSchema, + discounts: CheckoutWithDiscountResponseDiscountsSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithDiscountResponse = z.infer< typeof CheckoutWithDiscountResponseSchema >; -export const CheckoutWithFulfillmentCreateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemCreateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), - fulfillment: FulfillmentCreateRequestSchema.optional(), -}); +export const CheckoutWithFulfillmentCreateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemCreateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + fulfillment: FulfillmentCreateRequestSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithFulfillmentCreateRequest = z.infer< typeof CheckoutWithFulfillmentCreateRequestSchema >; -export const CheckoutWithFulfillmentUpdateRequestSchema = z.object({ - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - line_items: z.array(LineItemUpdateRequestSchema), - payment: CheckoutCreateRequestPaymentSchema.optional(), - signals: SignalsClassSchema.optional(), - fulfillment: FulfillmentUpdateRequestSchema.optional(), -}); +export const CheckoutWithFulfillmentUpdateRequestSchema = z + .object({ + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + line_items: z.array(LineItemUpdateRequestSchema), + payment: CheckoutCreateRequestPaymentSchema.optional(), + signals: SignalsClassSchema.optional(), + fulfillment: FulfillmentUpdateRequestSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithFulfillmentUpdateRequest = z.infer< typeof CheckoutWithFulfillmentUpdateRequestSchema >; @@ -2923,92 +3052,100 @@ export const FulfillmentResponseSchema = z.object({ }); export type FulfillmentResponse = z.infer; -export const CartResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema).optional(), - messages: z.array(MessageElementSchema).optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CartResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema).optional(), + messages: z.array(MessageElementSchema).optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpResponseSchema, -}); + }), + ucp: UcpResponseSchema, + }) + .catchall(z.any()); export type CartResponse = z.infer; -export const CheckoutWithCartResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithCartResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, - cart_id: z.string().optional(), -}); + }), + ucp: UcpCheckoutResponseSchema, + cart_id: z.string().optional(), + }) + .catchall(z.any()); export type CheckoutWithCartResponse = z.infer< typeof CheckoutWithCartResponseSchema >; @@ -3041,50 +3178,54 @@ export const SearchResponseSchema = z.object({ }); export type SearchResponse = z.infer; -export const CheckoutWithAp2MandateSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithAp2MandateSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, - ap2: CheckoutWithAp2MandateAp2Schema.optional(), -}); + }), + ucp: UcpCheckoutResponseSchema, + ap2: CheckoutWithAp2MandateAp2Schema.optional(), + }) + .catchall(z.any()); export type CheckoutWithAp2Mandate = z.infer< typeof CheckoutWithAp2MandateSchema >; @@ -3108,142 +3249,154 @@ export type LocationSearchResponse = z.infer< typeof LocationSearchResponseSchema >; -export const CheckoutWithLoyaltySchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithLoyaltySchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, - loyalty: z.record(z.string(), LoyaltyMembershipSchema).optional(), -}); + }), + ucp: UcpCheckoutResponseSchema, + loyalty: z.record(z.string(), LoyaltyMembershipSchema).optional(), + }) + .catchall(z.any()); export type CheckoutWithLoyalty = z.infer; -export const CheckoutWithPaymentTermsSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutWithPaymentTermsPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithPaymentTermsSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutWithPaymentTermsPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, -}); + }), + ucp: UcpCheckoutResponseSchema, + }) + .catchall(z.any()); export type CheckoutWithPaymentTerms = z.infer< typeof CheckoutWithPaymentTermsSchema >; -export const CheckoutWithSplitPaymentsSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutWithSplitPaymentsPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithSplitPaymentsSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutWithSplitPaymentsPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, -}); + }), + ucp: UcpCheckoutResponseSchema, + }) + .catchall(z.any()); export type CheckoutWithSplitPayments = z.infer< typeof CheckoutWithSplitPaymentsSchema >; @@ -3258,18 +3411,22 @@ export const A2AUcpMessageEnvelopeSchema = z.object({ }); export type A2AUcpMessageEnvelope = z.infer; -export const McpToolCallEnvelopeParamsSchema = z.object({ - arguments: ArgumentsSchema, - name: z.string().min(1), -}); +export const McpToolCallEnvelopeParamsSchema = z + .object({ + arguments: ArgumentsSchema, + name: z.string().min(1), + }) + .catchall(z.any()); export type McpToolCallEnvelopeParams = z.infer< typeof McpToolCallEnvelopeParamsSchema >; -export const UcpProfileDocumentSchema = z.object({ - keys: z.array(EcKeysCarryCrvXYSchema).optional(), - ucp: UcpResponseSchema, -}); +export const UcpProfileDocumentSchema = z + .object({ + keys: z.array(EcKeysCarryCrvXYSchema).optional(), + ucp: UcpResponseSchema, + }) + .catchall(z.any()); export type UcpProfileDocument = z.infer; export const ErrorResponseSchema = z.object({ @@ -3286,49 +3443,53 @@ export type UcpDiscoveryProfilePayment = z.infer< typeof UcpDiscoveryProfilePaymentSchema >; -export const CheckoutResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, -}); + }), + ucp: UcpCheckoutResponseSchema, + }) + .catchall(z.any()); export type CheckoutResponse = z.infer; export const OrderSchema = z.object({ @@ -3371,50 +3532,54 @@ export const OrderSchema = z.object({ }); export type Order = z.infer; -export const CheckoutWithFulfillmentResponseSchema = z.object({ - actions: z.record(z.string(), z.array(ActionElementSchema).min(1)).optional(), - attribution: z.record(z.string(), z.string()).optional(), - buyer: BuyerClassSchema.optional(), - context: ContextClassSchema.optional(), - continue_url: z.string().url().optional(), - currency: z.string(), - expires_at: z.string().datetime({ offset: true }).optional(), - id: z.string(), - line_items: z.array(LineItemResponseSchema), - links: z.array(LinkElementSchema), - messages: z.array(MessageElementSchema).optional(), - order: OrderClassSchema.optional(), - payment: CheckoutCreateRequestPaymentSchema.optional(), - policies: z.array(PolicyElementSchema).optional(), - signals: SignalsClassSchema.optional(), - status: CheckoutResponseStatusSchema, - totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { - for (const rule of [ - { property: "type", value: "subtotal", min: 1, max: 1 }, - { property: "type", value: "total", min: 1, max: 1 }, - ]) { - const matches = items.filter( - (item) => - item != null && - (item as Record)[rule.property] === rule.value - ).length; - if (rule.min !== undefined && matches < rule.min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, - }); - } - if (rule.max !== undefined && matches > rule.max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, - }); +export const CheckoutWithFulfillmentResponseSchema = z + .object({ + actions: z + .record(z.string(), z.array(ActionElementSchema).min(1)) + .optional(), + attribution: z.record(z.string(), z.string()).optional(), + buyer: BuyerClassSchema.optional(), + context: ContextClassSchema.optional(), + continue_url: z.string().url().optional(), + currency: z.string(), + expires_at: z.string().datetime({ offset: true }).optional(), + id: z.string(), + line_items: z.array(LineItemResponseSchema), + links: z.array(LinkElementSchema), + messages: z.array(MessageElementSchema).optional(), + order: OrderClassSchema.optional(), + payment: CheckoutCreateRequestPaymentSchema.optional(), + policies: z.array(PolicyElementSchema).optional(), + signals: SignalsClassSchema.optional(), + status: CheckoutResponseStatusSchema, + totals: z.array(CheckoutResponseTotalSchema).superRefine((items, ctx) => { + for (const rule of [ + { property: "type", value: "subtotal", min: 1, max: 1 }, + { property: "type", value: "total", min: 1, max: 1 }, + ]) { + const matches = items.filter( + (item) => + item != null && + (item as Record)[rule.property] === rule.value + ).length; + if (rule.min !== undefined && matches < rule.min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at least ${rule.min} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (minContains)`, + }); + } + if (rule.max !== undefined && matches > rule.max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Array must contain at most ${rule.max} item(s) where ${rule.property} = ${JSON.stringify(rule.value)} (maxContains)`, + }); + } } - } - }), - ucp: UcpCheckoutResponseSchema, - fulfillment: FulfillmentResponseSchema.optional(), -}); + }), + ucp: UcpCheckoutResponseSchema, + fulfillment: FulfillmentResponseSchema.optional(), + }) + .catchall(z.any()); export type CheckoutWithFulfillmentResponse = z.infer< typeof CheckoutWithFulfillmentResponseSchema >; @@ -3552,10 +3717,12 @@ export type PermalinkCapabilityBusiness = z.infer< typeof PermalinkCapabilityBusinessSchema >; -export const IdentityLinkingBusinessConfigSchema = z.object({ - providers: z.record(z.string(), z.array(IdentityProviderSchema)).optional(), - scopes: z.record(z.string(), ScopePolicySchema), -}); +export const IdentityLinkingBusinessConfigSchema = z + .object({ + providers: z.record(z.string(), z.array(IdentityProviderSchema)).optional(), + scopes: z.record(z.string(), ScopePolicySchema), + }) + .catchall(z.any()); export type IdentityLinkingBusinessConfig = z.infer< typeof IdentityLinkingBusinessConfigSchema >; diff --git a/tests/additional-properties.test.js b/tests/additional-properties.test.js new file mode 100644 index 0000000..1f387ee --- /dev/null +++ b/tests/additional-properties.test.js @@ -0,0 +1,374 @@ +// Copyright 2026 UCP Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// `additionalProperties: true` is dropped by quicktype's typescript-zod +// target, and a bare z.object strips unknown keys on parse, so every object +// the spec declares open lost data: a complete checkout request dropped +// `payment.instruments[].credential.token`, and a checkout parsed with the +// base schema dropped its extension fields. The retention step +// (scripts/retain-additional-properties.mjs) appends `.catchall(z.any())` to a +// generated object when every source schema of its property set is open, and +// leaves it as generated when one of them is closed or never declared itself +// open. +// +// The schemas are compiled from src/ by the "pretest" step so the test +// exercises the generated zod schemas and the hand-written extensions +// directly. + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); + +const SCRIPT = path.join( + __dirname, + "..", + "scripts", + "retain-additional-properties.mjs" +); + +const { + CartResponseSchema, + CheckoutCompleteRequestSchema, + CheckoutCreateRequestSchema, + CheckoutResponseSchema, + ErrorResponseSchema, + FulfillmentCreateRequestSchema, + PaymentCredentialSchema, + PostalAddressSchema, + TokenCredentialSchema, +} = require("./.dist/spec_generated.js"); +const { + ExtendedCheckoutCreateRequestSchema, + ExtendedCheckoutResponseSchema, + ExtendedPaymentCredentialSchema, +} = require("./.dist/extensions.js"); + +function withRetainer(trees, generated, fn) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ucp-retain-")); + try { + const schemaDirs = Object.entries(trees).map(([tree, schemas]) => { + const schemaDir = path.join(dir, tree); + fs.mkdirSync(schemaDir); + for (const [name, schema] of Object.entries(schemas)) { + fs.writeFileSync(path.join(schemaDir, name), JSON.stringify(schema)); + } + return schemaDir; + }); + const target = path.join(dir, "spec_generated.ts"); + fs.writeFileSync(target, 'import * as z from "zod";\n\n' + generated); + const run = () => + execFileSync("node", [SCRIPT, target, ...schemaDirs], { + encoding: "utf8", + }); + return fn(run, () => fs.readFileSync(target, "utf8")); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +const credential = { + $id: "https://ucp.dev/schemas/credential.json", + title: "Credential", + type: "object", + required: ["type"], + properties: { type: { type: "string" } }, + additionalProperties: true, +}; + +const credentialTs = + "export const CredentialSchema = z.object({\n" + + " type: z.string(),\n" + + "});\n"; + +// --- the retention step ---------------------------------------------------- + +test("retainer appends catchall to an object its source declares open", () => { + withRetainer( + { schemas: { "credential.json": credential } }, + credentialTs, + (run, read) => { + const report = run(); + assert.match(report, /1 open object schema\(s\) retain unknown keys/); + assert.match( + read(), + /CredentialSchema = z\.object\(\{[\s\S]*?\}\)\.catchall\(z\.any\(\)\);/ + ); + } + ); +}); + +test("retainer is idempotent", () => { + withRetainer( + { schemas: { "credential.json": credential } }, + credentialTs, + (run, read) => { + run(); + const afterFirst = read(); + run(); + assert.equal(read(), afterFirst, "a second pass must not add another"); + } + ); +}); + +test("retainer treats a schema that composes an open base through allOf as open", () => { + // token_credential.json declares no keyword of its own; its allOf branch is + // not an occurrence, so it cannot veto the open base it shares a shape with. + const token = { + $id: "https://ucp.dev/schemas/token.json", + title: "Token", + allOf: [ + { $ref: "credential.json" }, + { type: "object", properties: { type: { type: "string" } } }, + ], + }; + withRetainer( + { schemas: { "credential.json": credential, "token.json": token } }, + credentialTs, + (run, read) => { + run(); + assert.match(read(), /\.catchall\(z\.any\(\)\)/); + } + ); +}); + +test("retainer leaves a shape alone when a source of it is closed", () => { + const closed = { + $id: "https://ucp.dev/schemas/closed.json", + title: "Closed", + type: "object", + properties: { type: { type: "string" } }, + additionalProperties: false, + }; + withRetainer( + { schemas: { "credential.json": credential, "closed.json": closed } }, + credentialTs, + (run, read) => { + const report = run(); + assert.match(report, /CredentialSchema \(closed\+open\)/); + assert.doesNotMatch(read(), /catchall/); + } + ); +}); + +test("retainer leaves a shape alone when a source of it never declared itself open", () => { + const plain = { + $id: "https://ucp.dev/schemas/plain.json", + title: "Plain", + type: "object", + properties: { type: { type: "string" } }, + }; + withRetainer( + { schemas: { "credential.json": credential, "plain.json": plain } }, + credentialTs, + (run, read) => { + const report = run(); + assert.match(report, /CredentialSchema \(default\+open\)/); + assert.doesNotMatch(read(), /catchall/); + } + ); +}); + +test("retainer weighs every schema tree it is given together", () => { + // The authored tree alone says open; the projection adds a same-shape + // source that does not. A per-tree decision would open the shared object. + const plain = { + $id: "https://ucp.dev/schemas/plain.json", + title: "Plain", + type: "object", + properties: { type: { type: "string" } }, + }; + withRetainer( + { + raw: { "credential.json": credential }, + projected: { "plain.json": plain }, + }, + credentialTs, + (run, read) => { + run(); + assert.doesNotMatch(read(), /catchall/); + } + ); +}); + +test("retainer keeps catchall ahead of a refinement and skips a decided object", () => { + // `.catchall` does not exist on a refined schema, so it must precede the + // injector's superRefine; a chain that already decides unknown keys is left. + const generated = + "export const CredentialSchema = z.object({\n" + + " type: z.string(),\n" + + "}).superRefine((value, ctx) => {});\n" + + "export const PassSchema = z.object({\n" + + " type: z.string(),\n" + + "}).passthrough();\n"; + withRetainer( + { schemas: { "credential.json": credential } }, + generated, + (run, read) => { + const report = run(); + assert.match(report, /1 already decided/); + const output = read(); + assert.match( + output, + /CredentialSchema = z\.object\(\{[\s\S]*?\}\)\.catchall\(z\.any\(\)\)\.superRefine\(/ + ); + assert.match( + output, + /PassSchema = z\.object\(\{[\s\S]*?\}\)\.passthrough\(\);/ + ); + } + ); +}); + +// --- the generated schemas ------------------------------------------------- + +const instrument = { + id: "pi_tok_visa", + handler_id: "merchant_tokenizer", + type: "card", + credential: { type: "card", token: "tok_visa_123" }, +}; + +test("CheckoutCompleteRequestSchema keeps the instrument credential token", () => { + const result = CheckoutCompleteRequestSchema.safeParse({ + payment: { instruments: [instrument] }, + }); + assert.ok(result.success); + assert.equal( + result.data.payment.instruments[0].credential.token, + "tok_visa_123", + "payment_credential.json is open: the handler's credential fields survive" + ); +}); + +test("CheckoutCompleteRequestSchema keeps an extension field and handler-specific credential fields", () => { + const result = CheckoutCompleteRequestSchema.safeParse({ + payment: { + instruments: [ + { + ...instrument, + credential: { type: "encrypted", encrypted_data: "ZW5j" }, + }, + ], + }, + ap2: { checkout_mandate: "eyJhbGciOiJ" }, + }); + assert.ok(result.success); + assert.equal( + result.data.payment.instruments[0].credential.encrypted_data, + "ZW5j" + ); + assert.deepEqual(result.data.ap2, { checkout_mandate: "eyJhbGciOiJ" }); +}); + +test("CheckoutCreateRequestSchema keeps extension fields it does not model", () => { + const body = { + line_items: [{ item: { id: "sku_123" }, quantity: 1 }], + fulfillment: { + methods: [{ type: "pickup", selected_destination_id: "loc_1375" }], + }, + discounts: { codes: ["SAVE10"] }, + }; + const result = CheckoutCreateRequestSchema.safeParse(body); + assert.ok(result.success); + assert.deepEqual(result.data.fulfillment, body.fulfillment); + assert.deepEqual(result.data.discounts, body.discounts); +}); + +test("an open schema still validates the fields it models and keeps .shape", () => { + assert.equal( + CheckoutCompleteRequestSchema.safeParse({ payment: { instruments: "x" } }) + .success, + false, + "retaining unknown keys must not loosen a modelled field" + ); + for (const schema of [CheckoutResponseSchema, CartResponseSchema]) { + assert.ok(schema.shape, "extensions.ts needs .shape, .extend and .pick"); + } +}); + +test("TokenCredentialSchema stays the response projection: no token field, extras kept", () => { + // token_credential.json marks `token` ucp_response: omit, so the response + // shape does not model it; the credential is still open. + assert.equal("token" in TokenCredentialSchema.shape, false); + assert.equal(TokenCredentialSchema, PaymentCredentialSchema); + const result = PaymentCredentialSchema.safeParse({ + type: "card", + network: "visa", + }); + assert.ok(result.success); + assert.equal(result.data.network, "visa"); +}); + +test("a closed schema still strips unknown keys", () => { + // error_response.json is `additionalProperties: false`. + const result = ErrorResponseSchema.safeParse({ + ucp: { version: "2026-04-08", status: "error" }, + messages: [ + { + type: "error", + code: "timeout_error", + content: "An internal service timed out.", + severity: "recoverable", + }, + ], + debug: "not in the schema", + }); + assert.ok(result.success); + assert.equal("debug" in result.data, false); +}); + +test("an object the spec does not declare open still strips unknown keys", () => { + const address = PostalAddressSchema.safeParse({ + address_country: "US", + nickname: "home", + }); + assert.ok(address.success); + assert.equal("nickname" in address.data, false); + // The checkout fulfillment request shares its {methods} shape with the open + // catalog fulfillment but is not open itself. + const fulfillment = FulfillmentCreateRequestSchema.safeParse({ + methods: [], + note: "x", + }); + assert.ok(fulfillment.success); + assert.equal("note" in fulfillment.data, false); +}); + +// --- the hand-written extensions ------------------------------------------- + +test("Extended* schemas built with .extend() and .pick() keep unknown keys", () => { + const request = ExtendedCheckoutCreateRequestSchema.safeParse({ + line_items: [{ item: { id: "sku_123" }, quantity: 1 }], + "com.example.gift_wrap": true, + }); + assert.ok(request.success); + assert.equal(request.data["com.example.gift_wrap"], true); + + const credentialResult = ExtendedPaymentCredentialSchema.safeParse({ + type: "card", + token: "tok_visa_123", + network: "visa", + }); + assert.ok(credentialResult.success); + assert.equal(credentialResult.data.token, "tok_visa_123"); + assert.equal(credentialResult.data.network, "visa"); + + assert.ok( + ExtendedCheckoutResponseSchema.shape.fulfillment, + "the picked extension field is still modelled" + ); +}); From b24c9256cd4c03fdb79861fb6fb1f7cc6774444d Mon Sep 17 00:00:00 2001 From: Sakina Roufid Date: Sat, 3 Oct 2026 16:54:21 +0000 Subject: [PATCH 2/2] fix: keep detail option value fields on get_product responses catalog_lookup.json's detail_product composes product.json and redeclares options so that options[].values[] are detail_option_value.json, which adds available and exists. The generated GetProductResponseSchema still used the plain option value, so both fields were stripped on parse and DetailOptionValueSchema went unused. The cause is in quicktype: when it intersects two arrays for allOf, it drops the items of the first array it meets (updateArrayItemTypes in quicktype-core's ResolveIntersections), so the base's items won. The projection now moves an array property that redeclares an inherited array with different items into a trailing allOf branch, which is the array whose items quicktype keeps. detail_product is the only such case in the pinned spec. ProductClassSchema.options now uses a new OptionSchema whose values are DetailOptionValueSchema. No other generated schema changes. --- scripts/project-current-ucp-schemas.mjs | 96 +++++++++ src/spec_generated.ts | 24 ++- tests/project-array-redeclarations.test.js | 226 +++++++++++++++++++++ 3 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 tests/project-array-redeclarations.test.js diff --git a/scripts/project-current-ucp-schemas.mjs b/scripts/project-current-ucp-schemas.mjs index a2349de..09c812c 100644 --- a/scripts/project-current-ucp-schemas.mjs +++ b/scripts/project-current-ucp-schemas.mjs @@ -451,6 +451,7 @@ function projectSchemaNode(node, context) { } restoreBranchProperties(output, omittedProperties, context); + foldArrayRedeclarations(node, output, context); return output; } @@ -544,6 +545,101 @@ function restoreBranchProperties(output, omittedProperties, context) { } } +// quicktype resolves `allOf` as an intersection, and its intersection of two +// array types keeps the items of only one of them: quicktype-core's +// IntersectionAccumulator.updateArrayItemTypes never records the items of the +// first array it meets. A node that composes a base through `allOf` and +// redeclares one of the base's array properties with narrower items therefore +// generated the BASE items. catalog_lookup.json's detail_product narrows +// product.json's options[].values[] to detail_option_value.json, yet +// GetProductResponseSchema used the plain option value and stripped +// `available` and `exists` from every parsed value. +// +// Moving such a redeclaration into a trailing `allOf` branch makes it the array +// whose items survive. The spec's redeclared items refine the base's, so they +// are also what a correct intersection yields, and the move stays right if +// quicktype fixes this. +function foldArrayRedeclarations(node, output, context) { + if ( + !Array.isArray(node.allOf) || + !Array.isArray(output.allOf) || + !node.properties || + !output.properties + ) { + return; + } + + const inherited = {}; + for (const member of node.allOf) { + Object.assign( + inherited, + composedProperties(member, context.sourceRel, context.schemaCache) + ); + } + + const branch = { type: "object", properties: {} }; + for (const [name, schema] of Object.entries(node.properties)) { + const base = inherited[name]; + if ( + !(name in output.properties) || + schema?.type !== "array" || + base?.type !== "array" || + JSON.stringify(schema.items) === JSON.stringify(base.items) + ) { + continue; + } + branch.properties[name] = output.properties[name]; + delete output.properties[name]; + if (Array.isArray(output.required) && output.required.includes(name)) { + output.required = output.required.filter((entry) => entry !== name); + branch.required = [...(branch.required ?? []), name]; + } + } + + if (Object.keys(branch.properties).length === 0) { + return; + } + if (Object.keys(output.properties).length === 0) { + delete output.properties; + } + if (Array.isArray(output.required) && output.required.length === 0) { + delete output.required; + } + output.allOf = [...output.allOf, branch]; +} + +// The properties an `allOf` member contributes in the source tree, own and +// composed. +function composedProperties(member, sourceRel, schemaCache, depth = 0) { + if (!member || typeof member !== "object" || depth > 16) { + return {}; + } + if (typeof member.$ref === "string") { + const [refPath, fragment = ""] = member.$ref.split("#"); + if (/^[a-z]+:\/\//i.test(refPath)) { + return {}; + } + const targetRel = refPath + ? path.posix.normalize( + path.posix.join(path.posix.dirname(sourceRel), refPath) + ) + : sourceRel; + let target = schemaCache.get(targetRel); + for (const segment of fragment.split("/").filter(Boolean)) { + target = target?.[segment]; + } + return composedProperties(target, targetRel, schemaCache, depth + 1); + } + const properties = {}; + for (const part of member.allOf ?? []) { + Object.assign( + properties, + composedProperties(part, sourceRel, schemaCache, depth + 1) + ); + } + return Object.assign(properties, member.properties); +} + function titleSuffixForOutput(outputRel) { if (outputRel.endsWith(".create_req.json")) { return "Create Request"; diff --git a/src/spec_generated.ts b/src/spec_generated.ts index 355b601..2b28e3a 100644 --- a/src/spec_generated.ts +++ b/src/spec_generated.ts @@ -1010,6 +1010,14 @@ export type VariantSeller = z.infer; export const PurpleSellerSchema = VariantSellerSchema; export type PurpleSeller = VariantSeller; +export const DetailOptionValueSchema = z.object({ + available: z.boolean().optional(), + exists: z.boolean().optional(), + id: z.string().optional(), + label: z.string(), +}); +export type DetailOptionValue = z.infer; + export const SearchRequestPaginationSchema = z.object({ cursor: z.string().optional(), limit: z.number().int().gte(1).optional(), @@ -1501,14 +1509,6 @@ export const MultiDestinationSchema = z .catchall(z.any()); export type MultiDestination = z.infer; -export const DetailOptionValueSchema = z.object({ - available: z.boolean().optional(), - exists: z.boolean().optional(), - id: z.string().optional(), - label: z.string(), -}); -export type DetailOptionValue = z.infer; - export const FulfillmentDestinationCreateRequestSchema = z.object({ id: z.string().optional(), type: z.string().optional(), @@ -2123,6 +2123,12 @@ export const GetProductRequestSchema = z.object({ }); export type GetProductRequest = z.infer; +export const OptionSchema = z.object({ + name: z.string(), + values: z.array(DetailOptionValueSchema).min(1), +}); +export type Option = z.infer; + export const VariantElementSchema = z.object({ availability: AvailabilityClassSchema.optional(), barcodes: z.array(PurpleBarcodeSchema).optional(), @@ -2653,7 +2659,6 @@ export const ProductElementSchema = z.object({ export type ProductElement = z.infer; export const ProductClassSchema = z.object({ - options: z.array(OptionElementSchema).optional(), selected: z.array(SelectedOptionSchema).optional(), categories: z.array(CategoryElementSchema).optional(), description: DescriptionClassSchema, @@ -2662,6 +2667,7 @@ export const ProductClassSchema = z.object({ list_price_range: ListPriceRangeClassSchema.optional(), media: z.array(MediaElementSchema).optional(), metadata: z.record(z.string(), z.any()).optional(), + options: z.array(OptionSchema).optional(), price_range: ListPriceRangeClassSchema, rating: RatingClassSchema.optional(), tags: z.array(z.string()).optional(), diff --git a/tests/project-array-redeclarations.test.js b/tests/project-array-redeclarations.test.js new file mode 100644 index 0000000..ed73b90 --- /dev/null +++ b/tests/project-array-redeclarations.test.js @@ -0,0 +1,226 @@ +// Copyright 2026 UCP Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// quicktype intersects `allOf`, and its intersection of two arrays drops the +// items of the first array it meets. catalog_lookup.json's detail_product +// composes product.json and redeclares `options` so that options[].values[] +// are detail_option_value.json, but the generated GetProductResponseSchema +// kept product.json's plain option value and stripped `available` and +// `exists`. The projection (scripts/project-current-ucp-schemas.mjs) now +// moves such a redeclaration into a trailing allOf branch, the array whose +// items quicktype keeps. + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { execFileSync } = require("node:child_process"); + +const SCRIPT = path.join( + __dirname, + "..", + "scripts", + "project-current-ucp-schemas.mjs" +); + +const { GetProductResponseSchema } = require("./.dist/spec_generated.js"); + +// The smallest root schema set the projection needs to derive its discovery +// envelope; the shapes under test live in shopping/types. +function roots() { + const entity = { + type: "object", + properties: { schema: { type: "string" }, spec: { type: "string" } }, + }; + return { + "schemas/ucp.json": { + $defs: { + version: { type: "string" }, + entity, + base: { + type: "object", + required: ["version"], + properties: { version: { $ref: "#/$defs/version" } }, + }, + business_schema: { + allOf: [{ $ref: "#/$defs/base" }, { type: "object" }], + }, + }, + }, + "schemas/payment_handler.json": { + $defs: { response_schema: entity }, + }, + "schemas/service.json": { + $defs: { + base: { + allOf: [ + {}, + { type: "object", properties: { endpoint: { type: "string" } } }, + ], + }, + response_schema: entity, + }, + }, + "schemas/capability.json": { + $defs: { + base: { + allOf: [ + {}, + { type: "object", properties: { extends: { type: "string" } } }, + ], + }, + response_schema: entity, + }, + }, + }; +} + +const value = { + title: "Value", + type: "object", + required: ["label"], + properties: { label: { type: "string" } }, +}; + +const detailValue = { + title: "Detail Value", + type: "object", + allOf: [{ $ref: "value.json" }], + properties: { available: { type: "boolean" } }, +}; + +const item = { + title: "Item", + type: "object", + required: ["id"], + properties: { + id: { type: "string" }, + tags: { type: "array", items: { type: "string" } }, + values: { type: "array", items: { $ref: "value.json" } }, + }, +}; + +function project(detail) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ucp-project-")); + const source = path.join(dir, "source"); + const output = path.join(dir, "projected"); + try { + const files = { + ...roots(), + "schemas/shopping/types/value.json": value, + "schemas/shopping/types/detail_value.json": detailValue, + "schemas/shopping/types/item.json": item, + "schemas/shopping/types/detail_item.json": detail, + }; + for (const [rel, schema] of Object.entries(files)) { + const abs = path.join(source, rel); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, JSON.stringify(schema, null, 2)); + } + execFileSync("node", [SCRIPT, source, output], { stdio: "pipe" }); + return JSON.parse( + fs.readFileSync( + path.join(output, "schemas/shopping/types/detail_item.json"), + "utf8" + ) + ); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +test("projection moves an array redeclared with narrower items into a trailing allOf branch", () => { + const projected = project({ + title: "Detail Item", + type: "object", + allOf: [{ $ref: "item.json" }], + required: ["values"], + properties: { + selected: { type: "array", items: { type: "string" } }, + values: { type: "array", items: { $ref: "detail_value.json" } }, + }, + }); + assert.deepEqual(projected.allOf, [ + { $ref: "item.json" }, + { + type: "object", + properties: { + values: { type: "array", items: { $ref: "detail_value.json" } }, + }, + required: ["values"], + }, + ]); + assert.deepEqual(Object.keys(projected.properties), ["selected"]); + assert.equal(projected.required, undefined); +}); + +test("projection leaves a redeclaration with the base's own items in place", () => { + const projected = project({ + title: "Detail Item", + type: "object", + allOf: [{ $ref: "item.json" }], + properties: { + tags: { type: "array", items: { type: "string" }, minItems: 1 }, + }, + }); + assert.deepEqual(projected.allOf, [{ $ref: "item.json" }]); + assert.deepEqual(Object.keys(projected.properties), ["tags"]); +}); + +test("GetProductResponseSchema keeps detail option value availability", () => { + const product = { + id: "prod_1", + title: "Runner", + description: { plain: "A shoe." }, + price_range: { + min: { amount: 9900, currency: "USD" }, + max: { amount: 9900, currency: "USD" }, + }, + variants: [ + { + id: "var_1", + title: "Runner, 9", + description: { plain: "Size 9." }, + price: { amount: 9900, currency: "USD" }, + }, + ], + options: [ + { + name: "Size", + values: [ + { label: "9", available: true, exists: true }, + { label: "10", available: false, exists: true }, + ], + }, + ], + }; + const result = GetProductResponseSchema.safeParse({ + ucp: { version: "2026-08-25" }, + product, + }); + assert.ok(result.success, JSON.stringify(result.error?.issues)); + assert.deepEqual( + result.data.product.options[0].values, + product.options[0].values + ); + assert.equal( + GetProductResponseSchema.safeParse({ + ucp: { version: "2026-08-25" }, + product: { ...product, options: [{ name: "Size", values: [] }] }, + }).success, + false, + "values keeps minItems: 1" + ); +});