Repository: TanStack/router
Pull request #2000 (2024-07-21 19:50:01)

feat(react-router): support input types for `search` validation schemas
### Why

Previously we have always inferred the `Output` type for `zod` and friends. This causes TS headaches when using `Link` when a route definition has transformations in a schema.

For example, given a route definition like this:

```tsx
export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: z.object({
    searchPlaceholder: z.literal('searchPlaceholder'),
    page: z.number().optional().default(0),
    offset: z.number(),
    search: z.string(),
  }),
})
```

When navigating to this route, the `page` `search` param is always required, despite the fact it is an optional **input**.

```tsx
     <Link
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={{
          searchPlaceholder: 'searchPlaceholder',
          page: 0, // page is required here :(
          offset: 10,
          search: 'search',
          rootSearch: 0,
        }}
      >
        Search
      </Link>
```

This was because we never attempted to infer the input type and we had a workaround with a tag type of `SearchSchemaInput`.

With this PR `Link` and other navigation now uses  the `zod` `Input` type and `useSearch`  uses the `zod` `Output` type.

That means `page` in the above example is now optional.

~~The approach taken is we take advantage of TS being structurally typed, so we can infer `_input` and `_output` from the `zod` schema without having a dependency on `zod`~~

### Update

This PR introduces search validation adapters to separate validation libraries from the core of tanstack router while keeping the ability to infer `input` and `output` types. This change is made with no breaking changes to the types and we're just extending the types to also allow adapters.

A route definition can use an adapter by wrapping the validation schema. For example, with `zod` usage:

```typescript
import { zodSearchValidator } from '@tanstack/react-router-with-zod'

export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: zodSearchValidator(
    z.object({
      searchPlaceholder: z.literal('searchPlaceholder'),
      page: z.number().optional().default(0),
      offset: z.number(),
      search: z.string(),
    }),
  ),
  loader: (opts) =>
    opts.context.queryClient.ensureQueryData(searchQueryOptions),
})

```

Using `zodSearchValidator` will infer the `input` type for navigation like `Link` and infer the `output` type for `useSearch`

```tsx
      <Link
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={{
          searchPlaceholder: 'searchPlaceholder',
          page: 0, // this is optional
          offset: 10,
          search: 'search',
          rootSearch: 0,
        }}
      >
        Search
      </Link>
```

`search` here is the `zod` `input` type which means `page` is optional

```tsx
     <Link
        from={Route.fullPath}
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={(prev) => prev}
      >
        Search
      </Link>
```
`prev` here is the `zod` `output` type while the return type is `input` type

```typescript
 const search = Route.useSearch()
```

`search` here is the `output` type.

### Solving the `catch` problem

A design decision by `zod` means that when a `catch` is used in validation schema, the `input` type becomes `unknown`

```typescript
export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: zodSearchValidator(
    z.object({
      searchPlaceholder: z.literal('searchPlaceholder'),
      page: z.number().optional().default(0).catch(0),
      offset: z.number(),
      search: z.string(),
    }),
  ),
  loader: (opts) =>
    opts.context.queryClient.ensureQueryData(searchQueryOptions),
})
```
```tsx
      <Link
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={{
          searchPlaceholder: 'searchPlaceholder',
          page: 0, // this is now unknown :(
          offset: 10,
          search: 'search',
          rootSearch: 0,
        }}
      >
        Search
      </Link>
```

The `page` type is lost to `unknown`. Here I'm proposing a few solutions to this problem:

1. Accept this is a limitation of `zod` and maybe consider an alternative if keeping the type is important to you. `valibot` does not have the same issue with their `fallback` variation
2. Use a custom schema for a `fallback/catch` that keeps the type information
3. Change the `input` type to be the `output` type for navigation

### Use a custom schema for a `fallback/catch` that keeps the type information

This problem can be solved using a custom schema like so

```typescript
export const fallback = <TSchema extends z.ZodTypeAny>(
  schema: TSchema,
  fallback: TSchema['_input'],
) => {
  return z.custom<TSchema['_input']>().pipe(schema.catch(fallback))
}

export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: zodSearchValidator(
    z.object({
      searchPlaceholder: z.literal('searchPlaceholder'),
      page: fallback(z.number(), 0).optional().default(0),
      offset: z.number(),
      search: z.string(),
    }),
  ),
  loader: (opts) =>
    opts.context.queryClient.ensureQueryData(searchQueryOptions),
})
```

Here we're creating a custom `fallback` schema which forces the `input` type to remain and pipes a dummy `custom` schema to the actual `schema` which uses `catch`.

The result is the type we expect for `input`

The `fallback` custom schema can be provided by the adapter package?

```tsx
   <Link
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={{
          searchPlaceholder: 'searchPlaceholder',
          page: 0, // page is optional and a number (yay!) :)
          offset: 10,
          search: 'search',
          rootSearch: 0,
        }}
      >
        Search
      </Link>
```

### Change the `input` type to be the `output` type for navigation

The `zod` adapter is configurable so it possible to use the `output` type for `navigation` by passing a options object to `zodSearchValidator`

```typescript
export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: zodSearchValidator({
    schema: z.object({
      searchPlaceholder: z.literal('searchPlaceholder'),
      page: fallback(z.number(), 0).optional().default(0),
      offset: z.number(),
      search: z.string(),
    }),
    input: 'output',
  }),
  loader: (opts) =>
    opts.context.queryClient.ensureQueryData(searchQueryOptions),
})
```

The `input` type has now been set to the `zod` output type which means for navigation (`Link`, `useNavigate`), `page` is required again as it was previously and keeps the `number` type

```tsx
    <Link
        to="/search/searchPlaceholder"
        className="block py-1 text-blue-800 hover:text-blue-600"
        search={{
          searchPlaceholder: 'searchPlaceholder',
          page: 0, // page is now the output type and is required again
          offset: 10,
          search: 'search',
          rootSearch: 0,
        }}
      >
        Search
      </Link>
```
### TS Performance

The performance of tanstack router has been improved by resolving the full search schema to when we need it, rather than it being a defaulted type parameter. TS will instantiate all defaulted type parameters

Before
```
> tsc --extendedDiagnostics

Files:                         648
Lines of Library:            38680
Lines of Definitions:        83589
Lines of TypeScript:         21604
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                135323
Symbols:                    309502
Types:                      104016
Instantiations:             740250
Memory used:               333716K
Assignability cache size:    48247
Identity cache size:          8856
Subtype cache size:           2426
Strict subtype cache size:     413
I/O Read time:               0.09s
Parse time:                  0.59s
ResolveModule time:          0.14s
ResolveTypeReference time:   0.00s
ResolveLibrary time:         0.02s
Program time:                0.94s
Bind time:                   0.26s
Check time:                  5.27s
printTime time:              0.00s
Emit time:                   0.00s
Total time:                  6.47s
```

