@hookform/resolvers 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +17 -16
  2. package/ajv/package.json +1 -1
  3. package/arktype/package.json +1 -1
  4. package/computed-types/package.json +1 -1
  5. package/effect-ts/package.json +1 -1
  6. package/fluentvalidation-ts/package.json +1 -1
  7. package/io-ts/package.json +1 -1
  8. package/joi/package.json +1 -1
  9. package/nope/package.json +1 -1
  10. package/package.json +3 -3
  11. package/standard-schema/package.json +1 -1
  12. package/standard-schema/src/__tests__/__fixtures__/data.ts +1 -1
  13. package/standard-schema/src/__tests__/standard-schema.ts +1 -1
  14. package/superstruct/package.json +1 -1
  15. package/typanion/package.json +1 -1
  16. package/typebox/package.json +1 -1
  17. package/typebox/src/__tests__/typebox.ts +2 -2
  18. package/typeschema/dist/typeschema.d.ts +1 -1
  19. package/typeschema/dist/typeschema.js.map +1 -1
  20. package/typeschema/dist/typeschema.modern.mjs.map +1 -1
  21. package/typeschema/dist/typeschema.module.js.map +1 -1
  22. package/typeschema/dist/typeschema.umd.js.map +1 -1
  23. package/typeschema/package.json +1 -1
  24. package/typeschema/src/__tests__/Form-native-validation.tsx +1 -1
  25. package/typeschema/src/__tests__/Form.tsx +1 -1
  26. package/typeschema/src/__tests__/__fixtures__/data.ts +1 -1
  27. package/typeschema/src/__tests__/typeschema.ts +1 -1
  28. package/typeschema/src/typeschema.ts +1 -1
  29. package/valibot/package.json +1 -1
  30. package/vest/package.json +1 -1
  31. package/vine/package.json +1 -1
  32. package/yup/package.json +1 -1
  33. package/zod/dist/zod.d.ts +46 -7
  34. package/zod/dist/zod.js +1 -1
  35. package/zod/dist/zod.js.map +1 -1
  36. package/zod/dist/zod.mjs +1 -1
  37. package/zod/dist/zod.modern.mjs +1 -1
  38. package/zod/dist/zod.modern.mjs.map +1 -1
  39. package/zod/dist/zod.module.js +1 -1
  40. package/zod/dist/zod.module.js.map +1 -1
  41. package/zod/dist/zod.umd.js +1 -1
  42. package/zod/dist/zod.umd.js.map +1 -1
  43. package/zod/package.json +1 -1
  44. package/zod/src/__tests__/Form-native-validation.tsx +1 -1
  45. package/zod/src/__tests__/Form.tsx +1 -1
  46. package/zod/src/__tests__/__fixtures__/{data.ts → data-v3.ts} +1 -1
  47. package/zod/src/__tests__/__fixtures__/data-v4-mini.ts +98 -0
  48. package/zod/src/__tests__/__fixtures__/data-v4.ts +89 -0
  49. package/zod/src/__tests__/zod-v3.ts +178 -0
  50. package/zod/src/__tests__/zod-v4-mini.ts +182 -0
  51. package/zod/src/__tests__/{zod.ts → zod-v4.ts} +17 -2
  52. package/zod/src/zod.ts +227 -53
