Add support for Zod v4
fixes #4322
In Zod v4, we still need a fallback trick to prevent type loss. Added tests for Zod v4 and the fallback case
related discussion #4092
thanks for starting this. however, we need to discuss first whether / how we want to continue with the whole adapter setup
View your CI Pipeline Execution β for commit 2010d151a86f4074ab7dde87a6e1190167086a8c
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:eslint,test:unit,tes... |
β Failed | 1m 49s | View β |
βοΈ Nx Cloud last updated this comment at 2025-07-06 16:36:21 UTC
More templates
- tanstack-router-react-example-authenticated-routes
- tanstack-router-react-example-authenticated-routes-firebase
- tanstack-router-react-example-basic
- tanstack-router-react-example-basic-default-search-params
- tanstack-router-react-example-basic-devtools-panel
- tanstack-router-react-example-basic-file-based
- tanstack-router-react-example-basic-non-nested-devtools
- tanstack-router-react-example-react-query
- tanstack-router-react-example-basic-react-query-file-based
- tanstack-router-react-example-basic-ssr-file-based
- tanstack-router-react-example-basic-ssr-streaming-file-based
- tanstack-router-react-example-basic-virtual-file-based
- tanstack-router-react-example-basic-virtual-inside-file-based
- tanstack-router-react-example-deferred-data
- tanstack-router-react-example-kitchen-sink
- tanstack-router-react-example-kitchen-sink-file-based
- tanstack-router-react-example-kitchen-sink-react-query
- tanstack-router-react-example-kitchen-sink-react-query-file-based
- tanstack-router-react-example-large-file-based
- tanstack-router-react-example-location-masking
- tanstack-router-react-example-navigation-blocking
- tanstack-router-react-example-quickstart
- tanstack-router-react-example-quickstart-esbuild-file-based
- tanstack-router-react-example-quickstart-file-based
- tanstack-router-react-example-quickstart-rspack-file-based
- tanstack-router-react-example-quickstart-webpack-file-based
- router-monorepo-react-query
- router-mono-simple
- router-mono-simple-lazy
- tanstack-router-react-example-scroll-restoration
- tanstack-search-validator-adapters
- tanstack-start-example-bare
- tanstack-start-example-basic
- tanstack-start-example-basic-auth
- tanstack-start-example-basic-react-query
- tanstack-start-example-basic-rsc
- tanstack-start-example-basic-static
- tanstack-start-example-clerk-basic
- tanstack-start-example-convex-trellaux
- tanstack-start-example-counter
- tanstack-start-example-large
- tanstack-start-example-material-ui
- tanstack-start-example-supabase-basic
- tanstack-start-tailwind-v4
- tanstack-start-example-trellaux
- tanstack-start-example-workos
- tanstack-router-react-example-view-transitions
- tanstack-router-react-example-with-framer-motion
- tanstack-router-react-example-with-trpc
- tanstack-router-react-example-with-trpc-react-query
- tanstack-router-solid-example-basic
- tanstack-router-solid-example-basic-devtools-panel
- tanstack-router-solid-example-basic-file-based
- tanstack-router-solid-example-basic-non-nested-devtools
- tanstack-router-solid-example-basic-solid-query
- tanstack-router-solid-example-basic-solid-query-file-based
- tanstack-router-solid-example-basic-ssr-streaming-file-based
- tanstack-router-solid-example-kitchen-sink-file-based
- tanstack-router-solid-example-quickstart-file-based
- tanstack-solid-start-example-bare
- tanstack-solid-start-example-basic
- tanstack-solid-start-example-basic-static
@tanstack/arktype-adapter
npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@4442
@tanstack/directive-functions-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@4442
@tanstack/eslint-plugin-router
npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@4442
@tanstack/history
npm i https://pkg.pr.new/TanStack/router/@tanstack/history@4442
@tanstack/react-router
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@4442
@tanstack/react-router-devtools
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@4442
@tanstack/react-router-with-query
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-with-query@4442
@tanstack/react-start
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@4442
@tanstack/react-start-client
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@4442
@tanstack/react-start-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-plugin@4442
@tanstack/react-start-server
npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@4442
@tanstack/router-cli
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@4442
@tanstack/router-core
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@4442
@tanstack/router-devtools
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@4442
@tanstack/router-devtools-core
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@4442
@tanstack/router-generator
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@4442
@tanstack/router-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@4442
@tanstack/router-utils
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@4442
@tanstack/router-vite-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@4442
@tanstack/server-functions-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@4442
@tanstack/solid-router
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@4442
@tanstack/solid-router-devtools
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@4442
@tanstack/solid-start
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@4442
@tanstack/solid-start-client
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@4442
@tanstack/solid-start-plugin
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-plugin@4442
@tanstack/solid-start-server
npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@4442
@tanstack/start-client-core
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@4442
@tanstack/start-plugin-core
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@4442
@tanstack/start-server-core
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@4442
@tanstack/start-server-functions-client
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-client@4442
@tanstack/start-server-functions-fetcher
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-fetcher@4442
@tanstack/start-server-functions-server
npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-functions-server@4442
@tanstack/valibot-adapter
npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@4442
@tanstack/virtual-file-routes
npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@4442
@tanstack/zod-adapter
npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@4442
commit: 2010d15
this fails to build, can you please have a look?
https://cloud.nx.app/runs/q1gPhPfKu4/task/tanstack-search-validator-adapters%3Abuild
Build should be fixed
There are a few things we should be aware of before merging it:
- If your project enforces a different version of
Zodthan the one used by thezod-adapterpackage, you might encounter the errorType instantiation is excessively deep and possibly infinite.This issue affected the build (wrong peerDependency and devDependency in zod-adapter), and there's a long discussion about it here: https://github.com/colinhacks/zod/issues/2697 -
Zod 4introduces a "mini" version with a tree-shakeable API. However, in our case, it's complicated to use because you cannot chain the result of a fallback with "default," etc. - The bundle size increases by 20 kB after adding support for
Zod 4(using the mini version can reduce this to 6 kB).
Before this PR:
dist/index.html 0.39 kB β gzip: 0.26 kB
dist/assets/index-DjYYQclb.css 6.23 kB β gzip: 1.85 kB
dist/assets/Search-De6RLLiF.js 0.31 kB β gzip: 0.24 kB
dist/assets/valibot.index-CY1Ddu6G.js 0.42 kB β gzip: 0.28 kB
dist/assets/zod.index-DZLhYpWM.js 0.43 kB β gzip: 0.29 kB
dist/assets/arktype.index-B-cgD59V.js 0.43 kB β gzip: 0.29 kB
dist/assets/index-BhpiWg34.js 507.92 kB β gzip: 153.78 kB
After this PR:
dist/index.html 0.39 kB β gzip: 0.26 kB
dist/assets/index-DjYYQclb.css 6.23 kB β gzip: 1.85 kB
dist/assets/Search-Cy5gcvTr.js 0.31 kB β gzip: 0.24 kB
dist/assets/valibot.index-CC4lPNzG.js 0.42 kB β gzip: 0.28 kB
dist/assets/zod.index-C6nO_o6N.js 0.43 kB β gzip: 0.29 kB
dist/assets/arktype.index-B0G5htEQ.js 0.43 kB β gzip: 0.29 kB
dist/assets/index-BRnstef1.js 527.57 kB β gzip: 159.04 kB
I believe that for the functionality we are adding, this code introduces too much complexity. Instead, I think we should document how to write a helper function for your Zod / validator.
Instead, I think we should document how to write a helper function for your Zod / validator.
yes! do you want to create docs for this?
@niba Is there a reason why we have to specify both a fallback and a default rather than just a fallback. I can't imagine a scenario where you would want a different value for your default vs your fallback.
For example
sort: fallback(z.enum(['oldest', 'newest']), 'oldest').default('oldest')
vs
sort: fallback(z.enum(['oldest', 'newest']), 'oldest')
I did notice though that when a default is not provided then search parameters must be passed to the Link component
@michael-wolfenden They handle different cases, but it's true that you almost always use a combination of them. I also don't know of a scenario where you wouldn't want to use default.
@schiller-manuel
Sure! I can try. First, I need to process everything. Basically, the problem only exists with Zod because of how the schema is inferred (using standard schema).
In Zod 3, the problem was that catch destroyed the entire type.
In Zod 4, the type is inferred correctly, but it introduces a "whatever" type for catch that allows you to use any value.
const schema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch("newest"),
})
/*
Zod 3 output: {
page?: number | undefined;
filter?: string | undefined;
sort?: unknown;
}
*/
type Zod3Type = StandardSchemaV1.InferInput<typeof schema>
/*
Zod 4 output: {
page?: number | undefined;
filter?: string | undefined;
sort?: z4.core.util.Whatever | "newest" | "oldest" | "price";
} */
type Zod4Type = StandardSchemaV1.InferInput<typeof schemaZ4>
I don't understand why Zod4 generates this whatever type for catch. I think that most people use catch as a safety mechanism and don't want to "relax" the types at all. I will try to ask on zod repo.
I will try to ask on zod repo.
absolutely! as you wrote, this is only needed for zod, other libraries (e.g. arktype and valibot) just work with standard schema. we can document those workarounds for zod and then deprecate the adapter packages.
@hanneswidrig thanks for the review!
Zod v4 almost supported TanStack Router without requiring an adapter. The only issue was related to type inference when using β .catch().
I forwarded our issue to the Zod team, and they've already fixed the problem we were facing (https://github.com/colinhacks/zod/issues/4851).
With this change, the β zodAdapter is no longer necessary to achieve type safety in TanStack Router. We can now use β zod directly:
export const Route = createFileRoute('/shop/products/')({
validateSearch: z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch("newest"),
}),
})
I'm waiting for the new release of zod before opening a PR to update the documentation in Tanstack Router. Should we close this one? @schiller-manuel
@niba very cool! thanks for that!
yes, let's close this one and create a new one for the docs.
@niba @schiller-manuel This has landed in Zod 4.0.6, apologies for the delay!