skip to Main Content

I have a zod schema like this:

export const memberSchema = z
  .object({
    name: z.string().min(3),
    lastname: z.string().min(3),
    nationality: zCountries,
    birthdate: z.date(),
    email: z.string().email(),
    tutor: z
      .object({
        name: z.string(),
        lastname: z.string(),
        documentType: zDocumentationType,
        documentValue: z.string(),
        email: z.string().email(),
        phone: z.string().regex(/^d{9}$/)
      })
  }).refine((data) => .....)

Is it possible to validate tutor object is required only if birthdate is under 18 ?
Because if member is +18 this fields are not needed.

I know how to validate a field depending of another one in refine function, but I don’t know how to validate and entire object…

EXAMPLE:

  • { birthdate: ’20/02/2015′} -> ALL Fields in tutor has to be filled and passing zod tutor object validation
  • {birthdate: ’09/05/1988′ } -> Tutor object is undefined and returns true.

2

Answers


  1. Chosen as BEST ANSWER

    After many tries I got what I want with z.union between object and undefined and super refine.

    Here is my code. Hope it helps someone in future.

    export const memberSchema = z
      .object({
        name: z.string().min(3),
        lastname: z.string().min(3),
        nationality: zCountries,
        birthdate: z.date(),
        email: z.string().email(),
        tutor: z.union([
          z.object({
            name: zTutorString,
            lastname: zTutorString,
            document: zTutorString,
            email: zTutorString.email(),
            phone: zTutorString.regex(/^d{9}$/)
          }),
          z.undefined()
        ])
      })
      .superRefine((data, ctx) => {
        const validation =
          data.birthdate.getFullYear() + 18 < new Date().getFullYear() ||
          (data.birthdate.getFullYear() + 18 > new Date().getFullYear() && data.tutor !== undefined)
        if (!validation) {
          tutorFields.forEach((field) => {
            if (!data.tutor || (data.tutor && !data.tutor[field as keyof typeof data.tutor])) {
              ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: i18n.t('validation.required_tutor'),
                path: ['tutor', field]
              })
            }
          })
        }
      })
    

  2. You can use a superRefine call to add issues to the overall object. For example:

    export const memberSchema = z
      .object({
        name: z.string().min(3),
        lastname: z.string().min(3),
        nationality: zCountries,
        birthdate: z.date(),
        email: z.string().email(),
        tutor: tutorSchema.optional(),
      })
      .superRefine((obj, ctx) => {
        const ageInYears = getAgeInYears(obj.birthdate);
        if (ageInYears < 18 && obj.tutor === undefined) {
          ctx.addIssue({
            code: "custom",
            message: "Members under the age of 18 must have a tutor",
          });
        }
      });
    
    console.log(
      memberSchema.safeParse({
        name: "Test",
        lastname: "Testerson",
        nationality: "USA",
        birthdate: new Date("2010-1-1"), // Some date less than 18 years ago
        email: "[email protected]",
      })
    ); // Logs failure with custom message
    
    console.log(
      memberSchema.safeParse({
        name: "Test",
        lastname: "Testerson",
        nationality: "USA",
        birthdate: new Date("2010-1-1"), // Some date less than 18 years ago
        email: "[email protected]",
        tutor: {
          name: "Tutor",
          lastname: "Some tutor",
          phone: undefined,
          email: undefined,
          documentType: undefined,
          documentValue: undefined,
        },
      })
    ); // Logs success
    

    Note that going from the Date object to their age in years is potentially non-trivial, so I’ve left that as an exercise for the reader.

    The main trick is to make the tutor field optional, and then to use the refine to say, "no actually it must be defined if the user is over 18".

    A note on types

    Unfortunately, this approach does not give you any firmer type guarantees once you know that the user is under 18. If you want the type system to track this for you, you could split the types along the birthdate and use z.brand to attach that additional information, but it may be more trouble than it’s worth.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search