skip to Main Content

I’m working with a validation schema using Zod, where I have an items field that represents an array of objects. In this scenario, I’m specifically using it for invoice items. Each item is an object with various properties.

I’m facing a challenge in defining a validation schema that enforces the following condition: If the id property within an item is set, then all other fields within that item should be optional (indicating that the item is being updated). Conversely, if the id property is not set, then all other fields should be required (indicating that a new item is being created).

How can I achieve this conditional validation in Zod?

items: z.array(z.object({
        id: z.number({required_error: "'id' is required", invalid_type_error: "'id' must be a number"}).positive("'id' must be a positive number").optional(),
        articleId: z.number({required_error: "'articleId' is required", invalid_type_error: "'articleId' must be a number"}).positive("'articleId' must be a positive number"),
        sku: z.number({required_error: "'sku' is required", invalid_type_error: "'sku' must be a number"}).positive("'sku' must be a positiv number"),
        description: z.string({ required_error: "'description' is required" }).nonempty("'description' cannot be empty").max(1000, { message: "'description' is too long" }),
        quantity: z.number({required_error: "'quantity' is required", invalid_type_error: "'quantity' must be a number"}).positive("'quantity' must be a positive number"),
        price: z.number({required_error: "'price' is required", invalid_type_error: "'price' must be a number"}).positive("'price' must be a positive number"),
        term: z.enum(["month", "year", "flat", "piece"], { required_error: "'term' is required" }),
    })),

2

Answers


  1. Have you tried z.or()?

    const schema = z.object({
      id: z.string(),
    }).or(z.object({
      name: z.string(),
      whatever: z.number(),
      etc: z.string()
    }))
    
    
    type T = z.infer<typeof schema>
    /*
     T: {id: string} | {name: string; whatever: number; etc: string}
    */
    
    Login or Signup to reply.
  2. You can try using superRefine method to add conditional validation to your previous schema like this:

    const itemSchema = z
      .object({
        id: z
          .number({
            required_error: "'id' is required",
            invalid_type_error: "'id' must be a number",
          })
          .positive("'id' must be a positive number")
          .optional(),
        articleId: z
          .number({
            required_error: "'articleId' is required",
            invalid_type_error: "'articleId' must be a number",
          })
          .positive("'articleId' must be a positive number"),
        sku: z
          .number({
            required_error: "'sku' is required",
            invalid_type_error: "'sku' must be a number",
          })
          .positive("'sku' must be a positive number"),
        description: z
          .string({ required_error: "'description' is required" })
          .nonempty("'description' cannot be empty")
          .max(1000, { message: "'description' is too long" }),
        quantity: z
          .number({
            required_error: "'quantity' is required",
            invalid_type_error: "'quantity' must be a number",
          })
          .positive("'quantity' must be a positive number"),
        price: z
          .number({
            required_error: "'price' is required",
            invalid_type_error: "'price' must be a number",
          })
          .positive("'price' must be a positive number"),
        term: z.enum(["month", "year", "flat", "piece"], {
          required_error: "'term' is required",
        }),
      })
      .superRefine(
        (
          { id, articleId, sku, description, quantity, price, term },
          refinementContext
        ) => {
          if (id !== undefined) {
            // If id is defined, make all other fields optional
            return;
          } else {
            // If id is not defined, make all other fields required
            const fieldsToCheck = [
              "articleId",
              "sku",
              "description",
              "quantity",
              "price",
              "term",
            ];
            for (const field of fieldsToCheck) {
              if (eval(field) === undefined) {
                refinementContext.addIssue({
                  code: z.ZodIssueCode.custom,
                  message: `${field} is required when 'id' is not set`,
                  path: [field],
                });
              }
            }
          }
        }
      );
    
    const items = z.array(itemSchema);
    
    const validItem = {
      articleId: 123,
      sku: 456,
      description: "Product ABC",
      quantity: 5,
      price: 10.99,
      term: "month",
    };
    
    const isValid = items.safeParse([validItem]);
    
    console.log(isValid.success); // Should output true
    
    const invalidItem = {
      articleId: 123,
      sku: 456,
      //description is missing
      quantity: 5,
      price: 10.99,
      term: "month",
    };
    
    const isInvalid = items.safeParse([invalidItem]);
    
    console.log(isInvalid.success); // Should output false

    This code was adapted from this repository on GitHub, which contained an implementation similar to yours: https://github.com/colinhacks/zod/discussions/938#discussioncomment-6952970

    I hope this solves your problem.

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