@@ -0,0 +1,182 @@
1
+ import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
2
+ import { z } from 'zod/v4-mini';
3
+ import { zodResolver } from '..';
4
+ import {
5
+ fields,
6
+ invalidData,
7
+ schema,
8
+ validData,
9
+ } from './__fixtures__/data-v4-mini';
10
+
11
+ const shouldUseNativeValidation = false;
12
+
13
+ describe('zodResolver', () => {
14
+ it('should return values from zodResolver when validation pass & raw=true', async () => {
15
+ const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
16
+
17
+ const result = await zodResolver(schema, undefined, {
18
+ raw: true,
19
+ })(validData, undefined, {
20
+ fields,
21
+ shouldUseNativeValidation,
22
+ });
23
+ result;
24
+
25
+ expect(parseAsyncSpy).toHaveBeenCalledTimes(1);
26
+ expect(result).toEqual({ errors: {}, values: validData });
27
+ expectTypeOf(result.values);
28
+ });
29
+
30
+ it('should return parsed values from zodResolver with `mode: sync` when validation pass', async () => {
31
+ const parseSpy = vi.spyOn(schema, 'parse');
32
+ const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
33
+
34
+ const result = await zodResolver(schema, undefined, {
35
+ mode: 'sync',
36
+ })(validData, undefined, { fields, shouldUseNativeValidation });
37
+ expect(parseSpy).toHaveBeenCalledTimes(1);
38
+ expect(parseAsyncSpy).not.toHaveBeenCalled();
39
+ expect(result.errors).toEqual({});
40
+ expect(result).toMatchSnapshot();
41
+ });
42
+
43
+ it('should return a single error from zodResolver when validation fails', async () => {
44
+ const result = await zodResolver(schema)(invalidData, undefined, {
45
+ fields,
46
+ shouldUseNativeValidation,
47
+ });
48
+
49
+ expect(result).toMatchSnapshot();
50
+ });
51
+
52
+ it('should return a single error from zodResolver with `mode: sync` when validation fails', async () => {
53
+ const parseSpy = vi.spyOn(schema, 'parse');
54
+ const parseAsyncSpy = vi.spyOn(schema, 'parseAsync');
55
+
56
+ const result = await zodResolver(schema, undefined, {
57
+ mode: 'sync',
58
+ })(invalidData, undefined, { fields, shouldUseNativeValidation });
59
+
60
+ expect(parseSpy).toHaveBeenCalledTimes(1);
61
+ expect(parseAsyncSpy).not.toHaveBeenCalled();
62
+ expect(result).toMatchSnapshot();
63
+ });
64
+
65
+ it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
66
+ const result = await zodResolver(schema)(invalidData, undefined, {
67
+ fields,
68
+ criteriaMode: 'all',
69
+ shouldUseNativeValidation,
70
+ });
71
+
72
+ expect(result).toMatchSnapshot();
73
+ });
74
+
75
+ it('should return all the errors from zodResolver when validation fails with `validateAllFieldCriteria` set to true and `mode: sync`', async () => {
76
+ const result = await zodResolver(schema, undefined, { mode: 'sync' })(
77
+ invalidData,
78
+ undefined,
79
+ {
80
+ fields,
81
+ criteriaMode: 'all',
82
+ shouldUseNativeValidation,
83
+ },
84
+ );
85
+
86
+ expect(result).toMatchSnapshot();
87
+ });
88
+
89
+ it('should throw any error unrelated to Zod', async () => {
90
+ const schemaWithCustomError = schema.check(
91
+ z.refine(() => {
92
+ throw Error('custom error');
93
+ }),
94
+ );
95
+ const promise = zodResolver(schemaWithCustomError)(validData, undefined, {
96
+ fields,
97
+ shouldUseNativeValidation,
98
+ });
99
+
100
+ await expect(promise).rejects.toThrow('custom error');
101
+ });
102
+
103
+ /**
104
+ * Type inference tests
105
+ */
106
+ it('should correctly infer the output type from a zod schema', () => {
107
+ const resolver = zodResolver(z.object({ id: z.number() }));
108
+
109
+ expectTypeOf(resolver).toEqualTypeOf<
110
+ Resolver<{ id: number }, unknown, { id: number }>
111
+ >();
112
+ });
113
+
114
+ it('should correctly infer the output type from a zod schema using a transform', () => {
115
+ const resolver = zodResolver(
116
+ z.object({
117
+ id: z.pipe(
118
+ z.number(),
119
+ z.transform((val) => String(val)),
120
+ ),
121
+ }),
122
+ );
123
+
124
+ expectTypeOf(resolver).toEqualTypeOf<
125
+ Resolver<{ id: number }, unknown, { id: string }>
126
+ >();
127
+ });
128
+
129
+ it('should correctly infer the output type from a zod schema when a different input type is specified', () => {
130
+ const schema = z.pipe(
131
+ z.object({ id: z.number() }),
132
+ z.transform(({ id }) => {
133
+ return { id: String(id) };
134
+ }),
135
+ );
136
+
137
+ const resolver = zodResolver<{ id: number }, any, z.output<typeof schema>>(
138
+ schema,
139
+ );
140
+
141
+ expectTypeOf(resolver).toEqualTypeOf<
142
+ Resolver<{ id: number }, any, { id: string }>
143
+ >();
144
+ });
145
+
146
+ it('should correctly infer the output type from a Zod schema for the handleSubmit function in useForm', () => {
147
+ const schema = z.object({ id: z.number() });
148
+
149
+ const form = useForm({
150
+ resolver: zodResolver(schema),
151
+ });
152
+
153
+ expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
154
+
155
+ expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
156
+ SubmitHandler<{
157
+ id: number;
158
+ }>
159
+ >();
160
+ });
161
+
162
+ it('should correctly infer the output type from a Zod schema with a transform for the handleSubmit function in useForm', () => {
163
+ const schema = z.object({
164
+ id: z.pipe(
165
+ z.number(),
166
+ z.transform((val) => String(val)),
167
+ ),
168
+ });
169
+
170
+ const form = useForm({
171
+ resolver: zodResolver(schema),
172
+ });
173
+
174
+ expectTypeOf(form.watch('id')).toEqualTypeOf<number>();
175
+
176
+ expectTypeOf(form.handleSubmit).parameter(0).toEqualTypeOf<
177
+ SubmitHandler<{
178
+ id: string;
179
+ }>
180
+ >();
181
+ });
182
+ });
@@ -1,7 +1,7 @@
1
1
  import { Resolver, SubmitHandler, useForm } from 'react-hook-form';
