Skip to content

fix(codegen): keep every union intersection compact under Zod 4.6 - #3408

Merged
bokelley merged 1 commit into
mainfrom
chore/audit-zod-undici-body-parser
Oct 24, 2026
Merged

bokelley merged 1 commit into
mainfrom
chore/audit-zod-undici-body-parser

Conversation

@bokelley

@bokelley bokelley commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #3406. That PR already bumped zod to 4.6.7, so npm audit --audit-level=high is clean on main. It also annotated the Product format declaration so ProductSchema stays under the 1 MiB OpenAPI bound. This PR applies the same fix to every other generated intersection with a union.

Problem

Zod 4.6's toJSONSchema distributes an intersection over a union operand: A & (X | Y) is emitted as (A & X) | (A & Y), which copies A into every branch. Validation is the same, but the rendered schema grows. On main today, comparing every exported schema between Zod 4.4.3 and 4.6.7 (z.toJSONSchema, io: 'input'):

Zod 4.4.3 main (Zod 4.6.7) this PR (Zod 4.6.7)
Total, all 1,501 exports 61.22 MB 70.52 MB 61.25 MB
Exports grown > 20% n/a 66 3 (all < 6 KB)
ProductSchema 719 KB 917 KB 727 KB
PlacementSchema 167 KB 339 KB 167 KB
InlineDeclarationSchema 162 KB 369 KB 163 KB
MCPWebhookPayloadSchema 6.72 MB 7.73 MB 6.74 MB
GeoPostalAreasSchema 3.5 KB 11.9 KB 3.6 KB

Adopters bring their own Zod (peer ^4.1.5), so anyone on 4.6 rendering these schemas for OpenAPI, MCP tools/list or form tooling sees the larger output.

Change

A new generator pass, postProcessUnionIntersectionOperands in scripts/generate-zod-from-ts.ts, adds .describe("Must also satisfy at least one of these alternatives.") to every inline z.union([...]) that is a left or right operand of .and(...). That's 129 sites. Zod declines the distribution when the union carries an annotation, so the rendering stays a single allOf.

  • Generated diff: src/lib/types/schemas.generated.ts differs from main only by those inserted .describe(...) calls. I checked this by stripping them and diffing against main: the files are identical. Regeneration is idempotent.
  • Runtime: parsing is unchanged, because .describe() only adds metadata. Zod < 4.6 renders the same allOf as before, plus the description.
  • Not covered: unions referenced by name, such as .and(VendorPricingSchema). These are the 3 remaining > 20% cases, all under 6 KB. I left them alone because describing a referenced schema clones it and would break adopter $ref registration.
  • The browser single-schema bundle is unchanged at about 100 KB minified / 29 KB gzip, with 744 B of generated validators.

Tests

  • A generator unit test covers left, right and nested operands, already-described unions, union text inside string literals and chained operands. A Zod round-trip test asserts that an annotated union operand renders as one allOf with the shared properties emitted once.
  • Local: build:lib, typecheck, ci:codegen-strict, prettier, and the zod-openapi-portability, schemas-browser, zod-schemas and generator intersection suites pass.

🤖 Generated with Claude Code

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Oct 24, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve.

Clean, well-scoped patch. The single load-bearing change — postProcessUnionIntersectionOperands in scripts/generate-zod-from-ts.ts — annotates inline z.union([...]) operands of .and() intersections so Zod 4.6 stops distributing A & (X|Y) into (A&X)|(A&Y). Operand detection (both left- and right-operand cases) is correctly scoped; string literals are skipped via skipQuotedOrRegexLiteral, and already-annotated/chained unions are deliberately excluded (0 double-.describe() matches in generated output). Validation behavior is metadata-only and unchanged. Test coverage is thorough (all six branches plus exact-count and round-trip-through-z.toJSONSchema assertions). Changeset is present and correctly scoped (dev-only Zod 4.6.7 bump for GHSA-mrhx-c7xc-m3w5, peer range unchanged); bundle-size budgets and docs updated consistently.

No blocking or medium findings. high_risk is false, gated_paths is false, and no author team gate applies. Falls through to row 9.

Zod 4.6's toJSONSchema distributes an intersection over a union operand,
copying the other operand into every branch. #3406 annotated the Product
format declaration; this annotates every inline union operand of a
generated intersection, so the rendered JSON Schema across all exports
returns to the Zod 4.4.3 size (70.5 MB -> 61.2 MB in total). Validation
is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@bokelley
bokelley force-pushed the chore/audit-zod-undici-body-parser branch from 5d1d1bd to a085cfb Compare October 24, 2026 11:15
@bokelley bokelley changed the title fix(deps): bump zod to 4.6.7 and keep union intersections compact fix(codegen): keep every union intersection compact under Zod 4.6 Oct 24, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — clean pass, metadata-only codegen fix.

This PR adds a codegen post-processing step (postProcessUnionIntersectionOperands at scripts/generate-zod-from-ts.ts:35) that appends a .describe(...) note to inline z.union([...]) operands of .and(...), preventing Zod 4.6 from distributing intersections over unions (PlacementSchema 339 KB → 167 KB). Left/right-operand detection and the chained-case / string-literal guards are correct; all six fixtures and the Zod round-trip test match.

Change is metadata-only: parsing, wire shape, enums, tasks, and semantics are unchanged, so pathway-back (DR-0033) is N/A and witness-not-translator is unaffected. Changeset is present and the patch type fits a render-size regression fix.

No critical/high/medium findings. Consistent with the prior approve. No team gate, gated_paths: false, high_risk: false. Falls through to row 9.

@bokelley
bokelley merged commit 80cec20 into main Oct 24, 2026
46 checks passed
@bokelley
bokelley deleted the chore/audit-zod-undici-body-parser branch October 24, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant