Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/zod-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@tanstack/react-router": "workspace:^",
"zod": "^3.24.2",
"zod": "^3.25.0",
"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.

Copy link

Choose a reason for hiding this comment

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

A bit annoying for npm users when this version is not 4. Is there any discussion or issue that is attempting to update this?

"@tanstack/react-router": ">=1.43.2"
}
}
38 changes: 31 additions & 7 deletions packages/zod-adapter/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";

import type { ValidatorAdapter } from '@tanstack/react-router'

export interface ZodTypeLike {
Expand Down Expand Up @@ -58,12 +59,35 @@ export const zodValidator = <
}
}

export const fallback = <TSchema extends z.ZodTypeAny>(
const isZod4Schema = (
schema: z3.ZodTypeAny | z4.ZodType,
): schema is z4.ZodType => {
return (
'_zod' in schema && typeof schema._zod === 'object' && 'def' in schema._zod
)
}

export function fallback<TSchema extends z3.ZodTypeAny>(
schema: TSchema,
fallback: TSchema['_input'],
): z.ZodPipeline<
z.ZodType<TSchema['_input'], z.ZodTypeDef, TSchema['_input']>,
z.ZodCatch<TSchema>
> => {
return z.custom<TSchema['_input']>().pipe(schema.catch(fallback))
): z3.ZodPipeline<
z3.ZodType<TSchema['_input'], z3.ZodTypeDef, TSchema['_input']>,
z3.ZodCatch<TSchema>
>
export function fallback<TSchema extends z4.ZodType>(
schema: TSchema,
fallback: z4.input<TSchema>,
): z4.ZodPipe<
z4.ZodType<TSchema['_zod']['input'], TSchema['_zod']['input']>,
z4.ZodCatch<TSchema>
>
export function fallback<TSchema extends z3.ZodTypeAny | z4.ZodType>(
schema: TSchema,
fallback: any,
): any {
if (isZod4Schema(schema)) {
return z4.custom().pipe(schema.catch(fallback))
} else {
return z3.custom().pipe(schema.catch(fallback))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
Link,
} from '@tanstack/react-router'
import { test, expectTypeOf } from 'vitest'
import { zodValidator } from '../src'
import { fallback, zodValidator } from '../src'
import { z } from 'zod'

test('when creating a route with zod validation', () => {
Expand Down Expand Up @@ -56,6 +56,42 @@ test('when creating a route with zod validation', () => {
}>
})

test('when creating a route with zod validation and fallback handler', () => {
const rootRoute = createRootRoute({
validateSearch: zodValidator(
z.object({
page: z.number().optional().default(0),
sort: fallback(z.enum(['oldest', 'newest']), 'oldest').default(
'oldest',
),
}),
),
})

const router = createRouter({ routeTree: rootRoute })

expectTypeOf(Link<typeof router, string, '/'>)
.parameter(0)
.toHaveProperty('search')
.exclude<Function | true>()
.toEqualTypeOf<{ page?: number; sort?: 'oldest' | 'newest' } | undefined>()

expectTypeOf(Link<typeof router, string, '/'>)
.parameter(0)
.toHaveProperty('search')
.returns.toEqualTypeOf<{ page?: number; sort?: 'oldest' | 'newest' }>()

expectTypeOf(rootRoute.useSearch<typeof router>()).toEqualTypeOf<{
page: number
sort: 'oldest' | 'newest'
}>()

expectTypeOf(rootRoute.useSearch<typeof router>()).toEqualTypeOf<{
page: number
sort: 'oldest' | 'newest'
}>
})

test('when creating a route with zod validation where input is output', () => {
const rootRoute = createRootRoute({
validateSearch: zodValidator(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, expect, test, vi } from 'vitest'
import { zodValidator } from '../src'
import { fallback, zodValidator } from '../src'
import { z } from 'zod'
import {
createRootRoute,
Expand Down Expand Up @@ -78,6 +78,70 @@ test('when navigating to a route with zodValidator', async () => {
expect(await screen.findByText('Page: 0')).toBeInTheDocument()
})

test('when navigating to a route with zodValidator with fallback value', async () => {
const rootRoute = createRootRoute()

const Index = () => {
return (
<>
<h1>Index</h1>
<Link<typeof router, string, '/invoices'>
to="/invoices"
search={{
// to test fallback we need to cast to any to test invalid input
sort: 0 as any,
}}
>
To Invoices
</Link>
</>
)
}

const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Index,
})

const Invoices = () => {
const search = invoicesRoute.useSearch()

return (
<>
<h1>Invoices</h1>
<span>Sort by: {search.sort}</span>
</>
)
}

const invoicesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'invoices',
validateSearch: zodValidator(
z.object({
sort: fallback(z.enum(['oldest', 'newest']), 'oldest').default(
'oldest',
),
}),
),
component: Invoices,
})

const routeTree = rootRoute.addChildren([indexRoute, invoicesRoute])
const router = createRouter({ routeTree })

render(<RouterProvider router={router} />)

const invoicesLink = await screen.findByRole('link', {
name: 'To Invoices',
})

fireEvent.click(invoicesLink)

expect(await screen.findByText('Sort by: oldest')).toBeInTheDocument()
})

test('when navigating to a route with zodValidator input set to output', async () => {
const rootRoute = createRootRoute()

Expand Down
Loading