2
- import { z } from 'zod';
2
+ import { z } from 'zod/v4';
3
3
  import { zodResolver } from '..';
4
- import { fields, invalidData, schema, validData } from './__fixtures__/data';
4
+ import { fields, invalidData, schema, validData } from './__fixtures__/data-v4';
5
5
 
6
6
  const shouldUseNativeValidation = false;
7
7
 
@@ -92,6 +92,21 @@ describe('zodResolver', () => {
92
92
  await expect(promise).rejects.toThrow('custom error');
93
93
  });
94
94
 
95
+ it('should enforce parse params type signature', async () => {
96
+ const resolver = zodResolver(schema, {
97
+ jitless: true,
98
+ reportInput: true,
99
+ error(iss) {
100
+ iss.path;
101
+ iss.code;
102
+ iss.path;
103
+ return { message: 'asdf' };
104
+ },
105
+ });
106
+
107
+ resolver;
108
+ });
109
+
95
110
  /**
96
111
  * Type inference tests
97
112
  */
package/zod/src/zod.ts CHANGED
@@ -8,13 +8,29 @@ import {
8
8
  ResolverSuccess,
9
9
  appendErrors,
10
10
  } from 'react-hook-form';
11
- import { ZodError, z } from 'zod';
11
+ import * as z3 from 'zod/v3';
12
+ import * as z4 from 'zod/v4/core';
12
13
 
13
- const isZodError = (error: any): error is ZodError =>
14
- Array.isArray(error?.errors);
14
+ const isZod3Error = (error: any): error is z3.ZodError => {
15
+ return Array.isArray(error?.issues);
16
+ };
17
+ const isZod3Schema = (schema: any): schema is z3.ZodSchema => {
18
+ return (
19
+ '_def' in schema &&
20
+ typeof schema._def === 'object' &&
21
+ 'typeName' in schema._def
22
+ );
23
+ };
24
+ const isZod4Error = (error: any): error is z4.$ZodError => {
25
+ // instanceof is safe in Zod 4 (uses Symbol.hasInstance)
26
+ return error instanceof z4.$ZodError;
27
+ };
28
+ const isZod4Schema = (schema: any): schema is z4.$ZodType => {
29
+ return '_zod' in schema && typeof schema._zod === 'object';
30
+ };
15
31
 
