Skip to content

Add support for Zod v4 #4442

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

Closed
wants to merge 1 commit into from
Closed

Add support for Zod v4 #4442

wants to merge 1 commit into from

Conversation

niba
Copy link

@niba niba commented Jun 16, 2025

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

@schiller-manuel
Copy link
Contributor

thanks for starting this. however, we need to discuss first whether / how we want to continue with the whole adapter setup

Copy link

nx-cloud bot commented Jul 6, 2025

View your CI Pipeline Execution ↗ for commit 2010d15

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

Copy link

pkg-pr-new bot commented Jul 6, 2025

More templates

@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

@schiller-manuel
Copy link
Contributor

this fails to build, can you please have a look?

https://cloud.nx.app/runs/q1gPhPfKu4/task/tanstack-search-validator-adapters%3Abuild

@niba
Copy link
Author

niba commented Jul 7, 2025

Build should be fixed

There are a few things we should be aware of before merging it:

  1. If your project enforces a different version of Zod than the one used by the zod-adapter package, you might encounter the error Type 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: Zod 3.22.2 TS 5.2.2: TS2589: Type instantiation is excessively deep and possibly infinite colinhacks/zod#2697
  2. Zod 4 introduces 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.
  3. 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.

@schiller-manuel
Copy link
Contributor

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?

@michael-wolfenden
Copy link

@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

@niba
Copy link
Author

niba commented Jul 8, 2025

@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.

@schiller-manuel
Copy link
Contributor

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.

"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"peerDependencies": {
"zod": "^3.23.8",
"zod": "^3.25.0",
Copy link

@hanneswidrig hanneswidrig Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://zod.dev/library-authors

With zod 4.0.x released last week, this line needs to be modified slightly to include version four releases.

"zod": "^3.25.0 || ^4.0.0"

This value also needs to be set for devDependencies too.

@@ -1,4 +1,5 @@
import { z } from 'zod'
import * as z3 from 'zod/v3'
import * as z4 from 'zod/v4'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import should be modified slightly to include the /core path.

import * as z4 from "zod/v4/core";

@niba
Copy link
Author

niba commented Jul 16, 2025

@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 (colinhacks/zod#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

@schiller-manuel
Copy link
Contributor

@niba very cool! thanks for that!

yes, let's close this one and create a new one for the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Zod 4 support for @tanstack/zod-adapter (specifically fallback)
4 participants