Skip to content

Commit 2010d15

Browse files
nibaschiller-manuel
authored andcommitted
feat: add support for zod v4 (#4322)
1 parent 9603235 commit 2010d15

File tree

7 files changed

+776
-38
lines changed

7 files changed

+776
-38
lines changed

packages/zod-adapter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@testing-library/jest-dom": "^6.6.3",
6868
"@testing-library/react": "^16.2.0",
6969
"@tanstack/react-router": "workspace:^",
70-
"zod": "^3.24.2",
70+
"zod": "^3.25.64",
7171
"react": "^19.0.0",
7272
"react-dom": "^19.0.0"
7373
},

packages/zod-adapter/src/index.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { z } from 'zod'
1+
import * as z3 from 'zod'
2+
import * as z4 from 'zod/v4'
23
import type { ValidatorAdapter } from '@tanstack/react-router'
34

45
export interface ZodTypeLike {
@@ -58,12 +59,32 @@ export const zodValidator = <
5859
}
5960
}
6061

61-
export const fallback = <TSchema extends z.ZodTypeAny>(
62+
const isZod3Schema = (schema: any): schema is z3.ZodSchema => {
63+
return (
64+
'_def' in schema &&
65+
typeof schema._def === 'object' &&
66+
'typeName' in schema._def
67+
)
68+
}
69+
70+
export function fallback<TSchema extends z3.ZodTypeAny>(
6271
schema: TSchema,
6372
fallback: TSchema['_input'],
64-
): z.ZodPipeline<
65-
z.ZodType<TSchema['_input'], z.ZodTypeDef, TSchema['_input']>,
66-
z.ZodCatch<TSchema>
67-
> => {
68-
return z.custom<TSchema['_input']>().pipe(schema.catch(fallback))
73+
): z3.ZodPipeline<
74+
z3.ZodType<TSchema['_input'], z3.ZodTypeDef, TSchema['_input']>,
75+
z3.ZodCatch<TSchema>
76+
>
77+
export function fallback<TSchema extends z4.ZodTypeAny>(
78+
schema: TSchema,
79+
fallback: z4.output<TSchema>,
80+
): z4.ZodPipe<
81+
z4.ZodType<TSchema['_input'], TSchema['_input']>,
82+
z4.ZodCatch<TSchema>
83+
>
84+
export function fallback(schema: any, fallback: any): any {
85+
if (isZod3Schema(schema)) {
86+
return z3.custom().pipe(schema.catch(fallback))
87+
} else {
88+
return z4.custom().pipe(schema.catch(fallback))
89+
}
6990
}

packages/zod-adapter/tests/index.test-d.ts renamed to packages/zod-adapter/tests/zod-v3.test-d.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Link,
66
} from '@tanstack/react-router'
77
import { test, expectTypeOf } from 'vitest'
8-
import { zodValidator } from '../src'
8+
import { fallback, zodValidator } from '../src'
99
import { z } from 'zod'
1010

1111
test('when creating a route with zod validation', () => {
@@ -56,6 +56,42 @@ test('when creating a route with zod validation', () => {
5656
}>
5757
})
5858

59+
test('when creating a route with zod validation and fallback handler', () => {
60+
const rootRoute = createRootRoute({
61+
validateSearch: zodValidator(
62+
z.object({
63+
page: z.number().optional().default(0),
64+
sort: fallback(z.enum(['oldest', 'newest']), 'oldest').default(
65+
'oldest',
66+
),
67+
}),
68+
),
69+
})
70+
71+
const router = createRouter({ routeTree: rootRoute })
72+
73+
expectTypeOf(Link<typeof router, string, '/'>)
74+
.parameter(0)
75+
.toHaveProperty('search')
76+
.exclude<Function | true>()
77+
.toEqualTypeOf<{ page?: number; sort?: 'oldest' | 'newest' } | undefined>()
78+
79+
expectTypeOf(Link<typeof router, string, '/'>)
80+
.parameter(0)
81+
.toHaveProperty('search')
82+
.returns.toEqualTypeOf<{ page?: number; sort?: 'oldest' | 'newest' }>()
83+
84+
expectTypeOf(rootRoute.useSearch<typeof router>()).toEqualTypeOf<{
85+
page: number
86+
sort: 'oldest' | 'newest'
87+
}>()
88+
89+
expectTypeOf(rootRoute.useSearch<typeof router>()).toEqualTypeOf<{
90+
page: number
91+
sort: 'oldest' | 'newest'
92+
}>
93+
})
94+
5995
test('when creating a route with zod validation where input is output', () => {
6096
const rootRoute = createRootRoute({
6197
validateSearch: zodValidator(

packages/zod-adapter/tests/index.test.tsx renamed to packages/zod-adapter/tests/zod-v3.test.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterEach, expect, test, vi } from 'vitest'
2-
import { zodValidator } from '../src'
2+
import { fallback, zodValidator } from '../src'
33
import { z } from 'zod'
44
import {
55
createRootRoute,
@@ -78,6 +78,70 @@ test('when navigating to a route with zodValidator', async () => {
7878
expect(await screen.findByText('Page: 0')).toBeInTheDocument()
7979
})
8080

81+
test('when navigating to a route with zodValidator with fallback value', async () => {
82+
const rootRoute = createRootRoute()
83+
84+
const Index = () => {
85+
return (
86+
<>
87+
<h1>Index</h1>
88+
<Link<typeof router, string, '/invoices'>
89+
to="/invoices"
90+
search={{
91+
// to test fallback we need to cast to any to test invalid input
92+
sort: 0 as any,
93+
}}
94+
>
95+
To Invoices
96+
</Link>
97+
</>
98+
)
99+
}
100+
101+
const indexRoute = createRoute({
102+
getParentRoute: () => rootRoute,
103+
path: '/',
104+
component: Index,
105+
})
106+
107+
const Invoices = () => {
108+
const search = invoicesRoute.useSearch()
109+
110+
return (
111+
<>
112+
<h1>Invoices</h1>
113+
<span>Sort by: {search.sort}</span>
114+
</>
115+
)
116+
}
117+
118+
const invoicesRoute = createRoute({
119+
getParentRoute: () => rootRoute,
120+
path: 'invoices',
121+
validateSearch: zodValidator(
122+
z.object({
123+
sort: fallback(z.enum(['oldest', 'newest']), 'oldest').default(
124+
'oldest',
125+
),
126+
}),
127+
),
128+
component: Invoices,
129+
})
130+
131+
const routeTree = rootRoute.addChildren([indexRoute, invoicesRoute])
132+
const router = createRouter({ routeTree })
133+
134+
render(<RouterProvider router={router} />)
135+
136+
const invoicesLink = await screen.findByRole('link', {
137+
name: 'To Invoices',
138+
})
139+
140+
fireEvent.click(invoicesLink)
141+
142+
expect(await screen.findByText('Sort by: oldest')).toBeInTheDocument()
143+
})
144+
81145
test('when navigating to a route with zodValidator input set to output', async () => {
82146
const rootRoute = createRootRoute()
83147

0 commit comments

Comments
 (0)