16
- function parseErrorSchema(
17
- zodErrors: z.ZodIssue[],
32
+ function parseZod3Issues(
33
+ zodErrors: z3.ZodIssue[],
18
34
  validateAllFieldCriteria: boolean,
19
35
  ) {
20
36
  const errors: Record<string, FieldError> = {};
@@ -63,37 +79,156 @@ function parseErrorSchema(
63
79
  return errors;
64
80
  }
65
81
 
82
+ function parseZod4Issues(
83
+ zodErrors: z4.$ZodIssue[],
84
+ validateAllFieldCriteria: boolean,
85
+ ) {
86
+ const errors: Record<string, FieldError> = {};
87
+ // const _zodErrors = zodErrors as z4.$ZodISsue; //
88
+ for (; zodErrors.length; ) {
89
+ const error = zodErrors[0];
90
+ const { code, message, path } = error;
91
+ const _path = path.join('.');
92
+
93
+ if (!errors[_path]) {
94
+ if (error.code === 'invalid_union') {
95
+ const unionError = error.errors[0][0];
96
+
97
+ errors[_path] = {
98
+ message: unionError.message,
99
+ type: unionError.code,
100
+ };
101
+ } else {
102
+ errors[_path] = { message, type: code };
103
+ }
104
+ }
105
+
106
+ if (error.code === 'invalid_union') {
107
+ error.errors.forEach((unionError) =>
108
+ unionError.forEach((e) => zodErrors.push(e)),
109
+ );
110
+ }
111
+
112
+ if (validateAllFieldCriteria) {
113
+ const types = errors[_path].types;
114
+ const messages = types && types[error.code];
115
+
116
+ errors[_path] = appendErrors(
117
+ _path,
118
+ validateAllFieldCriteria,
119
+ errors,
120
+ code,
121
+ messages
122
+ ? ([] as string[]).concat(messages as string[], error.message)
123
+ : error.message,
124
+ ) as FieldError;
125
+ }
126
+
127
+ zodErrors.shift();
128
+ }
129
+
130
+ return errors;
131
+ }
132
+
133
+ type RawResolverOptions = {
134
+ mode?: 'async' | 'sync';
135
+ raw: true;
136
+ };
137
+ type NonRawResolverOptions = {
138
+ mode?: 'async' | 'sync';
139
+ raw?: false;
140
+ };
141
+
142
+ // minimal interfaces to avoid asssignability issues between versions
143
+ interface Zod3Type<O = unknown, I = unknown> {
144
+ _output: O;
145
+ _input: I;
146
+ _def: {
147
+ typeName: string;
148
+ };
149
+ }
150
+
151
+ // some type magic to make versions pre-3.25.0 still work
152
+ type IsUnresolved<T> = PropertyKey extends keyof T ? true : false;
153
+ type UnresolvedFallback<T, Fallback> = IsUnresolved<typeof z3> extends true
154
+ ? Fallback
155
+ : T;
156
+ type FallbackIssue = {
157
+ code: string;
158
+ message: string;
159
+ path: (string | number)[];
160
+ };
161
+ type Zod3ParseParams = UnresolvedFallback<
162
+ z3.ParseParams,
163
+ // fallback if user is on <3.25.0
164
+ {
165
+ path?: (string | number)[];
166
+ errorMap?: (
167
+ iss: FallbackIssue,
168
+ ctx: {
169
+ defaultError: string;
170
+ data: any;
171
+ },
172
+ ) => { message: string };
173
+ async?: boolean;
174
+ }
175
+ >;
176
+ type Zod4ParseParams = UnresolvedFallback<
177
+ z4.ParseContext<z4.$ZodIssue>,
178
+ // fallback if user is on <3.25.0
179
+ {
180
+ readonly error?: (
181
+ iss: FallbackIssue,
182
+ ) => null | undefined | string | { message: string };
183
+ readonly reportInput?: boolean;
184
+ readonly jitless?: boolean;
185
+ }
186
+ >;
187
+
66
188
  export function zodResolver<Input extends FieldValues, Context, Output>(
67
- schema: z.ZodSchema<Output, any, Input>,
68
- schemaOptions?: Partial<z.ParseParams>,
69
- resolverOptions?: {
70
- mode?: 'async' | 'sync';
71
- raw?: false;
72
- },
189
+ schema: Zod3Type<Output, Input>,
190
+ schemaOptions?: Zod3ParseParams,
191
+ resolverOptions?: NonRawResolverOptions,
73
192
  ): Resolver<Input, Context, Output>;
74
-
75
193
  export function zodResolver<Input extends FieldValues, Context, Output>(
76
- schema: z.ZodSchema<Output, any, Input>,
77
- schemaOptions: Partial<z.ParseParams> | undefined,
78
- resolverOptions: {
79
- mode?: 'async' | 'sync';
80
- raw: true;
81
- },
194
+ schema: Zod3Type<Output, Input>,
195
+ schemaOptions: Zod3ParseParams | undefined,
196
+ resolverOptions: RawResolverOptions,
82
197
  ): Resolver<Input, Context, Input>;
83
-
198
+ // the Zod 4 overloads need to be generic for complicated reasons
199
+ export function zodResolver<
200
+ Input extends FieldValues,
201
+ Context,
202
+ Output,
203
+ T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>,
204
+ >(
205
+ schema: T,
206
+ schemaOptions?: Zod4ParseParams, // already partial
207
+ resolverOptions?: NonRawResolverOptions,
208
+ ): Resolver<z4.input<T>, Context, z4.output<T>>;
209
+ export function zodResolver<
210
+ Input extends FieldValues,
211
+ Context,
212
+ Output,
213
+ T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>,
214
+ >(
215
+ schema: z4.$ZodType<Output, Input>,
216
+ schemaOptions: Zod4ParseParams | undefined, // already partial
217
+ resolverOptions: RawResolverOptions,
218
+ ): Resolver<z4.input<T>, Context, z4.input<T>>;
84
219
  /**
85
220
  * Creates a resolver function for react-hook-form that validates form data using a Zod schema
86
- * @param {z.ZodSchema<Input>} schema - The Zod schema used to validate the form data
87
- * @param {Partial<z.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing
221
+ * @param {z3.ZodSchema<Input>} schema - The Zod schema used to validate the form data
222
+ * @param {Partial<z3.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing
88
223
  * @param {Object} [resolverOptions] - Optional resolver-specific configuration
89
224
  * @param {('async'|'sync')} [resolverOptions.mode='async'] - Validation mode. Use 'sync' for synchronous validation
90
225
  * @param {boolean} [resolverOptions.raw=false] - If true, returns the raw form values instead of the parsed data
91
- * @returns {Resolver<z.output<typeof schema>>} A resolver function compatible with react-hook-form
226
+ * @returns {Resolver<z3.output<typeof schema>>} A resolver function compatible with react-hook-form
92
227
  * @throws {Error} Throws if validation fails with a non-Zod error
93
228
  * @example
94
- * const schema = z.object({
95
- * name: z.string().min(2),
96
- * age: z.number().min(18)
229
+ * const schema = z3.object({
230
+ * name: z3.string().min(2),
231
+ * age: z3.number().min(18)
97
232
  * });
98
233
  *
99
234
  * useForm({
@@ -101,41 +236,80 @@ export function zodResolver<Input extends FieldValues, Context, Output>(
101
236
  * });
102
237
  */
103
238
  export function zodResolver<Input extends FieldValues, Context, Output>(
104
- schema: z.ZodSchema<Output, any, Input>,
105
- schemaOptions?: Partial<z.ParseParams>,
239
+ schema: object,
240
+ schemaOptions?: object,
106
241
  resolverOptions: {
107
242
  mode?: 'async' | 'sync';
108
243
  raw?: boolean;
109
244
  } = {},
110
245
  ): Resolver<Input, Context, Output | Input> {
111
- return async (values: Input, _, options) => {
112
- try {
113
- const data = await schema[
114
- resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync'
115
- ](values, schemaOptions);
116
-
117
- options.shouldUseNativeValidation && validateFieldsNatively({}, options);
118
-
119
- return {
120
- errors: {} as FieldErrors,
121
- values: resolverOptions.raw ? Object.assign({}, values) : data,
122
- } satisfies ResolverSuccess<Output | Input>;
123
- } catch (error) {
124
- if (isZodError(error)) {
246
+ if (isZod3Schema(schema)) {
247
+ return async (values: Input, _, options) => {
248
+ try {
249
+ const data = await schema[
250
+ resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync'
251
+ ](values, schemaOptions);
252
+
253
+ options.shouldUseNativeValidation &&
254
+ validateFieldsNatively({}, options);
255
+
125
256
  return {
126
- values: {},
127
- errors: toNestErrors(
128
- parseErrorSchema(
129
- error.errors,
130
- !options.shouldUseNativeValidation &&
131
- options.criteriaMode === 'all',
257
+ errors: {} as FieldErrors,
258
+ values: resolverOptions.raw ? Object.assign({}, values) : data,
259
+ } satisfies ResolverSuccess<Output | Input>;
260
+ } catch (error) {
261
+ if (isZod3Error(error)) {
262
+ return {
263
+ values: {},
264
+ errors: toNestErrors(
265
+ parseZod3Issues(
266
+ error.errors,
267
+ !options.shouldUseNativeValidation &&
268
+ options.criteriaMode === 'all',
269
+ ),
270
+ options,
132
271
  ),
133
- options,
134
- ),
135
- } satisfies ResolverError<Input>;
272
+ } satisfies ResolverError<Input>;
273
+ }
274
+
275
+ throw error;
136
276
  }
277
+ };
278
+ }
137
279
 
138
- throw error;
139
- }
140
- };
280
+ if (isZod4Schema(schema)) {
281
+ return async (values: Input, _, options) => {
282
+ try {
283
+ const parseFn =
284
+ resolverOptions.mode === 'sync' ? z4.parse : z4.parseAsync;
285
+ const data: any = await parseFn(schema, values, schemaOptions);
286
+
287
+ options.shouldUseNativeValidation &&
288
+ validateFieldsNatively({}, options);
289
+
290
+ return {
291
+ errors: {} as FieldErrors,
292
+ values: resolverOptions.raw ? Object.assign({}, values) : data,
293
+ } satisfies ResolverSuccess<Output | Input>;
294
+ } catch (error) {
295
+ if (isZod4Error(error)) {
296
+ return {
297
+ values: {},
298
+ errors: toNestErrors(
299
+ parseZod4Issues(
300
+ error.issues,
301
+ !options.shouldUseNativeValidation &&
302
+ options.criteriaMode === 'all',
303
+ ),
304
+ options,
305
+ ),
306
+ } satisfies ResolverError<Input>;
307
+ }
308
+
309
+ throw error;
310
+ }
311
+ };
312
+ }
313
+
314
+ throw new Error('Invalid input: not a Zod schema');
141
315
  }