-
-
Notifications
You must be signed in to change notification settings - Fork 192
feat: zod v4 support #768 #776
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -5,6 +5,7 @@ | |||||||
"name": "@hookform/resolvers", | ||||||||
"dependencies": { | ||||||||
"@standard-schema/utils": "^0.3.0", | ||||||||
"zod-v4": "npm:zod@^3.25.0", | ||||||||
}, | ||||||||
"devDependencies": { | ||||||||
"@sinclair/typebox": "^0.34.30", | ||||||||
|
@@ -54,7 +55,7 @@ | |||||||
"vite-tsconfig-paths": "^5.1.4", | ||||||||
"vitest": "^3.0.9", | ||||||||
"yup": "^1.6.1", | ||||||||
"zod": "^3.24.2", | ||||||||
"zod": "3.24.4", | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixing @CHC383 's suggestion by moving
Suggested change
|
||||||||
}, | ||||||||
"peerDependencies": { | ||||||||
"react-hook-form": "^7.55.0", | ||||||||
|
@@ -1444,7 +1445,9 @@ | |||||||
|
||||||||
"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=="], | ||||||||
|
||||||||
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], | ||||||||
"zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], | ||||||||
|
||||||||
"zod-v4": ["zod@3.25.42", "", {}, "sha512-PcALTLskaucbeHc41tU/xfjfhcz8z0GdhhDcSgrCTmSazUuqnYqiXO63M0QUBVwpBlsLsNVn5qHSC5Dw3KZvaQ=="], | ||||||||
|
||||||||
"@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], | ||||||||
|
||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './zod'; | ||
export { zodResolver } from './zod'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replacing Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do it this way to be able to export both resolvers of both versions. |
||
export { zodResolver as zodResolverV4 } from './zodv4'; |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,134 @@ | ||||||
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; | ||||||
import { | ||||||
FieldError, | ||||||
FieldErrors, | ||||||
FieldValues, | ||||||
Resolver, | ||||||
ResolverError, | ||||||
ResolverSuccess, | ||||||
appendErrors, | ||||||
} from 'react-hook-form'; | ||||||
import { ZodError } from 'zod-v4/v4'; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think for v4-mini compatibility, the code should use |
||||||
import * as core from 'zod-v4/v4/core'; | ||||||
|
||||||
function parseErrorSchema( | ||||||
zodErrors: core.$ZodIssue[], | ||||||
validateAllFieldCriteria: boolean, | ||||||
) { | ||||||
const errors: Record<string, FieldError> = {}; | ||||||
for (; zodErrors.length; ) { | ||||||
const error = zodErrors[0]; | ||||||
const { code, message, path } = error; | ||||||
const _path = path.join('.'); | ||||||
|
||||||
if (!errors[_path]) { | ||||||
if (error.code === 'invalid_union') { | ||||||
const unionError = error.errors[0][0]; | ||||||
|
||||||
errors[_path] = { | ||||||
message: unionError.message, | ||||||
type: unionError.code, | ||||||
}; | ||||||
} else { | ||||||
errors[_path] = { message, type: code }; | ||||||
} | ||||||
} | ||||||
|
||||||
if (error.code === 'invalid_union') { | ||||||
error.errors.forEach((unionError: any[]) => | ||||||
unionError.forEach((e) => | ||||||
zodErrors.push({ | ||||||
...e, | ||||||
path: [...path, ...e.path], | ||||||
}), | ||||||
), | ||||||
); | ||||||
} | ||||||
|
||||||
if (validateAllFieldCriteria) { | ||||||
const types = errors[_path].types; | ||||||
const messages = types && types[error.code]; | ||||||
|
||||||
errors[_path] = appendErrors( | ||||||
_path, | ||||||
validateAllFieldCriteria, | ||||||
errors, | ||||||
code, | ||||||
messages | ||||||
? ([] as string[]).concat(messages as string[], error.message) | ||||||
: error.message, | ||||||
) as FieldError; | ||||||
} | ||||||
|
||||||
zodErrors.shift(); | ||||||
} | ||||||
|
||||||
return errors; | ||||||
} | ||||||
|
||||||
export function zodResolver<Input extends FieldValues, Context, Output>( | ||||||
schema: core.$ZodType<Output, Input>, | ||||||
schemaOptions?: Partial<core.ParseContext<core.$ZodIssue>>, | ||||||
resolverOptions?: { | ||||||
mode?: 'async' | 'sync'; | ||||||
raw?: false; | ||||||
}, | ||||||
): Resolver<Input, Context, Output>; | ||||||
Comment on lines
+69
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems like the overload for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see #776 (comment) |
||||||
|
||||||
/** | ||||||
* Creates a resolver function for react-hook-form that validates form data using a Zod schema | ||||||
* @param {z.ZodSchema<Input>} schema - The Zod schema used to validate the form data | ||||||
* @param {Partial<z.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update
Suggested change
|
||||||
* @param {Object} [resolverOptions] - Optional resolver-specific configuration | ||||||
* @param {('async'|'sync')} [resolverOptions.mode='async'] - Validation mode. Use 'sync' for synchronous validation | ||||||
* @param {boolean} [resolverOptions.raw=false] - If true, returns the raw form values instead of the parsed data | ||||||
* @returns {Resolver<z.output<typeof schema>>} A resolver function compatible with react-hook-form | ||||||
* @throws {Error} Throws if validation fails with a non-Zod error | ||||||
* @example | ||||||
* const schema = z.object({ | ||||||
* @param {z.core.ParseContext<z.core.$ZodIssue>} [schemaOptions] - Optional configuration options for Zod parsing * age: z.number().min(18) | ||||||
* }); | ||||||
* | ||||||
* useForm({ | ||||||
* resolver: zodResolver(schema) | ||||||
* }); | ||||||
*/ | ||||||
export function zodResolver<Input extends FieldValues, Context, Output>( | ||||||
schema: core.$ZodType<Output, Input>, | ||||||
schemaOptions?: Partial<core.ParseContext<core.$ZodIssue>>, | ||||||
resolverOptions: { | ||||||
mode?: 'async' | 'sync'; | ||||||
raw?: boolean; | ||||||
} = {}, | ||||||
): Resolver<Input, Context, Output | Input> { | ||||||
return async (values: Input, _, options) => { | ||||||
try { | ||||||
const data = await (resolverOptions.mode === 'sync' | ||||||
? core.parse(schema, values, schemaOptions) | ||||||
: core.parseAsync(schema, values, schemaOptions)); | ||||||
|
||||||
options.shouldUseNativeValidation && validateFieldsNatively({}, options); | ||||||
|
||||||
return { | ||||||
errors: {} as FieldErrors, | ||||||
values: resolverOptions.raw ? values : data, | ||||||
} satisfies ResolverSuccess<Output | Input>; | ||||||
} catch (error) { | ||||||
if (error instanceof ZodError) { | ||||||
return { | ||||||
values: {} as Input, | ||||||
errors: toNestErrors( | ||||||
parseErrorSchema( | ||||||
(error as { issues: core.$ZodIssue[] }).issues, | ||||||
!options.shouldUseNativeValidation && | ||||||
options.criteriaMode === 'all', | ||||||
), | ||||||
options, | ||||||
), | ||||||
} satisfies ResolverError<Input>; | ||||||
} | ||||||
|
||||||
throw error; | ||||||
} | ||||||
}; | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lock file also needs to be updated after moving zod-v4 to devDependencies
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't
zod-v4
be under devDependencies? Only the standard schema is included as a dependency.