After
```
> tsc --extendedDiagnostics

Files:                         648
Lines of Library:            38680
Lines of Definitions:        83583
Lines of TypeScript:         21604
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                135323
Symbols:                    309441
Types:                      101158
Instantiations:             705433
Memory used:               331772K
Assignability cache size:    48334
Identity cache size:          8654
Subtype cache size:           2426
Strict subtype cache size:     413
I/O Read time:               0.08s
Parse time:                  0.56s
ResolveModule time:          0.13s
ResolveTypeReference time:   0.00s
ResolveLibrary time:         0.02s
Program time:                0.88s
Bind time:                   0.25s
Check time:                  5.08s
printTime time:              0.00s
Emit time:                   0.00s
Total time:                  6.21s
```

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 543402163503777ceafd4ef80f07c831a6661ebe.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/669d667fe7c4da309aed8cb6?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
<a href="https:///pr.new/TanStack/router/pull/2000"><img src="https://developer.stackblitz.com/img/review_pr_small.svg" alt="Review PR in StackBlitz Codeflow" align="left" width="103" height="20"></a> _Run & review this pull request in [StackBlitz Codeflow](https:///pr.new/TanStack/router/pull/2000)._

_commit: <a href="https://github.com/TanStack/router/runs/27720792514"><code>5434021</code></a>_


<details><summary><b>@tanstack/history</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/history@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-cross-context@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router-with-query@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-cli@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-devtools@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-generator@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-plugin@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start@2000
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@2000
```

</p></details>
      
    

---


<details><summary><b>templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/8879f55c-4b0b-4046-b9ff-17618b4c402c)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/59959911-3564-4485-b74f-665b3e3d7f0e)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/796c9314-7ecf-42d9-a259-af66a6f808b0)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/5f7dbb76-e46c-462a-ad8a-8e7cc1e1922c)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/44a6743c-b8b5-4164-afd1-a592ea062d0b)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/9a627463-380b-44c0-8fa1-16105340f03e)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/fb4db3b1-c46e-43ac-bfe6-ed26dd9016fb)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/58f20b49-c35d-4355-9c86-4dce39edf05c)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/b3f92749-dbe0-4113-9a35-9386844eb55f)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/de3cc66b-0793-4df9-b20b-76392a49267c)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/d102cfde-d1ab-4eab-99b8-e9763dc863ce)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/7110524b-0e90-4ddb-b0bb-6a045882c9a3)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/a0991835-5f51-428d-a980-2d3096d179f8)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/c7e5337e-7c00-404c-9cf9-208a5aa17ff3)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/a7609822-630c-4a2c-af75-f150850ecfea)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/3505bd19-2530-435f-a2ba-ab04759e96e8)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/49e4b87c-a39d-45c2-ba62-0f7d2d1880bf)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/df19dd6d-b007-40ee-8449-efa1ab7bf66f)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/549a136c-1506-4dba-8e47-5416e54824e8)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/034e1e6e-595d-474b-a22d-d041e58bd9ab)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/44ea7914-813a-4241-a8a8-a40de748d3ee)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/9995dff2-4276-48f7-8520-8ec14e2deeea)
- [tanstack-router-example-react-start-basic](https://pkg.pr.new/template/406df552-230d-4b7a-bdcc-cc723749a52d)
- [tanstack-router-example-react-start-basic-counter](https://pkg.pr.new/template/dfe998d4-a08b-46be-a9a5-fd7827851527)
- [tanstack-router-example-react-start-basic-react-query](https://pkg.pr.new/template/a6b33455-6736-4160-b926-2e7bb50d13f6)
- [tanstack-router-example-react-start-basic-rsc](https://pkg.pr.new/template/4a1a98e8-d0c0-4292-b47d-d5047dd31581)
- [tanstack-router-example-react-start-basic-trellaux](https://pkg.pr.new/template/0e1bc186-5216-4e7d-8ee7-9745d7f84df9)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/26ed429b-b1c1-4c82-8d17-b142218af631)
- [tanstack-router-react-example-waku-rsc](https://pkg.pr.new/template/7225e130-caa8-4c22-89f5-de4aec223ba2)
- [tanstack-router-react-example-with-bling](https://pkg.pr.new/template/f5c0a8c1-fd03-4717-ba3d-38d85e816fa1)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/215f49ef-1126-4723-aecb-d3b3c5ffb72c)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/3769680c-a57e-40ce-a99a-b75e07fc38ec)

</p></details>
      
    

chorobin: Left to do
[x]  - Zod input types
[  ] - Valibot input type
[  ] - Arktype input type
[  ] - Type tests
[  ] - Runtime tests
[  ] - Optimise types
schiller-manuel: I don't think putting zod specific (and other library specific stuff) into the core library. I would make this an optional package that a user of that specific library can install.
chorobin: > I don't think putting zod specific (and other library specific) stuff into the router core library. I would make this an optional package that a user of that specific library can install.

What is `zod` specific here? `ZodSearchValidator` is our own type which is 'zod like' but is not type from `zod`? Much like how we accepted an object with `parse` method. We did this to support `zod`, I am assuming.
schiller-manuel: You are right about the `parse` method, but it wasn't a good design IMO.

> What is zod specific here?

The usage of zod internal structure that might or might not change in the future.
Let's assume `_input` is renamed, would we update router? if yes, this would mean users who update router would also need to update zod. or we try to support old versions, but this will get complicated.

> I'm not sure about the extra package. How would this look? And how would you infer the types from it?

I haven't thought about this in detail. Can we use declaration merging to bring in "type logic" from a separate package into router?

One more thought:
I assume if we add support for various validation libraries, this would slow down type checking even though a user would likely only use a single one.
chorobin: > You are right about the `parse` method, but it wasn't a good design IMO.
> 
> > What is zod specific here?
> 
> The usage of zod internal structure that might or might not change in the future. Let's assume `_input` is renamed, would we update router? if yes, this would mean users who update router would also need to update zod. or we try to support old versions, but this will get complicated.
> 
> > I'm not sure about the extra package. How would this look? And how would you infer the types from it?
> 
> I haven't thought about this in detail. Can we use declaration merging to bring in "type logic" from a separate package into router?
> 
> One more thought: I assume if we add support for various validation libraries, this would slow down type checking even though a user would likely only use a single one.



> You are right about the `parse` method, but it wasn't a good design IMO.
> 
> > What is zod specific here?
> 
> The usage of zod internal structure that might or might not change in the future. Let's assume `_input` is renamed, would we update router? if yes, this would mean users who update router would also need to update zod. or we try to support old versions, but this will get complicated.
> 
> > I'm not sure about the extra package. How would this look? And how would you infer the types from it?
> 
> I haven't thought about this in detail. Can we use declaration merging to bring in "type logic" from a separate package into router?
> 
> One more thought: I assume if we add support for various validation libraries, this would slow down type checking even though a user would likely only use a single one.

Maybe there is a way with an extra package but this is definitely all tradeoffs, we either accept the validation schema directly or wrap it like this?

```tsx

import {zod} from '@tanstack/react-router-zod`

export const Route = createFileRoute('/search/searchPlaceholder')({
  component: SearchComponent,
  validateSearch: zod(z.object({
    searchPlaceholder: z.literal('searchPlaceholder'),
    page: z.number().optional().default(0),
    offset: z.number(),
    search: z.string(),
  })),
})
```


chorobin: > You are right about the `parse` method, but it wasn't a good design IMO.
> 
> > What is zod specific here?
> 
> The usage of zod internal structure that might or might not change in the future. Let's assume `_input` is renamed, would we update router? if yes, this would mean users who update router would also need to update zod. or we try to support old versions, but this will get complicated.
> 
> > I'm not sure about the extra package. How would this look? And how would you infer the types from it?
> 
> I haven't thought about this in detail. Can we use declaration merging to bring in "type logic" from a separate package into router?
> 
> One more thought: I assume if we add support for various validation libraries, this would slow down type checking even though a user would likely only use a single one.

In terms of performance theres. This is currently worse but is likely to get better if we move the resolving of `input` and `output` to later, not in a defaulted type parameter. 

Likely with the seperate package approach, TS still need to look inside an interface of a base type likely.

```
// validators can implement this base interface 
interface SearchValidator<TInput, TOutput> {
}

type ResolveSearchSchemaInput<TSearchValidator extends SearchValidator> = TSearchValidator extends SearchValidator<infer TInput, any> ? TInput : never
```
SeanCassiere: It's expected that the preview workflow will fail since the new package hasn't been published on npm yet.
chorobin: > Can we confirm that this doesn't break the type inference for anyone already `validateSearch` in its current form?
> 
> ```ts
> {
>   validateSearch: (inp) => searchSchema.parse(inp),
>   validateSearch: searchSchema,
>   validateSearch: (inp: Record<string, unknown>): SearchOutputType => {
>   // ...
>   return {} satisfies SearchOutputType
>   },  
> }
> ```

I do have type tests that are passing. I will check if I've covered all of it
schiller-manuel: a full blown example in router-examples would be nice
schiller-manuel: docs should also be added

Patch:
diff --git a/docs/framework/react/guide/search-params.md b/docs/framework/react/guide/search-params.md
index 54abc880f..abd516038 100644
--- a/docs/framework/react/guide/search-params.md
+++ b/docs/framework/react/guide/search-params.md
@@ -172,6 +172,150 @@ In the above example, we used Zod's `.catch()` modifier instead of `.default()`
 
 The underlying mechanics why this works relies on the `validateSearch` function throwing an error. If an error is thrown, the route's `onError` option will be triggered (and `error.routerCode` will be set to `VALIDATE_SEARCH` and the `errorComponent` will be rendered instead of the route's `component` where you can handle the search param error however you'd like.
 
+#### Adapters
+
+When using a library like [Zod](https://zod.dev/) to validate search params you might want to `transform` search params before committing the search params to the URL. A common `zod` `transform` is `default` for example.
+
+```tsx
+import { createFileRoute } from '@tanstack/react-router'
+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/')({
+  validateSearch: productSearchSchema,
+})
+```
+
+It might be surprising that when you try to navigate to this route, `search` is required. The following `Link` will type error as `search` is missing.
+
+```tsx
+<Link to="/shop/products" />
+```
+
+For validation libraries we recommend using adapters which infer the correct `input` and `output` types.
+
+### Zod Adapter
+
+An adapter is provided for [Zod](https://zod.dev/) which will pipe through the correct `input` type and `output` type
+
+```tsx
+import { createFileRoute } from '@tanstack/react-router'
+import { zodSearchValidator } from '@tanstack/router-zod-adapter'
+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/')({
+  validateSearch: zodSearchValidator(productSearchSchema),
+})
+```
+
+The important part here is the following use of `Link` no longer requires `search` params
+
+```tsx
+<Link to="/shop/products" />
+```
+
+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
+
+```tsx
+import { createFileRoute } from '@tanstack/react-router'
+import { fallback, zodSearchValidator } from '@tanstack/router-zod-adapter'
+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(
+    'newest',
+  ),
+})
+
+export const Route = createFileRoute('/shop/products/')({
+  validateSearch: zodSearchValidator(productSearchSchema),
+})
+```
+
+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
+
+```tsx
+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(
+    'newest',
+  ),
+})
+
+export const Route = createFileRoute('/shop/products/')({
+  validateSearch: zodSearchValidator({
+    schema: productSearchSchema,
+    input: 'output',
+    output: 'input',
+  }),
+})
+```
+
+This provides flexibility in which type you want to infer for navigation and which types you want to infer for reading search params.
+
+### Valibot Adapter
+
+When using [Valibot](https://valibot.dev/) we recommend using the adapter. This ensures the correct `input` and `output` types are used for navigation and reading search params
+
+```tsx
+import { valibotSearchValidator } from '@tanstack/router-valibot-adapter'
+import { createFileRoute } from '@tanstack/react-router'
+import * as v from 'valibot'
+
+const productSearchSchema = v.object({
+  page: v.optional(v.fallback(v.number(), 1), 1),
+  filter: v.optional(v.fallback(v.string(), ''), ''),
+  sort: v.optional(
+    v.fallback(v.picklist(['newest', 'oldest', 'price']), 'newest'),
+    'newest',
+  ),
+})
+
+export const Route = createFileRoute('/shop/products/')({
+  validateSearch: valibotSearchValidator(productSearchSchema),
+})
+```
+
+### Arktype Adapter
+
+[!WARNING]
+This adapter expects the arktype beta
+
+When using [ArkType](https://arktype.io/) we recommend using the adapter. This ensures the correct `input` and `output` types are used for navigation and reading search params
+
+```tsx
+import { arkTypeSearchValidator } from '@tanstack/router-arktype-adapter'
+import { createFileRoute } from '@tanstack/react-router'
+import * as v from 'valibot'
+import { type } from 'arktype'
+
+const productSearchSchema = type({
+  page: 'number = 1',
+  filter: 'string = ""',
+  sort: '"newest" | "oldest" | "price" = "newest"',
+})
+
+export const Route = createFileRoute('/shop/products/')({
+  validateSearch: arkTypeSearchValidator(productSearchSchema),
+})
+```
+
 ## Reading Search Params
 
 Once your search params have been validated and typed, you're finally ready to start reading and writing to them. There are a few ways to do this in TanStack Router, so let's check them out.
diff --git a/examples/react/authenticated-routes/package.json b/examples/react/authenticated-routes/package.json
index bd84102f6..b79ee6bd0 100644
--- a/examples/react/authenticated-routes/package.json
+++ b/examples/react/authenticated-routes/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/basic-default-search-params/package.json b/examples/react/basic-default-search-params/package.json
index 7e0b7b174..614df9549 100644
--- a/examples/react/basic-default-search-params/package.json
+++ b/examples/react/basic-default-search-params/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-file-based-codesplitting/package.json b/examples/react/basic-file-based-codesplitting/package.json
index 24f125528..e4f6912bd 100644
--- a/examples/react/basic-file-based-codesplitting/package.json
+++ b/examples/react/basic-file-based-codesplitting/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/basic-file-based/package.json b/examples/react/basic-file-based/package.json
index b753ab513..76645bd33 100644
--- a/examples/react/basic-file-based/package.json
+++ b/examples/react/basic-file-based/package.json
@@ -10,8 +10,8 @@
     "test:e2e": "playwright test --project=chromium"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/basic-react-query-file-based/package.json b/examples/react/basic-react-query-file-based/package.json
index 2d29329df..3311485b9 100644
--- a/examples/react/basic-react-query-file-based/package.json
+++ b/examples/react/basic-react-query-file-based/package.json
@@ -12,8 +12,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/basic-react-query/package.json b/examples/react/basic-react-query/package.json
index 39db4cfb1..e782fbd37 100644
--- a/examples/react/basic-react-query/package.json
+++ b/examples/react/basic-react-query/package.json
@@ -12,8 +12,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/basic-ssr-file-based/package.json b/examples/react/basic-ssr-file-based/package.json
index 2683de96c..850b46926 100644
--- a/examples/react/basic-ssr-file-based/package.json
+++ b/examples/react/basic-ssr-file-based/package.json
@@ -11,10 +11,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic-ssr-streaming-file-based/package.json b/examples/react/basic-ssr-streaming-file-based/package.json
index dd5f28218..4e258b147 100644
--- a/examples/react/basic-ssr-streaming-file-based/package.json
+++ b/examples/react/basic-ssr-streaming-file-based/package.json
@@ -11,10 +11,10 @@
     "debug": "node --inspect-brk server"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "get-port": "^7.1.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
diff --git a/examples/react/basic/package.json b/examples/react/basic/package.json
index e211ad55d..6978ea7d2 100644
--- a/examples/react/basic/package.json
+++ b/examples/react/basic/package.json
@@ -10,8 +10,8 @@
     "test:e2e": "playwright test --project=chromium"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "redaxios": "^0.5.1"
diff --git a/examples/react/deferred-data/package.json b/examples/react/deferred-data/package.json
index 0bab38847..aaadea8b9 100644
--- a/examples/react/deferred-data/package.json
+++ b/examples/react/deferred-data/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-file-based/package.json b/examples/react/kitchen-sink-file-based/package.json
index 9a1cef2dc..752552182 100644
--- a/examples/react/kitchen-sink-file-based/package.json
+++ b/examples/react/kitchen-sink-file-based/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query-file-based/package.json b/examples/react/kitchen-sink-react-query-file-based/package.json
index 1109ba514..a5bbf6b77 100644
--- a/examples/react/kitchen-sink-react-query-file-based/package.json
+++ b/examples/react/kitchen-sink-react-query-file-based/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink-react-query/package.json b/examples/react/kitchen-sink-react-query/package.json
index b805be205..99a40d402 100644
--- a/examples/react/kitchen-sink-react-query/package.json
+++ b/examples/react/kitchen-sink-react-query/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/kitchen-sink/package.json b/examples/react/kitchen-sink/package.json
index 750f7fb40..aff20e19e 100644
--- a/examples/react/kitchen-sink/package.json
+++ b/examples/react/kitchen-sink/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/large-file-based/package.json b/examples/react/large-file-based/package.json
index 0eea55651..e8cd90116 100644
--- a/examples/react/large-file-based/package.json
+++ b/examples/react/large-file-based/package.json
@@ -12,8 +12,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/location-masking/package.json b/examples/react/location-masking/package.json
index eed689573..40da22d8a 100644
--- a/examples/react/location-masking/package.json
+++ b/examples/react/location-masking/package.json
@@ -11,8 +11,8 @@
   "dependencies": {
     "@radix-ui/react-dialog": "^1.1.1",
     "@tanstack/react-query": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/navigation-blocking/package.json b/examples/react/navigation-blocking/package.json
index f57572475..1cde5b41a 100644
--- a/examples/react/navigation-blocking/package.json
+++ b/examples/react/navigation-blocking/package.json
@@ -10,8 +10,8 @@
   },
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
diff --git a/examples/react/quickstart-file-based/package.json b/examples/react/quickstart-file-based/package.json
index 27d1802c9..7e2d4dee2 100644
--- a/examples/react/quickstart-file-based/package.json
+++ b/examples/react/quickstart-file-based/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "immer": "^10.1.1",
     "react": "^18.2.0",
diff --git a/examples/react/quickstart-rspack-file-based/package.json b/examples/react/quickstart-rspack-file-based/package.json
index cf63fc7c3..3d5010144 100644
--- a/examples/react/quickstart-rspack-file-based/package.json
+++ b/examples/react/quickstart-rspack-file-based/package.json
@@ -8,8 +8,8 @@
     "preview": "rsbuild preview"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
   },
diff --git a/examples/react/quickstart-webpack-file-based/package.json b/examples/react/quickstart-webpack-file-based/package.json
index c36646588..d14f16fe9 100644
--- a/examples/react/quickstart-webpack-file-based/package.json
+++ b/examples/react/quickstart-webpack-file-based/package.json
@@ -7,8 +7,8 @@
     "build": "webpack build"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
   },
diff --git a/examples/react/quickstart/package.json b/examples/react/quickstart/package.json
index 74c72043e..020df6b3e 100644
--- a/examples/react/quickstart/package.json
+++ b/examples/react/quickstart/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/scroll-restoration/package.json b/examples/react/scroll-restoration/package.json
index ae237eb44..9c9833bf0 100644
--- a/examples/react/scroll-restoration/package.json
+++ b/examples/react/scroll-restoration/package.json
@@ -9,9 +9,9 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
     "@tanstack/react-virtual": "^3.8.4",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/router-devtools": "^1.47.5",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },
diff --git a/examples/react/search-validator-adapters/.gitignore b/examples/react/search-validator-adapters/.gitignore
new file mode 100644
index 000000000..d451ff16c
--- /dev/null
+++ b/examples/react/search-validator-adapters/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/examples/react/search-validator-adapters/README.md b/examples/react/search-validator-adapters/README.md
new file mode 100644
index 000000000..115199d29
--- /dev/null
+++ b/examples/react/search-validator-adapters/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm start` or `yarn start`
diff --git a/examples/react/search-validator-adapters/index.html b/examples/react/search-validator-adapters/index.html
new file mode 100644
index 000000000..a4b334943
--- /dev/null
+++ b/examples/react/search-validator-adapters/index.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite App</title>
+    <script src="https://cdn.tailwindcss.com"></script>
+    <style type="text/tailwindcss">
+      html {
+        color-scheme: light dark;
+      }
+      * {
+        @apply border-gray-200 dark:border-gray-800;
+      }
+      body {
+        @apply bg-gray-50 text-gray-950 dark:bg-gray-900 dark:text-gray-200;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>
diff --git a/examples/react/search-validator-adapters/package.json b/examples/react/search-validator-adapters/package.json
new file mode 100644
index 000000000..27e66f080
--- /dev/null
+++ b/examples/react/search-validator-adapters/package.json
@@ -0,0 +1,31 @@
+{
+  "name": "tanstack-router-search-validator-adapters",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --port=3001",
+    "build": "vite build",
+    "serve": "vite preview",
+    "start": "vite"
+  },
+  "dependencies": {
+    "@tanstack/react-router": "^1.47.1",
+    "@tanstack/router-devtools": "^^1.47.1",
+    "@tanstack/router-plugin": "^1.47.0",
+    "@tanstack/router-zod-adapter": "^1.45.11",
+    "@tanstack/router-valibot-adapter": "^1.45.11",
+    "@tanstack/router-arktype-adapter": "^1.45.11",
+    "@tanstack/react-query": "^5.51.21",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "zod": "^3.23.8",
+    "valibot": "^0.37.0",
+    "arktype": "^2.0.0-beta.5"
+  },
+  "devDependencies": {
+    "@types/react": "^18.2.47",
+    "@types/react-dom": "^18.2.18",
+    "@vitejs/plugin-react": "^4.3.1",
+    "vite": "^5.3.5"
+  }
+}
diff --git a/examples/react/search-validator-adapters/src/components/Content.tsx b/examples/react/search-validator-adapters/src/components/Content.tsx
new file mode 100644
index 000000000..b350a3035
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/components/Content.tsx
@@ -0,0 +1,9 @@
+export interface ContentProps {
+  readonly children: React.ReactNode
+}
+
+export const Content: React.FunctionComponent<ContentProps> = ({
+  children,
+}) => {
+  return <section className="my-2 flex flex-col gap-2">{children}</section>
+}
diff --git a/examples/react/search-validator-adapters/src/components/Header.tsx b/examples/react/search-validator-adapters/src/components/Header.tsx
new file mode 100644
index 000000000..ed2a9b38d
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/components/Header.tsx
@@ -0,0 +1,23 @@
+import { Link } from '@tanstack/react-router'
+import * as React from 'react'
+
+const HeaderLink: typeof Link = (props) => {
+  return <Link className="text-lg text-blue-700" {...props} />
+}
+
+export interface HeaderProps {
+  readonly title: string
+}
+
+export const Header: React.FunctionComponent<HeaderProps> = ({ title }) => {
+  return (
+    <header className="flex items-end gap-8 border-b-2 border-slate-150 py-4">
+      <h1 className="text-4xl">{title}</h1>
+      <nav className="flex gap-4">
+        <HeaderLink to="/users/zod">Zod</HeaderLink>
+        <HeaderLink to="/users/valibot">Valibot</HeaderLink>
+        <HeaderLink to="/users/arktype">Arktype</HeaderLink>
+      </nav>
+    </header>
+  )
+}
diff --git a/examples/react/search-validator-adapters/src/components/Search.tsx b/examples/react/search-validator-adapters/src/components/Search.tsx
new file mode 100644
index 000000000..a7e799aa3
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/components/Search.tsx
@@ -0,0 +1,18 @@
+export interface SearchProps {
+  search: string
+  onChange: (search: string) => void
+}
+
+export const Search = ({ search, onChange }: SearchProps) => {
+  return (
+    <div className="flex gap-2 align-center items-center">
+      <label>Search</label>
+      <input
+        type="search"
+        className="border p-1 px-2 rounded"
+        value={search}
+        onChange={(e) => onChange(e.target.value)}
+      ></input>
+    </div>
+  )
+}
diff --git a/examples/react/search-validator-adapters/src/components/Users.tsx b/examples/react/search-validator-adapters/src/components/Users.tsx
new file mode 100644
index 000000000..e6cb7eec6
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/components/Users.tsx
@@ -0,0 +1,84 @@
+import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
+
+export interface Geo {
+  lat: string
+  lng: string
+}
+
+export interface Address {
+  street: string
+  suite: string
+  city: string
+  zipcode: string
+  geo: Geo
+}
+
+export interface Company {
+  name: string
+  catchPhrase: string
+  bs: string
+}
+
+export interface User {
+  id: number
+  name: string
+  username: string
+  email: string
+  address: Address
+  phone: string
+  website: string
+  company: Company
+}
+
+export const getUsers = async (): Promise<Array<User>> => {
+  const response = await fetch('https://jsonplaceholder.typicode.com/users')
+  const users: Array<User> = await response.json()
+  return users
+}
+
+export const searchUsers = async (search: string): Promise<Array<User>> => {
+  const users = await getUsers()
+  const normalizedSearch = search.toLowerCase()
+  return users.filter(
+    (user) =>
+      user.name.toLowerCase().includes(normalizedSearch) ||
+      user.username.toLowerCase().includes(normalizedSearch),
+  )
+}
+
+export const usersQueryOptions = (search: string) =>
+  queryOptions({
+    queryKey: ['users', search],
+    queryFn: async () => {
+      return await searchUsers(search)
+    },
+  })
+
+export interface UsersProps {
+  search: string
+}
+
+export const Users = ({ search }: UsersProps) => {
+  const { data: users } = useSuspenseQuery(usersQueryOptions(search))
+
+  return (
+    <table className="table-auto">
+      <thead>
+        <tr className="border text-left">
+          <th className="p-4">Username</th>
+          <th className="p-4">Name</th>
+          <th className="p-4">Email</th>
+        </tr>
+      </thead>
+      <tbody>
+        {users.map((user) => (
+          <tr key={user.id} className="border">
+            <td className="p-4">{user.username}</td>
+            <td className="p-4">{user.name}</td>
+            <td className="p-4">{user.email}</td>
+          </tr>
+        ))}
+      </tbody>
+    </table>
+  )
+}
diff --git a/examples/react/search-validator-adapters/src/main.tsx b/examples/react/search-validator-adapters/src/main.tsx
new file mode 100644
index 000000000..07637f2b1
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/main.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react'
+import ReactDOM from 'react-dom/client'
+import { RouterProvider, createRouter } from '@tanstack/react-router'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { routeTree } from './routeTree.gen'
+
+const queryClient = new QueryClient()
+
+const router = createRouter({
+  routeTree,
+  context: {
+    queryClient,
+  },
+})
+
+declare module '@tanstack/react-router' {
+  interface Register {
+    router: typeof router
+  }
+}
+
+function App() {
+  return <RouterProvider router={router} />
+}
+
+const rootElement = document.getElementById('app')!
+if (!rootElement.innerHTML) {
+  const root = ReactDOM.createRoot(rootElement)
+  root.render(
+    <React.StrictMode>
+      <QueryClientProvider client={queryClient}>
+        <App />
+      </QueryClientProvider>
+    </React.StrictMode>,
+  )
+}
diff --git a/examples/react/search-validator-adapters/src/routeTree.gen.ts b/examples/react/search-validator-adapters/src/routeTree.gen.ts
new file mode 100644
index 000000000..1e35a2af1
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routeTree.gen.ts
@@ -0,0 +1,113 @@
+/* prettier-ignore-start */
+
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file is auto-generated by TanStack Router
+
+// Import Routes
+
+import { Route as rootRoute } from './routes/__root'
+import { Route as IndexImport } from './routes/index'
+import { Route as UsersZodIndexImport } from './routes/users/zod.index'
+import { Route as UsersValibotIndexImport } from './routes/users/valibot.index'
+import { Route as UsersArktypeIndexImport } from './routes/users/arktype.index'
+
+// Create/Update Routes
+
+const IndexRoute = IndexImport.update({
+  path: '/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const UsersZodIndexRoute = UsersZodIndexImport.update({
+  path: '/users/zod/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const UsersValibotIndexRoute = UsersValibotIndexImport.update({
+  path: '/users/valibot/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+const UsersArktypeIndexRoute = UsersArktypeIndexImport.update({
+  path: '/users/arktype/',
+  getParentRoute: () => rootRoute,
+} as any)
+
+// Populate the FileRoutesByPath interface
+
+declare module '@tanstack/react-router' {
+  interface FileRoutesByPath {
+    '/': {
+      id: '/'
+      path: '/'
+      fullPath: '/'
+      preLoaderRoute: typeof IndexImport
+      parentRoute: typeof rootRoute
+    }
+    '/users/arktype/': {
+      id: '/users/arktype/'
+      path: '/users/arktype'
+      fullPath: '/users/arktype'
+      preLoaderRoute: typeof UsersArktypeIndexImport
+      parentRoute: typeof rootRoute
+    }
+    '/users/valibot/': {
+      id: '/users/valibot/'
+      path: '/users/valibot'
+      fullPath: '/users/valibot'
+      preLoaderRoute: typeof UsersValibotIndexImport
+      parentRoute: typeof rootRoute
+    }
+    '/users/zod/': {
+      id: '/users/zod/'
+      path: '/users/zod'
+      fullPath: '/users/zod'
+      preLoaderRoute: typeof UsersZodIndexImport
+      parentRoute: typeof rootRoute
+    }
+  }
+}
+
+// Create and export the route tree
+
+export const routeTree = rootRoute.addChildren({
+  IndexRoute,
+  UsersArktypeIndexRoute,
+  UsersValibotIndexRoute,
+  UsersZodIndexRoute,
+})
+
+/* prettier-ignore-end */
+
+/* ROUTE_MANIFEST_START
+{
+  "routes": {
+    "__root__": {
+      "filePath": "__root.tsx",
+      "children": [
+        "/",
+        "/users/arktype/",
+        "/users/valibot/",
+        "/users/zod/"
+      ]
+    },
+    "/": {
+      "filePath": "index.tsx"
+    },
+    "/users/arktype/": {
+      "filePath": "users/arktype.index.tsx"
+    },
+    "/users/valibot/": {
+      "filePath": "users/valibot.index.tsx"
+    },
+    "/users/zod/": {
+      "filePath": "users/zod.index.tsx"
+    }
+  }
+}
+ROUTE_MANIFEST_END */
diff --git a/examples/react/search-validator-adapters/src/routes/__root.tsx b/examples/react/search-validator-adapters/src/routes/__root.tsx
new file mode 100644
index 000000000..d7f9d605a
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routes/__root.tsx
@@ -0,0 +1,20 @@
+import { type QueryClient } from '@tanstack/react-query'
+import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
+import { TanStackRouterDevtools } from '@tanstack/router-devtools'
+
+export interface Context {
+  queryClient: QueryClient
+}
+
+export const Route = createRootRouteWithContext<Context>()({
+  component: RootComponent,
+})
+
+function RootComponent() {
+  return (
+    <div className="m-4">
+      <Outlet />
+      <TanStackRouterDevtools />
+    </div>
+  )
+}
diff --git a/examples/react/search-validator-adapters/src/routes/index.tsx b/examples/react/search-validator-adapters/src/routes/index.tsx
new file mode 100644
index 000000000..c00341b61
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routes/index.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+import { Header } from '../components/Header'
+import { Content } from '../components/Content'
+
+export const Index: React.FunctionComponent = () => {
+  return (
+    <>
+      <Header title="Home" />
+      <Content>Welcome home</Content>
+    </>
+  )
+}
+
+export const Route = createFileRoute('/')({
+  component: Index,
+})
diff --git a/examples/react/search-validator-adapters/src/routes/users/arktype.index.tsx b/examples/react/search-validator-adapters/src/routes/users/arktype.index.tsx
new file mode 100644
index 000000000..970b4f89a
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routes/users/arktype.index.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+import { arkTypeSearchValidator } from '@tanstack/router-arktype-adapter'
+import { type } from 'arktype'
+import { Header } from '../../components/Header'
+import { Users, usersQueryOptions } from '../../components/Users'
+import { Content } from '../../components/Content'
+import { Search } from '../../components/Search'
+
+const ArkType = () => {
+  const search = Route.useSearch({
+    select: (search) => search.search ?? '',
+  })
+  const navigate = useNavigate({ from: Route.fullPath })
+
+  return (
+    <>
+      <Header title="ArkType" />
+      <Content>
+        <Search
+          search={search}
+          onChange={(search) => navigate({ search: { search }, replace: true })}
+        />
+        <React.Suspense>
+          <Users search={search} />
+        </React.Suspense>
+      </Content>
+    </>
+  )
+}
+
+export const Route = createFileRoute('/users/arktype/')({
+  validateSearch: arkTypeSearchValidator(
+    type({
+      'search?': 'string',
+    }),
+  ),
+  loaderDeps: (opt) => ({ search: opt.search }),
+  loader: (opt) => {
+    opt.context.queryClient.ensureQueryData(
+      usersQueryOptions(opt.deps.search.search ?? ''),
+    )
+  },
+  component: ArkType,
+})
diff --git a/examples/react/search-validator-adapters/src/routes/users/valibot.index.tsx b/examples/react/search-validator-adapters/src/routes/users/valibot.index.tsx
new file mode 100644
index 000000000..849e10c22
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routes/users/valibot.index.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+import { valibotSearchValidator } from '@tanstack/router-valibot-adapter'
+import * as v from 'valibot'
+import { Header } from '../../components/Header'
+import { Users, usersQueryOptions } from '../../components/Users'
+import { Content } from '../../components/Content'
+import { Search } from '../../components/Search'
+
+const Valibot = () => {
+  const search = Route.useSearch({
+    select: (search) => search.search ?? '',
+  })
+  const navigate = useNavigate({ from: Route.fullPath })
+
+  return (
+    <>
+      <Header title="Valibot" />
+      <Content>
+        <Search
+          search={search}
+          onChange={(search) => navigate({ search: { search }, replace: true })}
+        />
+        <React.Suspense>
+          <Users search={search} />
+        </React.Suspense>
+      </Content>
+    </>
+  )
+}
+
+export const Route = createFileRoute('/users/valibot/')({
+  validateSearch: valibotSearchValidator(
+    v.object({
+      search: v.fallback(v.optional(v.string()), undefined),
+    }),
+  ),
+  loaderDeps: (opt) => ({ search: opt.search }),
+  loader: (opt) => {
+    opt.context.queryClient.ensureQueryData(
+      usersQueryOptions(opt.deps.search.search ?? ''),
+    )
+  },
+  component: Valibot,
+})
diff --git a/examples/react/search-validator-adapters/src/routes/users/zod.index.tsx b/examples/react/search-validator-adapters/src/routes/users/zod.index.tsx
new file mode 100644
index 000000000..26fc32712
--- /dev/null
+++ b/examples/react/search-validator-adapters/src/routes/users/zod.index.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react'
+import { createFileRoute, useNavigate } from '@tanstack/react-router'
+import { fallback, zodSearchValidator } from '@tanstack/router-zod-adapter'
+import { z } from 'zod'
+import { Header } from '../../components/Header'
+import { Users, usersQueryOptions } from '../../components/Users'
+import { Content } from '../../components/Content'
+import { Search } from '../../components/Search'
+
+const Zod = () => {
+  const search = Route.useSearch({
+    select: (search) => search.search ?? '',
+  })
+  const navigate = useNavigate({ from: Route.fullPath })
+
+  return (
+    <>
+      <Header title="Zod" />
+      <Content>
+        <Search
+          search={search}
+          onChange={(search) => navigate({ search: { search }, replace: true })}
+        />
+        <React.Suspense>
+          <Users search={search} />
+        </React.Suspense>
+      </Content>
+    </>
+  )
+}
+
+export const Route = createFileRoute('/users/zod/')({
+  validateSearch: zodSearchValidator(
+    z.object({
+      search: fallback(z.string().optional(), undefined),
+    }),
+  ),
+  loaderDeps: (opt) => ({ search: opt.search }),
+  loader: (opt) => {
+    opt.context.queryClient.ensureQueryData(
+      usersQueryOptions(opt.deps.search.search ?? ''),
+    )
+  },
+  component: Zod,
+})
diff --git a/examples/react/search-validator-adapters/tsconfig.dev.json b/examples/react/search-validator-adapters/tsconfig.dev.json
new file mode 100644
index 000000000..285a09b0d
--- /dev/null
+++ b/examples/react/search-validator-adapters/tsconfig.dev.json
@@ -0,0 +1,10 @@
+{
+  "composite": true,
+  "extends": "../../../tsconfig.base.json",
+
+  "files": ["src/main.tsx"],
+  "include": [
+    "src"
+    // "__tests__/**/*.test.*"
+  ]
+}
diff --git a/examples/react/search-validator-adapters/tsconfig.json b/examples/react/search-validator-adapters/tsconfig.json
new file mode 100644
index 000000000..c9e17e2b6
--- /dev/null
+++ b/examples/react/search-validator-adapters/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "compilerOptions": {
+    "strict": true,
+    "esModuleInterop": true,
+    "jsx": "react-jsx"
+  }
+}
diff --git a/examples/react/search-validator-adapters/vite.config.js b/examples/react/search-validator-adapters/vite.config.js
new file mode 100644
index 000000000..9cb8cad86
--- /dev/null
+++ b/examples/react/search-validator-adapters/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [TanStackRouterVite(), react()],
+})
diff --git a/examples/react/start-basic-counter/package.json b/examples/react/start-basic-counter/package.json
index 00e8982f0..2a69d71c6 100644
--- a/examples/react/start-basic-counter/package.json
+++ b/examples/react/start-basic-counter/package.json
@@ -11,8 +11,8 @@
     "format": "prettier --write '**/*' --ignore-unknown"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/start": "^1.47.5",
     "@typescript-eslint/parser": "^7.18.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
