-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: add support for Zod v4 while maintaining v3 compatibility #6321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add support for Zod v4 while maintaining v3 compatibility #6321
Conversation
This change adds runtime detection to support both Zod v3 and v4
simultaneously, allowing users to upgrade to Zod v4 without breaking
existing v3 users.
Changes:
- Added compatibility helpers that detect Zod version at runtime
- Updated z.function() calls to use new v4 API when available
- Falls back to v3 API for existing installations
- Updated zod dependency to "^3.25.0 || ^4.0.0" in all packages
Technical Details:
- Detects Zod v4 by checking for '_zod' property on schemas
- Uses z.function({ input: [], output: ... }) for v4
- Uses z.function().args(...).returns(...) for v3
- Minimum v3.25.0 required for compatibility layer support
Affected packages:
- @tanstack/router-generator
- @tanstack/router-plugin
- @tanstack/start-plugin-core
Fixes TanStack#6138
Related to TanStack#4322, TanStack#4092
📝 WalkthroughWalkthroughThis pull request adds cross-version compatibility for Zod v3 and v4 across three packages. Package dependencies are broadened to accept both versions. Runtime version-detection helpers are introduced in config and schema files to dynamically select the appropriate Zod API for function schema generation. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In @packages/start-plugin-core/src/schema.ts:
- Around line 6-31: createFunctionSchema currently uses a fragile per-call
detection via the internal `_zod` property, accepts/returns `any`, and swallows
errors; change it to perform a one-time memoized feature detection (e.g., a
module-level flag like isZodV4Detected computed once by safe feature checks on
`z`), avoid referencing `_zod`, and replace `any` with proper generics and Zod
types for `args`, `returns`, and the function return type (use Zod's exported
types such as ZodTypeAny/ZodTuple/ZodFunction to express signatures). Update the
detection branch to use stable public feature checks on `z` (e.g., presence/type
of `z.function` and expected API shape) and ensure the catch block does not
silently swallow exceptions — surface or log the error and fall back to the v3
path explicitly. Keep the exported symbol `createFunctionSchema(args, returns)`
but change its signature to typed generics and memoized detection to fix
performance, safety, and error handling.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
packages/router-generator/package.jsonpackages/router-generator/src/config.tspackages/router-plugin/package.jsonpackages/router-plugin/src/core/config.tspackages/start-plugin-core/package.jsonpackages/start-plugin-core/src/schema.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/package.json
📄 CodeRabbit inference engine (AGENTS.md)
Use workspace protocol
workspace:*for internal dependencies in package.json files
Files:
packages/start-plugin-core/package.jsonpackages/router-plugin/package.jsonpackages/router-generator/package.json
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use TypeScript strict mode with extensive type safety for all code
Files:
packages/router-plugin/src/core/config.tspackages/start-plugin-core/src/schema.tspackages/router-generator/src/config.ts
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Implement ESLint rules for router best practices using the ESLint plugin router
Files:
packages/router-plugin/src/core/config.tspackages/start-plugin-core/src/schema.tspackages/router-generator/src/config.ts
🧠 Learnings (3)
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.
Applied to files:
packages/router-plugin/src/core/config.tspackages/router-generator/src/config.ts
📚 Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{js,ts,tsx} : Implement ESLint rules for router best practices using the ESLint plugin router
Applied to files:
packages/start-plugin-core/src/schema.ts
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.
Applied to files:
packages/router-generator/src/config.ts
🔇 Additional comments (7)
packages/router-generator/package.json (1)
75-75: LGTM - Zod version range updated for cross-version compatibility.The dependency update aligns with the PR's objective to support both Zod v3 (>=3.25.0) and v4 simultaneously.
packages/router-plugin/package.json (1)
120-120: LGTM - Consistent cross-version support.The dependency update matches the other packages in this PR, enabling Zod v3/v4 compatibility.
packages/router-generator/src/config.ts (1)
66-66: Usage of createFunctionSchema appears correct.The helper is properly integrated into the schema definition. However, ensure the detection mechanism flagged above is addressed before merging.
packages/start-plugin-core/src/schema.ts (3)
124-130: LGTM - Correct usage of compatibility helper.The
onSuccesscallback schema properly uses the helper with typed input and output schemas. The logic is sound, assuming the helper's detection mechanism is improved.
186-192: LGTM - Correct usage for generateFunctionId.The function schema correctly specifies input parameters (filename, functionName) and output type (optional string). The implementation aligns with the intended API.
208-208: LGTM - Filter callback schema is correctly defined.The schema properly validates the filter function signature using the compatibility helper.
packages/start-plugin-core/package.json (1)
83-83: Zod v3.25.0 minimum version is justified.The minimum version 3.25.0 is necessary. Zod 3.24.x contained a types-related breaking change (type recursion/compatibility issue, #4027), and 3.25.0 introduces the versioning strategy that enables Zod 4 support via subpath imports ("zod/v4") while maintaining v3 stability. This aligns with the PR objective of supporting both Zod v3 and v4. No breaking changes are introduced by 3.25.0 itself.
| // Helper to create a function schema compatible with both Zod v3 and v4 | ||
| function createFunctionSchema() { | ||
| // Try Zod v4 syntax first | ||
| if (typeof (z as any).function === 'function') { | ||
| try { | ||
| // Check if this is Zod v4 by testing for the new API | ||
| const testSchema = z.string() | ||
| if ('_zod' in testSchema) { | ||
| // Zod v4: use new function API | ||
| return (z as any).function({ | ||
| input: [], | ||
| output: z.array(z.string()), | ||
| }) | ||
| } | ||
| } catch (e) { | ||
| // Fall through to v3 | ||
| } | ||
| } | ||
|
|
||
| // Zod v3: use old function API | ||
| return (z as any).function().returns(z.array(z.string())) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and read the actual file
find . -name "config.ts" -path "*/router-generator/*" | head -5Repository: TanStack/router
Length of output: 161
🏁 Script executed:
# Also check for package.json to understand dependencies
find . -name "package.json" -path "*/router-generator/*" | head -3Repository: TanStack/router
Length of output: 99
🏁 Script executed:
# Look for tsconfig to verify strict mode
find . -name "tsconfig.json" | head -5Repository: TanStack/router
Length of output: 314
🌐 Web query:
Zod v4 migration guide version detection API changes
💡 Result:
Summary of Zod v4 "version detection" / API changes (concise):
-
Subpath/versioning: Zod v4 is published under "zod/v4" (and v3 under "zod/v3") so consumers and library authors should use subpath imports to target a specific major version; later the package root ("zod") was switched to v4. This is the recommended way for libraries to detect/support v3 vs v4. [1]
-
Public API renames & reorganizations that affect detection by type/shape:
- Many core types and issue types moved/renamed (e.g., Zod issue types now live under z.core.$ZodIssue*). This means runtime/type checks relying on old names (or instanceof of old classes) may break. [2]
- Transforms, preprocessors, branded types, and refinements were reworked (ZodTransform / ZodPipe, checks concept), so code that inspected schema classes must be updated. [2][3]
-
Error/customization API changed: the old message/invalid_type_error/required_error/errorMap semantics were unified into a single error parameter/function, so libraries that detected Zod behavior by reading those options must adapt. [3]
-
Guidance for library authors: support both versions by importing the specific subpath(s) and feature-detecting differences (e.g., presence of z.core namespaces / new methods) rather than relying on package.json peer-dep resolution. The Zod docs explain this migration approach. [1]
References
- Zod versioning & subpath migration guidance. [1]
- Zod v4 changelog / breaking changes (issues, checks, transforms). [2]
- Introducing Zod 4 (error API and examples). [3]
If you want, I can: (a) show small code snippets for feature-detection (runtime checks) to support both v3 and v4, or (b) scan your codebase for patterns that will break and suggest exact fixes.
🏁 Script executed:
# Read the actual config.ts file
cat -n ./packages/router-generator/src/config.ts | head -50Repository: TanStack/router
Length of output: 2191
🏁 Script executed:
# Check router-generator package.json for Zod dependency
cat ./packages/router-generator/package.json | jq '.dependencies, .peerDependencies, .devDependencies | select(. != null) | with_entries(select(.key | contains("zod")))'Repository: TanStack/router
Length of output: 94
🏁 Script executed:
# Find tsconfig.json closest to router-generator
cat ./packages/router-generator/tsconfig.json 2>/dev/null || cat ./tsconfig.json 2>/dev/null | head -30Repository: TanStack/router
Length of output: 484
🏁 Script executed:
# Search for usages of createFunctionSchema to see if it's cached or called frequently
rg -n "createFunctionSchema" ./packages/router-generator/src/Repository: TanStack/router
Length of output: 211
🏁 Script executed:
# Search for tests of this function
rg -n "createFunctionSchema" ./packages/router-generator/ --type ts --type tsxRepository: TanStack/router
Length of output: 86
🏁 Script executed:
# Check root tsconfig.json for strict mode setting
cat ./tsconfig.json | jq '.compilerOptions.strict'Repository: TanStack/router
Length of output: 63
🏁 Script executed:
# Check context around line 66 where createFunctionSchema is used
sed -n '60,80p' ./packages/router-generator/src/config.tsRepository: TanStack/router
Length of output: 759
🏁 Script executed:
# Search for any version detection patterns in the codebase
rg -n "zod.*version|Zod.*v[34]|_zod|subpath.*zod" ./packages/router-generator/src/ --type tsRepository: TanStack/router
Length of output: 596
🏁 Script executed:
# Check if Zod v4 actually has the _zod property by searching package
cat node_modules/zod/package.json 2>/dev/null | jq '.version' || echo "Zod not in node_modules or not accessible"Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Let's verify if _zod is actually an internal property in Zod
# Check Zod's type definitions
cat node_modules/zod/lib/types.d.ts 2>/dev/null | head -100 || echo "Type defs not accessible"Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Search for _zod in Zod source to understand if it's internal
rg -n "readonly _zod|_zod:|_zod =" node_modules/zod/lib/ 2>/dev/null | head -20 || echo "Search failed or no results"Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Check Zod's official docs or package for version detection guidance
cat node_modules/zod/README.md 2>/dev/null | grep -A10 -i "version\|v3\|v4" | head -40 || echo "README not found"Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Test if the detection logic actually works by checking what properties exist in Zod v4
python3 << 'EOF'
import subprocess
import json
# Get Zod version from node_modules
try:
result = subprocess.run(['cat', 'node_modules/zod/package.json'], capture_output=True, text=True)
pkg = json.loads(result.stdout)
print(f"Zod version: {pkg.get('version', 'unknown')}")
except Exception as e:
print(f"Error: {e}")
EOFRepository: TanStack/router
Length of output: 145
🏁 Script executed:
# Search for how other projects or the Zod repo suggest version detection
rg -i "detect.*version|version.*detect|subpath" ./packages/router-generator/ --type tsRepository: TanStack/router
Length of output: 41
🏁 Script executed:
# Check if there are any tests that verify the detection logic works
find ./packages/router-generator -name "*.test.*" -o -name "*.spec.*" | head -10Repository: TanStack/router
Length of output: 273
🏁 Script executed:
# Look for any existing issues or comments about Zod v3/v4 compatibility
rg -i "zod.*compat|v3.*v4|version.*detect" ./packages/ --type ts --type tsx | head -20Repository: TanStack/router
Length of output: 86
🏁 Script executed:
# Check the actual Zod exports to understand the API
# We'll look at what the z object actually exposes in v3 vs v4
ls -la node_modules/zod/lib/ 2>/dev/null | head -20 || echo "node_modules not accessible in current context"Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Try to find Zod documentation or changelog in the repo
find . -name "*zod*" -type f \( -name "*.md" -o -name "CHANGELOG*" \) 2>/dev/null | head -10Repository: TanStack/router
Length of output: 41
🏁 Script executed:
# Check the config test file for any tests related to createFunctionSchema
cat ./packages/router-generator/tests/config.test.tsRepository: TanStack/router
Length of output: 882
🏁 Script executed:
# Look at what Zod v3 and v4 documentation say about the function schema API
# Search the web for specific API differences
web_searchRepository: TanStack/router
Length of output: 109
🏁 Script executed:
# Check if the function schema created actually works correctly
# Look for any error handling or validation around it
rg -B5 -A10 "createFunctionSchema" ./packages/router-generator/src/config.tsRepository: TanStack/router
Length of output: 1154
Use official Zod version detection strategy instead of undocumented internal properties.
The detection mechanism relies on the _zod property, which is not part of Zod's documented public API. Official Zod v4 migration guidance recommends using subpath imports (zod/v3 and zod/v4) or feature-detection via try/catch on the actual API signature.
Current issues:
- API stability risk: The
_zodproperty is undocumented and not guaranteed across minor/patch releases. - Type safety: Multiple
anycasts violate TypeScript strict mode (enabled in this project). - Unnecessary runtime overhead: Creates a test schema during schema initialization.
- Silent failures: Errors are swallowed without logging, making debugging harder if detection fails.
Replace with the simpler try/catch approach on the actual API signature:
Recommended fix
function createFunctionSchema() {
- // Try Zod v4 syntax first
- if (typeof (z as any).function === 'function') {
- try {
- // Check if this is Zod v4 by testing for the new API
- const testSchema = z.string()
- if ('_zod' in testSchema) {
- // Zod v4: use new function API
- return (z as any).function({
- input: [],
- output: z.array(z.string()),
- })
- }
- } catch (e) {
- // Fall through to v3
- }
- }
-
- // Zod v3: use old function API
- return (z as any).function().returns(z.array(z.string()))
+ try {
+ // Try v4 API - if it accepts an object config, it's v4
+ return (z as any).function({
+ input: [],
+ output: z.array(z.string()),
+ })
+ } catch {
+ // Fall back to v3 chained API
+ return (z as any).function().returns(z.array(z.string()))
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Helper to create a function schema compatible with both Zod v3 and v4 | |
| function createFunctionSchema() { | |
| // Try Zod v4 syntax first | |
| if (typeof (z as any).function === 'function') { | |
| try { | |
| // Check if this is Zod v4 by testing for the new API | |
| const testSchema = z.string() | |
| if ('_zod' in testSchema) { | |
| // Zod v4: use new function API | |
| return (z as any).function({ | |
| input: [], | |
| output: z.array(z.string()), | |
| }) | |
| } | |
| } catch (e) { | |
| // Fall through to v3 | |
| } | |
| } | |
| // Zod v3: use old function API | |
| return (z as any).function().returns(z.array(z.string())) | |
| } | |
| // Helper to create a function schema compatible with both Zod v3 and v4 | |
| function createFunctionSchema() { | |
| try { | |
| // Try v4 API - if it accepts an object config, it's v4 | |
| return (z as any).function({ | |
| input: [], | |
| output: z.array(z.string()), | |
| }) | |
| } catch { | |
| // Fall back to v3 chained API | |
| return (z as any).function().returns(z.array(z.string())) | |
| } | |
| } |
| // Helper to create a function schema compatible with both Zod v3 and v4 | ||
| function createGenericFunctionSchema(): any { | ||
| // Try Zod v4 syntax first | ||
| if (typeof (z as any).function === 'function') { | ||
| try { | ||
| // Check if this is Zod v4 by testing for the new API | ||
| const testSchema = z.string() | ||
| if ('_zod' in testSchema) { | ||
| // Zod v4: use new function API with any input/output | ||
| return (z as any).function({ | ||
| input: [z.any()], | ||
| output: z.any(), | ||
| }) | ||
| } | ||
| } catch (e) { | ||
| // Fall through to v3 | ||
| } | ||
| } | ||
|
|
||
| // Zod v3: use old function API | ||
| return (z as any).function() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/router-plugin/src/core/config.ts | head -100Repository: TanStack/router
Length of output: 3699
🌐 Web query:
Zod v3 vs v4 differences and how to detect version at runtime
💡 Result:
Summary — main v3 → v4 differences
- Packaging/versioning: v4 was released using subpaths (import * as z from "zod/v4") while "zod" root stayed v3 during transition; later zod@4 became the package root. [1][2]
- Error API and error shape: unified error customization (message/required_error/invalid_type_error → single error param), renamed/merged issue types and changes to ZodError helpers (e.g., .format/.flatten replaced by z.treeifyError and other tools). [3][5]
- Refinements / transforms / checks: v4 reworks refinements (checks) and introduces separate transform types/behavior (ZodTransform); transform/refine ordering/behavior changed in some cases. [6][3]
- API renames/option changes: many validators/options renamed (e.g., message → error), some helper methods removed or replaced; expect breaking signatures for adding custom errors. [5][4]
- Internal shape and types: many internal types/namespaces changed, so libraries depending on Zod internals often break. [3][7]
Sources: Zod docs & changelog, community migration notes. [1][3][5]
How to detect Zod v3 vs v4 at runtime
(Choose one depending on environment—Node, bundler, or browser)
- Check package.json version (Node / server-side)
- const v = require('zod/package.json').version
- if v.startsWith('4') → v4 else startsWith('3') → v3.
(Works when package.json is available at runtime). [1]
- Feature-detection (safe in bundlers & runtime)
- Check for v4-specific exports/APIs. Examples that indicate v4:
- typeof z.treeifyError === 'function' // v4 error helpers. [3]
- typeof z.toJSONSchema === 'function' // v4 added JSON Schema helpers. [6]
- typeof (z as any).ZodTransform !== 'undefined' or 'check' on schema prototype — presence of ZodTransform/check indicates v4. [6][5]
If any of those exist, treat as v4; otherwise treat as v3.
- Import-path detection (at build-time / when you control imports)
- If your code imports from "zod/v4" you know it’s v4; "zod/v3" or root "zod" may be v3 during transition. [1]
Notes / practical tips
- Prefer feature-detection (2) for runtime-bundled code (works even if package root changed). [6][1]
- If you need binary compatibility for libraries, use subpath imports (zod/v3 and zod/v4) per Zod’s official guidance to support both in one package. [1]
- For robust multi-version support consider using or inspecting compatibility libraries (example: @coji/zodix) which detect version via feature checks. [8]
References
- Zod versioning & subpath strategy. [1]
- Zod v4 changelog / migration guide (breaking changes, error API). [3][5]
- Notes on transforms / refinement behavior and v4 runtime issues. [6][3]
- Community compatibility / tools (examples). [8]
If you want, I can give a tiny code snippet for each detection method matching your runtime (Node, browser, bundler).
🌐 Web query:
Zod official documentation for function schema API between v3 and v4
💡 Result:
Short summary — function-schema API changed from v3 → v4
-
Zod v3: z.function(...) returned a Zod schema (function "factory") you could use as a schema type. Common forms: z.function(argsTuple, returnSchema) or z.function().args(...).returns(...). It could be used like other schemas (e.g., .optional()). [1][2]
-
Zod v4: z.function(...) no longer returns a Zod schema — it is a standalone "function factory" for building runtime-validated functions. The API requires you define input/output up front (input/output shape on the factory) and you cannot treat the result as an ordinary schema (so patterns like using it inside object schemas or calling .optional() no longer work). Workarounds include wrapping the factory with a custom schema (or using $ZodFunction metadata) when you need a schema-like value. [1][2][3]
References
- Zod v4 changelog / migration guide (explains function-schema redesign). [1]
- GitHub discussion: "v4: what is the replacement for using z.function as a schema?" (design notes & examples). [2]
- zod2md / community docs: suggested workaround to convert a v4 function factory into a schema-like value. [3]
Would you like concrete code examples showing the v3 usage, the direct v4 factory usage, and the recommended v4 workaround?
🏁 Script executed:
# Check if there are any other version detection patterns in the codebase
rg "version|v3|v4" packages/router-plugin/src/core/config.ts -A 2 -B 2
# Look for how Zod is used elsewhere to understand compatibility patterns
fd "\.ts$" packages/router-plugin/src -type f | head -10Repository: TanStack/router
Length of output: 902
🌐 Web query:
Zod v4 function factory schema workaround how to use z.function in object schema
💡 Result:
Short answer: you can't embed z.function(...) directly inside a z.object in Zod v4 because z.function now returns a "function factory" (not a Zod schema). Use a plain runtime function-check schema (z.custom / z.instanceof / z.unknown().refine) for objects, and keep z.function for separately creating/validating implemented functions. (github.com)
Examples:
-
Simple runtime function field:
-
Using z.custom (keeps TypeScript typing via generics)
const Fn = z.custom<(...a: any[]) => any>(v => typeof v === 'function');
const Obj = z.object({ evaluate: Fn }); -
Using instanceof:
const Obj2 = z.object({ evaluate: z.instanceof(Function) }); -
Using refine on unknown:
const Obj3 = z.object({ evaluate: z.unknown().refine(v => typeof v === 'function', { message: 'must be function' }) });
-
-
If you want Zod-validated call semantics for a function, define that with z.function separately and use .implement() (or .validate()) to produce a runtime-validated function; but you cannot place that factory value as a nested schema inside z.object. Use the runtime-check field above and pair it with a separately-created validated function when needed. (odocs-zod.vercel.app)
If you want, tell me which pattern you prefer and I’ll produce a copy-paste example tailored to your types (sync/async, args/return).
Citations:
- 1: v4: what is the replacement for using z.function as a schema? colinhacks/zod#4143
- 2: https://odocs-zod.vercel.app/?utm_source=openai
Rewrite for Zod v3/v4 compatibility and address type safety violations.
The current implementation has a critical architectural flaw: Zod v4's z.function() returns a function factory (not a schema) and cannot be used directly in object schemas with methods like .optional(). The try-catch silently masks this incompatibility.
Additionally:
-
Version detection is unreliable: The
_zodproperty check is an internal/unstable Zod API. Use feature detection instead (e.g., check fortypeof z.treeifyError === 'function'ortypeof z.toJSONSchema === 'function'). -
v4 incompatibility: For Zod v4, use
z.custom<(...args: any[]) => any>(v => typeof v === 'function')orz.unknown().refine(v => typeof v === 'function', ...)instead of the function factory. -
Type safety: Replace
anyreturn type and type casts with proper union types or version-specific schema definitions. -
Silent error handling: The try-catch (lines 27–29) hides real errors. Either remove it once detection is reliable, or log errors for debugging.
-
Redundant check: The
typeof (z as any).function === 'function'check on line 16 is unnecessary sincez.functionexists in both versions.
Consider separating schemas by version or using a workaround that's compatible with both APIs.
| // Helper to create a function schema compatible with both Zod v3 and v4 | ||
| function createFunctionSchema( | ||
| args: any, | ||
| returns: any, | ||
| ): any { | ||
| // Try Zod v4 syntax first | ||
| if (typeof (z as any).function === 'function') { | ||
| try { | ||
| // Check if this is Zod v4 by testing for the new API | ||
| const testSchema = z.string() | ||
| if ('_zod' in testSchema) { | ||
| // Zod v4: use new function API | ||
| return (z as any).function({ | ||
| input: Array.isArray(args) ? args : [args], | ||
| output: returns, | ||
| }) | ||
| } | ||
| } catch (e) { | ||
| // Fall through to v3 | ||
| } | ||
| } | ||
|
|
||
| // Zod v3: use old function API | ||
| const argsArray = Array.isArray(args) ? args : [args] | ||
| return (z as any).function().args(...argsArray).returns(returns) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same fragile detection mechanism and type safety concerns.
This helper has identical issues to the one in packages/router-generator/src/config.ts:
- Internal API dependency: Relies on undocumented
_zodproperty for version detection. - Type safety violation: Parameters and return type use
any, which contradicts the coding guideline requiring "TypeScript strict mode with extensive type safety." - Performance: Detection logic executes on every call without memoization.
- Error handling: Silent catch block could mask real errors.
🔎 Proposed improvement with better type safety
+// Cache the Zod version detection
+let cachedIsZodV4: boolean | undefined
+
+function isZodV4(): boolean {
+ if (cachedIsZodV4 !== undefined) return cachedIsZodV4
+
+ try {
+ // Test if v4 API is available
+ const testFn = (z as any).function({ input: [], output: z.string() })
+ cachedIsZodV4 = testFn !== undefined
+ } catch {
+ cachedIsZodV4 = false
+ }
+ return cachedIsZodV4
+}
+
-function createFunctionSchema(
- args: any,
- returns: any,
-): any {
+function createFunctionSchema<TArgs extends z.ZodTypeAny | z.ZodTypeAny[], TReturn extends z.ZodTypeAny>(
+ args: TArgs,
+ returns: TReturn,
+): z.ZodType<(...args: any[]) => any> {
- // Try Zod v4 syntax first
- if (typeof (z as any).function === 'function') {
- try {
- // Check if this is Zod v4 by testing for the new API
- const testSchema = z.string()
- if ('_zod' in testSchema) {
- // Zod v4: use new function API
- return (z as any).function({
- input: Array.isArray(args) ? args : [args],
- output: returns,
- })
- }
- } catch (e) {
- // Fall through to v3
- }
- }
-
- // Zod v3: use old function API
+ if (isZodV4()) {
+ return (z as any).function({
+ input: Array.isArray(args) ? args : [args],
+ output: returns,
+ })
+ }
+
const argsArray = Array.isArray(args) ? args : [args]
return (z as any).function().args(...argsArray).returns(returns)
}As per coding guidelines requiring TypeScript strict mode with extensive type safety.
🤖 Prompt for AI Agents
In @packages/start-plugin-core/src/schema.ts around lines 6 - 31,
createFunctionSchema currently uses a fragile per-call detection via the
internal `_zod` property, accepts/returns `any`, and swallows errors; change it
to perform a one-time memoized feature detection (e.g., a module-level flag like
isZodV4Detected computed once by safe feature checks on `z`), avoid referencing
`_zod`, and replace `any` with proper generics and Zod types for `args`,
`returns`, and the function return type (use Zod's exported types such as
ZodTypeAny/ZodTuple/ZodFunction to express signatures). Update the detection
branch to use stable public feature checks on `z` (e.g., presence/type of
`z.function` and expected API shape) and ensure the catch block does not
silently swallow exceptions — surface or log the error and fall back to the v3
path explicitly. Keep the exported symbol `createFunctionSchema(args, returns)`
but change its signature to typed generics and memoized detection to fix
performance, safety, and error handling.
This change adds runtime detection to support both Zod v3 and v4 simultaneously, allowing users to upgrade to Zod v4 without breaking existing v3 users.
Changes:
Technical Details:
Affected packages:
Fixes #6138
Related to #4322, #4092
I verified this using a pnpm patch on zephyr cloud's main app. @swalker326
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.