Published on

Conditional Validation in Zod: A Comprehensive Guide

Authors
  • Name
    Ripal & Zalak
    Twitter

Introduction

Zod is a powerful TypeScript-first schema validation library that allows developers to define schemas for data validation. A common requirement is implementing conditional validation—where certain fields in an object are required or optional depending on the value of another field. This blog provides a step-by-step guide to implementing conditional validation in Zod with practical examples.

Use Case

Consider a scenario where you're validating an array of invoice items. Each item is an object with various properties. The requirement is:

  • If the id property is set, all other fields should be optional (indicating an update).
  • If the id property is not set, all other fields should be required (indicating a new item).

Implementing Conditional Validation in Zod

Step 1: Define the Base Schema

Create a base schema for your items with all fields.

import { z } from 'zod'

const baseItemSchema = z.object({
  id: z.number().positive().optional(),
  articleId: z.number().positive(),
  sku: z.number().positive(),
  description: z.string().nonempty().max(1000),
  quantity: z.number().positive(),
  price: z.number().positive(),
  term: z.enum(['month', 'year', 'flat', 'piece']),
})

Step 2: Add Conditional Validation Using superRefine

Use the superRefine method to enforce conditional validation logic.

const conditionalItemSchema = baseItemSchema.superRefine((data, context) => {
  if (data.id !== undefined) {
    // If `id` is defined, other fields are optional.
    return
  } else {
    // If `id` is not defined, ensure all other fields are provided.
    const requiredFields = ['articleId', 'sku', 'description', 'quantity', 'price', 'term']

    requiredFields.forEach((field) => {
      if (data[field] === undefined || data[field] === null) {
        context.addIssue({
          code: z.ZodIssueCode.custom,
          message: `${field} is required when 'id' is not set`,
          path: [field],
        })
      }
    })
  }
})

Step 3: Define the Array Schema

Wrap the conditionalItemSchema in a Zod array schema to validate multiple items.

const itemsSchema = z.array(conditionalItemSchema)

Step 4: Validate Data

Test the schema with different inputs.

Valid Data Example (Update Scenario)

const validUpdate = [
  {
    id: 1,
    articleId: undefined,
    sku: undefined,
    description: undefined,
    quantity: undefined,
    price: undefined,
    term: undefined,
  },
]

console.log(itemsSchema.safeParse(validUpdate).success) // true

Invalid Data Example (New Item Scenario)

const invalidNewItem = [
  {
    articleId: 123,
    sku: undefined, // Missing required field
    description: 'Product Description',
    quantity: 5,
    price: 100,
    term: 'month',
  },
]

console.log(itemsSchema.safeParse(invalidNewItem).success) // false

Advanced Techniques

Using refine for Simpler Cases

For simpler conditional validation scenarios, you can use refine with custom conditions.

const schema = z
  .object({
    type: z.enum(['business', 'personal']),
    abn: z.string().optional(),
  })
  .refine(
    (data) => {
      return data.type === 'business' ? !!data.abn : true
    },
    {
      message: 'ABN is required for business type',
    }
  )

Leveraging Utility Functions

To avoid repetitive code, create a utility function for conditional validation logic.

function requireFieldsWhenIdAbsent(fields: string[], data: any, context: z.RefinementCtx) {
  fields.forEach((field) => {
    if (data[field] === undefined) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: `${field} is required when 'id' is not set`,
        path: [field],
      })
    }
  })
}

Conclusion

Conditional validation in Zod allows you to enforce complex business rules while keeping your code clean and maintainable. By combining superRefine and modular utility functions, you can handle a wide range of validation scenarios with ease.

Have insights or additional tips? Share them in the comments below!