diff --git a/examples/react/start-basic-react-query/package.json b/examples/react/start-basic-react-query/package.json
index e381ac1ee..2d0743aba 100644
--- a/examples/react/start-basic-react-query/package.json
+++ b/examples/react/start-basic-react-query/package.json
@@ -14,11 +14,11 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/react-router-with-query": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/react-router-with-query": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "@typescript-eslint/parser": "^7.18.0",
     "@vitejs/plugin-react": "^4.3.1",
     "isbot": "^5.1.14",
diff --git a/examples/react/start-basic-rsc/package.json b/examples/react/start-basic-rsc/package.json
index ee1885280..974cb16ee 100644
--- a/examples/react/start-basic-rsc/package.json
+++ b/examples/react/start-basic-rsc/package.json
@@ -12,10 +12,10 @@
   },
   "dependencies": {
     "@babel/plugin-syntax-typescript": "^7.24.7",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "redaxios": "^0.5.1",
     "tailwind-merge": "^2.4.0",
     "vinxi": "0.4.1"
diff --git a/examples/react/start-basic/package.json b/examples/react/start-basic/package.json
index 2a03cf4a6..e928033b9 100644
--- a/examples/react/start-basic/package.json
+++ b/examples/react/start-basic/package.json
@@ -12,10 +12,10 @@
     "test:e2e": "playwright test --project=chromium"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "@typescript-eslint/parser": "^7.18.0",
     "@vitejs/plugin-react": "^4.3.1",
     "isbot": "^5.1.14",
diff --git a/examples/react/start-convex-trellaux/package.json b/examples/react/start-convex-trellaux/package.json
index 691e82f4b..4551c70f2 100644
--- a/examples/react/start-convex-trellaux/package.json
+++ b/examples/react/start-convex-trellaux/package.json
@@ -15,11 +15,11 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/react-router-with-query": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/react-router-with-query": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "@typescript-eslint/parser": "^7.18.0",
     "@vitejs/plugin-react": "^4.3.1",
     "@convex-dev/react-query": "0.0.0-alpha.5",
diff --git a/examples/react/start-trellaux/package.json b/examples/react/start-trellaux/package.json
index 15249d87b..de1e09cde 100644
--- a/examples/react/start-trellaux/package.json
+++ b/examples/react/start-trellaux/package.json
@@ -13,11 +13,11 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/react-router-with-query": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/react-router-with-query": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
-    "@tanstack/start": "^1.47.3",
+    "@tanstack/start": "^1.47.5",
     "@typescript-eslint/parser": "^7.18.0",
     "@vitejs/plugin-react": "^4.3.1",
     "isbot": "^5.1.14",
diff --git a/examples/react/with-framer-motion/package.json b/examples/react/with-framer-motion/package.json
index fa47383d8..1a411f9b9 100644
--- a/examples/react/with-framer-motion/package.json
+++ b/examples/react/with-framer-motion/package.json
@@ -9,8 +9,8 @@
     "start": "vite"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "redaxios": "^0.5.1",
     "framer-motion": "^11.3.21",
     "immer": "^10.1.1",
diff --git a/examples/react/with-trpc-react-query/package.json b/examples/react/with-trpc-react-query/package.json
index 6c10bf581..212de38de 100644
--- a/examples/react/with-trpc-react-query/package.json
+++ b/examples/react/with-trpc-react-query/package.json
@@ -10,8 +10,8 @@
   "dependencies": {
     "@tanstack/react-query": "^5.51.21",
     "@tanstack/react-query-devtools": "^5.51.21",
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "@trpc/client": "11.0.0-rc.477",
     "@trpc/react-query": "11.0.0-rc.477",
diff --git a/examples/react/with-trpc/package.json b/examples/react/with-trpc/package.json
index b5b48d0f5..da017efb4 100644
--- a/examples/react/with-trpc/package.json
+++ b/examples/react/with-trpc/package.json
@@ -8,8 +8,8 @@
     "start": "vinxi start"
   },
   "dependencies": {
-    "@tanstack/react-router": "^1.47.1",
-    "@tanstack/router-devtools": "^1.47.1",
+    "@tanstack/react-router": "^1.47.5",
+    "@tanstack/router-devtools": "^1.47.5",
     "@tanstack/router-plugin": "^1.47.0",
     "@trpc/client": "11.0.0-rc.477",
     "@trpc/server": "11.0.0-rc.477",
diff --git a/package.json b/package.json
index 8cf726027..d097e9f39 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,9 @@
       "@tanstack/router-plugin": "workspace:*",
       "@tanstack/router-vite-plugin": "workspace:*",
       "@tanstack/react-router-with-query": "workspace:*",
+      "@tanstack/router-zod-adapter": "workspace:*",
+      "@tanstack/router-valibot-adapter": "workspace:*",
+      "@tanstack/router-arktype-adapter": "workspace:*",
       "@tanstack/start": "workspace:*",
       "@tanstack/start-vite-plugin": "workspace:*",
       "temp-react": "0.0.0-experimental-035a41c4e-20230704",
diff --git a/packages/react-router-with-query/package.json b/packages/react-router-with-query/package.json
index f9c391fba..49e99280f 100644
--- a/packages/react-router-with-query/package.json
+++ b/packages/react-router-with-query/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/react-router-with-query",
-  "version": "1.47.1",
+  "version": "1.47.5",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index f59ac9717..91f2528d6 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/react-router",
-  "version": "1.47.1",
+  "version": "1.47.5",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 7e6b4a8da..24cfe0681 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -111,75 +111,27 @@ export const MatchInner = React.memo(function MatchInnerImpl({
   matchId: string
 }): any {
   const router = useRouter()
-  const routeId = useRouterState({
-    select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
-  })
-
-  const route = router.routesById[routeId]!
-
-  const matchIndex = useRouterState({
-    select: (s) => {
-      return s.matches.findIndex((d) => d.id === matchId)
-    },
-  })
 
-  const match = useRouterState({
+  const { match, matchIndex, routeId } = useRouterState({
     select: (s) => {
+      const matchIndex = s.matches.findIndex((d) => d.id === matchId)
       const match = s.matches[matchIndex]!
-      return pick(match, [
-        'id',
-        'status',
-        'error',
-        'loadPromise',
-        'minPendingPromise',
-      ])
+      const routeId = match.routeId as string
+      return {
+        routeId,
+        matchIndex,
+        match: pick(match, ['id', 'status', 'error', 'loadPromise']),
+      }
     },
   })
 
+  const route = router.routesById[routeId]!
+
   const out = React.useMemo(() => {
     const Comp = route.options.component ?? router.options.defaultComponent
     return Comp ? <Comp key={routeId} /> : <Outlet />
   }, [routeId, route.options.component, router.options.defaultComponent])
 
-  React.useEffect(() => {
-    if (match.status === 'pending') {
-      // We're pending, and if we have a minPendingMs, we need to wait for it
-      const pendingMinMs =
-        route.options.pendingMinMs ?? router.options.defaultPendingMinMs
-
-      if (pendingMinMs && !match.minPendingPromise) {
-        // Create a promise that will resolve after the minPendingMs
-        if (!router.isServer) {
-          const minPendingPromise = createControlledPromise<void>()
-
-          router.updateMatch(match.id, (prev) => ({
-            ...prev,
-            minPendingPromise,
-          }))
-
-          const id = setTimeout(() => {
-            minPendingPromise.resolve()
-
-            // We've handled the minPendingPromise, so we can delete it
-            router.updateMatch(match.id, (prev) => ({
-              ...prev,
-              minPendingPromise: undefined,
-            }))
-          }, pendingMinMs)
-          return () => clearTimeout(id)
-        }
-      }
-    }
-    return undefined
-  }, [
-    match.id,
-    match.loadPromise,
-    match.minPendingPromise,
-    match.status,
-    route.options.pendingMinMs,
-    router,
-  ])
-
   // function useChangedDiff(value: any) {
   //   const ref = React.useRef(value)
   //   const changed = ref.current !== value
@@ -258,6 +210,33 @@ export const MatchInner = React.memo(function MatchInnerImpl({
   }
 
   if (match.status === 'pending') {
+    // We're pending, and if we have a minPendingMs, we need to wait for it
+    const pendingMinMs =
+      route.options.pendingMinMs ?? router.options.defaultPendingMinMs
+
+    if (pendingMinMs && !router.getMatch(match.id)?.minPendingPromise) {
+      // Create a promise that will resolve after the minPendingMs
+      if (!router.isServer) {
+        const minPendingPromise = createControlledPromise<void>()
+
+        Promise.resolve().then(() => {
+          router.updateMatch(match.id, (prev) => ({
+            ...prev,
+            minPendingPromise,
+          }))
+        })
+
+        setTimeout(() => {
+          minPendingPromise.resolve()
+
+          // We've handled the minPendingPromise, so we can delete it
+          router.updateMatch(match.id, (prev) => ({
+            ...prev,
+            minPendingPromise: undefined,
+          }))
+        }, pendingMinMs)
+      }
+    }
     throw match.loadPromise
   }
 
diff --git a/packages/react-router/src/fileRoute.ts b/packages/react-router/src/fileRoute.ts
index 640acda26..db78b6d0a 100644
--- a/packages/react-router/src/fileRoute.ts
+++ b/packages/react-router/src/fileRoute.ts
@@ -12,16 +12,14 @@ import type {
   AnyContext,
   AnyPathParams,
   AnyRoute,
-  AnySearchSchema,
+  AnySearchValidator,
+  DefaultSearchValidator,
   FileBaseRouteOptions,
   InferAllContext,
   ResolveAllContext,
   ResolveAllParamsFromParent,
-  ResolveFullSearchSchema,
-  ResolveFullSearchSchemaInput,
   ResolveLoaderData,
   ResolveRouteContext,
-  ResolveSearchSchemaUsed,
   Route,
   RouteConstraints,
   RouteContext,
@@ -75,17 +73,7 @@ export class FileRoute<
   }
 
   createRoute = <
-    TSearchSchemaInput = Record<string, unknown>,
-    TSearchSchema = {},
-    TSearchSchemaUsed = ResolveSearchSchemaUsed<
-      TSearchSchemaInput,
-      TSearchSchema
-    >,
-    TFullSearchSchemaInput = ResolveFullSearchSchemaInput<
-      TParentRoute,
-      TSearchSchemaUsed
-    >,
-    TFullSearchSchema = ResolveFullSearchSchema<TParentRoute, TSearchSchema>,
+    TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
     TParams = Record<ParsePathParams<TPath>, string>,
     TAllParams = ResolveAllParamsFromParent<TParentRoute, TParams>,
     TRouteContextReturn = RouteContext,
@@ -97,10 +85,9 @@ export class FileRoute<
     TChildren = unknown,
   >(
     options?: FileBaseRouteOptions<
+      TParentRoute,
       TPath,
-      TSearchSchemaInput,
-      TSearchSchema,
-      TFullSearchSchema,
+      TSearchValidator,
       TParams,
       TAllParams,
       TRouteContextReturn,
@@ -110,9 +97,10 @@ export class FileRoute<
       TLoaderDataReturn
     > &
       UpdatableRouteOptions<
+        TParentRoute,
         TId,
         TAllParams,
-        TFullSearchSchema,
+        TSearchValidator,
         TLoaderData,
         TAllContext,
         TRouteContext,
@@ -124,11 +112,7 @@ export class FileRoute<
     TFullPath,
     TFilePath,
     TId,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TSearchSchemaUsed,
-    TFullSearchSchemaInput,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -181,9 +165,10 @@ export function FileRouteLoader<
 
 export type LazyRouteOptions = Pick<
   UpdatableRouteOptions<
+    AnyRoute,
     string,
     AnyPathParams,
-    AnySearchSchema,
+    AnySearchValidator,
     {},
     AnyContext,
     AnyContext,
@@ -268,13 +253,6 @@ export function createLazyRoute<
 export function createLazyFileRoute<
   TFilePath extends keyof FileRoutesByPath,
   TRoute extends FileRoutesByPath[TFilePath]['preLoaderRoute'],
->(path: TFilePath) {
-  const id = removeGroups(path)
+>(id: TFilePath) {
   return (opts: LazyRouteOptions) => new LazyRoute<TRoute>({ id, ...opts })
 }
-
-const routeGroupPatternRegex = /\(.+\)/g
-
-function removeGroups(s: string) {
-  return s.replaceAll(routeGroupPatternRegex, '').replaceAll('//', '/')
-}
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 7e913537a..ea67ed7b9 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -139,6 +139,7 @@ export {
 export type {
   AnyPathParams,
   SearchSchemaInput,
+  SearchValidatorAdapter,
   AnySearchSchema,
   AnyContext,
   RouteContext,
@@ -154,9 +155,6 @@ export type {
   MetaDescriptor,
   RouteLinkEntry,
   ParseParamsFn,
-  SearchSchemaValidator,
-  SearchSchemaValidatorObj,
-  SearchSchemaValidatorFn,
   RouteLoaderFn,
   LoaderFnContext,
   SearchFilter,
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 4d994a039..376a3b3c8 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -54,9 +54,7 @@ export type RouteOptions<
   TParentRoute extends AnyRoute = AnyRoute,
   TCustomId extends string = string,
   TPath extends string = string,
-  TSearchSchemaInput = Record<string, unknown>,
-  TSearchSchema = {},
-  TFullSearchSchema = TSearchSchema,
+  TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TParams = AnyPathParams,
   TAllParams = TParams,
   TRouteContextReturn = RouteContext,
@@ -70,9 +68,7 @@ export type RouteOptions<
   TParentRoute,
   TCustomId,
   TPath,
-  TSearchSchemaInput,
-  TSearchSchema,
-  TFullSearchSchema,
+  TSearchValidator,
   TParams,
   TAllParams,
   TRouteContextReturn,
@@ -82,9 +78,10 @@ export type RouteOptions<
   TLoaderDataReturn
 > &
   UpdatableRouteOptions<
+    NoInfer<TParentRoute>,
     NoInfer<TCustomId>,
     NoInfer<TAllParams>,
-    NoInfer<TFullSearchSchema>,
+    NoInfer<TSearchValidator>,
     NoInfer<TLoaderData>,
     NoInfer<TAllContext>,
     NoInfer<TRouteContext>,
@@ -123,10 +120,9 @@ export interface FullSearchSchemaOption<TFullSearchSchema> {
 }
 
 export type FileBaseRouteOptions<
+  TParentRoute extends AnyRoute = AnyRoute,
   TPath extends string = string,
-  TSearchSchemaInput = Record<string, unknown>,
-  TSearchSchema = {},
-  TFullSearchSchema = TSearchSchema,
+  TSearchValidator extends AnySearchValidator = undefined,
   TParams = {},
   TAllParams = {},
   TRouteContextReturn = RouteContext,
@@ -135,22 +131,32 @@ export type FileBaseRouteOptions<
   TLoaderDeps extends Record<string, any> = {},
   TLoaderDataReturn = {},
 > = {
-  validateSearch?:
-    | ((input: TSearchSchemaInput) => TSearchSchema)
-    | { parse: (input: TSearchSchemaInput) => TSearchSchema }
+  validateSearch?: TSearchValidator
   shouldReload?:
     | boolean
     | ((
-        match: LoaderFnContext<TAllParams, TFullSearchSchema, TAllContext>,
+        match: LoaderFnContext<
+          TAllParams,
+          ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+          TAllContext
+        >,
       ) => any)
   // This async function is called before a route is loaded.
   // If an error is thrown here, the route's loader will not be called.
   // If thrown during a navigation, the navigation will be cancelled and the error will be passed to the `onError` function.
   // If thrown during a preload event, the error will be logged to the console.
   beforeLoad?: (
-    ctx: BeforeLoadContext<TFullSearchSchema, TAllParams, TParentAllContext>,
+    ctx: BeforeLoadContext<
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+      TAllParams,
+      TParentAllContext
+    >,
   ) => Promise<TRouteContextReturn> | TRouteContextReturn | void
-  loaderDeps?: (opts: FullSearchSchemaOption<TFullSearchSchema>) => TLoaderDeps
+  loaderDeps?: (
+    opts: FullSearchSchemaOption<
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>
+    >,
+  ) => TLoaderDeps
   loader?: (
     ctx: LoaderFnContext<TAllParams, TLoaderDeps, TAllContext>,
   ) => TLoaderDataReturn | Promise<TLoaderDataReturn>
@@ -160,9 +166,7 @@ export type BaseRouteOptions<
   TParentRoute extends AnyRoute = AnyRoute,
   TCustomId extends string = string,
   TPath extends string = string,
-  TSearchSchemaInput = Record<string, unknown>,
-  TSearchSchema = {},
-  TFullSearchSchema = TSearchSchema,
+  TSearchValidator extends AnySearchValidator = undefined,
   TParams = {},
   TAllParams = {},
   TRouteContextReturn = RouteContext,
@@ -172,10 +176,9 @@ export type BaseRouteOptions<
   TLoaderDataReturn = {},
 > = RoutePathOptions<TCustomId, TPath> &
   FileBaseRouteOptions<
+    TParentRoute,
     TPath,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -206,22 +209,14 @@ export interface BeforeLoadContext<
 }
 
 export type UpdatableRouteOptions<
+  TParentRoute extends AnyRoute,
   TRouteId,
   TAllParams,
-  TFullSearchSchema,
+  TSearchValidator extends AnySearchValidator,
   TLoaderData,
   TAllContext,
   TRouteContext,
   TLoaderDeps,
-  TRouteMatch = RouteMatch<
-    TRouteId,
-    TAllParams,
-    TFullSearchSchema,
-    TLoaderData,
-    TAllContext,
-    TRouteContext,
-    TLoaderDeps
-  >,
 > = {
   // test?: (args: TAllContext) => void
   // If true, this route will be matched as case-sensitive
@@ -241,20 +236,72 @@ export type UpdatableRouteOptions<
   preloadGcTime?: number
   // Filter functions that can manipulate search params *before* they are passed to links and navigate
   // calls that match this route.
-  preSearchFilters?: Array<SearchFilter<TFullSearchSchema>>
+  preSearchFilters?: Array<
+    SearchFilter<ResolveFullSearchSchema<TParentRoute, TSearchValidator>>
+  >
   // Filter functions that can manipulate search params *after* they are passed to links and navigate
   // calls that match this route.
-  postSearchFilters?: Array<SearchFilter<TFullSearchSchema>>
+  postSearchFilters?: Array<
+    SearchFilter<ResolveFullSearchSchema<TParentRoute, TSearchValidator>>
+  >
   onCatch?: (error: Error, errorInfo: React.ErrorInfo) => void
   onError?: (err: any) => void
   // These functions are called as route matches are loaded, stick around and leave the active
   // matches
-  onEnter?: (match: TRouteMatch) => void
-  onStay?: (match: TRouteMatch) => void
-  onLeave?: (match: TRouteMatch) => void
+  onEnter?: (
+    match: RouteMatch<
+      TRouteId,
+      TAllParams,
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+      TLoaderData,
+      TAllContext,
+      TRouteContext,
+      TLoaderDeps
+    >,
+  ) => void
+  onStay?: (
+    match: RouteMatch<
+      TRouteId,
+      TAllParams,
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+      TLoaderData,
+      TAllContext,
+      TRouteContext,
+      TLoaderDeps
+    >,
+  ) => void
+  onLeave?: (
+    match: RouteMatch<
+      TRouteId,
+      TAllParams,
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+      TLoaderData,
+      TAllContext,
+      TRouteContext,
+      TLoaderDeps
+    >,
+  ) => void
   meta?: (ctx: {
-    matches: Array<TRouteMatch>
-    match: TRouteMatch
+    matches: Array<
+      RouteMatch<
+        TRouteId,
+        TAllParams,
+        ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+        TLoaderData,
+        TAllContext,
+        TRouteContext,
+        TLoaderDeps
+      >
+    >
+    match: RouteMatch<
+      TRouteId,
+      TAllParams,
+      ResolveFullSearchSchema<TParentRoute, TSearchValidator>,
+      TLoaderData,
+      TAllContext,
+      TRouteContext,
+      TLoaderDeps
+    >
     params: TAllParams
     loaderData: TLoaderData
   }) => Array<React.JSX.IntrinsicElements['meta']>
@@ -291,18 +338,38 @@ type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray
 
 export type RouteLinkEntry = {}
 
-// The parse type here allows a zod schema to be passed directly to the validator
-export type SearchSchemaValidator<TInput, TReturn> =
-  | SearchSchemaValidatorObj<TInput, TReturn>
-  | SearchSchemaValidatorFn<TInput, TReturn>
+export interface SearchValidatorObj<TInput, TOutput> {
+  parse: SearchValidatorFn<TInput, TOutput>
+}
+
+export type AnySearchValidatorObj = SearchValidatorObj<any, any>
 
-export type SearchSchemaValidatorObj<TInput, TReturn> = {
-  parse?: SearchSchemaValidatorFn<TInput, TReturn>
+export interface SearchValidatorAdapter<TInput, TOutput> {
+  types: {
+    input: TInput
+    output: TOutput
+  }
+  parse: (input: unknown) => TOutput
 }
 
-export type SearchSchemaValidatorFn<TInput, TReturn> = (
-  searchObj: TInput,
-) => TReturn
+export type AnySearchValidatorAdapter = SearchValidatorAdapter<any, any>
+
+export type AnySearchValidatorFn = SearchValidatorFn<any, any>
+
+export type SearchValidatorFn<TInput, TOutput> = (input: TInput) => TOutput
+
+export type SearchValidator<TInput, TOutput> =
+  | SearchValidatorObj<TInput, TOutput>
+  | SearchValidatorFn<TInput, TOutput>
+  | SearchValidatorAdapter<TInput, TOutput>
+  | undefined
+
+export type AnySearchValidator = SearchValidator<any, any>
+
+export type DefaultSearchValidator = SearchValidator<
+  Record<string, unknown>,
+  AnySearchSchema
+>
 
 export type RouteLoaderFn<
   in out TAllParams = {},
@@ -375,20 +442,53 @@ export type InferAllContext<TRoute> = TRoute extends {
   ? TAllContext
   : {}
 
-export type ResolveSearchSchemaUsed<TSearchSchemaInput, TSearchSchema> =
-  TSearchSchemaInput extends SearchSchemaInput
+export type ResolveSearchSchemaFnInput<
+  TSearchValidator extends AnySearchValidator,
+> = TSearchValidator extends (input: infer TSearchSchemaInput) => any
+  ? TSearchSchemaInput extends SearchSchemaInput
     ? Omit<TSearchSchemaInput, keyof SearchSchemaInput>
-    : TSearchSchema
+    : ResolveSearchSchemaFn<TSearchValidator>
+  : AnySearchSchema
+
+export type ResolveSearchSchemaInput<
+  TSearchValidator extends AnySearchValidator,
+> = TSearchValidator extends AnySearchValidatorAdapter
+  ? TSearchValidator['types']['input']
+  : TSearchValidator extends AnySearchValidatorObj
+    ? ResolveSearchSchemaFnInput<TSearchValidator['parse']>
+    : ResolveSearchSchemaFnInput<TSearchValidator>
+
+export type ResolveSearchSchemaFn<TSearchValidator extends AnySearchValidator> =
+  TSearchValidator extends (...args: any) => infer TSearchSchema
+    ? TSearchSchema
+    : AnySearchSchema
+
+export type ResolveSearchSchema<TSearchValidator extends AnySearchValidator> =
+  unknown extends TSearchValidator
+    ? TSearchValidator
+    : TSearchValidator extends AnySearchValidatorAdapter
+      ? TSearchValidator['types']['output']
+      : TSearchValidator extends AnySearchValidatorObj
+        ? ResolveSearchSchemaFn<TSearchValidator['parse']>
+        : ResolveSearchSchemaFn<TSearchValidator>
 
 export type ResolveFullSearchSchema<
   TParentRoute extends AnyRoute,
-  TSearchSchema,
-> = Assign<InferFullSearchSchema<TParentRoute>, TSearchSchema>
+  TSearchValidator extends AnySearchValidator,
+> = unknown extends TParentRoute
+  ? ResolveSearchSchema<TSearchValidator>
+  : Assign<
+      InferFullSearchSchema<TParentRoute>,
+      ResolveSearchSchema<TSearchValidator>
+    >
 
 export type ResolveFullSearchSchemaInput<
   TParentRoute extends AnyRoute,
-  TSearchSchemaUsed,
-> = Assign<InferFullSearchSchemaInput<TParentRoute>, TSearchSchemaUsed>
+  TSearchValidator extends AnySearchValidator,
+> = Assign<
+  InferFullSearchSchemaInput<TParentRoute>,
+  ResolveSearchSchemaInput<TSearchValidator>
+>
 
 export type ResolveRouteContext<TRouteContextReturn> = [
   TRouteContextReturn,
@@ -423,10 +523,6 @@ export interface AnyRoute
     any,
     any,
     any,
-    any,
-    any,
-    any,
-    any,
     any
   > {}
 
@@ -443,12 +539,8 @@ export type AnyRouteWithContext<TContext> = Route<
   any,
   any,
   any,
-  any,
-  any,
   TContext,
   any,
-  any,
-  any,
   any
 >
 
@@ -581,20 +673,7 @@ export class Route<
     TCustomId,
     TPath
   >,
-  in out TSearchSchemaInput = Record<string, unknown>,
-  in out TSearchSchema = {},
-  in out TSearchSchemaUsed = ResolveSearchSchemaUsed<
-    TSearchSchemaInput,
-    TSearchSchema
-  >,
-  in out TFullSearchSchemaInput = ResolveFullSearchSchemaInput<
-    TParentRoute,
-    TSearchSchemaUsed
-  >,
-  in out TFullSearchSchema = ResolveFullSearchSchema<
-    TParentRoute,
-    TSearchSchema
-  >,
+  in out TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   in out TParams = Record<ParsePathParams<TPath>, string>,
   in out TAllParams = ResolveAllParamsFromParent<TParentRoute, TParams>,
   TRouteContextReturn = RouteContext,
@@ -610,9 +689,7 @@ export class Route<
     TParentRoute,
     TCustomId,
     TPath,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -648,9 +725,7 @@ export class Route<
       TParentRoute,
       TCustomId,
       TPath,
-      TSearchSchemaInput,
-      TSearchSchema,
-      TFullSearchSchema,
+      TSearchValidator,
       TParams,
       TAllParams,
       TRouteContextReturn,
@@ -679,11 +754,14 @@ export class Route<
     fullPath: TFullPath
     customId: TCustomId
     id: TId
-    searchSchema: TSearchSchema
-    searchSchemaInput: TSearchSchemaInput
-    searchSchemaUsed: TSearchSchemaUsed
-    fullSearchSchema: TFullSearchSchema
-    fullSearchSchemaInput: TFullSearchSchemaInput
+    searchSchema: ResolveSearchSchema<TSearchValidator>
+    searchSchemaInput: ResolveSearchSchemaInput<TSearchValidator>
+    searchValidator: TSearchValidator
+    fullSearchSchema: ResolveFullSearchSchema<TParentRoute, TSearchValidator>
+    fullSearchSchemaInput: ResolveFullSearchSchemaInput<
+      TParentRoute,
+      TSearchValidator
+    >
     params: TParams
     allParams: TAllParams
     routeContext: TRouteContext
@@ -701,9 +779,7 @@ export class Route<
           TParentRoute,
           TCustomId,
           TPath,
-          TSearchSchemaInput,
-          TSearchSchema,
-          TFullSearchSchema,
+          TSearchValidator,
           TParams,
           TAllParams,
           TRouteContextReturn,
@@ -778,11 +854,7 @@ export class Route<
     TFullPath,
     TCustomId,
     TId,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TSearchSchemaUsed,
-    TFullSearchSchemaInput,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -809,11 +881,7 @@ export class Route<
       TFullPath,
       TCustomId,
       TId,
-      TSearchSchemaInput,
-      TSearchSchema,
-      TSearchSchemaUsed,
-      TFullSearchSchemaInput,
-      TFullSearchSchema,
+      TSearchValidator,
       TParams,
       TAllParams,
       TRouteContextReturn,
@@ -827,9 +895,10 @@ export class Route<
 
   update = (
     options: UpdatableRouteOptions<
+      TParentRoute,
       TCustomId,
       TAllParams,
-      TFullSearchSchema,
+      TSearchValidator,
       TLoaderData,
       TAllContext,
       TRouteContext,
@@ -866,8 +935,12 @@ export class Route<
     })
   }
 
-  useSearch = <TSelected = Expand<TFullSearchSchema>>(opts?: {
-    select?: (search: Expand<TFullSearchSchema>) => TSelected
+  useSearch = <
+    TSelected = Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>>,
+  >(opts?: {
+    select?: (
+      search: Expand<ResolveFullSearchSchema<TParentRoute, TSearchValidator>>,
+    ) => TSelected
   }): TSelected => {
     return useSearch({ ...opts, from: this.id })
   }
@@ -908,17 +981,7 @@ export function createRoute<
     TCustomId,
     TPath
   >,
-  TSearchSchemaInput = Record<string, unknown>,
-  TSearchSchema = {},
-  TSearchSchemaUsed = ResolveSearchSchemaUsed<
-    TSearchSchemaInput,
-    TSearchSchema
-  >,
-  TFullSearchSchemaInput = ResolveFullSearchSchemaInput<
-    TParentRoute,
-    TSearchSchemaUsed
-  >,
-  TFullSearchSchema = ResolveFullSearchSchema<TParentRoute, TSearchSchema>,
+  TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TParams = Record<ParsePathParams<TPath>, string>,
   TAllParams = ResolveAllParamsFromParent<TParentRoute, TParams>,
   TRouteContextReturn = RouteContext,
@@ -933,9 +996,7 @@ export function createRoute<
     TParentRoute,
     TCustomId,
     TPath,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -953,11 +1014,7 @@ export function createRoute<
     TFullPath,
     TCustomId,
     TId,
-    TSearchSchemaInput,
-    TSearchSchema,
-    TSearchSchemaUsed,
-    TFullSearchSchemaInput,
-    TFullSearchSchema,
+    TSearchValidator,
     TParams,
     TAllParams,
     TRouteContextReturn,
@@ -973,8 +1030,7 @@ export function createRoute<
 export type AnyRootRoute = RootRoute<any, any, any, any, any, any, any, any>
 
 export type RootRouteOptions<
-  TSearchSchemaInput = {},
-  TSearchSchema = {},
+  TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TRouteContextReturn = RouteContext,
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
@@ -986,9 +1042,7 @@ export type RootRouteOptions<
     any, // TParentRoute
     RootRouteId, // TCustomId
     '', // TPath
-    TSearchSchemaInput, // TSearchSchemaInput
-    TSearchSchema, // TSearchSchema
-    TSearchSchema, // TFullSearchSchema
+    TSearchValidator,
     {}, // TParams
     {}, // TAllParams
     TRouteContextReturn, // TRouteContextReturn
@@ -1010,12 +1064,7 @@ export type RootRouteOptions<
 
 export function createRootRouteWithContext<TRouterContext extends {}>() {
   return <
-    TSearchSchemaInput = {},
-    TSearchSchema = {},
-    TSearchSchemaUsed = ResolveSearchSchemaUsed<
-      TSearchSchemaInput,
-      TSearchSchema
-    >,
+    TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
     TRouteContextReturn extends RouteContext = RouteContext,
     TRouteContext extends
       RouteContext = ResolveRouteContext<TRouteContextReturn>,
@@ -1024,8 +1073,7 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
     TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
   >(
     options?: RootRouteOptions<
-      TSearchSchemaInput,
-      TSearchSchema,
+      TSearchValidator,
       TRouteContextReturn,
       TRouteContext,
       TRouterContext,
@@ -1035,9 +1083,7 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
     >,
   ) => {
     return createRootRoute<
-      TSearchSchemaInput,
-      TSearchSchema,
-      TSearchSchemaUsed,
+      TSearchValidator,
       TRouteContextReturn,
       TRouteContext,
       TRouterContext,
@@ -1053,9 +1099,7 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
 export const rootRouteWithContext = createRootRouteWithContext
 
 export class RootRoute<
-  in out TSearchSchemaInput = {},
-  in out TSearchSchema = {},
-  in out TSearchSchemaUsed = {},
+  in out TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TRouteContextReturn = RouteContext,
   in out TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   in out TRouterContext = {},
@@ -1069,11 +1113,7 @@ export class RootRoute<
   '/', // TFullPath
   string, // TCustomId
   RootRouteId, // TId
-  TSearchSchemaInput, // TSearchSchemaInput
-  TSearchSchema, // TSearchSchema
-  TSearchSchemaUsed,
-  TSearchSchemaUsed, // TFullSearchSchemaInput
-  TSearchSchema, // TFullSearchSchema
+  TSearchValidator, // TSearchValidator
   {}, // TParams
   {}, // TAllParams
   TRouteContextReturn, // TRouteContextReturn
@@ -1089,8 +1129,7 @@ export class RootRoute<
    */
   constructor(
     options?: RootRouteOptions<
-      TSearchSchemaInput,
-      TSearchSchema,
+      TSearchValidator,
       TRouteContextReturn,
       TRouteContext,
       TRouterContext,
@@ -1109,9 +1148,7 @@ export class RootRoute<
   >(
     children: TNewChildren,
   ): RootRoute<
-    TSearchSchemaInput,
-    TSearchSchema,
-    TSearchSchemaUsed,
+    TSearchValidator,
     TRouteContextReturn,
     TRouteContext,
     TRouterContext,
@@ -1125,12 +1162,7 @@ export class RootRoute<
 }
 
 export function createRootRoute<
-  TSearchSchemaInput = {},
-  TSearchSchema = {},
-  TSearchSchemaUsed = ResolveSearchSchemaUsed<
-    TSearchSchemaInput,
-    TSearchSchema
-  >,
+  TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TRouteContextReturn = RouteContext,
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
@@ -1138,37 +1170,18 @@ export function createRootRoute<
   TLoaderDataReturn = {},
   TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
 >(
-  options?: Omit<
-    RouteOptions<
-      any, // TParentRoute
-      RootRouteId, // TCustomId
-      '', // TPath
-      TSearchSchemaInput, // TSearchSchemaInput
-      TSearchSchema, // TSearchSchema
-      TSearchSchema,
-      {}, // TParams
-      {}, // TAllParams
-      TRouteContextReturn, // TRouteContextReturn
-      TRouteContext, // TRouteContext
-      TRouterContext,
-      Assign<TRouterContext, TRouteContext>, // TAllContext
-      TLoaderDeps,
-      TLoaderDataReturn,
-      TLoaderData
-    >,
-    | 'path'
-    | 'id'
-    | 'getParentRoute'
-    | 'caseSensitive'
-    | 'parseParams'
-    | 'stringifyParams'
-    | 'params'
+  options?: RootRouteOptions<
+    TSearchValidator,
+    TRouteContextReturn,
+    TRouteContext,
+    TRouterContext,
+    TLoaderDeps,
+    TLoaderDataReturn,
+    TLoaderData
   >,
 ) {
   return new RootRoute<
-    TSearchSchemaInput,
-    TSearchSchema,
-    TSearchSchemaUsed,
+    TSearchValidator,
     TRouteContextReturn,
     TRouteContext,
     TRouterContext,
@@ -1276,14 +1289,7 @@ export type NotFoundRouteComponent = SyncRouteComponent<NotFoundRouteProps>
 
 export class NotFoundRoute<
   TParentRoute extends AnyRootRoute,
-  TSearchSchemaInput = Record<string, unknown>,
-  TSearchSchema = {},
-  TSearchSchemaUsed = {},
-  TFullSearchSchemaInput = ResolveFullSearchSchemaInput<
-    TParentRoute,
-    TSearchSchemaUsed
-  >,
-  TFullSearchSchema = ResolveFullSearchSchema<TParentRoute, TSearchSchema>,
+  TSearchValidator extends AnySearchValidator = DefaultSearchValidator,
   TRouteContextReturn = AnyContext,
   TRouteContext = RouteContext,
   TAllContext = ResolveAllContext<TParentRoute, TRouteContext>,
@@ -1297,11 +1303,7 @@ export class NotFoundRoute<
   '/404',
   '404',
   '404',
-  TSearchSchemaInput,
-  TSearchSchema,
-  TSearchSchemaUsed,
-  TFullSearchSchemaInput,
-  TFullSearchSchema,
+  TSearchValidator,
   {},
   {},
   TRouteContextReturn,
@@ -1318,9 +1320,7 @@ export class NotFoundRoute<
         TParentRoute,
         string,
         string,
-        TSearchSchemaInput,
-        TSearchSchema,
-        TFullSearchSchema,
+        TSearchValidator,
         {},
         {},
         TRouteContextReturn,
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 44af4400d..0c7b0ef46 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -120,8 +120,6 @@ export type HydrationCtx = {
 
 export type InferRouterContext<TRouteTree extends AnyRoute> =
   TRouteTree extends RootRoute<
-    any,
-    any,
     any,
     any,
     any,
diff --git a/packages/router-arktype-adapter/eslint.config.js b/packages/router-arktype-adapter/eslint.config.js
new file mode 100644
index 000000000..8ce6ad05f
--- /dev/null
+++ b/packages/router-arktype-adapter/eslint.config.js
@@ -0,0 +1,5 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [...rootConfig]
diff --git a/packages/router-arktype-adapter/package.json b/packages/router-arktype-adapter/package.json
new file mode 100644
index 000000000..a982fa7e0
--- /dev/null
+++ b/packages/router-arktype-adapter/package.json
@@ -0,0 +1,76 @@
+{
+  "name": "@tanstack/router-arktype-adapter",
+  "version": "1.45.11",
+  "description": "Modern and scalable routing for React applications",
+  "author": "Tanner Linsley",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/TanStack/router.git",
+    "directory": "packages/router-arktype-adapter"
+  },
+  "homepage": "https://tanstack.com/router",
+  "funding": {
+    "type": "github",
+    "url": "https://github.com/sponsors/tannerlinsley"
+  },
+  "keywords": [
+    "react",
+    "location",
+    "router",
+    "routing",
+    "async",
+    "async router",
+    "typescript"
+  ],
+  "scripts": {
+    "clean": "rimraf ./dist && rimraf ./coverage",
+    "test:eslint": "eslint ./src",
+    "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+    "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js",
+    "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
+    "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+    "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
+    "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+    "test:types:ts55": "tsc",
+    "test:unit": "vitest",
+    "test:unit:dev": "pnpm run test:unit --watch --typecheck",
+    "test:build": "publint --strict",
+    "build": "vite build"
+  },
+  "type": "module",
+  "types": "dist/esm/index.d.ts",
+  "main": "dist/cjs/index.cjs",
+  "module": "dist/esm/index.js",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/esm/index.d.ts",
+        "default": "./dist/esm/index.js"
+      },
+      "require": {
+        "types": "./dist/cjs/index.d.cts",
+        "default": "./dist/cjs/index.cjs"
+      }
+    },
+    "./package.json": "./package.json"
+  },
+  "sideEffects": false,
+  "files": [
+    "dist",
+    "src"
+  ],
+  "engines": {
+    "node": ">=12"
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^6.4.6",
+    "@testing-library/react": "^16.0.0",
+    "@tanstack/react-router": "workspace:*",
+    "arktype": "^2.0.0-beta.2"
+  },
+  "peerDependencies": {
+    "arktype": ">=2.0.0-beta <3",
+    "@tanstack/react-router": ">=1.43.2"
+  }
+}
diff --git a/packages/router-arktype-adapter/src/index.ts b/packages/router-arktype-adapter/src/index.ts
new file mode 100644
index 000000000..2a77d9210
--- /dev/null
+++ b/packages/router-arktype-adapter/src/index.ts
@@ -0,0 +1,22 @@
+import { type SearchValidatorAdapter } from '@tanstack/react-router'
+
+export interface ArkTypeLike {
+  infer: any
+  inferIn: any
+  assert: (input: any) => any
+}
+
+export type ArkTypeSearchValidatorAdapter<TOptions extends ArkTypeLike> =
+  SearchValidatorAdapter<TOptions['inferIn'], TOptions['infer']>
+
+export const arkTypeSearchValidator = <TOptions extends ArkTypeLike>(
+  options: TOptions,
+): ArkTypeSearchValidatorAdapter<TOptions> => {
+  return {
+    types: {
+      input: options.inferIn,
+      output: options.infer,
+    },
+    parse: (input) => options.assert(input),
+  }
+}
diff --git a/packages/router-arktype-adapter/tsconfig.json b/packages/router-arktype-adapter/tsconfig.json
new file mode 100644
index 000000000..041d9fe35
--- /dev/null
+++ b/packages/router-arktype-adapter/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "jsx": "react-jsx"
+  },
+  "include": ["src", "tests", "vite.config.ts"]
+}
diff --git a/packages/router-arktype-adapter/vite.config.ts b/packages/router-arktype-adapter/vite.config.ts
new file mode 100644
index 000000000..5edc0264c
--- /dev/null
+++ b/packages/router-arktype-adapter/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import packageJson from './package.json'
+
+const config = defineConfig({
+  test: {
+    name: packageJson.name,
+    dir: './tests',
+    watch: false,
+    environment: 'jsdom',
+    typecheck: { enabled: true },
+  },
+})
+
+export default mergeConfig(
+  config,
+  tanstackViteConfig({
+    entry: './src/index.ts',
+    srcDir: './src',
+  }),
+)
diff --git a/packages/router-devtools/package.json b/packages/router-devtools/package.json
index 946f2b02f..f13bc9450 100644
--- a/packages/router-devtools/package.json
+++ b/packages/router-devtools/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/router-devtools",
-  "version": "1.47.1",
+  "version": "1.47.5",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/packages/router-valibot-adapter/eslint.config.js b/packages/router-valibot-adapter/eslint.config.js
new file mode 100644
index 000000000..8ce6ad05f
--- /dev/null
+++ b/packages/router-valibot-adapter/eslint.config.js
@@ -0,0 +1,5 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [...rootConfig]
diff --git a/packages/router-valibot-adapter/package.json b/packages/router-valibot-adapter/package.json
new file mode 100644
index 000000000..bb771664a
--- /dev/null
+++ b/packages/router-valibot-adapter/package.json
@@ -0,0 +1,76 @@
+{
+  "name": "@tanstack/router-valibot-adapter",
+  "version": "1.45.11",
+  "description": "Modern and scalable routing for React applications",
+  "author": "Tanner Linsley",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/TanStack/router.git",
+    "directory": "packages/router-valibot-adapter"
+  },
+  "homepage": "https://tanstack.com/router",
+  "funding": {
+    "type": "github",
+    "url": "https://github.com/sponsors/tannerlinsley"
+  },
+  "keywords": [
+    "react",
+    "location",
+    "router",
+    "routing",
+    "async",
+    "async router",
+    "typescript"
+  ],
+  "scripts": {
+    "clean": "rimraf ./dist && rimraf ./coverage",
+    "test:eslint": "eslint ./src",
+    "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+    "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js",
+    "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
+    "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+    "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
+    "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+    "test:types:ts55": "tsc",
+    "test:unit": "vitest",
+    "test:unit:dev": "pnpm run test:unit --watch --typecheck",
+    "test:build": "publint --strict",
+    "build": "vite build"
+  },
+  "type": "module",
+  "types": "dist/esm/index.d.ts",
+  "main": "dist/cjs/index.cjs",
+  "module": "dist/esm/index.js",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/esm/index.d.ts",
+        "default": "./dist/esm/index.js"
+      },
+      "require": {
+        "types": "./dist/cjs/index.d.cts",
+        "default": "./dist/cjs/index.cjs"
+      }
+    },
+    "./package.json": "./package.json"
+  },
+  "sideEffects": false,
+  "files": [
+    "dist",
+    "src"
+  ],
+  "engines": {
+    "node": ">=12"
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^6.4.6",
+    "@testing-library/react": "^16.0.0",
+    "@tanstack/react-router": "workspace:*",
+    "valibot": "^0.36.0"
+  },
+  "peerDependencies": {
+    "valibot": ">=0.36.0",
+    "@tanstack/react-router": ">=1.43.2"
+  }
+}
diff --git a/packages/router-valibot-adapter/src/index.ts b/packages/router-valibot-adapter/src/index.ts
new file mode 100644
index 000000000..6d904f8e7
--- /dev/null
+++ b/packages/router-valibot-adapter/src/index.ts
@@ -0,0 +1,20 @@
+import { type SearchValidatorAdapter } from '@tanstack/react-router'
+import { type GenericSchema, parse } from 'valibot'
+
+export type ValibotSearchValidatorAdapter<TOptions extends GenericSchema> =
+  SearchValidatorAdapter<
+    NonNullable<TOptions['_types']>['input'],
+    NonNullable<TOptions['_types']>['output']
+  >
+
+export const valibotSearchValidator = <TOptions extends GenericSchema>(
+  options: TOptions,
+): ValibotSearchValidatorAdapter<TOptions> => {
+  return {
+    types: {
+      input: options._types?.input,
+      output: options._types?.output,
+    },
+    parse: (input) => parse(options, input),
+  }
+}
diff --git a/packages/router-valibot-adapter/tsconfig.json b/packages/router-valibot-adapter/tsconfig.json
new file mode 100644
index 000000000..041d9fe35
--- /dev/null
+++ b/packages/router-valibot-adapter/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "jsx": "react-jsx"
+  },
+  "include": ["src", "tests", "vite.config.ts"]
+}
diff --git a/packages/router-valibot-adapter/vite.config.ts b/packages/router-valibot-adapter/vite.config.ts
new file mode 100644
index 000000000..5edc0264c
--- /dev/null
+++ b/packages/router-valibot-adapter/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import packageJson from './package.json'
+
+const config = defineConfig({
+  test: {
+    name: packageJson.name,
+    dir: './tests',
+    watch: false,
+    environment: 'jsdom',
+    typecheck: { enabled: true },
+  },
+})
+
+export default mergeConfig(
+  config,
+  tanstackViteConfig({
+    entry: './src/index.ts',
+    srcDir: './src',
+  }),
+)
diff --git a/packages/router-zod-adapter/eslint.config.js b/packages/router-zod-adapter/eslint.config.js
new file mode 100644
index 000000000..8ce6ad05f
--- /dev/null
+++ b/packages/router-zod-adapter/eslint.config.js
@@ -0,0 +1,5 @@
+// @ts-check
+
+import rootConfig from '../../eslint.config.js'
+
+export default [...rootConfig]
diff --git a/packages/router-zod-adapter/package.json b/packages/router-zod-adapter/package.json
new file mode 100644
index 000000000..71fdbc331
--- /dev/null
+++ b/packages/router-zod-adapter/package.json
@@ -0,0 +1,76 @@
+{
+  "name": "@tanstack/router-zod-adapter",
+  "version": "1.45.11",
+  "description": "Modern and scalable routing for React applications",
+  "author": "Tanner Linsley",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/TanStack/router.git",
+    "directory": "packages/router-zod-adapter"
+  },
+  "homepage": "https://tanstack.com/router",
+  "funding": {
+    "type": "github",
+    "url": "https://github.com/sponsors/tannerlinsley"
+  },
+  "keywords": [
+    "react",
+    "location",
+    "router",
+    "routing",
+    "async",
+    "async router",
+    "typescript"
+  ],
+  "scripts": {
+    "clean": "rimraf ./dist && rimraf ./coverage",
+    "test:eslint": "eslint ./src",
+    "test:types": "pnpm run \"/^test:types:ts[0-9]{2}$/\"",
+    "test:types:ts50": "node ../../node_modules/typescript50/lib/tsc.js",
+    "test:types:ts51": "node ../../node_modules/typescript51/lib/tsc.js",
+    "test:types:ts52": "node ../../node_modules/typescript52/lib/tsc.js",
+    "test:types:ts53": "node ../../node_modules/typescript53/lib/tsc.js",
+    "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js",
+    "test:types:ts55": "tsc",
+    "test:unit": "vitest",
+    "test:unit:dev": "pnpm run test:unit --watch --typecheck",
+    "test:build": "publint --strict",
+    "build": "vite build"
+  },
+  "type": "module",
+  "types": "dist/esm/index.d.ts",
+  "main": "dist/cjs/index.cjs",
+  "module": "dist/esm/index.js",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/esm/index.d.ts",
+        "default": "./dist/esm/index.js"
+      },
+      "require": {
+        "types": "./dist/cjs/index.d.cts",
+        "default": "./dist/cjs/index.cjs"
+      }
+    },
+    "./package.json": "./package.json"
+  },
+  "sideEffects": false,
+  "files": [
+    "dist",
+    "src"
+  ],
+  "engines": {
+    "node": ">=12"
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^6.4.6",
+    "@testing-library/react": "^16.0.0",
+    "@tanstack/react-router": "workspace:*",
+    "zod": "^3.23.8"
+  },
+  "peerDependencies": {
+    "zod": ">=3",
+    "@tanstack/react-router": ">=1.43.2"
+  }
+}
diff --git a/packages/router-zod-adapter/src/index.ts b/packages/router-zod-adapter/src/index.ts
new file mode 100644
index 000000000..4151d0f2b
--- /dev/null
+++ b/packages/router-zod-adapter/src/index.ts
@@ -0,0 +1,72 @@
+import { type SearchValidatorAdapter } from '@tanstack/react-router'
+import { z } from 'zod'
+
+export interface ZodTypeLike {
+  _input: any
+  _output: any
+  parse: (input: any) => any
+}
+
+export type InputOutputOption = 'input' | 'output'
+
+export interface ZodSearchValidatorOptions {
+  readonly schema: ZodTypeLike
+  readonly input?: InputOutputOption
+  readonly output?: InputOutputOption
+}
+
+export type ZodSearchValidatorInput<
+  TOptions extends ZodTypeLike | ZodSearchValidatorOptions,
+> = TOptions extends ZodSearchValidatorOptions
+  ? 'input' extends TOptions['input']
+    ? TOptions['schema']['_input']
+    : TOptions['schema']['_output']
+  : TOptions extends ZodTypeLike
+    ? TOptions['_input']
+    : never
+
+export type ZodSearchValidatorOutput<
+  TOptions extends ZodTypeLike | ZodSearchValidatorOptions,
+> = TOptions extends ZodSearchValidatorOptions
+  ? 'output' extends TOptions['output']
+    ? TOptions['schema']['_output']
+    : TOptions['schema']['_input']
+  : TOptions extends ZodTypeLike
+    ? TOptions['_output']
+    : never
+
+export type ZodSearchValidatorAdapter<
+  TOptions extends ZodTypeLike | ZodSearchValidatorOptions,
+> = SearchValidatorAdapter<
+  ZodSearchValidatorInput<TOptions>,
+  ZodSearchValidatorOutput<TOptions>
+>
+
+export const zodSearchValidator = <
+  TOptions extends ZodTypeLike | ZodSearchValidatorOptions,
+>(
+  options: TOptions,
+): ZodSearchValidatorAdapter<TOptions> => {
+  const input = 'input' in options ? options.input : 'input'
+  const output = 'output' in options ? options.output : 'output'
+  const _input = 'schema' in options ? options.schema._input : options._input
+  const _output = 'schema' in options ? options.schema._output : options._output
+  return {
+    types: {
+      input: input === 'output' ? _output : _input,
+      output: output === 'input' ? _input : _output,
+    },
+    parse: (input) =>
+      'schema' in options ? options.schema.parse(input) : options.parse(input),
+  }
+}
+
+export const fallback = <TSchema extends z.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))
+}
diff --git a/packages/router-zod-adapter/tsconfig.json b/packages/router-zod-adapter/tsconfig.json
new file mode 100644
index 000000000..041d9fe35
--- /dev/null
+++ b/packages/router-zod-adapter/tsconfig.json
@@ -0,0 +1,7 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "jsx": "react-jsx"
+  },
+  "include": ["src", "tests", "vite.config.ts"]
+}
diff --git a/packages/router-zod-adapter/vite.config.ts b/packages/router-zod-adapter/vite.config.ts
new file mode 100644
index 000000000..5edc0264c
--- /dev/null
+++ b/packages/router-zod-adapter/vite.config.ts
@@ -0,0 +1,21 @@
+import { defineConfig, mergeConfig } from 'vitest/config'
+import { tanstackViteConfig } from '@tanstack/config/vite'
+import packageJson from './package.json'
+
+const config = defineConfig({
+  test: {
+    name: packageJson.name,
+    dir: './tests',
+    watch: false,
+    environment: 'jsdom',
+    typecheck: { enabled: true },
+  },
+})
+
+export default mergeConfig(
+  config,
+  tanstackViteConfig({
+    entry: './src/index.ts',
+    srcDir: './src',
+  }),
+)
diff --git a/packages/start/package.json b/packages/start/package.json
index 0ea709274..298fbf873 100644
--- a/packages/start/package.json
+++ b/packages/start/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@tanstack/start",
-  "version": "1.47.3",
+  "version": "1.47.5",
   "description": "Modern and scalable routing for React applications",
   "author": "Tanner Linsley",
   "license": "MIT",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index be7d8b597..af70eff13 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,6 +16,9 @@ overrides:
   '@tanstack/router-plugin': workspace:*
   '@tanstack/router-vite-plugin': workspace:*
   '@tanstack/react-router-with-query': workspace:*
