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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .yarn/versions/b1c2bfb9.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
react-router-typesafe-routes: minor
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Allow using Zod v4 in `zod()`.

## [2.1.0] - 2025-07-06

### Added
Expand Down Expand Up @@ -203,6 +209,8 @@ setTypedSearchParams((prevParams) => ({
- Hook dependencies are now properly listed, which is checked by ESLint. This fixes `useTypedSearchParams` for dynamic routes.
- Prevent access to internal `useUpdatingRef` helper.

[unreleased]: https://github.com/fenok/react-router-typesafe-routes/tree/dev
[2.1.0]: https://github.com/fenok/react-router-typesafe-routes/tree/v2.1.0
[2.0.0]: https://github.com/fenok/react-router-typesafe-routes/tree/v2.0.0
[1.2.2]: https://github.com/fenok/react-router-typesafe-routes/tree/v1.2.2
[1.2.1]: https://github.com/fenok/react-router-typesafe-routes/tree/v1.2.1
Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Enhanced type safety via validation for all route params in React Router v7.

The library provides type safety for all route params (pathname params, search params (including multiple keys), state, and hash) on building and parsing/validating URL parts and state. There are no unsafe type casts whatsoever.

If you want, you can use a validation library. There is first-party support for [Standard Schema](https://github.com/standard-schema/standard-schema), [Zod (v3)](https://github.com/colinhacks/zod), and [Yup](https://github.com/jquense/yup), and other libraries can be integrated with ease. Otherwise, you can use other built-in types and fine-tune their validation instead.
If you want, you can use a validation library. There is first-party support for [Standard Schema](https://github.com/standard-schema/standard-schema), [Zod](https://github.com/colinhacks/zod), and [Yup](https://github.com/jquense/yup), and other libraries can be integrated with ease. Otherwise, you can use other built-in types and fine-tune their validation instead.

In built-in types, parsing and validation errors are caught and replaced with `undefined`. You can also return a default value or throw an error in case of an absent or invalid param. All these adjustments reflect in types, too!

Expand All @@ -26,7 +26,7 @@ Note that `react-router` and `react` are peer dependencies.
There are optional entry points for types based on third-party validation libraries:

- `react-router-typesafe-routes/standard-schema` exports `schema` type that accepts any Standard Schema;
- `react-router-typesafe-routes/zod` exports `zod` type (for Zod v3 only), `zod` is a peer dependency;
- `react-router-typesafe-routes/zod` exports `zod` type, `zod` is a peer dependency;
- `react-router-typesafe-routes/yup` exports `yup` type, `yup` is a peer dependency;

The library is targeting ES6 (ES2015).
Expand Down Expand Up @@ -404,30 +404,28 @@ const myRoute = route({
```tsx
import { route, parser } from "react-router-typesafe-routes";
import { schema } from "react-router-typesafe-routes/standard-schema";
import { z } from "zod/v4"; // Zod v4 implements Standard Schema
import { type } from "arktype"; // Or any other library supporting Standard Schema

const myRoute = route({
path: ":id",
// There is no way to get the type hint from a Standard Schema in runtime, so we need to specify it explicitly.
// For the built-in parser, this is only necessary for strings and dates. It's also type-safe!
// It's needed for omitting wrapping quotes in serialized values.
params: { id: schema(z.string().uuid(), parser("string")) },
params: { id: schema(type("string.uuid"), parser("string")) },
});
```

> ❗Zod doesn't do coercion by default, but you may need it for complex values returned from `JSON.parse` (for instance, a date wrapped in an object).

</details>

### Use Zod v3
### Use Zod

<details>
<summary>Click to expand</summary>

```tsx
import { route } from "react-router-typesafe-routes";
import { zod } from "react-router-typesafe-routes/zod";
import { z } from "zod";
import { z } from "zod"; // zod/v4 or zod/v3

const myRoute = route({
path: ":id",
Expand Down Expand Up @@ -791,7 +789,7 @@ There is also somewhat specific `union()` helper that accepts an enum (or an enu

##### Third-party validation libraries

If you can, you should use a validation library for all types. You can use Standard Schema, Zod v3, and Yup out of the box via the `schema()`, `zod()`, and `yup()` helpers, and you should be able to integrate any third-party validation library via the `type()` helper. See [Advanced examples](#advanced-examples).
If you can, you should use a validation library for all types. You can use Standard Schema, Zod, and Yup out of the box via the `schema()`, `zod()`, and `yup()` helpers, and you should be able to integrate any third-party validation library via the `type()` helper. See [Advanced examples](#advanced-examples).

#### Type objects

Expand Down Expand Up @@ -1017,7 +1015,7 @@ There are built-in helpers for common types:
There are also built-in helpers for third-party validation libraries:

- `schema()` - a wrapper around `type()` for creating type objects based on Standard Schemas. Uses a separate entry point: `react-router-typesafe-routes/standard-schema`.
- `zod()` - a wrapper around `type()` for creating type objects based on Zod v3 Types. Uses a separate entry point: `react-router-typesafe-routes/zod`.
- `zod()` - a wrapper around `type()` for creating type objects based on Zod Types. Uses a separate entry point: `react-router-typesafe-routes/zod`.
- `yup()` - a wrapper around `type()` for creating type objects based on Yup Schemas. Uses a separate entry point: `react-router-typesafe-routes/yup`.

All of them use the built-in parser with auto-detected hint by default, and all of them allow to supply a custom parser.
Expand Down
67 changes: 66 additions & 1 deletion src/lib/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1813,7 +1813,72 @@ it("allows to use standard schema v1", () => {
});
});

it("allows to use zod", () => {
it("allows to use zod v4", () => {
const TEST_ROUTE = route({
path: "",

searchParams: {
a: zod(z4.string().optional()),
b: zod(z4.number()),
c: zod(z4.boolean()),
d: zod(z4.date()),
e: zod(z4.string().nullable()),
f: zod(z4.string().nullable()),
g: zod(z4.object({ d: z4.coerce.date() })), // We have to coerce the result of JSON.parse
},
});

assert<
IsExact<
ReturnType<typeof TEST_ROUTE.$deserializeSearchParams>,
{
a?: string;
b?: number;
c?: boolean;
d?: Date;
e?: string | null;
f?: string | null;
g?: { d: Date };
}
>
>(true);

const testDate = new Date();

const plainSearchParams = TEST_ROUTE.$serializeSearchParams({
searchParams: {
a: "test",
b: 0,
c: false,
d: testDate,
e: "null",
f: null,
g: { d: testDate },
},
});

expect(urlSearchParamsToRecord(plainSearchParams)).toStrictEqual({
a: "test",
b: "0",
c: "false",
d: testDate.toISOString(),
e: '"null"',
f: "null",
g: JSON.stringify({ d: testDate }),
});

expect(TEST_ROUTE.$deserializeSearchParams(plainSearchParams)).toStrictEqual({
a: "test",
b: 0,
c: false,
d: testDate,
e: "null",
f: null,
g: { d: testDate },
});
});

it("allows to use zod v3", () => {
const TEST_ROUTE = route({
path: "",

Expand Down
28 changes: 21 additions & 7 deletions src/zod/zod.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import { type, Type, ParserHint, parser, Parser } from "../lib/index.js";
import { ZodType, ZodOptional, ZodString, ZodDate, ZodTypeAny, ZodNumber, ZodBoolean } from "zod";
import { ZodType, ZodOptional, ZodString, ZodDate, ZodNumber, ZodBoolean } from "zod/v4";
import {
ZodType as ZodTypeV3,
ZodOptional as ZodOptionalV3,
ZodString as ZodStringV3,
ZodDate as ZodDateV3,
ZodTypeAny as ZodTypeAnyV3,
ZodNumber as ZodNumberV3,
ZodBoolean as ZodBooleanV3,
} from "zod/v3";

interface ConfigureOptions {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
parserFactory: (hint?: ParserHint) => Parser<any, ParserHint>;
}

function configure({ parserFactory }: ConfigureOptions) {
function zod<T>(zodType: ZodType<T | undefined>, parser?: Parser<T>): Type<T> {
const unwrappedZodType = zodType instanceof ZodOptional ? (zodType.unwrap() as ZodTypeAny) : zodType;
function zod<T>(zodType: ZodType<T | undefined> | ZodTypeV3<T | undefined>, parser?: Parser<T>): Type<T> {
const unwrappedZodType =
zodType instanceof ZodOptional
? zodType.unwrap()
: zodType instanceof ZodOptionalV3
? (zodType.unwrap() as ZodTypeAnyV3)
: zodType;

let typeHint: ParserHint = "unknown";

if (unwrappedZodType instanceof ZodString) {
if (unwrappedZodType instanceof ZodString || unwrappedZodType instanceof ZodStringV3) {
typeHint = "string";
} else if (unwrappedZodType instanceof ZodNumber) {
} else if (unwrappedZodType instanceof ZodNumber || unwrappedZodType instanceof ZodNumberV3) {
typeHint = "number";
} else if (unwrappedZodType instanceof ZodBoolean) {
} else if (unwrappedZodType instanceof ZodBoolean || unwrappedZodType instanceof ZodBooleanV3) {
typeHint = "boolean";
} else if (unwrappedZodType instanceof ZodDate) {
} else if (unwrappedZodType instanceof ZodDate || unwrappedZodType instanceof ZodDateV3) {
typeHint = "date";
}

Expand Down