Skip to content

chore: migrate circuit-json schemas to Zod v4#661

Open
DPS0340 wants to merge 1 commit into
tscircuit:mainfrom
DPS0340:chore/zod-v4-compat
Open

chore: migrate circuit-json schemas to Zod v4#661
DPS0340 wants to merge 1 commit into
tscircuit:mainfrom
DPS0340:chore/zod-v4-compat

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 24, 2026

Copy link
Copy Markdown

Summary

  • require zod ^4 as a peer dependency and development dependency
  • migrate record, default, and schema traversal APIs to Zod v4
  • preserve transformed-string defaults with prefault and preserve optional-field absence
  • add coverage for partial supplier records, dynamic records, transformed defaults, and optional defaults

Release coordination

Zod v3 and v4 schema instances cannot be composed. When this PR merges, the repository release workflow will publish circuit-json@0.0.455. Dependent PRs require that exact patch range, then release in this order: circuit-json, footprinter #726, @tscircuit/props #752, then tscircuit/core.

Consumers that inspect raw Zod errors must use the Zod v4 error APIs. Zod v4 also rejects non-finite numbers and unsafe integers where Zod v3 accepted them.

Verification

  • bun test
  • bunx tsc --noEmit
  • bun run build
  • bun run lint:zod
  • bun run check-snake-case
  • npm pack --dry-run

Related to tscircuit/core#2810

@DPS0340

DPS0340 commented Jul 24, 2026

Copy link
Copy Markdown
Author

Hi @seveibar — this CI-green migration is the prerequisite for the Zod v4 work tracked in tscircuit/core#2810. Once merged, the release workflow will publish circuit-json@0.0.455; Footprinter #726 and Props #752 are ready to validate against it. Could you review when you have a moment?

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

@seveibar this one is the head of the Zod v4 chain, and it's the only piece that can land right now — all checks pass here (test, type-check, lint, check-snake-case).

The dependency order matters, so flagging it explicitly:

  1. circuit-json#661 ← this PR. Self-contained, green.
  2. props#752 and footprinter#726 — both currently red, and they cannot go green until chore: migrate circuit-json schemas to Zod v4 #661 is merged and published.

The reason is concrete: the published circuit-json@0.0.455 still ships Zod v3-shaped types. From its dist/index.d.mts:

declare const point: z.ZodObject<{
    x: z.ZodEffects<z.ZodUnion<[z.ZodString, z.ZodNumber]>, number, string | number>;
    ...
}, "strip", z.ZodTypeAny, { ... }, { ... }>;

z.ZodEffects and the 5-parameter ZodObject don't exist in Zod v4, so any downstream package that composes these schemas under v4 fails type-check — regardless of what the downstream PR does. That's exactly the failure on props#752.

So there's nothing I can fix in the downstream PRs until this one is released. If you'd rather I close #752/#726 and reopen them after a circuit-json release, happy to do that — just let me know which you prefer.

@DPS0340
DPS0340 force-pushed the chore/zod-v4-compat branch from 332a83b to 2b31a1a Compare July 25, 2026 06:55
@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Rebased onto current main — this now includes #662 (pcb_packing_error), which landed after this PR was opened. No conflicts, and the new schema needed no changes to work under Zod v4.

Re-verified on the rebased head:

  • bun install with zod@4.4.3
  • tsc --noEmit — clean
  • bun test — 276 pass / 0 fail (107 files)
  • CI here: check-snake-case, lint, test, type-check all green, MERGEABLE / CLEAN

Worth restating why this one is the bottleneck rather than just one of several open PRs: the published circuit-json@0.0.455 is still compiled against Zod v3 and exports ZodEffects in dist/index.d.mts (2800 occurrences). That symbol doesn't exist in Zod v4, so every downstream package that tries to move fails at type-check no matter what its own diff looks like — that's exactly what's red on footprinter #726 and props #752 right now.

Merging and releasing this unblocks that chain in one step; nothing downstream needs code changes, just a re-run. Happy to rebase again if more lands first.

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Adding the measurement that I think makes this PR worth prioritizing — it isn't just a dependency bump.

tscircuit/tscircuit#4077 reports circuit-JSON generation OOMing past ~4.4 GB RSS on a mid-size board, and its heap-snapshot retainer analysis names the cause precisely:

1,245,000  Zod / native_bind nodes total
  713,856  native_bind      retained via  .bound_this
   ~9,952  ZodOptional instances, each retaining ~15 bound methods

Zod v3 eagerly binds .parse, .safeParse, .parseAsync, .spa, .refine, .superRefine, … onto every schema instance at construction. That is the bound_this retainer. Zod v4 does not.

I measured the schema set this package exports, walking every object reachable from all 259 exported schemas and counting bound-method closures. origin/main with its own zod@3.25.76 installed, versus this branch with zod@4.4.3:

origin/main (zod 3) this PR (zod 4)
exported schemas 259 259
bound-method closures reachable 11,976 1,252

−89.5%, and that's just the static schema set — the multiplier in #4077 comes from those closures being retained per parsed element during generation.

I re-ran at depth caps 25 and 45 to rule out the traversal limit hiding something:

main:     depth=25 → 11,976    depth=45 → 11,976
this PR:  depth=25 →  1,252    depth=45 →  1,263

Stable, so the gap is real and not a traversal artifact.

Separately, I profiled where allocation actually goes during generation in core, on a board with heavy inline footprint geometry (50 chips × 1,920 inline silkscreen points): Zod is 37.6% of sampled allocation, and _parse is the top named frame. It is the single largest remaining contributor after #2832.

Why this PR specifically: it's the head of the chain. props#752 and footprinter#726 are red right now only because published circuit-json@0.0.455 still ships v3-shaped types (z.ZodEffects, 5-parameter ZodObject) — neither type exists in v4, so any downstream package composing these schemas under v4 fails type-check no matter what its own diff looks like. This must merge and publish before those two can go green. Nothing downstream can move until it does.

This PR is self-contained and green on its own. Happy to rebase whenever.

cc @seveibar @imrishabh18

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Re-confirming today that this PR is the single blocker for two others, with fresh evidence.

The currently published circuit-json@0.0.455 still ships Zod v3-shaped types:

$ grep -c "ZodEffects" node_modules/circuit-json/dist/index.d.mts
2813

ZodEffects was removed in Zod v4, so any downstream package composing these schemas under v4 fails type-check regardless of its own changes. Verified concretely on props: with 0.0.455 installed, tsc --noEmit produces errors like

lib/common/cadModel.ts(63,62): error TS2345: Argument of type 'true' is not assignable to parameter of type '`missing props ${any}`'

Blocked on this PR: tscircuit/props#752 and tscircuit/footprinter#726. Neither can go green until this merges and publishes. Both are otherwise ready.

To restate the case from my earlier comment, since it's the part that makes this more than a version bump — measured across all 259 exported schemas:

main (zod 3.25.76) this PR (zod 4.4.3)
bound-method closures reachable 11,976 1,252

−89.5%. That's the native_bind / bound_this retainer named in tscircuit/tscircuit#4077's heap analysis, where circuit-JSON generation climbs past 4.4 GB RSS and fails to complete on mid-size boards. Stable at traversal depths 25 and 45, so it isn't an artifact of the walk.

This PR is self-contained, MERGEABLE/CLEAN, and depends on nothing else. Happy to rebase on request.

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