+  '@tanstack/router-zod-adapter': workspace:*
+  '@tanstack/router-valibot-adapter': workspace:*
+  '@tanstack/router-arktype-adapter': workspace:*
   '@tanstack/start': workspace:*
   '@tanstack/start-vite-plugin': workspace:*
   temp-react: 0.0.0-experimental-035a41c4e-20230704
@@ -57,7 +60,7 @@ importers:
         version: 10.4.5
       nx:
         specifier: ^19.5.6
-        version: 19.5.6(@swc/core@1.7.6)
+        version: 19.5.6(@swc/core@1.7.6(@swc/helpers@0.5.3))
       prettier:
         specifier: ^3.3.3
         version: 3.3.3
@@ -1011,6 +1014,58 @@ importers:
         specifier: ^5.3.5
         version: 5.3.5(@types/node@20.14.9)(terser@5.31.1)
 
+  examples/react/search-validator-adapters:
+    dependencies:
+      '@tanstack/react-query':
+        specifier: ^5.51.21
+        version: 5.51.21(react@18.3.1)
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../../../packages/react-router
+      '@tanstack/router-arktype-adapter':
+        specifier: workspace:*
+        version: link:../../../packages/router-arktype-adapter
+      '@tanstack/router-devtools':
+        specifier: workspace:*
+        version: link:../../../packages/router-devtools
+      '@tanstack/router-plugin':
+        specifier: workspace:*
+        version: link:../../../packages/router-plugin
+      '@tanstack/router-valibot-adapter':
+        specifier: workspace:*
+        version: link:../../../packages/router-valibot-adapter
+      '@tanstack/router-zod-adapter':
+        specifier: workspace:*
+        version: link:../../../packages/router-zod-adapter
+      arktype:
+        specifier: ^2.0.0-beta.5
+        version: 2.0.0-beta.5
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
+      valibot:
+        specifier: ^0.37.0
+        version: 0.37.0(typescript@5.5.3)
+      zod:
+        specifier: ^3.23.8
+        version: 3.23.8
+    devDependencies:
+      '@types/react':
+        specifier: ^18.2.47
+        version: 18.3.3
+      '@types/react-dom':
+        specifier: ^18.2.18
+        version: 18.3.0
+      '@vitejs/plugin-react':
+        specifier: ^4.3.1
+        version: 4.3.1(vite@5.3.5(@types/node@20.14.9)(terser@5.31.1))
+      vite:
+        specifier: ^5.3.5
+        version: 5.3.5(@types/node@20.14.9)(terser@5.31.1)
+
   examples/react/start-basic:
     dependencies:
       '@tanstack/react-router':
