Skip to content

Commit 8d083bd

Browse files
authored
feat: support Zod 4 (#777)
* Add Zod 4 support * Format * Add fallbacks * Fix inference * Bump lockfile dep
1 parent 3bc2ad5 commit 8d083bd

File tree

20 files changed

+809
-73
lines changed

20 files changed

+809
-73
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Example:
6868
```tsx
6969
import { useForm } from 'react-hook-form';
7070
import { zodResolver } from '@hookform/resolvers/zod';
71-
import { z } from 'zod';
71+
import { z } from 'zod'; // or 'zod/v4'
7272

7373
const schema = z.object({
7474
id: z.number(),
@@ -175,7 +175,7 @@ const App = () => {
175175
};
176176
```
177177

178-
### [Zod](https://github.com/vriad/zod)
178+
### [Zod](https://github.com/colinhacks/zod)
179179

180180
TypeScript-first schema validation with static type inference
181181

@@ -186,7 +186,7 @@ TypeScript-first schema validation with static type inference
186186
```tsx
187187
import { useForm } from 'react-hook-form';
188188
import { zodResolver } from '@hookform/resolvers/zod';
189-
import { z } from 'zod';
189+
import { z } from 'zod'; // or 'zod/v4'
190190

191191
const schema = z.object({
192192
name: z.string().min(1, { message: 'Required' }),

bun.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"vite-tsconfig-paths": "^5.1.4",
5555
"vitest": "^3.0.9",
5656
"yup": "^1.6.1",
57-
"zod": "^3.24.2",
57+
"zod": "^3.25.0",
5858
},
5959
"peerDependencies": {
6060
"react-hook-form": "^7.55.0",
@@ -1444,7 +1444,7 @@
14441444

14451445
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
14461446

1447-
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
1447+
"zod": ["zod@3.25.51", "", {}, "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="],
14481448

14491449
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
14501450

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@
314314
"vite-tsconfig-paths": "^5.1.4",
315315
"vitest": "^3.0.9",
316316
"yup": "^1.6.1",
317-
"zod": "^3.24.2"
317+
"zod": "^3.25.0"
318318
},
319319
"peerDependencies": {
320320
"react-hook-form": "^7.55.0"

standard-schema/src/__tests__/__fixtures__/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { StandardSchemaV1 } from '@standard-schema/spec';
22
import { Field, InternalFieldName } from 'react-hook-form';
3-
import { z } from 'zod';
3+
import { z } from 'zod/v3';
44

55
export const schema = z
66
.object({

standard-schema/src/__tests__/standard-schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
2-
import { z } from 'zod';
2+
import { z } from 'zod/v3';
33
import { standardSchemaResolver } from '..';
44
import {
55
customSchema,

typebox/src/__tests__/typebox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { Type } from '@sinclair/typebox';
2+
import { TypeCompiler } from '@sinclair/typebox/compiler';
13
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
24
import { typeboxResolver } from '..';
35
import { fields, invalidData, schema, validData } from './__fixtures__/data';
4-
import { Type } from '@sinclair/typebox';
5-
import { TypeCompiler } from '@sinclair/typebox/compiler';
66

77
const shouldUseNativeValidation = false;
88

typeschema/src/__tests__/Form-native-validation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import user from '@testing-library/user-event';
33
import type { Infer } from '@typeschema/main';
44
import React from 'react';
55
import { useForm } from 'react-hook-form';
6-
import { z } from 'zod';
6+
import { z } from 'zod/v3';
77
import { typeschemaResolver } from '..';
88

99
const USERNAME_REQUIRED_MESSAGE = 'username field is required';

typeschema/src/__tests__/Form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import user from '@testing-library/user-event';
33
import type { Infer } from '@typeschema/main';
44
import React from 'react';
55
import { useForm } from 'react-hook-form';
6-
import { z } from 'zod';
6+
import { z } from 'zod/v3';
77
import { typeschemaResolver } from '..';
88

99
const schema = z.object({

typeschema/src/__tests__/__fixtures__/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Field, InternalFieldName } from 'react-hook-form';
2-
import { z } from 'zod';
2+
import { z } from 'zod/v3';
33

44
export const schema = z
55
.object({

typeschema/src/__tests__/typeschema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as typeschema from '@typeschema/main';
22
import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
3-
import { z } from 'zod';
3+
import { z } from 'zod/v3';
44
import { typeschemaResolver } from '..';
55
import { fields, invalidData, schema, validData } from './__fixtures__/data';
66

typeschema/src/typeschema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
2+
import { StandardSchemaV1 } from '@standard-schema/spec';
23
import {
34
FieldError,
45
FieldErrors,
56
FieldValues,
67
Resolver,
78
appendErrors,
89
} from 'react-hook-form';
9-
import { StandardSchemaV1 } from 'zod/lib/standard-schema';
1010

1111
const parseErrorSchema = (
1212
typeschemaErrors: readonly StandardSchemaV1.Issue[],

zod/src/__tests__/Form-native-validation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
22
import user from '@testing-library/user-event';
33
import React from 'react';
44
import { useForm } from 'react-hook-form';
5-
import { z } from 'zod';
5+
import { z } from 'zod/v3';
66
import { zodResolver } from '..';
77

88
const USERNAME_REQUIRED_MESSAGE = 'username field is required';

zod/src/__tests__/Form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
22
import user from '@testing-library/user-event';
33
import React from 'react';
44
import { useForm } from 'react-hook-form';
5-
import { z } from 'zod';
5+
import { z } from 'zod/v3';
66
import { zodResolver } from '..';
77

88
const schema = z.object({

zod/src/__tests__/__fixtures__/data.ts renamed to zod/src/__tests__/__fixtures__/data-v3.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Field, InternalFieldName } from 'react-hook-form';
2-
import { z } from 'zod';
2+
import { z } from 'zod/v3';
33

44
export const schema = z
55
.object({
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Field, InternalFieldName } from 'react-hook-form';
2+
import { z } from 'zod/v4-mini';
3+
4+
export const schema = z
5+
.object({
6+
username: z
7+
.string()
8+
.check(z.regex(/^\w+$/), z.minLength(3), z.maxLength(30)),
9+
password: z
10+
.string()
11+
.check(
12+
z.regex(new RegExp('.*[A-Z].*'), 'One uppercase character'),
13+
z.regex(new RegExp('.*[a-z].*'), 'One lowercase character'),
14+
z.regex(new RegExp('.*\\d.*'), 'One number'),
15+
z.regex(
16+
new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
17+
'One special character',
18+
),
19+
z.minLength(8, 'Must be at least 8 characters in length'),
20+
),
21+
repeatPassword: z.string(),
22+
accessToken: z.union([z.string(), z.number()]),
23+
birthYear: z.optional(z.number().check(z.minimum(1900), z.maximum(2013))),
24+
email: z.optional(z.email()),
25+
tags: z.array(z.string()),
26+
enabled: z.boolean(),
27+
url: z.union([z.url('Custom error url'), z.literal('')]),
28+
like: z.optional(
29+
z.array(
30+
z.object({
31+
id: z.number(),
32+
name: z.string().check(z.length(4)),
33+
}),
34+
),
35+
),
36+
dateStr: z
37+
.pipe(
38+
z.string(),
39+
z.transform((value) => new Date(value)),
40+
)
41+
.check(
42+
z.refine((value) => !isNaN(value.getTime()), {
43+
message: 'Invalid date',
44+
}),
45+
),
46+
})
47+
.check(
48+
z.refine((obj) => obj.password === obj.repeatPassword, {
49+
message: 'Passwords do not match',
50+
path: ['confirm'],
51+
}),
52+
);
53+
54+
export const validData = {
55+
username: 'Doe',
56+
password: 'Password123_',
57+
repeatPassword: 'Password123_',
58+
birthYear: 2000,
59+
email: 'john@doe.com',
60+
tags: ['tag1', 'tag2'],
61+
enabled: true,
62+
accessToken: 'accessToken',
63+
url: 'https://react-hook-form.com/',
64+
like: [
65+
{
66+
id: 1,
67+
name: 'name',
68+
},
69+
],
70+
dateStr: '2020-01-01',
71+
} satisfies z.input<typeof schema>;
72+
73+
export const invalidData = {
74+
password: '___',
75+
email: '',
76+
birthYear: 'birthYear',
77+
like: [{ id: 'z' }],
78+
url: 'abc',
79+
} as unknown as z.input<typeof schema>;
80+
81+
export const fields: Record<InternalFieldName, Field['_f']> = {
82+
username: {
83+
ref: { name: 'username' },
84+
name: 'username',
85+
},
86+
password: {
87+
ref: { name: 'password' },
88+
name: 'password',
89+
},
90+
email: {
91+
ref: { name: 'email' },
92+
name: 'email',
93+
},
94+
birthday: {
95+
ref: { name: 'birthday' },
96+
name: 'birthday',
97+
},
98+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Field, InternalFieldName } from 'react-hook-form';
2+
import { z } from 'zod/v4';
3+
4+
export const schema = z
5+
.object({
6+
username: z.string().regex(/^\w+$/).min(3).max(30),
7+
password: z
8+
.string()
9+
.regex(new RegExp('.*[A-Z].*'), 'One uppercase character')
10+
.regex(new RegExp('.*[a-z].*'), 'One lowercase character')
11+
.regex(new RegExp('.*\\d.*'), 'One number')
12+
.regex(
13+
new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
14+
'One special character',
15+
)
16+
.min(8, 'Must be at least 8 characters in length'),
17+
repeatPassword: z.string(),
18+
accessToken: z.union([z.string(), z.number()]),
19+
birthYear: z.number().min(1900).max(2013).optional(),
20+
email: z.string().email().optional(),
21+
tags: z.array(z.string()),
22+
23+
enabled: z.boolean(),
24+
url: z.string().url('Custom error url').or(z.literal('')),
25+
like: z
26+
.array(
27+
z.object({
28+
id: z.number(),
29+
name: z.string().length(4),
30+
}),
31+
)
32+
.optional(),
33+
dateStr: z
34+
.string()
35+
.transform((value) => new Date(value))
36+
.refine((value) => !isNaN(value.getTime()), {
37+
message: 'Invalid date',
38+
}),
39+
})
40+
.refine((obj) => obj.password === obj.repeatPassword, {
41+
message: 'Passwords do not match',
42+
path: ['confirm'],
43+
});
44+
45+
export const validData = {
46+
username: 'Doe',
47+
password: 'Password123_',
48+
repeatPassword: 'Password123_',
49+
birthYear: 2000,
50+
email: 'john@doe.com',
51+
tags: ['tag1', 'tag2'],
52+
enabled: true,
53+
accessToken: 'accessToken',
54+
url: 'https://react-hook-form.com/',
55+
like: [
56+
{
57+
id: 1,
58+
name: 'name',
59+
},
60+
],
61+
dateStr: '2020-01-01',
62+
} satisfies z.input<typeof schema>;
63+
64+
export const invalidData = {
65+
password: '___',
66+
email: '',
67+
birthYear: 'birthYear',
68+
like: [{ id: 'z' }],
69+
url: 'abc',
70+
} as unknown as z.input<typeof schema>;
71+
72+
export const fields: Record<InternalFieldName, Field['_f']> = {
73+
username: {
74+
ref: { name: 'username' },
75+
name: 'username',
76+
},
77+
password: {
78+
ref: { name: 'password' },
79+
name: 'password',
80+
},
81+
email: {
82+
ref: { name: 'email' },
83+
name: 'email',
84+
},
85+
birthday: {
86+
ref: { name: 'birthday' },
87+
name: 'birthday',
88+
},
89+
};

0 commit comments

Comments
 (0)