reactjsvalidationzod

Zod validation schema make field required based on another array field


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!


Solution

  • 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.