I have a Zod validation schema with an array field and a string field:
const zodSchema = z.object({
goals: z.array(z.string()).nonempty("At least one goal is required"),
goals_other: z.string(),
});
How do I make the goals_other
field required ONLY if goals
array includes the string "Other"?
I tried a refine function like the following but it didn't work
const zodSchema = z
.object({
goals: z.array(z.string()).nonempty("At least one goal is required"),
goals_other: z.string(),
})
.refine((data) => (data.goals.includes("Other") ? true : false), {
message: "Required, please specify other goals",
path: ["goals_other"],
});
Any help is appreciated!
There is a straightforward way to get the behavior you want assuming that you are ok with the schema failing to parse if bad data is present in the goals_other
field when 'Other'
is not included in the list. I will show this approach first:
import { z } from "zod";
const zodSchema = z
.object({
goals: z.array(z.string()).nonempty("At least one goal is required"),
goals_other: z.string().optional() // optional to avoid failing when it's missing
})
.superRefine(({ goals, goals_other }, ctx) => {
// Here we add the extra check to assert the field exists when
// the "Other" goal is present.
if (goals.includes("Other") && goals_other === undefined) {
ctx.addIssue({
code: "custom",
message: "Required, please specify other goals",
path: ["goals_other"]
});
}
});
console.log(zodSchema.safeParse({
goals: ['test'],
})); // success
console.log(zodSchema.safeParse({
goals: ['Other'],
})); // failure
console.log(zodSchema.safeParse({
goals: ['Other'],
goals_other: 11,
})); // failure (because goals_other is not a string)
console.log(zodSchema.safeParse({
goals: ['Other'],
goals_other: 'test',
})); // success
But there is a problem IMO with this:
zodSchema.safeParse({
goals: ['test'],
goals_other: 11,
}); // Failure
This attempt to parse is a failure, because the goals_other
field is expected to be a string | undefined
and it got a number. If you truly do not care about the goals_other
field unless there is the "Other"
goal in your goals list, then what you really want is to ignore the field until you find the other string and then to validate.
const zodSchema = z
.object({
goals: z.array(z.string()).nonempty("At least one goal is required"),
goals_other: z.unknown(),
})
.superRefine(({ goals, goals_other }, ctx) => {
if (goals.includes("Other")) {
if (goals_other === undefined) {
ctx.addIssue({
code: "custom",
message: "Required, please specify other goals",
path: ["goals_other"]
});
} else if (typeof goals_other !== 'string') {
ctx.addIssue({
code: 'custom',
message: 'expected a string',
path: ['goals_other'],
});
}
}
});
This schema would parse correctly, but it doesn't refine the output type. other_goals
will have type unknown
which is somewhat unhelpful. You could fix this with a type assertion in a transform
but that feels a little bit awkward too. It might be best to just go ahead with the first schema and accept that you're not actually ignoring the goals_other
field when "Other"
is not present. So long as other data doesn't end up in there in the values you're parsing, you're not going to have a problem.