-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
In Zod 3, it was possible to implement reactive cross-field validation for password confirmation using field.superRefine.
Example (Zod 3):
.superRefine((data, ctx) => {
if (data.confirm !== data.password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('validation.passwordsMatch'),
path: ['confirm'],
});
}
This worked as expected with React Hook Form and mode: "onChange":
The confirm field would show a password mismatch error reactively as the user typed
No submission was required to trigger the error
Note: This reactive feedback occurred only on the confirm field, which is the expected and natural behavior.
Problem in Zod 4
In Zod 4, ctx.parent was removed. The recommended approach is now to use object.refine() with a path to target the error:
const schema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords must match",
path: ["confirmPassword"],
});
However, when using React Hook Form with mode: "onChange":
The confirm error does not appear while typing
No mismatch feedback is shown until the form is submitted
After submit, reactive updates work, but the initial typing gives no error
This is a regression compared to Zod 3.
Expected Behavior
Cross-field validation for confirm should trigger reactively as the user types
Password mismatch errors should appear without submitting the form
Behavior should match Zod 3 superRefine experience
Environment
Zod 4.x
React Hook Form 7.x
React 19.x
Notes / Reproduction
This issue occurs even when using:
useForm({
resolver: zodResolver(schema),
mode: "onChange",
})
It seems to be a limitation in the interaction between Zod 4's object-level .refine() and React Hook Form's partial validation strategy.
A minimal reproduction in CodeSandbox would make this behavior easy to verify.