@@ -1698,6 +1753,21 @@ importers:
         specifier: '>=18'
         version: 18.3.1(react@18.3.1)
 
+  packages/router-arktype-adapter:
+    devDependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../react-router
+      '@testing-library/jest-dom':
+        specifier: ^6.4.6
+        version: 6.4.8
+      '@testing-library/react':
+        specifier: ^16.0.0
+        version: 16.0.0(@testing-library/dom@10.0.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      arktype:
+        specifier: ^2.0.0-beta.2
+        version: 2.0.0-beta.5
+
   packages/router-cli:
     dependencies:
       '@tanstack/router-generator':
@@ -1803,7 +1873,7 @@ importers:
         version: 1.11.0
       webpack:
         specifier: '>=5.92.0'
-        version: 5.93.0(@swc/core@1.7.6)(esbuild@0.21.5)
+        version: 5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)
       zod:
         specifier: ^3.23.8
         version: 3.23.8
@@ -1812,6 +1882,21 @@ importers:
         specifier: ^5.3.5
         version: 5.3.5(@types/node@20.14.9)(terser@5.31.1)
 
+  packages/router-valibot-adapter:
+    devDependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../react-router
+      '@testing-library/jest-dom':
+        specifier: ^6.4.6
+        version: 6.4.8
+      '@testing-library/react':
+        specifier: ^16.0.0
+        version: 16.0.0(@testing-library/dom@10.0.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      valibot:
+        specifier: ^0.36.0
+        version: 0.36.0
+
   packages/router-vite-plugin:
     dependencies:
       '@tanstack/router-plugin':
@@ -1822,6 +1907,21 @@ importers:
         specifier: ^5.3.5
         version: 5.3.5(@types/node@20.14.9)(terser@5.31.1)
 
+  packages/router-zod-adapter:
+    devDependencies:
+      '@tanstack/react-router':
+        specifier: workspace:*
+        version: link:../react-router
+      '@testing-library/jest-dom':
+        specifier: ^6.4.6
+        version: 6.4.8
+      '@testing-library/react':
+        specifier: ^16.0.0
+        version: 16.0.0(@testing-library/dom@10.0.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      zod:
+        specifier: ^3.23.8
+        version: 3.23.8
+
   packages/start:
     dependencies:
       '@tanstack/react-cross-context':
@@ -1957,6 +2057,12 @@ packages:
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
 
+  '@ark/schema@0.3.2':
+    resolution: {integrity: sha512-StWlBec++TxgDgpEUAkOxB7rqCZmOJr1dsW0P44vcgoitGtlQN13eCW1MlHlzScTUWA5iOtyHD+TsA0gKm4X0w==}
+
+  '@ark/util@0.2.1':
+    resolution: {integrity: sha512-V+h43ZoXCT79K0OiwRE7oebcIeVHZ8YL/Xf0mCUCoIEuWnAJOS8zPwQzwGJGwys7MFHgn3YQYWL/nETuEvgcpQ==}
+
   '@babel/code-frame@7.24.7':
     resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
     engines: {node: '>=6.9.0'}
@@ -4869,6 +4975,9 @@ packages:
   aria-query@5.3.0:
     resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
 
+  arktype@2.0.0-beta.5:
+    resolution: {integrity: sha512-3bNUyiA2iuvyV/rfiErLli9HrrvC6W4XT8Ja6SAEihljM60PRQhYJEr5jM4MGGtj1k/oFxgOkHd++VjQrm6SoA==}
+
   array-buffer-byte-length@1.0.1:
     resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
     engines: {node: '>= 0.4'}
@@ -8861,6 +8970,17 @@ packages:
     resolution: {integrity: sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==}
     engines: {node: '>= 10.13.0'}
 
+  valibot@0.36.0:
+    resolution: {integrity: sha512-CjF1XN4sUce8sBK9TixrDqFM7RwNkuXdJu174/AwmQUB62QbCQADg5lLe8ldBalFgtj1uKj+pKwDJiNo4Mn+eQ==}
+
+  valibot@0.37.0:
+    resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==}
+    peerDependencies:
+      typescript: '>=5'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   validator@13.12.0:
     resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
     engines: {node: '>= 0.10'}
@@ -9218,6 +9338,12 @@ snapshots:
       '@jridgewell/gen-mapping': 0.3.5
       '@jridgewell/trace-mapping': 0.3.25
 
+  '@ark/schema@0.3.2':
+    dependencies:
+      '@ark/util': 0.2.1
+
+  '@ark/util@0.2.1': {}
+
   '@babel/code-frame@7.24.7':
     dependencies:
       '@babel/highlight': 7.24.7
@@ -10772,9 +10898,9 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.17.1
 
-  '@nrwl/tao@19.5.6(@swc/core@1.7.6)':
+  '@nrwl/tao@19.5.6(@swc/core@1.7.6(@swc/helpers@0.5.3))':
     dependencies:
-      nx: 19.5.6(@swc/core@1.7.6)
+      nx: 19.5.6(@swc/core@1.7.6(@swc/helpers@0.5.3))
       tslib: 2.6.2
     transitivePeerDependencies:
       - '@swc-node/register'
@@ -12402,6 +12528,11 @@ snapshots:
     dependencies:
       dequal: 2.0.3
 
+  arktype@2.0.0-beta.5:
+    dependencies:
+      '@ark/schema': 0.3.2
+      '@ark/util': 0.2.1
+
   array-buffer-byte-length@1.0.1:
     dependencies:
       call-bind: 1.0.7
@@ -15311,10 +15442,10 @@ snapshots:
 
   nwsapi@2.2.12: {}
 
-  nx@19.5.6(@swc/core@1.7.6):
+  nx@19.5.6(@swc/core@1.7.6(@swc/helpers@0.5.3)):
     dependencies:
       '@napi-rs/wasm-runtime': 0.2.4
-      '@nrwl/tao': 19.5.6(@swc/core@1.7.6)
+      '@nrwl/tao': 19.5.6(@swc/core@1.7.6(@swc/helpers@0.5.3))
       '@yarnpkg/lockfile': 1.1.0
       '@yarnpkg/parsers': 3.0.0-rc.46
       '@zkochan/js-yaml': 0.0.7
@@ -16492,14 +16623,14 @@ snapshots:
       '@swc/core': 1.7.6(@swc/helpers@0.5.3)
       esbuild: 0.21.5
 
-  terser-webpack-plugin@5.3.10(@swc/core@1.7.6)(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6)(esbuild@0.21.5)):
+  terser-webpack-plugin@5.3.10(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)):
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25
       jest-worker: 27.5.1
       schema-utils: 3.3.0
       serialize-javascript: 6.0.2
       terser: 5.31.1
