Skip to content

Follow zod guidance on supporting v4 - for users incrementally migrating from v3#158

Closed
Sam152 wants to merge 2 commits into
mcampa:masterfrom
Sam152:gentler-migration-from-v3
Closed

Follow zod guidance on supporting v4 - for users incrementally migrating from v3#158
Sam152 wants to merge 2 commits into
mcampa:masterfrom
Sam152:gentler-migration-from-v3

Conversation

@Sam152
Copy link
Copy Markdown

@Sam152 Sam152 commented May 19, 2026

This PR does not add support for zod v3, but instead makes the integration of this package gentler on folks with large v3 codebases, trying to incrementally adopt v4.

In the zod docs for package authors it describes the following:

To support Zod 4, update the minimum version for your "zod" peer dependency to ^3.25.0 || ^4.0.0.
...
Do not import from these subpaths:
"zod" — ❌ In 3.x releases, this exports Zod 3. In 4.x releases, this will export Zod 4. Use the permalinks instead.

Importing from zod/v4 acts as a permalink to the v4 version, essentially allowing either v3 or v4 to be installed - but maintaining the direct dependencies on actual v4 code.

@Sam152 Sam152 marked this pull request as ready for review May 19, 2026 08:43
@mcampa
Copy link
Copy Markdown
Owner

mcampa commented May 22, 2026

Investigated the CI failure — it's an OOM in tsc --noEmit (not Jest):

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Reproduced locally: pnpm exec tsc --noEmit runs ~37s then SIGABRTs.

Root cause

This PR switches src/ imports from 'zod''zod/v4', but the test files (test/generator.test.ts, test/adapters/*.test.ts) still import { z } from 'zod'. TypeScript treats ZodObject from the two specifiers as nominally distinct (even though they resolve to the same runtime class in zod@4.0.14). When those mixed types flow through tRPC's deeply-generic Procedure/Router/RouterRecord machinery via OpenApiMeta, structural comparison explodes — producing TS2589 "Type instantiation is excessively deep" at one or two files, and OOMing the heap when all six src files compound it.

Bisect

State Result
All 6 src files at zod/v4, tests at zod OOM
Only src/types.ts at zod/v4, rest at zod 25s, 3× TS2589 errors (no OOM)
All src/ and all test/ at zod/v4 ✅ tsc clean in 2.3s

Suggested fix

Update the 8 test files to also import from 'zod/v4':

sed -i '' "s|from 'zod'|from 'zod/v4'|g" test/**/*.ts

After that, one real v4 API issue surfaces at test/generator.test.ts:3168.meta({ examples: { Lily: ..., John: ... } }) needs to become an array in v4. That's a genuine v4 semantic change, unrelated to the OOM.


Side note: master is also currently red, but for a different reason — #157 introduced OpenApiContactObject that fails TS2322 against zod-openapi's InfoObject (missing x-${string} index signature). Worth fixing separately.

@mcampa
Copy link
Copy Markdown
Owner

mcampa commented May 22, 2026

Hi @cpimhoff — thanks for the work on this! I've opened #161 which builds on your PR:

Closing this in favor of #161 — full credit to you for the original migration. 🙏

@mcampa mcampa closed this May 22, 2026
mcampa added a commit that referenced this pull request May 22, 2026
* Progress

* Progress

* test: align test files with zod/v4 imports and fix examples typing

Test files were importing z from 'zod' while src/ now imports from
'zod/v4'. TypeScript treats those as nominally distinct types, and the
mismatch causes type instantiation to explode through tRPC's generic
machinery, OOMing tsc.

Also cast the named-object examples in the multipleExamples test to
preserve existing runtime behavior — zod-openapi v5 types meta examples
as unknown[], but the runtime still accepts the v3-style record form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: split type-only zod/v4 imports and drop unused ZodTypeAny

Addresses Copilot review on #161:

- ZodSchema, ZodObject, ZodAny, ZodRawShape, ZodType, ZodError are only
  used in type positions — move them to `import type` so stricter
  TypeScript settings (verbatimModuleSyntax, preserveValueImports)
  don't emit unused value imports.
- ZodTypeAny was imported in node-http/core.ts but never referenced —
  remove it.
- Replace the broad `as never` cast on the multipleExamples test with
  `as unknown as unknown[]` plus a comment explaining why the runtime
  still accepts the v3-style named-record form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Sam <Sam152@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mcampa mcampa mentioned this pull request May 22, 2026
4 tasks
@cpimhoff
Copy link
Copy Markdown

I think you got me mixed up with @Sam152 😄 but I'm certainly happy to see this change!

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.

3 participants