-
-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,29 @@ import type { | |
| } from '@tanstack/router-core' | ||
| import type { CodeSplitGroupings } from './constants' | ||
|
|
||
| // 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() | ||
| } | ||
|
Comment on lines
+13
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
💡 Result: Summary — main v3 → v4 differences
Sources: Zod docs & changelog, community migration notes. [1][3][5] How to detect Zod v3 vs v4 at runtime
Notes / practical tips
References
If you want, I can give a tiny code snippet for each detection method matching your runtime (Node, browser, bundler). 🌐 Web query:
💡 Result: Short summary — function-schema API changed from v3 → v4
References
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:
💡 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:
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:
Rewrite for Zod v3/v4 compatibility and address type safety violations. The current implementation has a critical architectural flaw: Zod v4's Additionally:
Consider separating schemas by version or using a workaround that's compatible with both APIs. |
||
|
|
||
| export const splitGroupingsSchema = z | ||
| .array( | ||
| z.array( | ||
|
|
@@ -73,7 +96,7 @@ export type CodeSplittingOptions = { | |
| } | ||
|
|
||
| const codeSplittingOptionsSchema = z.object({ | ||
| splitBehavior: z.function().optional(), | ||
| splitBehavior: createGenericFunctionSchema().optional(), | ||
| defaultBehavior: splitGroupingsSchema.optional(), | ||
| deleteNodes: z.array(z.string()).optional(), | ||
| addHmr: z.boolean().optional().default(true), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,33 @@ import { z } from 'zod' | |
| import { configSchema, getConfig } from '@tanstack/router-plugin' | ||
| import type { TanStackStartVitePluginCoreOptions } from './types' | ||
|
|
||
| // 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) | ||
| } | ||
|
Comment on lines
+6
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
🔎 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 |
||
|
|
||
| const tsrConfig = configSchema | ||
| .omit({ autoCodeSplitting: true, target: true, verboseFileRoutes: true }) | ||
| .partial() | ||
|
|
@@ -94,16 +121,13 @@ const pagePrerenderOptionsSchema = z.object({ | |
| crawlLinks: z.boolean().optional(), | ||
| retryCount: z.number().optional(), | ||
| retryDelay: z.number().optional(), | ||
| onSuccess: z | ||
| .function() | ||
| .args( | ||
| z.object({ | ||
| page: pageBaseSchema, | ||
| html: z.string(), | ||
| }), | ||
| ) | ||
| .returns(z.any()) | ||
| .optional(), | ||
| onSuccess: createFunctionSchema( | ||
| z.object({ | ||
| page: pageBaseSchema, | ||
| html: z.string(), | ||
| }), | ||
| z.any(), | ||
| ).optional(), | ||
| headers: z.record(z.string(), z.string()).optional(), | ||
| }) | ||
|
|
||
|
|
@@ -159,16 +183,13 @@ const tanstackStartOptionsSchema = z | |
| serverFns: z | ||
| .object({ | ||
| base: z.string().optional().default('/_serverFn'), | ||
| generateFunctionId: z | ||
| .function() | ||
| .args( | ||
| z.object({ | ||
| filename: z.string(), | ||
| functionName: z.string(), | ||
| }), | ||
| ) | ||
| .returns(z.string().optional()) | ||
| .optional(), | ||
| generateFunctionId: createFunctionSchema( | ||
| z.object({ | ||
| filename: z.string(), | ||
| functionName: z.string(), | ||
| }), | ||
| z.string().optional(), | ||
| ).optional(), | ||
| }) | ||
| .optional() | ||
| .default({}), | ||
|
|
@@ -184,7 +205,7 @@ const tanstackStartOptionsSchema = z | |
| .object({ | ||
| enabled: z.boolean().optional(), | ||
| concurrency: z.number().optional(), | ||
| filter: z.function().args(pageSchema).returns(z.any()).optional(), | ||
| filter: createFunctionSchema(pageSchema, z.any()).optional(), | ||
| failOnError: z.boolean().optional(), | ||
| autoStaticPathsDiscovery: z.boolean().optional(), | ||
| maxRedirects: z.number().min(0).optional(), | ||
|
|
||
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:
Repository: TanStack/router
Length of output: 161
🏁 Script executed:
Repository: TanStack/router
Length of output: 99
🏁 Script executed:
Repository: 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:
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
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:
Repository: TanStack/router
Length of output: 2191
🏁 Script executed:
Repository: TanStack/router
Length of output: 94
🏁 Script executed:
Repository: TanStack/router
Length of output: 484
🏁 Script executed:
Repository: TanStack/router
Length of output: 211
🏁 Script executed:
Repository: TanStack/router
Length of output: 86
🏁 Script executed:
Repository: TanStack/router
Length of output: 63
🏁 Script executed:
Repository: TanStack/router
Length of output: 759
🏁 Script executed:
Repository: TanStack/router
Length of output: 596
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: TanStack/router
Length of output: 145
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: TanStack/router
Length of output: 273
🏁 Script executed:
Repository: TanStack/router
Length of output: 86
🏁 Script executed:
Repository: TanStack/router
Length of output: 41
🏁 Script executed:
Repository: 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:
Repository: TanStack/router
Length of output: 109
🏁 Script executed:
Repository: TanStack/router
Length of output: 1154
Use official Zod version detection strategy instead of undocumented internal properties.
The detection mechanism relies on the
_zodproperty, which is not part of Zod's documented public API. Official Zod v4 migration guidance recommends using subpath imports (zod/v3andzod/v4) or feature-detection via try/catch on the actual API signature.Current issues:
_zodproperty is undocumented and not guaranteed across minor/patch releases.anycasts violate TypeScript strict mode (enabled in this project).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