Change part of zod schema object based on field value?

1.4k Views Asked by At

I have a form with some fields and a switch which is used to show/hide some part of the form like this:

    <Field name="field1" value={value1} />
    <Field name="field2" value={value2} />
    <Switch value={showShippingAddress} / > 
     { showShippingAddress && (
     <>
     <Field name="field3" value={value3} />
     <Field name="field4" value={value4} />
     </>
    )}

in the zod schema I would like to achieve something like this

    z
     .object({
       field1: z.string()
       field2: z.string(),
       showShippingAddress: z.boolean(),
       field3: showShippingAddressValue ? z.required : z.string(), // wrong just for describing desired result
       field4: showShippingAddressValue ? z.string.required.regex('') : z.string(), // also wrong 

The question is how can I make this in a better way, better than the only way I can see at the moment:


    z
     .object({
      field1: z.string()
      field2: z.string(),
    })
    .and(
      z.object({
        showShippingAddress: z.boolean(),
        field3: z.string().nullable(),
        field4: z.string().nullable(),
      })
      .superRefine(
        ({ showShippingAddress, field3, field4}, { addIssue }) => {
          if(showShippingAddress && notEmptyCheckHelper(field3)) {
            .addIssue({
              code: z.ZodIssueCode.custom,
              message: `Field3 is required.`
            })
         }

         if(showShippingAddress && notEmptyCheckHelper(field4)) {
           .addIssue({
              code: z.ZodIssueCode.custom,
              message: `Field4 is required.`
           })
         }

         if(showShippingAddress && regexCheckHelper(field4)) {
           .addIssue({
              code: z.ZodIssueCode.custom,
              message: `Field4 is not valid.`
           })
        }

        // if there are more validations on Field 4 the checks here also grow and 
        // the same for more fields  that depend on this showShippingAddress value

    }))

1

There are 1 best solutions below

3
On BEST ANSWER

Discriminated Unions are exactly what you are looking for.

You will need 2 objects, one with showShippingAddress as a literal 'true' and another with showShippingAddress as a literal 'false'. Then make a discriminated union.

So in your case, something like this should work

z.discriminatedUnion("showShippingAddressValue", [
    z.object({
        showShippingAddressValue: z.literal(false),
        field1: z.string(),
        field2: z.string(),
        field3: z.string().optional(),
        field4: z.string().optional()
    }),
    z.object({
        showShippingAddressValue: z.literal(true),
        field1: z.string(),
        field2: z.string(),
        field3: z.string(),
        field4: z.string()
    })
]);

Do note that just using z.string() makes it a required field, while adding a .optional() will make the field optional.

So the schema I provided above will successfully parse:

final.parse({
    showShippingAddressValue: false,
    field1: "some value",
    field2: "some value"
});

final.parse({
    showShippingAddressValue: true,
    field1: "some value",
    field2: "some value",
    field3: "some value",
    field4: "some value"
});

But it will fail to parse

final.parse({
    showShippingAddressValue: true,
    field1: "some value",
    field2: "some value"
});

Also, one extra note, if you don't want the strings to be empty, you may want to use the .nonempty() method