Search code examples
typescriptreact-hook-formzod

How can I make a field required based in other field value with Zod?


I'm trying to validate a form with Zod and react-hook-form, and I need to make some fields required based on the value of other field. Here is my schema:

const formSchema = z.object({
    cli_rut: z.string(),
    cli_persona_natural: z.enum(["SI", "NO"]),
    cli_nombres: z.string().optional(),
    cli_apellido_paterno: z.string().optional(),
    cli_apellido_materno: z.string().optional(),
    cli_razon_social: z.string().optional(),
    cli_estado: z.string().default("ACTIVO").optional(),
});

in this case I need to make required cli_nombres, cli_apellido_materno, and cli_apellido_paterno if cli_persona_natural = "SI", any ideas of how can i do it? Thanks

I'm expecting the fields to be required if cli_persona_natural is "SI"


Solution

  • You can use the superRefine method to specify additional constraints like this, or you could use discriminatedUnion in this case and end up with slightly stronger types.

    Super Refine Approach:

    const formSchema = z
      .object({
        cli_rut: z.string(),
        cli_persona_natural: z.enum(["SI", "NO"]),
        cli_nombres: z.string().optional(),
        cli_apellido_paterno: z.string().optional(),
        cli_apellido_materno: z.string().optional(),
        cli_razon_social: z.string().optional(),
        cli_estado: z.string().default("ACTIVO").optional(),
      })
      .superRefine((data, ctx) => {
        if (data.cli_persona_natural === "SI") {
          if (data.cli_nombres === undefined) {
            // I took a best guess at the messages but it's not my native language
            ctx.addIssue({
              code: z.ZodIssueCode.custom,
              message: "El campo nombres es requerido",
            });
          }
          if (data.cli_apellido_paterno === undefined) {
            ctx.addIssue({
              code: z.ZodIssueCode.custom,
              message: "El campo apellido paterno es requerido",
            });
          }
          if (data.cli_apellido_materno === undefined) {
            ctx.addIssue({
              code: z.ZodIssueCode.custom,
              message: "El campo apellido materno es requerido",
            });
          }
        }
      });
    

    Discriminated Union Approach

    import { z } from "zod";
    
    const baseSchema = z.object({
      cli_rut: z.string(),
      cli_razon_social: z.string().optional(),
      cli_estado: z.string().default("ACTIVO").optional(),
    });
    
    const personaNaturalSchema = z.object({
      cli_persona_natural: z.literal("SI"),
      cli_nombres: z.string(),
      cli_apellido_paterno: z.string(),
      cli_apellido_materno: z.string(),
    });
    
    // Again no clue if this naming makes sense in practice
    const personaJuridicaSchema = z.object({
      cli_persona_natural: z.literal("NO"),
      cli_nombres: z.string().optional(),
      cli_apellido_paterno: z.string().optional(),
      cli_apellido_materno: z.string().optional(),
    });
    
    const formSchema = z
      .discriminatedUnion("cli_persona_natural", [
        personaNaturalSchema,
        personaJuridicaSchema,
      ])
      .and(baseSchema);
    
    console.log(
      formSchema.safeParse({
        cli_persona_natural: "SI",
        cli_rut: "12345678",
        cli_nombres: "John",
        cli_apellido_paterno: "Steve",
        cli_apellido_materno: "Tessa",
      })
    ); // success
    console.log(
      formSchema.safeParse({
        cli_persona_natural: "NO",
        cli_rut: "12345678",
      })
    ); // success
    console.log(
      formSchema.safeParse({
        cli_persona_natural: "SI",
        cli_rut: "12345678",
      })
    ); // failure
    

    This approach has a benefit with the types. If you check the value of cli_persona_natural you will get type inference about whether or not the other fields are required.