I'm trying to conditionally validate a react-hook-form that looks like this with zod:
The features of the validation are:
- The user must tick at least one contact method, or they see an error
- They must then provide the value for the contact method they've ticked
- The values are then refined and transformed (eg. trimmed, parsed into the right format etc)
My schema looks like this:
const schema = z
.object({
// other fields...
contactMethods: z
.array(z.enum(["email", "phone"]), {
invalid_type_error: "At least one contact method is required",
})
.min(1, "At least one contact method is required"),
email: z
.string({ invalid_type_error: "Enter an email1" })
.trim()
.email("Enter a valid email"),
phone: z
.string({ invalid_type_error: "Enter a phone number" })
.trim()
.transform(val => val.replaceAll(" ", "")) // spaces confuse validator function below
.refine(val => validator.isMobilePhone(val), "Enter a valid phone number") // check value is parsable as phone number
.transform(val => parsePhoneNumber(val, "GB")?.number) // parse into +44 e.164 format if not already
})
.refine(vals => vals.contactMethods.includes("email") ? vals.email : true, { message: "Enter an email", path: ["email"] })
.refine(vals => vals.contactMethods.includes("phone") ? vals.phone : true, { message: "Enter a phone number", path: ["phone"] })
I'm trying to adapt the solution in this closed zod issue.
This doesn't do the job because:
- The form has a few other fields (omitted here for clarity), and I need to make sure they pass validation before triggering the
.refine()blocks further down. At the moment, the user can resolve what they think are all the errors, and then see more once they finish. - Error messages for the nested fields hang around once I've started typing in one of them, even if I untick that contact method
I know zod supports union OR types, which seems helpful here in principle, but I'm struggling to understand how to adapt the code to my situation.
Also open to suggestions for simplifying this code more generally.

One way to do this would be to split the schema down into three parts. The first part checks there is at least one contact method, but it doesn't care data all about
emailorphone.This is then intersected with two other schemas. One to handle
emailand one to handlephone. Each of those schemas is a union of the two possibilities for each respective one:phone/emailis not in contact methods.phone/emailis in contact methods, andphone/emailis valid.The whole thing comes together in a single schema.