From abfda7b975216e0386de5ff79c59584e946c231a Mon Sep 17 00:00:00 2001 From: Rob Ballou Date: Tue, 17 Mar 2026 10:58:57 -0600 Subject: [PATCH 1/4] Update search parameters guide for zod v4 Zod v4 does not require use of the adapter and can use the schemas directly. See https://github.com/TanStack/router/issues/4322 --- docs/router/guide/search-params.md | 33 ++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/router/guide/search-params.md b/docs/router/guide/search-params.md index 53e9cd1c8a1..cbc84c55267 100644 --- a/docs/router/guide/search-params.md +++ b/docs/router/guide/search-params.md @@ -196,7 +196,9 @@ For validation libraries we recommend using adapters which infer the correct `in ### Zod -An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct `input` type and `output` type +An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct `input` type and `output` type. + +For Zod v3: ```tsx import { zodValidator } from '@tanstack/zod-adapter' @@ -213,13 +215,30 @@ export const Route = createFileRoute('/shop/products/')({ }) ``` -The important part here is the following use of `Link` no longer requires `search` params +With Zod v4, you should directly use the schema in `validateSearch`: + +```tsx +import { z } from 'zod' + +const productSearchSchema = z.object({ + page: z.number().default(1), + filter: z.string().default(''), + sort: z.enum(['newest', 'oldest', 'price']).default('newest'), +}) + +export const Route = createFileRoute('/shop/products/')({ + // With Zod v4, we can use the schema without the adapter + validateSearch: productSearchSchema, +}) +``` + +The important part here is the following use of `Link` no longer requires `search` params: ```tsx ``` -However the use of `catch` here overrides the types and makes `page`, `filter` and `sort` `unknown` causing type loss. We have handled this case by providing a `fallback` generic function which retains the types but provides a `fallback` value when validation fails +In Zod v3, the use of `catch` here overrides the types and makes `page`, `filter` and `sort` `unknown` causing type loss. We have handled this case by providing a `fallback` generic function which retains the types but provides a `fallback` value when validation fails: ```tsx import { fallback, zodValidator } from '@tanstack/zod-adapter' @@ -240,7 +259,9 @@ export const Route = createFileRoute('/shop/products/')({ Therefore when navigating to this route, `search` is optional and retains the correct types. -While not recommended, it is also possible to configure `input` and `output` type in case the `output` type is more accurate than the `input` type +In Zod v4, schemas may use `catch` instead of the fallback and will retain type inference throughout. + +While not recommended, it is also possible to configure `input` and `output` type in case the `output` type is more accurate than the `input` type: ```tsx const productSearchSchema = z.object({ @@ -457,7 +478,7 @@ Now that you've learned how to read your route's search params, you'll be happy The best way to update search params is to use the `search` prop on the `` component. -If the search for the current page shall be updated and the `from` prop is specified, the `to` prop can be omitted. +If the search for the current page shall be updated and the `from` prop is specified, the `to` prop can be omitted. Here's an example: ```tsx title="src/routes/shop/products.tsx" @@ -478,7 +499,7 @@ const ProductList = () => { If you want to update the search params in a generic component that is rendered on multiple routes, specifying `from` can be challenging. -In this scenario you can set `to="."` which will give you access to loosely typed search params. +In this scenario you can set `to="."` which will give you access to loosely typed search params. Here is an example that illustrates this: ```tsx From fc4901872b1c729c954e4c30a5a5612ad5705da4 Mon Sep 17 00:00:00 2001 From: Rob Ballou Date: Wed, 18 Mar 2026 09:11:57 -0600 Subject: [PATCH 2/4] Update additional documentation --- .../api/router/retainSearchParamsFunction.md | 8 +- .../api/router/stripSearchParamsFunction.md | 12 +- .../how-to/setup-basic-search-params.md | 103 ++++++++++-------- .../share-search-params-across-routes.md | 11 +- docs/router/how-to/validate-search-params.md | 29 ++++- .../migrate-from-react-router/SKILL.md | 11 +- .../skills/router-core/search-params/SKILL.md | 27 ++--- 7 files changed, 108 insertions(+), 93 deletions(-) diff --git a/docs/router/api/router/retainSearchParamsFunction.md b/docs/router/api/router/retainSearchParamsFunction.md index eeac38bcd78..18600fd346d 100644 --- a/docs/router/api/router/retainSearchParamsFunction.md +++ b/docs/router/api/router/retainSearchParamsFunction.md @@ -15,14 +15,14 @@ If `true` is passed in, all search params will be retained. ```tsx import { z } from 'zod' import { createRootRoute, retainSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' const searchSchema = z.object({ rootValue: z.string().optional(), }) export const Route = createRootRoute({ - validateSearch: zodValidator(searchSchema), + // Use the schema directly for Zod v4 + validateSearch: searchSchema, search: { middlewares: [retainSearchParams(['rootValue'])], }, @@ -32,7 +32,6 @@ export const Route = createRootRoute({ ```tsx import { z } from 'zod' import { createFileRoute, retainSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' const searchSchema = z.object({ one: z.string().optional(), @@ -40,7 +39,8 @@ const searchSchema = z.object({ }) export const Route = createFileRoute('/')({ - validateSearch: zodValidator(searchSchema), + // Use the schema directly for Zod v4 + validateSearch: searchSchema, search: { middlewares: [retainSearchParams(true)], }, diff --git a/docs/router/api/router/stripSearchParamsFunction.md b/docs/router/api/router/stripSearchParamsFunction.md index bfbb57a7406..08164c39ba9 100644 --- a/docs/router/api/router/stripSearchParamsFunction.md +++ b/docs/router/api/router/stripSearchParamsFunction.md @@ -18,7 +18,6 @@ title: Search middleware to strip search params ```tsx import { z } from 'zod' import { createFileRoute, stripSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' const defaultValues = { one: 'abc', @@ -31,7 +30,8 @@ const searchSchema = z.object({ }) export const Route = createFileRoute('/')({ - validateSearch: zodValidator(searchSchema), + // for Zod v4, use the schema directly + validateSearch: searchSchema, search: { // strip default values middlewares: [stripSearchParams(defaultValues)], @@ -42,7 +42,6 @@ export const Route = createFileRoute('/')({ ```tsx import { z } from 'zod' import { createRootRoute, stripSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' const searchSchema = z.object({ hello: z.string().default('world'), @@ -50,7 +49,8 @@ const searchSchema = z.object({ }) export const Route = createRootRoute({ - validateSearch: zodValidator(searchSchema), + // for Zod v4, use the schema directly + validateSearch: searchSchema, search: { // always remove `hello` middlewares: [stripSearchParams(['hello'])], @@ -61,7 +61,6 @@ export const Route = createRootRoute({ ```tsx import { z } from 'zod' import { createFileRoute, stripSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' const searchSchema = z.object({ one: z.string().default('abc'), @@ -69,7 +68,8 @@ const searchSchema = z.object({ }) export const Route = createFileRoute('/')({ - validateSearch: zodValidator(searchSchema), + // for Zod v4, use the schema directly + validateSearch: searchSchema, search: { // remove all search params middlewares: [stripSearchParams(true)], diff --git a/docs/router/how-to/setup-basic-search-params.md b/docs/router/how-to/setup-basic-search-params.md index 8c9cbbe2e8b..9215cfa9b39 100644 --- a/docs/router/how-to/setup-basic-search-params.md +++ b/docs/router/how-to/setup-basic-search-params.md @@ -10,7 +10,6 @@ Set up search parameters with schema validation (recommended for production): ```tsx import { createFileRoute } from '@tanstack/react-router' -import { zodValidator, fallback } from '@tanstack/zod-adapter' import { z } from 'zod' const productSearchSchema = z.object({ @@ -20,7 +19,7 @@ const productSearchSchema = z.object({ }) export const Route = createFileRoute('/products')({ - validateSearch: zodValidator(productSearchSchema), + validateSearch: productSearchSchema, component: ProductsPage, }) @@ -50,7 +49,25 @@ function ProductsPage() { ## Validation Library Setup -TanStack Router supports any standard schema-compliant validation library. This guide focuses on Zod for examples, but you can use any validation library: +TanStack Router supports any standard schema-compliant validation library. This guide focuses on Zod for examples, but you can use any validation library. + +Using Zod v4: + +```tsx +import { z } from 'zod' + +const searchSchema = z.object({ + page: z.number().default(1), + category: z.string().default('all').catch('all'), +}) + +export const Route = createFileRoute('/products')({ + validateSearch: searchSchema, + component: ProductsPage, +}) +``` + +For Zod v3: ```bash npm install zod @tanstack/zod-adapter @@ -75,15 +92,9 @@ export const Route = createFileRoute('/products')({ ## Step-by-Step Setup with Zod -The rest of this guide uses Zod for examples, but the patterns apply to any validation library. +The rest of this guide uses Zod v4 for examples, but the patterns apply to any validation library. -### Step 1: Install Dependencies - -```bash -npm install zod @tanstack/zod-adapter -``` - -### Step 2: Define Your Search Schema +### Step 1: Define Your Search Schema Start by identifying what search parameters your route needs: @@ -93,40 +104,38 @@ import { fallback } from '@tanstack/zod-adapter' const shopSearchSchema = z.object({ // Pagination - page: fallback(z.number(), 1).default(1), - limit: fallback(z.number(), 20).default(20), + page: z.number().default(1), + limit: z.number().default(20), // Filtering - category: fallback(z.string(), 'all').default('all'), - minPrice: fallback(z.number(), 0).default(0), - maxPrice: fallback(z.number(), 1000).default(1000), + category: z.string().default('all'), + minPrice: z.number().default(0), + maxPrice: z.number().default(1000), // Settings - sort: fallback(z.enum(['name', 'price', 'date']), 'name').default('name'), - ascending: fallback(z.boolean(), true).default(true), + sort: z.enum(['name', 'price', 'date']).default('name'), + ascending: z.boolean().default(true), // Optional parameters searchTerm: z.string().optional(), - showOnlyInStock: fallback(z.boolean(), false).default(false), + showOnlyInStock: z.boolean().default(false), }) type ShopSearch = z.infer ``` -### Step 3: Add Schema Validation to Route +### Step 2: Add Schema Validation to Route -Use the validation adapter to connect your schema to the route: +Connect your schema to the route: ```tsx -import { zodValidator } from '@tanstack/zod-adapter' - export const Route = createFileRoute('/shop')({ - validateSearch: zodValidator(shopSearchSchema), + validateSearch: shopSearchSchema, component: ShopPage, }) ``` -### Step 4: Read Search Parameters in Components +### Step 3: Read Search Parameters in Components Use the route's `useSearch()` hook to access validated and typed search parameters: @@ -166,12 +175,12 @@ function ShopPage() { ```tsx const paginationSchema = z.object({ - page: fallback(z.number().min(1), 1).default(1), - limit: fallback(z.number().min(10).max(100), 20).default(20), + page: z.number().min(1).default(1), + limit: z.number().min(10).max(100).default(20), }) export const Route = createFileRoute('/posts')({ - validateSearch: zodValidator(paginationSchema), + validateSearch: paginationSchema, component: PostsPage, }) @@ -196,16 +205,15 @@ function PostsPage() { ```tsx const catalogSchema = z.object({ - sort: fallback(z.enum(['name', 'date', 'price']), 'name').default('name'), - category: fallback( - z.enum(['electronics', 'clothing', 'books', 'all']), - 'all', - ).default('all'), - ascending: fallback(z.boolean(), true).default(true), + sort: z.enum(['name', 'date', 'price']).default('name'), + category: + z.enum(['electronics', 'clothing', 'books', 'all']) + .default('all'), + ascending: z.boolean().default(true), }) export const Route = createFileRoute('/catalog')({ - validateSearch: zodValidator(catalogSchema), + validateSearch: catalogSchema, component: CatalogPage, }) ``` @@ -215,27 +223,26 @@ export const Route = createFileRoute('/catalog')({ ```tsx const dashboardSchema = z.object({ // Numbers with validation - userId: fallback(z.number().positive(), 1).default(1), - refreshInterval: fallback(z.number().min(1000).max(60000), 5000).default( + userId: z.number().positive().default(1), + refreshInterval: z.number().min(1000).max(60000).default( 5000, ), // Strings with validation - theme: fallback(z.enum(['light', 'dark']), 'light').default('light'), + theme: z.enum(['light', 'dark']).default('light'), timezone: z.string().optional(), // Arrays with validation - selectedIds: fallback(z.number().array(), []).default([]), - tags: fallback(z.string().array(), []).default([]), + selectedIds: z.number().array().default([]), + tags: z.string().array().default([]), // Objects with validation - filters: fallback( + filters: z.object({ status: z.enum(['active', 'inactive']).optional(), type: z.string().optional(), - }), - {}, - ).default({}), + }) + .default({}), }) ``` @@ -245,8 +252,8 @@ const dashboardSchema = z.object({ const reportSchema = z.object({ startDate: z.string().pipe(z.coerce.date()).optional(), endDate: z.string().pipe(z.coerce.date()).optional(), - format: fallback(z.enum(['pdf', 'csv', 'excel']), 'pdf').default('pdf'), - includeCharts: fallback(z.boolean(), true).default(true), + format: z.enum(['pdf', 'csv', 'excel']).default('pdf').catch('pdf'), + includeCharts: z.boolean().default(true), }) ``` @@ -330,7 +337,7 @@ export const Route = createFileRoute('/example')({ ### Problem: Search Parameters Cause TypeScript Errors -**Cause:** Missing or incorrect schema definition. +**Cause:** Missing or incorrect schema definition with Zod v3. **Solution:** Ensure your schema covers all search parameters and use proper types: @@ -366,7 +373,7 @@ const schema = z.object({ // ✅ Graceful fallback handling const schema = z.object({ - page: fallback(z.number(), 1).default(1), // Safe fallback to 1 + page: z.number().default(1).catch(1), // Safe fallback to 1 }) ``` diff --git a/docs/router/how-to/share-search-params-across-routes.md b/docs/router/how-to/share-search-params-across-routes.md index 145d1f089b3..e1e24c1e0d1 100644 --- a/docs/router/how-to/share-search-params-across-routes.md +++ b/docs/router/how-to/share-search-params-across-routes.md @@ -21,7 +21,6 @@ Share parameters across your entire application by validating them in the root r ```tsx // routes/__root.tsx import { createRootRoute, Outlet } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const globalSearchSchema = z.object({ @@ -31,7 +30,7 @@ const globalSearchSchema = z.object({ }) export const Route = createRootRoute({ - validateSearch: zodValidator(globalSearchSchema), + validateSearch: globalSearchSchema, component: RootComponent, }) @@ -50,7 +49,6 @@ function RootComponent() { ```tsx // routes/products/index.tsx import { createFileRoute } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const productSearchSchema = z.object({ @@ -59,7 +57,7 @@ const productSearchSchema = z.object({ }) export const Route = createFileRoute('/products/')({ - validateSearch: zodValidator(productSearchSchema), + validateSearch: productSearchSchema, component: ProductsPage, }) @@ -84,7 +82,6 @@ Share parameters within a section of your app using layout routes: ```tsx // routes/_authenticated.tsx import { createFileRoute, Outlet } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const authSearchSchema = z.object({ @@ -94,7 +91,7 @@ const authSearchSchema = z.object({ }) export const Route = createFileRoute('/_authenticated')({ - validateSearch: zodValidator(authSearchSchema), + validateSearch: authSearchSchema, component: AuthenticatedLayout, }) @@ -181,7 +178,7 @@ function ProductsPage() { ```tsx // ✅ Root route validates shared parameters export const Route = createRootRoute({ - validateSearch: zodValidator(globalSearchSchema), + validateSearch: globalSearchSchema, component: RootComponent, }) ``` diff --git a/docs/router/how-to/validate-search-params.md b/docs/router/how-to/validate-search-params.md index bb141a5a196..5abd435cef8 100644 --- a/docs/router/how-to/validate-search-params.md +++ b/docs/router/how-to/validate-search-params.md @@ -12,15 +12,14 @@ Add robust validation with custom error messages, complex types, and production- ```tsx import { createFileRoute, useRouter } from '@tanstack/react-router' -import { zodValidator, fallback } from '@tanstack/zod-adapter' import { z } from 'zod' const productSearchSchema = z.object({ query: z.string().min(1, 'Search query required'), category: z.enum(['electronics', 'clothing', 'books', 'home']).optional(), - minPrice: fallback(z.number().min(0, 'Price cannot be negative'), 0), - maxPrice: fallback(z.number().min(0, 'Price cannot be negative'), 1000), - inStock: fallback(z.boolean(), true), + minPrice: z.number().min(0, 'Price cannot be negative').default(0), + maxPrice: z.number().min(0, 'Price cannot be negative').default(1000), + inStock: z.boolean().default(true), tags: z.array(z.string()).optional(), dateRange: z .object({ @@ -31,7 +30,7 @@ const productSearchSchema = z.object({ }) export const Route = createFileRoute('/products')({ - validateSearch: zodValidator(productSearchSchema), + validateSearch: productSearchSchema, errorComponent: ({ error }) => { const router = useRouter() return ( @@ -80,7 +79,7 @@ TanStack Router supports multiple validation libraries through adapters: ### Zod (Recommended) -Most popular with excellent TypeScript integration: +Most popular with excellent TypeScript integration. For Zod v3, use `@tanstack/zod-adapter` for validation: ```tsx import { zodValidator, fallback } from '@tanstack/zod-adapter' @@ -99,6 +98,24 @@ export const Route = createFileRoute('/search')({ }) ``` +For Zod v4, the adapter is no longer necessary: + +```tsx +import { z } from 'zod' + +const searchSchema = z.object({ + query: z.string().min(1).max(100), + page: z.number().int().positive().default(1), + sortBy: z.enum(['name', 'date', 'relevance']).optional(), + filters: z.array(z.string()).optional(), +}) + +export const Route = createFileRoute('/search')({ + validateSearch: searchSchema, + component: SearchPage, +}) +``` + ### Valibot Lightweight alternative with modular design: diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md index 602c49a7a65..febc2eb2479 100644 --- a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md +++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md @@ -243,14 +243,12 @@ TanStack Router: // In the route definition: import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' -import { zodValidator, fallback } from '@tanstack/zod-adapter' export const Route = createFileRoute('/posts')({ - validateSearch: zodValidator( + validateSearch: z.object({ - page: fallback(z.number(), 1).default(1), + page: z.number().default(1).catch(1), }), - ), component: Posts, }) @@ -441,11 +439,10 @@ const page = Number(searchParams.get('page')) // CORRECT — TanStack Router pattern, returns typed object // Route definition: -validateSearch: zodValidator( +validateSearch: z.object({ - page: fallback(z.number(), 1).default(1), + page: z.number().default(1).catch(1), }), -) // Component: const { page } = Route.useSearch() diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md index d714969416d..e08f6e46243 100644 --- a/packages/router-core/skills/router-core/search-params/SKILL.md +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -23,7 +23,7 @@ sources: TanStack Router treats search params as JSON-first application state. They are automatically parsed from the URL into structured objects (numbers, booleans, arrays, nested objects) and validated via `validateSearch` on each route. -> **CRITICAL**: When using `zodValidator()`, use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` with the zod adapter makes the output type `unknown`, destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). +> **CRITICAL**: When using `zodValidator()` and Zod v3, use `fallback()` from `@tanstack/zod-adapter`, NOT zod's `.catch()`. Using `.catch()` with the zod adapter makes the output type `unknown`, destroying type safety. This does not apply to Valibot or ArkType (which use their own fallback mechanisms). It also does not apply to Zod v4, which should use `.catch()` and not use the `zodValidator()`. > **CRITICAL**: Types are fully inferred. Never annotate the return of `useSearch()`. ## Setup: Zod Adapter (Recommended) @@ -35,7 +35,6 @@ npm install zod @tanstack/zod-adapter ```tsx // src/routes/products.tsx import { createFileRoute } from '@tanstack/react-router' -import { zodValidator, fallback } from '@tanstack/zod-adapter' import { z } from 'zod' const productSearchSchema = z.object({ @@ -47,7 +46,7 @@ const productSearchSchema = z.object({ }) export const Route = createFileRoute('/products')({ - validateSearch: zodValidator(productSearchSchema), + validateSearch: productSearchSchema, component: ProductsPage, }) @@ -168,15 +167,14 @@ Parent route search params are automatically merged into child routes: ```tsx // src/routes/shop.tsx — parent defines shared params import { createFileRoute } from '@tanstack/react-router' -import { zodValidator, fallback } from '@tanstack/zod-adapter' import { z } from 'zod' const shopSearchSchema = z.object({ - currency: fallback(z.enum(['USD', 'EUR']), 'USD').default('USD'), + currency: z.enum(['USD', 'EUR']).default('USD').catch('USD'), }) export const Route = createFileRoute('/shop')({ - validateSearch: zodValidator(shopSearchSchema), + validateSearch: shopSearchSchema, }) ``` @@ -201,7 +199,6 @@ function ShopProducts() { ```tsx import { createRootRoute, retainSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const rootSearchSchema = z.object({ @@ -209,7 +206,7 @@ const rootSearchSchema = z.object({ }) export const Route = createRootRoute({ - validateSearch: zodValidator(rootSearchSchema), + validateSearch: rootSearchSchema, search: { middlewares: [retainSearchParams(['debug'])], }, @@ -220,7 +217,6 @@ export const Route = createRootRoute({ ```tsx import { createFileRoute, stripSearchParams } from '@tanstack/react-router' -import { zodValidator } from '@tanstack/zod-adapter' import { z } from 'zod' const defaults = { sort: 'newest', page: 1 } @@ -231,7 +227,7 @@ const searchSchema = z.object({ }) export const Route = createFileRoute('/items')({ - validateSearch: zodValidator(searchSchema), + validateSearch: searchSchema, search: { middlewares: [stripSearchParams(defaults)], }, @@ -242,13 +238,12 @@ export const Route = createFileRoute('/items')({ ```tsx export const Route = createFileRoute('/search')({ - validateSearch: zodValidator( + validateSearch: z.object({ retainMe: z.string().optional(), arrayWithDefaults: z.string().array().default(['foo', 'bar']), required: z.string(), }), - ), search: { middlewares: [ retainSearchParams(['retainMe']), @@ -281,7 +276,7 @@ const router = createRouter({ ```tsx export const Route = createFileRoute('/products')({ - validateSearch: zodValidator(productSearchSchema), + validateSearch: productSearchSchema, // Pick ONLY the params the loader needs — not the entire search object loaderDeps: ({ search }) => ({ page: search.page }), loader: async ({ deps }) => { @@ -292,7 +287,7 @@ export const Route = createFileRoute('/products')({ ## Common Mistakes -### 1. HIGH: Using zod `.catch()` with `zodValidator()` instead of adapter `fallback()` +### 1. HIGH: Using zod v3's `.catch()` with `zodValidator()` instead of adapter `fallback()` ```tsx // WRONG — .catch() with zodValidator makes the type unknown @@ -304,6 +299,8 @@ import { fallback } from '@tanstack/zod-adapter' const schema = z.object({ page: fallback(z.number(), 1) }) ``` +**Important:** This only applies when using Zod v3, not when using Zod v4. For v4, using `.catch()` is correct. + ### 2. HIGH: Returning entire search object from `loaderDeps` ```tsx @@ -335,7 +332,7 @@ export const Route = createRootRoute({ // CORRECT — parent must define validateSearch for children to inherit export const Route = createRootRoute({ - validateSearch: zodValidator(globalSearchSchema), + validateSearch: globalSearchSchema, component: RootComponent, }) ``` From 0451f0d2d58e2cfca015b63e52085cf37dc0262b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:16:13 +0000 Subject: [PATCH 3/4] ci: apply automated fixes --- docs/router/how-to/setup-basic-search-params.md | 12 ++++-------- .../lifecycle/migrate-from-react-router/SKILL.md | 7 +++---- .../skills/router-core/search-params/SKILL.md | 11 +++++------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/router/how-to/setup-basic-search-params.md b/docs/router/how-to/setup-basic-search-params.md index 9215cfa9b39..27e49f7584a 100644 --- a/docs/router/how-to/setup-basic-search-params.md +++ b/docs/router/how-to/setup-basic-search-params.md @@ -206,9 +206,7 @@ function PostsPage() { ```tsx const catalogSchema = z.object({ sort: z.enum(['name', 'date', 'price']).default('name'), - category: - z.enum(['electronics', 'clothing', 'books', 'all']) - .default('all'), + category: z.enum(['electronics', 'clothing', 'books', 'all']).default('all'), ascending: z.boolean().default(true), }) @@ -224,9 +222,7 @@ export const Route = createFileRoute('/catalog')({ const dashboardSchema = z.object({ // Numbers with validation userId: z.number().positive().default(1), - refreshInterval: z.number().min(1000).max(60000).default( - 5000, - ), + refreshInterval: z.number().min(1000).max(60000).default(5000), // Strings with validation theme: z.enum(['light', 'dark']).default('light'), @@ -237,8 +233,8 @@ const dashboardSchema = z.object({ tags: z.string().array().default([]), // Objects with validation - filters: - z.object({ + filters: z + .object({ status: z.enum(['active', 'inactive']).optional(), type: z.string().optional(), }) diff --git a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md index febc2eb2479..c9f53358e6f 100644 --- a/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md +++ b/packages/react-router/skills/lifecycle/migrate-from-react-router/SKILL.md @@ -245,10 +245,9 @@ import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' export const Route = createFileRoute('/posts')({ - validateSearch: - z.object({ - page: z.number().default(1).catch(1), - }), + validateSearch: z.object({ + page: z.number().default(1).catch(1), + }), component: Posts, }) diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md index e08f6e46243..f52d24818a3 100644 --- a/packages/router-core/skills/router-core/search-params/SKILL.md +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -238,12 +238,11 @@ export const Route = createFileRoute('/items')({ ```tsx export const Route = createFileRoute('/search')({ - validateSearch: - z.object({ - retainMe: z.string().optional(), - arrayWithDefaults: z.string().array().default(['foo', 'bar']), - required: z.string(), - }), + validateSearch: z.object({ + retainMe: z.string().optional(), + arrayWithDefaults: z.string().array().default(['foo', 'bar']), + required: z.string(), + }), search: { middlewares: [ retainSearchParams(['retainMe']), From 5c7ffb5c56b64b3ab3cf3f8ebf098b80f0567031 Mon Sep 17 00:00:00 2001 From: Rob Ballou Date: Wed, 18 Mar 2026 10:04:30 -0600 Subject: [PATCH 4/4] Additional documentation fix --- .../router-core/skills/router-core/search-params/SKILL.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/router-core/skills/router-core/search-params/SKILL.md b/packages/router-core/skills/router-core/search-params/SKILL.md index f52d24818a3..5f294afdf90 100644 --- a/packages/router-core/skills/router-core/search-params/SKILL.md +++ b/packages/router-core/skills/router-core/search-params/SKILL.md @@ -38,11 +38,11 @@ import { createFileRoute } from '@tanstack/react-router' import { z } from 'zod' const productSearchSchema = z.object({ - page: fallback(z.number(), 1).default(1), - filter: fallback(z.string(), '').default(''), - sort: fallback(z.enum(['newest', 'oldest', 'price']), 'newest').default( + page: z.number().default(1).catch(1), + filter: z.string().default(''), + sort: z.enum(['newest', 'oldest', 'price']).default( 'newest', - ), + ).catch('newest'), }) export const Route = createFileRoute('/products')({