Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Regression: Confirm field cross-validation (password match) no longer reactive in Zod 4 + RHF #5707

@PinkHonkKongGmr

Description

@PinkHonkKongGmr

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions