Skip to content

feat: enforce uniqueItems and contains/minContains/maxContains in generated zod schemas - #37

Merged
damaz91 merged 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:feat/zod-const-uniqueitems-mincontains
Jul 30, 2026
Merged

feat: enforce uniqueItems and contains/minContains/maxContains in generated zod schemas#37
damaz91 merged 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:feat/zod-const-uniqueitems-mincontains

Conversation

@vishkaty

Copy link
Copy Markdown
Contributor

Follow-on to #34 (which closed #33). #34 restored the scalar, length, and items constraints that quicktype's typescript-zod target drops during generation. This restores the array set and cardinality constraints it also drops, using the same post-generation, object-scoped in-place injector - no regeneration against a moving base.

What it enforces

  • uniqueItems -> appended .refine(). One field: Context.eligibility (CheckoutCreateRequestContextSchema, aliased by LookupRequestContextSchema).
  • contains + minContains + maxContains -> appended .superRefine(). The top-level totals of the 8 checkout/cart/order response schemas MUST contain exactly one subtotal and one total entry.

Object-scoping is what keeps this correct: checkout totals (which $refs types/totals.json carrying the contains rules) is constrained, while fulfillment-option, line-item, and adjustment totals (inline arrays without those rules) are left untouched. A test covers that distinction explicitly.

Approach

Same mechanism as #34: constraints keyed by the parent object's full property-name set and applied only when the generated field's base kind agrees (the schema-drift guard), spliced onto the base z.array(...) call via the TS compiler API, wired idempotently into the existing generate_models.sh step. .refine()/.superRefine() are used for the two keywords zod has no native method for. zod is parse-only, so this is a safe, non-breaking addition.

Scope note: const

const was evaluated and has no injectable target in the generated 2026-04-08 schemas: the surviving occurrences are either unified away by quicktype (the message_* type consts collapse into a single MessageType enum), live on subtypes that are not generated (PaymentInstrument.type is correctly unconstrained at the base), rewritten by the compat projection, or sit inside an if/then conditional. The only load-bearing consts are the discriminators inside totals' contains clauses, which this PR enforces via the contains mechanism. So const is intentionally out of scope.

Verification

  • npm run build (strict tsc) clean; npm test 24/24 pass (12 baseline from fix: enforce JSON Schema value constraints in generated zod schemas (#33) #34 + 12 new).
  • Kill-test: reverting the injected spec_generated.ts fails exactly the 7 negative assertions (uniqueItems duplicate + checkout contains rejections + order cardinality); positives still pass. Non-vacuous.
  • Injector is idempotent (re-run reports the fields already constrained, no double-injection).
  • pre-commit (prettier v4.0.0-alpha.8, ShellCheck, codespell) clean.

…erated zod schemas

Follows js-sdk#34 (value-constraint injector, Universal-Commerce-Protocol#33): the same post-generation,
object-scoped in-place injection now also restores the JSON Schema array
set/cardinality constraints quicktype's typescript-zod target drops.

- uniqueItems: true -> an appended .refine() uniqueness check
  (Context.eligibility).
- contains + minContains/maxContains -> an appended .superRefine() cardinality
  check (checkout/cart/order totals MUST hold exactly one subtotal and one
  total entry).

Injection stays object-scoped by the parent object's property-name set, so a
checkout totals (references types/totals.json, which carries the contains
rules) is constrained while a fulfillment-option/line-item totals (an inline
total.json array with no contains) is left untouched. Idempotent and wired
into the existing generate_models.sh injector step. Tests mirror
spec-constraints.test.js and exercise each array constraint via .shape.
@damaz91 damaz91 added status:needs-triage Signal that the PR is ready for human triage status:under-review and removed status:needs-triage Signal that the PR is ready for human triage labels Jul 29, 2026
@damaz91
damaz91 merged commit 958d263 into Universal-Commerce-Protocol:main Jul 30, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: generated zod schemas drop all value/type constraints (money amount is number, not integer ≥ 0)

2 participants