Skip to content

fix: externalise zod / zod-form-data to fix cross-realm refinement loss (#222) - #223

Open
daichiyasunami-vottia wants to merge 1 commit into
blomqma:mainfrom
daichiyasunami-vottia:fix/zod-peer-dependency
Open

fix: externalise zod / zod-form-data to fix cross-realm refinement loss (#222)#223
daichiyasunami-vottia wants to merge 1 commit into
blomqma:mainfrom
daichiyasunami-vottia:fix/zod-peer-dependency

Conversation

@daichiyasunami-vottia

@daichiyasunami-vottia daichiyasunami-vottia commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Fixes #222.

zod (and zod-form-data) were bundled into dist/ by tsup, so all Zod 4 introspection inside this package (e.g. toJSONSchema in getJsonSchema) ran in the bundled Zod's realm while user-provided schemas were built by the host's installed Zod. When the host ran a different minor version, the bundled Zod's JSONSchemaGenerator could not read _zod.bag / _zod.def.checks from the host's schemas, and refinements (.min(), .max(), .email(), .url(), .uuid(), .iso.datetime(), .regex(), .multipleOf(), …) silently disappeared from the generated OpenAPI schema.

Concretely: with next-rest-framework@6.1.1 bundling zod@4.1.13, a host using zod@4.4.3 and writing z.string().min(1) produced { "type": "string" } (no minLength) in the generated OpenAPI.

Changes

  • packages/next-rest-framework/package.json — moves zod (required) and zod-form-data (optional) into peerDependencies with peerDependenciesMeta marking zod-form-data as optional. Both remain in devDependencies for local development and tests.

  • packages/next-rest-framework/tsup.config.ts — marks zod and zod-form-data external so they are no longer inlined into dist/. Bundle size dropped roughly 40 % (≈ 19 000 → 7 900 lines for dist/index.js).

  • packages/next-rest-framework/tests/app-router/route.test.ts — adds a regression test (preserves Zod refinements in generated OpenAPI schema) asserting the full set of common refinements survive getPathsFromRoute:

    Source Expected on the generated schema
    z.string().min(1).max(100) minLength: 1, maxLength: 100
    z.string().regex(/^[a-z-]+$/) pattern: '^[a-z-]+$'
    z.email() / z.url() / z.uuid() / z.iso.datetime() format: 'email' / 'uri' / 'uuid' / 'date-time'
    z.number().min(0).max(150) minimum: 0, maximum: 150
    z.number().int() type: 'integer'
    z.number().multipleOf(5) multipleOf: 5
    z.array(...).min(1).max(5) minItems: 1, maxItems: 5
    z.enum([...]) / z.literal(...) enum: [...]
    z.boolean().default(false) default: false, not in required
    z.string().optional() not in required
  • .github/workflows/ci.yml — adds a new zod-matrix job that runs the package's build + test against Zod 4.1.13 / 4.2.0 / 4.4.3 / latest, so future divergence between host and library realms is detected on every push. The existing Run CI pipeline job is untouched.

Test plan

  • pnpm run build — succeeds; verified dist/index.js no longer contains a copy of Zod (var version = { major: 4, minor: 1, patch: 13 } is gone) and now contains require("zod") / import { z } from "zod".
  • pnpm test — 100 tests pass (was 99; new test added).
  • pnpm test against zod@4.1.13 — passes.
  • pnpm test against zod@4.4.3 — passes (this is the version that previously failed cross-realm).
  • packages/next-rest-framework pnpm run lint (tsc) — clean.
  • prettier --check '**/*.{ts,json}' (the project's lint glob) — clean.

Breaking change

Consumers now need to install zod themselves (and zod-form-data if they use the form-data helpers). For pnpm / npm 7+, this happens automatically via peer dependency resolution; for older clients an explicit install is required.

The smallest migration is:

npm install zod
# or
pnpm add zod

A major version bump (6.x -> 7.0) is appropriate. Happy to draft a migration note for the README/CHANGELOG if you'd like.

Notes

@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

@daichiyasunami-vottia is attempting to deploy a commit to the Markus Blomqvist's projects Team on Vercel.

A member of the Team first needs to authorize it.

)

`zod` and `zod-form-data` were bundled by tsup, so the schema introspection
inside this package (Zod 4's `toJSONSchema`) ran in the bundled Zod's realm
while user schemas were built by the host's installed Zod. When the host
ran a different minor version, `_zod.bag` was unreadable across realms and
refinements (`.min()`, `.max()`, `.email()`, `.regex()`, `.uuid()`, ...)
silently disappeared from the generated OpenAPI schema.

This commit:

- Moves `zod` (required) and `zod-form-data` (optional) from devDependencies
  into peerDependencies so the host install is used at runtime.
- Marks `zod` and `zod-form-data` external in `tsup.config.ts` so they are
  no longer inlined into the bundle.
- Adds a regression test that asserts the full set of common Zod refinements
  (`.min`, `.max`, `.regex`, `.email`, `.url`, `.uuid`, `.iso.datetime`,
  `.int`, `.multipleOf`, `.enum`, `.literal`, `.default`, `.optional`) are
  preserved in the generated OpenAPI schema.
- Adds a CI matrix over multiple Zod 4 minor versions (4.1.13 / 4.2.0 /
  4.4.3 / latest) so future divergence between host and library realms is
  caught early.

BREAKING CHANGE: consumers must install `zod` themselves. `zod-form-data`
remains optional and only needs to be installed when using the form-data
features.
@daichiyasunami-vottia

Copy link
Copy Markdown
Author

Updated to fix the Run CI pipeline failure:

apps/example/package.json did not declare zod as a direct dependency — it was previously satisfied through the library's bundled copy. After the externalisation, tsc over the example's source could no longer resolve a Zod type identity ('ZodArray<...>' is missing 'toJSONSchema', 'with', 'exactOptional', 'apply' etc.) because the example resolved a stale set of types from the workspace tree.

Added "zod": "^4.1.13" to apps/example as a direct dependency to match the new peer requirement of next-rest-framework. This is also a concrete demo of the migration step real consumers will need: any project that didn't explicitly install zod before now needs to.

Locally:

  • pnpm run -r lint — clean (was failing on apps/example before)
  • pnpm run lint (root, including prettier --check '**/*.{ts,json}' and swagger-cli validate ./apps/example/public/openapi.json) — clean
  • pnpm test — 100 / 100 pass
  • Zod matrix locally exercised at 4.1.13 and 4.4.3

The Vercel deploy failures are unrelated — they're "Authorization required to deploy" for outside-contributor PRs.

@daichiyasunami-vottia

Copy link
Copy Markdown
Author

Quick clarification on the "checks failed" state shown on this PR page:

The two failing checks are both Vercel — they fail with Authorization required to deploy because outside-contributor PRs can't deploy to the maintainer's Vercel team. These are unrelated to the code change.

The actual GitHub Actions CI does not appear on this PR page because the upstream's .github/workflows/ci.yml is triggered only on push: (no pull_request: trigger), so it never runs for fork PRs from outside contributors. It does run on the fork on every push, and the latest commit 746dbf3 is fully green:

  • Run CI pipeline (node 18) ✓
  • Run CI pipeline (node 20) ✓
  • Zod 4.1.13 compatibility ✓
  • Zod 4.2.0 compatibility ✓
  • Zod 4.4.3 compatibility ✓
  • Zod latest compatibility ✓

Full run: https://github.com/daichiyasunami-vottia/next-rest-framework/actions/runs/26892303285

If you'd like CI to show up directly on outside-contributor PRs going forward, the smallest change would be to add pull_request: to the workflow trigger. Happy to fold that in here or send a separate one-line PR — whichever you prefer.

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.

Bundled Zod (4.1.13) cannot read schema internals from host Zod, dropping .min() / .max() etc. from generated OpenAPI

1 participant