-      webpack: 5.93.0(@swc/core@1.7.6)(esbuild@0.21.5)
+      webpack: 5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)
     optionalDependencies:
       '@swc/core': 1.7.6(@swc/helpers@0.5.3)
       esbuild: 0.21.5
@@ -16877,6 +17008,12 @@ snapshots:
 
   v8flags@4.0.1: {}
 
+  valibot@0.36.0: {}
+
+  valibot@0.37.0(typescript@5.5.3):
+    optionalDependencies:
+      typescript: 5.5.3
+
   validator@13.12.0: {}
 
   vary@1.1.2: {}
@@ -17162,7 +17299,7 @@ snapshots:
 
   webpack-virtual-modules@0.6.1: {}
 
-  webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack-cli@5.1.4):
+  webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.5
@@ -17185,17 +17322,15 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 3.3.0
       tapable: 2.2.1
-      terser-webpack-plugin: 5.3.10(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack-cli@5.1.4))
+      terser-webpack-plugin: 5.3.10(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5))
       watchpack: 2.4.1
       webpack-sources: 3.2.3
-    optionalDependencies:
-      webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.93.0)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild
       - uglify-js
 
-  webpack@5.93.0(@swc/core@1.7.6)(esbuild@0.21.5):
+  webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack-cli@5.1.4):
     dependencies:
       '@types/eslint-scope': 3.7.7
       '@types/estree': 1.0.5
