Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional validation in zod

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" }),
    })),
like image 724
hantoren Avatar asked Jul 19 '26 16:07

hantoren


2 Answers

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}
*/
like image 169
Ahmed Abdelbaset Avatar answered Jul 21 '26 05:07

Ahmed Abdelbaset


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.

like image 42
Enoque Tembe Avatar answered Jul 21 '26 04:07

Enoque Tembe