Published on

How to dynamically generate Zod Schemas | Zod Validation Guide

Authors
  • Name
    Ripal & Zalak
    Twitter

How to dynamically generate Zod Schemas in TypeScript

The Problem

Suppose you have a reusable form component in a React app built with react-hook-form. The form is defined dynamically, accepting an array of field configurations. You want to:

  1. Parse form inputs and validate them according to dynamic specifications.
  2. Infer types for better developer experience during development.

Example:

// Manually coded schema
const schema = z.object({
  foo: z.string(),
  bar: z.number(),
})

type Fields = z.infer<typeof schema> // { foo: string; bar: number; }

// Dynamic schema configuration
const fields = [
  { name: 'foo', fieldType: z.string() },
  { name: 'bar', fieldType: z.number() },
]

const generateSchemaFromFields = (fields: FieldConfig[]) => {
  // Implementation needed
}

const generatedSchema = generateSchemaFromFields(fields)

The Solution

We can dynamically create a Zod schema using the field configurations. Here's a step-by-step solution:

Step 1: Define Field Configurations

Define the field configuration that includes the field name and validation rules:

const fields = [
  { name: 'foo', fieldType: z.string() },
  { name: 'bar', fieldType: z.number() },
] as { name: string; fieldType: z.ZodSchema }[]

Step 2: Generate Schema Dynamically

Convert the fields into a Zod schema object:

const generateSchemaFromFields = (fields: { name: string; fieldType: z.ZodSchema }[]) => {
  const schemaObject = Object.fromEntries(fields.map((field) => [field.name, field.fieldType]))

  return z.object(schemaObject)
}

const schema = generateSchemaFromFields(fields)

// Test the schema
const validInput = { foo: 'Hello', bar: 42 }
console.log(schema.safeParse(validInput).success) // true

const invalidInput = { foo: 42, bar: 'Oops' }
console.log(schema.safeParse(invalidInput).success) // false

Step 3: Handle Validation Rules Dynamically

Extend the solution to handle validation rules, such as min or max for strings or numbers:

function generateZodSchema(fields: { name: string; fieldType: string; validation?: any[] }[]) {
  const schema: Record<string, z.ZodType<any>> = {}

  fields.forEach((field) => {
    let fieldSchema: z.ZodType<any>

    switch (field.fieldType) {
      case 'string':
        fieldSchema = z.string()
        break
      case 'number':
        fieldSchema = z.number()
        break
      default:
        throw new Error(`Unsupported field type: ${field.fieldType}`)
    }

    // Apply validation rules
    field.validation?.forEach((rule) => {
      switch (rule.type) {
        case 'min':
          fieldSchema = fieldSchema.min(rule.value, rule.message)
          break
        case 'max':
          fieldSchema = fieldSchema.max(rule.value, rule.message)
          break
        default:
          throw new Error(`Unsupported validation type: ${rule.type}`)
      }
    })

    schema[field.name] = fieldSchema
  })

  return z.object(schema)
}

const dynamicFields = [
  {
    name: 'username',
    fieldType: 'string',
    validation: [{ type: 'min', value: 3, message: 'Must be at least 3 characters' }],
  },
  {
    name: 'age',
    fieldType: 'number',
    validation: [{ type: 'min', value: 18, message: 'Must be at least 18' }],
  },
]

const dynamicSchema = generateZodSchema(dynamicFields)

console.log(dynamicSchema.safeParse({ username: 'John', age: 25 }).success) // true
console.log(dynamicSchema.safeParse({ username: 'Jo', age: 17 }).success) // false

FAQs

Q: Why use Zod for schema generation?

Zod provides an intuitive, composable API for schema validation. It integrates seamlessly with react-hook-form using zodResolver.

Q: Can this handle nested schemas?

Yes, you can extend the generateZodSchema function to support nested field configurations by recursively creating schemas.

Q: Is TypeScript inference supported?

Yes, Zod allows you to infer TypeScript types using z.infer<typeof schema>, which ensures type safety in your application.