@@ -17218,9 +17353,11 @@ snapshots:
       neo-async: 2.6.2
       schema-utils: 3.3.0
       tapable: 2.2.1
-      terser-webpack-plugin: 5.3.10(@swc/core@1.7.6)(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6)(esbuild@0.21.5))
+      terser-webpack-plugin: 5.3.10(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack@5.93.0(@swc/core@1.7.6(@swc/helpers@0.5.3))(esbuild@0.21.5)(webpack-cli@5.1.4))
       watchpack: 2.4.1
       webpack-sources: 3.2.3
+    optionalDependencies:
+      webpack-cli: 5.1.4(webpack-dev-server@5.0.4)(webpack@5.93.0)
     transitivePeerDependencies:
       - '@swc/core'
       - esbuild
diff --git a/scripts/publish.js b/scripts/publish.js
index 350f88afb..64dbe71ca 100644
--- a/scripts/publish.js
+++ b/scripts/publish.js
@@ -20,6 +20,18 @@ await publish({
       name: '@tanstack/react-router-with-query',
       packageDir: 'packages/react-router-with-query',
     },
+    {
+      name: '@tanstack/router-zod-adapter',
+      packageDir: 'packages/router-zod-adapter',
+    },
+    {
+      name: '@tanstack/router-valibot-adapter',
+      packageDir: 'packages/router-valibot-adapter',
+    },
+    {
+      name: '@tanstack/router-arktype-adapter',
+      packageDir: 'packages/router-arktype-adapter',
+    },
     {
       name: '@tanstack/router-devtools',
       packageDir: 'packages/router-devtools',


Test patch:
diff --git a/packages/react-router/tests/link.test-d.tsx b/packages/react-router/tests/link.test-d.tsx
index 3d75e87bd..888c0cf0a 100644
--- a/packages/react-router/tests/link.test-d.tsx
+++ b/packages/react-router/tests/link.test-d.tsx
@@ -1,6 +1,11 @@
-import { expect, expectTypeOf, test } from 'vitest'
-
-import { Link, createRootRoute, createRoute, createRouter } from '../src'
+import { expectTypeOf, test } from 'vitest'
+import {
+  Link,
+  type SearchSchemaInput,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '../src'
 
 const rootRoute = createRootRoute({
   validateSearch: (): { rootPage?: number } => ({ rootPage: 0 }),
@@ -59,13 +64,25 @@ const detailRoute = createRoute({
   path: '$detailId',
 })
 
+const linesRoute = createRoute({
+  getParentRoute: () => detailRoute,
+  path: 'lines',
+  validateSearch: (input: { linesPage?: number } & SearchSchemaInput) => {
+    if (typeof input.linesPage !== 'number') throw new Error()
+
+    return {
+      linesPage: input.linesPage,
+    }
+  },
+})
+
 const routeTreeTuples = rootRoute.addChildren([
   postsRoute.addChildren([postRoute, postsIndexRoute]),
   invoicesRoute.addChildren([
     invoicesIndexRoute,
     invoiceRoute.addChildren([
       invoiceEditRoute,
-      invoiceDetailsRoute.addChildren([detailRoute]),
+      invoiceDetailsRoute.addChildren([detailRoute.addChildren([linesRoute])]),
     ]),
   ]),
   indexRoute,
@@ -77,7 +94,9 @@ const routeTreeObjects = rootRoute.addChildren({
     invoicesIndexRoute,
     invoiceRoute: invoiceRoute.addChildren({
       invoiceEditRoute,
-      invoiceDetailsRoute: invoiceDetailsRoute.addChildren({ detailRoute }),
+      invoiceDetailsRoute: invoiceDetailsRoute.addChildren({
+        detailRoute: detailRoute.addChildren({ linesRoute }),
+      }),
     }),
   }),
   indexRoute,
@@ -146,6 +165,7 @@ test('when navigating to the root', () => {
       | '/invoices/$invoiceId'
       | '/invoices/$invoiceId/details/$detailId'
       | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/invoices/$invoiceId/edit'
       | '/posts'
       | '/posts/$postId'
@@ -162,6 +182,7 @@ test('when navigating to the root', () => {
       | '/invoices'
       | '/invoices/$invoiceId'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/edit'
       | '/posts'
@@ -179,6 +200,7 @@ test('when navigating to the root', () => {
       | '/invoices/'
       | '/invoices/$invoiceId/'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/edit/'
       | '/posts/'
@@ -196,6 +218,7 @@ test('when navigating to the root', () => {
       | '/invoices'
       | '/invoices/$invoiceId'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/edit'
       | '/posts'
@@ -216,6 +239,8 @@ test('when navigating to the root', () => {
       | '/invoices/$invoiceId/'
       | '/invoices/$invoiceId/details/$detailId'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/edit'
@@ -285,6 +310,7 @@ test('when navigating to the root', () => {
       page?: number
       rootIndexPage?: number
       rootPage?: number
+      linesPage?: number
     }>()
 
   expectTypeOf(DefaultRouterObjectsLink)
@@ -295,6 +321,7 @@ test('when navigating to the root', () => {
       page?: number
       rootIndexPage?: number
       rootPage?: number
+      linesPage?: number
     }>()
 
   expectTypeOf(RouterAlwaysTrailingSlashLink)
@@ -305,6 +332,7 @@ test('when navigating to the root', () => {
       page?: number
       rootIndexPage?: number
       rootPage?: number
+      linesPage?: number
     }>()
 
   expectTypeOf(RouterNeverTrailingSlashLink)
@@ -315,6 +343,7 @@ test('when navigating to the root', () => {
       page?: number
       rootIndexPage?: number
       rootPage?: number
+      linesPage?: number
     }>()
 
   expectTypeOf(RouterPreserveTrailingSlashLink)
@@ -325,6 +354,7 @@ test('when navigating to the root', () => {
       page?: number
       rootIndexPage?: number
       rootPage?: number
+      linesPage?: number
     }>()
 
   expectTypeOf(DefaultRouterLink)
@@ -367,6 +397,7 @@ test('when navigating from a route with no params and no search to the root', ()
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/posts'
       | '/posts/$postId'
       | '$postId'
@@ -386,6 +417,7 @@ test('when navigating from a route with no params and no search to the root', ()
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/posts'
       | '/posts/$postId'
       | '$postId'
@@ -405,6 +437,7 @@ test('when navigating from a route with no params and no search to the root', ()
       | '/invoices/$invoiceId/edit/'
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | '/posts/'
       | '/posts/$postId/'
       | '$postId/'
@@ -424,6 +457,7 @@ test('when navigating from a route with no params and no search to the root', ()
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | '/posts'
       | '/posts/$postId'
       | '$postId'
@@ -449,6 +483,8 @@ test('when navigating from a route with no params and no search to the root', ()
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/details/$detailId'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | '/posts'
       | '/posts/'
       | '/posts/$postId'
@@ -457,6 +493,116 @@ test('when navigating from a route with no params and no search to the root', ()
       | '$postId/'
       | undefined
     >()
+
+  expectTypeOf(Link<DefaultRouter, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function>().toEqualTypeOf<{
+    rootPage?: number
+    rootIndexPage: number
+  }>
+
+  expectTypeOf(Link<DefaultRouterObjects, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function>().toEqualTypeOf<{
+    rootPage?: number
+    rootIndexPage: number
+  }>
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function>().toEqualTypeOf<{
+    rootPage?: number
+    rootIndexPage: number
+  }>
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function>().toEqualTypeOf<{
+    rootPage?: number
+    rootIndexPage: number
+  }>
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts' | '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function>().toEqualTypeOf<{
+    rootPage?: number
+    rootIndexPage: number
+  }>
+
+  expectTypeOf(Link<DefaultRouter, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      rootIndexPage: number
+    }>()
+
+  expectTypeOf(Link<DefaultRouterObjects, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      rootIndexPage: number
+    }>()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      rootIndexPage: number
+    }>()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      rootIndexPage: number
+    }>()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts' | '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      rootIndexPage: number
+    }>()
+
+  expectTypeOf(Link<DefaultRouter, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(Link<DefaultRouterObjects, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts' | '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
 })
 
 test('when navigating from a route with no params and no search to the current route', () => {
@@ -582,6 +728,7 @@ test('when navigating from a route with no params and no search to the parent ro
       | '../invoices/$invoiceId/edit'
       | '../invoices/$invoiceId/details'
       | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices/$invoiceId/details/$detailId/lines'
       | '../invoices'
       | '../'
       | undefined
@@ -597,6 +744,7 @@ test('when navigating from a route with no params and no search to the parent ro
       | '../invoices/$invoiceId/edit'
       | '../invoices/$invoiceId/details'
       | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices/$invoiceId/details/$detailId/lines'
       | '../invoices'
       | '../'
       | undefined
@@ -612,6 +760,7 @@ test('when navigating from a route with no params and no search to the parent ro
       | '../invoices/$invoiceId/edit/'
       | '../invoices/$invoiceId/details/'
       | '../invoices/$invoiceId/details/$detailId/'
+      | '../invoices/$invoiceId/details/$detailId/lines/'
       | '../invoices/'
       | '../'
       | undefined
@@ -627,6 +776,7 @@ test('when navigating from a route with no params and no search to the parent ro
       | '../invoices/$invoiceId/edit'
       | '../invoices/$invoiceId/details'
       | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices/$invoiceId/details/$detailId/lines'
       | '../invoices'
       | '../'
       | undefined
@@ -648,6 +798,8 @@ test('when navigating from a route with no params and no search to the parent ro
       | '../invoices/$invoiceId/details/'
       | '../invoices/$invoiceId/details/$detailId'
       | '../invoices/$invoiceId/details/$detailId/'
+      | '../invoices/$invoiceId/details/$detailId/lines'
+      | '../invoices/$invoiceId/details/$detailId/lines/'
       | '../invoices'
       | '../invoices/'
       | '../'
@@ -668,6 +820,7 @@ test('cannot navigate to a branch with an index', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | './'
       | '../'
       | undefined
@@ -685,6 +838,7 @@ test('cannot navigate to a branch with an index', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | './'
       | '../'
       | undefined
@@ -704,6 +858,7 @@ test('cannot navigate to a branch with an index', () => {
       | '/invoices/$invoiceId/edit/'
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | './'
       | '../'
       | undefined
@@ -721,6 +876,7 @@ test('cannot navigate to a branch with an index', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | './'
       | '../'
       | undefined
@@ -747,6 +903,8 @@ test('cannot navigate to a branch with an index', () => {
       | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/details/$detailId'
       | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+      | '/invoices/$invoiceId/details/$detailId/lines/'
       | './'
       | '../'
       | undefined
@@ -771,6 +929,7 @@ test('from autocompletes to all absolute routes', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | undefined
     >()
 
@@ -788,6 +947,7 @@ test('from autocompletes to all absolute routes', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | undefined
     >()
 })
@@ -810,6 +970,7 @@ test('from does not allow invalid routes', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | undefined
     >()
 
@@ -827,6 +988,7 @@ test('from does not allow invalid routes', () => {
       | '/invoices/$invoiceId/edit'
       | '/invoices/$invoiceId/details'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/lines'
       | undefined
     >()
 })
@@ -2319,30 +2481,35 @@ test('when navigating to a route with search params', () => {
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 })
 
@@ -2499,30 +2666,35 @@ test('when navigating to a route with optional search params', () => {
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 })
 
@@ -2805,30 +2977,35 @@ test('when navigating to a union of routes with search params', () => {
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerAlwaysTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerNeverTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerPreserveTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 })
 
@@ -2992,30 +3169,35 @@ test('when navigating to a union of routes with search params including the root
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   defaultRouterObjectsSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 
   routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
     page?: number
     rootIndexPage?: number
     rootPage?: number
+    linesPage?: number
   }>()
 })
 
@@ -3155,3 +3337,209 @@ test('when navigating from the root to /posts', () => {
     .toHaveProperty('search')
     .returns.toEqualTypeOf<{ rootPage?: number }>()
 })
+
+test('when navigating to a route with SearchSchemaInput', () => {
+  expectTypeOf(
+    Link<
+      DefaultRouter,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<
+      { rootPage?: number; page?: number; linesPage?: number } | undefined
+    >()
+
+  expectTypeOf(
+    Link<
+      DefaultRouterObjects,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<
+      { rootPage?: number; page?: number; linesPage?: number } | undefined
+    >()
+
+  expectTypeOf(
+    Link<
+      RouterAlwaysTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines/'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<
+      { rootPage?: number; page?: number; linesPage?: number } | undefined
+    >()
+
+  expectTypeOf(
+    Link<
+      RouterNeverTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<
+      { rootPage?: number; page?: number; linesPage?: number } | undefined
+    >()
+
+  expectTypeOf(
+    Link<
+      RouterPreserveTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      | '/invoices/$invoiceId/details/$detailId/lines/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<
+      { rootPage?: number; page?: number; linesPage?: number } | undefined
+    >()
+
+  expectTypeOf(
+    Link<DefaultRouter, string, '/invoices/$invoiceId/details/$detailId/lines'>,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      page?: number
+      linesPage?: number
+    }>()
+
+  expectTypeOf(
+    Link<
+      DefaultRouterObjects,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      page?: number
+      linesPage?: number
+    }>()
+
+  expectTypeOf(
+    Link<
+      RouterAlwaysTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines/'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      page?: number
+      linesPage?: number
+    }>()
+
+  expectTypeOf(
+    Link<
+      RouterNeverTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      page?: number
+      linesPage?: number
+    }>()
+
+  expectTypeOf(
+    Link<
+      RouterPreserveTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      | '/invoices/$invoiceId/details/$detailId/lines/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{
+      rootPage?: number
+      page?: number
+      linesPage?: number
+    }>()
+
+  expectTypeOf(
+    Link<
+      DefaultRouter,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number; page?: number; linesPage: number }>()
+
+  expectTypeOf(
+    Link<
+      DefaultRouterObjects,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number; page?: number; linesPage: number }>()
+
+  expectTypeOf(
+    Link<
+      RouterAlwaysTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines/'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number; page?: number; linesPage: number }>()
+
+  expectTypeOf(
+    Link<
+      RouterNeverTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      '/invoices/$invoiceId/details/$detailId/lines/'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number; page?: number; linesPage: number }>()
+
+  expectTypeOf(
+    Link<
+      RouterPreserveTrailingSlashes,
+      '/invoices/$invoiceId/details/$detailId/lines',
+      | '/invoices/$invoiceId/details/$detailId/lines/'
+      | '/invoices/$invoiceId/details/$detailId/lines'
+    >,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number; page?: number; linesPage: number }>()
+})
diff --git a/packages/react-router/tests/route.test-d.tsx b/packages/react-router/tests/route.test-d.tsx
index 7e2eededa..4d8da3bbd 100644
--- a/packages/react-router/tests/route.test-d.tsx
+++ b/packages/react-router/tests/route.test-d.tsx
@@ -5,7 +5,7 @@ import {
   createRoute,
   createRouter,
 } from '../src'
-import type { ControlledPromise } from '../src'
+import type { ControlledPromise, SearchSchemaInput } from '../src'
 
 test('when creating the root', () => {
   const rootRoute = createRootRoute()
@@ -884,3 +884,124 @@ test('when creating a child route with params.parse and params.stringify with me
     invoiceId: number
   }>()
 })
+
+test('when creating a child route with no explicit search input', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: (input) => {
+      expectTypeOf(input).toEqualTypeOf<Record<string, unknown>>()
+      return {
+        page: 0,
+      }
+    },
+  })
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+
+  const rootRouteWithContext = createRootRouteWithContext()({
+    validateSearch: (input) => {
+      expectTypeOf(input).toEqualTypeOf<Record<string, unknown>>()
+      return {
+        page: 0,
+      }
+    },
+  })
+
+  expectTypeOf(rootRouteWithContext.useSearch()).toEqualTypeOf<{
+    page: number
+  }>()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input) => {
+      expectTypeOf(input).toEqualTypeOf<Record<string, unknown>>()
+      return {
+        page: 0,
+      }
+    },
+  })
+
+  expectTypeOf(indexRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  const navigate = indexRoute.useNavigate()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page: number }>()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number }>()
+})
+
+test('when creating a child route with an explicit search input', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: (input: SearchSchemaInput & { input: string }) => {
+      return {
+        page: input.input,
+      }
+    },
+  })
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: string }>()
+
+  const rootRouteWithContext = createRootRouteWithContext()({
+    validateSearch: (input: SearchSchemaInput & { input: string }) => {
+      return {
+        page: input.input,
+      }
+    },
+  })
+
+  expectTypeOf(rootRouteWithContext.useSearch()).toEqualTypeOf<{
+    page: string
+  }>()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input: SearchSchemaInput & { input: string }) => {
+      return {
+        page: input.input,
+      }
+    },
+  })
+
+  expectTypeOf(indexRoute.useSearch()).toEqualTypeOf<{ page: string }>()
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  const navigate = indexRoute.useNavigate()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ input: string }>()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ input: string }>()
+
+  expectTypeOf(navigate<'/', typeof router, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: string }>()
+})
diff --git a/packages/react-router/tests/useSearch.test-d.tsx b/packages/react-router/tests/useSearch.test-d.tsx
index 73af7e8b5..2fa578b78 100644
--- a/packages/react-router/tests/useSearch.test-d.tsx
+++ b/packages/react-router/tests/useSearch.test-d.tsx
@@ -1,6 +1,7 @@
 import { expectTypeOf, test } from 'vitest'
 import {
   type FullSearchSchema,
+  SearchSchemaInput,
   createRootRoute,
   createRoute,
   createRouter,
@@ -482,3 +483,21 @@ test('when the root has search params but the index does', () => {
       | undefined
     >()
 })
+
+test('when a route has search params using SearchSchemaInput', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input: { page?: number } & SearchSchemaInput) => {
+      return { page: input.page ?? 0 }
+    },
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  expectTypeOf(useSearch<typeof routeTree, '/'>).returns.toEqualTypeOf<{
+    page: number
+  }>
+})
diff --git a/packages/router-arktype-adapter/tests/index.test-d.ts b/packages/router-arktype-adapter/tests/index.test-d.ts
new file mode 100644
index 000000000..d17d29c40
--- /dev/null
+++ b/packages/router-arktype-adapter/tests/index.test-d.ts
@@ -0,0 +1,95 @@
+import {
+  Link,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '@tanstack/react-router'
+import { expectTypeOf, test } from 'vitest'
+import { type } from 'arktype'
+import { arkTypeSearchValidator } from '../src'
+
+test('when creating a route with arktype validation', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: arkTypeSearchValidator(
+      type({
+        page: 'number = 0',
+      }),
+    ),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: arkTypeSearchValidator(
+      type({
+        indexPage: 'number = 0',
+      }),
+    ),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page?: number; indexPage?: number } | undefined>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page?: number; indexPage?: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
+
+test('when using arktype schema with function', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: (input) =>
+      type({
+        page: 'number = 0',
+      }).assert(input),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input) =>
+      type({
+        indexPage: 'number = 0',
+      }).assert(input),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
diff --git a/packages/router-arktype-adapter/tests/index.test.tsx b/packages/router-arktype-adapter/tests/index.test.tsx
new file mode 100644
index 000000000..1b98c6744
--- /dev/null
+++ b/packages/router-arktype-adapter/tests/index.test.tsx
@@ -0,0 +1,79 @@
+import {
+  Link,
+  RouterProvider,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '@tanstack/react-router'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { type } from 'arktype'
+import { arkTypeSearchValidator } from '../src'
+import '@testing-library/jest-dom/vitest'
+
+afterEach(() => {
+  vi.resetAllMocks()
+  window.history.replaceState(null, 'root', '/')
+  cleanup()
+})
+
+test('when navigating to a route with arkTypeSearchValidator', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: arkTypeSearchValidator(
+      type({
+        page: 'number',
+      }),
+    ),
+    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('Page: 0')).toBeInTheDocument()
+})
diff --git a/packages/router-valibot-adapter/tests/index.test-d.ts b/packages/router-valibot-adapter/tests/index.test-d.ts
new file mode 100644
index 000000000..7b96f04b6
--- /dev/null
+++ b/packages/router-valibot-adapter/tests/index.test-d.ts
@@ -0,0 +1,101 @@
+import {
+  Link,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '@tanstack/react-router'
+import { expectTypeOf, test } from 'vitest'
+import * as v from 'valibot'
+import { valibotSearchValidator } from '../src'
+
+test('when creating a route with valibot validation', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: valibotSearchValidator(
+      v.object({
+        page: v.optional(v.number(), 0),
+      }),
+    ),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: valibotSearchValidator(
+      v.object({
+        indexPage: v.optional(v.number(), 0),
+      }),
+    ),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page?: number; indexPage?: number } | undefined>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page?: number; indexPage?: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
+
+test('when using valibot schema with function', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: (input) =>
+      v.parse(
+        v.object({
+          page: v.optional(v.number(), 0),
+        }),
+        input,
+      ),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input) =>
+      v.parse(
+        v.object({
+          indexPage: v.optional(v.number(), 0),
+        }),
+        input,
+      ),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
diff --git a/packages/router-valibot-adapter/tests/index.test.tsx b/packages/router-valibot-adapter/tests/index.test.tsx
new file mode 100644
index 000000000..b9a28cc21
--- /dev/null
+++ b/packages/router-valibot-adapter/tests/index.test.tsx
@@ -0,0 +1,79 @@
+import * as v from 'valibot'
+import {
+  Link,
+  RouterProvider,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '@tanstack/react-router'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import { valibotSearchValidator } from '../src'
+import '@testing-library/jest-dom/vitest'
+
+afterEach(() => {
+  vi.resetAllMocks()
+  window.history.replaceState(null, 'root', '/')
+  cleanup()
+})
+
+test('when navigating to a route with valibotSearchValidator', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: valibotSearchValidator(
+      v.object({
+        page: v.number(),
+      }),
+    ),
+    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('Page: 0')).toBeInTheDocument()
+})
diff --git a/packages/router-zod-adapter/tests/index.test-d.ts b/packages/router-zod-adapter/tests/index.test-d.ts
new file mode 100644
index 000000000..6cdbb272e
--- /dev/null
+++ b/packages/router-zod-adapter/tests/index.test-d.ts
@@ -0,0 +1,230 @@
+import {
+  createRootRoute,
+  createRoute,
+  createRouter,
+  Link,
+} from '@tanstack/react-router'
+import { test, expectTypeOf } from 'vitest'
+import { zodSearchValidator } from '../src'
+import { z } from 'zod'
+
+test('when creating a route with zod validation', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: zodSearchValidator(
+      z.object({
+        page: z.number().optional().default(0),
+      }),
+    ),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: zodSearchValidator(
+      z.object({
+        indexPage: z.number().optional().default(0),
+      }),
+    ),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page?: number; indexPage?: number } | undefined>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page?: number; indexPage?: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
+
+test('when creating a route with zod validation where input is output', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: zodSearchValidator(
+      z.object({
+        page: z.number().optional().default(0),
+      }),
+    ),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: zodSearchValidator({
+      schema: z.object({
+        indexPage: z.number().optional().default(0),
+      }),
+      input: 'output',
+    }),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page?: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page?: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
+
+test('when creating a route with zod validation where output is input', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: zodSearchValidator({
+      schema: z.object({
+        page: z.number().optional().default(0),
+      }),
+      output: 'input',
+    }),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: zodSearchValidator({
+      schema: z.object({
+        indexPage: z.number().optional().default(0),
+      }),
+      output: 'input',
+    }),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page?: number; indexPage?: number } | undefined>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page?: number; indexPage?: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page?: number } | { page?: number; indexPage?: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page?: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page?: number }>
+})
+
+test('when using zod schema without adapter', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: z.object({
+      page: z.number().optional().default(0),
+    }),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: z.object({
+      indexPage: z.number().optional().default(0),
+    }),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
+
+test('when using zod schema with function', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: (input) =>
+      z
+        .object({
+          page: z.number().optional().default(0),
+        })
+        .parse(input),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: (input) =>
+      z
+        .object({
+          indexPage: z.number().optional().default(0),
+        })
+        .parse(input),
+  })
+
+  const routeTree = rootRoute.addChildren([indexRoute])
+
+  const router = createRouter({ routeTree })
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | true>()
+    .toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ page: number; indexPage: number }>()
+
+  expectTypeOf(Link<typeof router, '/', '/'>)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{ page: number } | { page: number; indexPage: number }>()
+
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>()
+  expectTypeOf(rootRoute.useSearch()).toEqualTypeOf<{ page: number }>
+})
diff --git a/packages/router-zod-adapter/tests/index.test.tsx b/packages/router-zod-adapter/tests/index.test.tsx
new file mode 100644
index 000000000..f6f6665c1
--- /dev/null
+++ b/packages/router-zod-adapter/tests/index.test.tsx
@@ -0,0 +1,262 @@
+import { afterEach, expect, test, vi } from 'vitest'
+import { zodSearchValidator } from '../src'
+import { z } from 'zod'
+import {
+  createRootRoute,
+  createRoute,
+  createRouter,
+  Link,
+  RouterProvider,
+} from '@tanstack/react-router'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import '@testing-library/jest-dom/vitest'
+
+afterEach(() => {
+  vi.resetAllMocks()
+  window.history.replaceState(null, 'root', '/')
+  cleanup()
+})
+
+test('when navigating to a route with zodSearchValidator', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: zodSearchValidator(
+      z.object({
+        page: z.number(),
+      }),
+    ),
+    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('Page: 0')).toBeInTheDocument()
+})
+
+test('when navigating to a route with zodSearchValidator input set to output', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: zodSearchValidator({
+      schema: z.object({
+        page: z.number(),
+      }),
+      input: 'input',
+    }),
+    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('Page: 0')).toBeInTheDocument()
+})
+
+test('when navigating to a route using zod without the adapter', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: z.object({
+      page: z.number(),
+    }),
+    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('Page: 0')).toBeInTheDocument()
+})
+
+test('when navigating to a route using zod in a function without the adapter', async () => {
+  const rootRoute = createRootRoute()
+
+  const Index = () => {
+    return (
+      <>
+        <h1>Index</h1>
+        <Link<typeof router, string, '/invoices'>
+          to="/invoices"
+          search={{
+            page: 0,
+          }}
+        >
+          To Invoices
+        </Link>
+      </>
+    )
+  }
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    component: Index,
+  })
+
+  const Invoices = () => {
+    const search = invoicesRoute.useSearch()
+
+    return (
+      <>
+        <h1>Invoices</h1>
+        <span>Page: {search.page}</span>
+      </>
+    )
+  }
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+    validateSearch: (input) =>
+      z
+        .object({
+          page: z.number(),
+        })
+        .parse(input),
+    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('Page: 0')).toBeInTheDocument()
+})