Skip to Content

Comprehensive Zod Documentation & Usage Guide

Overview

Zod is a TypeScript-first runtime schema validation library, used to enforce data shapes, types, and business rules across client and server. It allows you to define runtime schemas for objects, arrays, primitives, and more, while keeping type inference fully compatible with TypeScript. Zod works seamlessly with TypeScript for full type inference and is widely used for form validation, API request validation, and ensuring data consistency across client and server.

Key Features:

  • Schema Definition: Validate primitives, objects, arrays, enums.
  • Type Inference: Use z.infer<typeof schema> for TypeScript types.
  • Optional / Nullable Fields: optional(), nullable() handle missing or null values.
  • Default Values: default(value) auto-fills undefined fields.
  • Transformations: .transform() to normalize or modify input before output.
  • Custom Validation: .refine() for single-field rules, .superRefine() for cross-field logic.
  • Schema Composition: merge() or intersection() to combine schemas.

Usage Patterns:

  • Normal Zod: Define schema → parse/validate data → handle errors via parse() or safeParse().
  • Form Validation: Integrates with React Hook Form via zodResolver.
    • Works with MUI components using Controller.
    • Works with Shadcn UI forms via <FormField> and useForm.
  • Special Cases: Conditional validation, cross-field checks, arrays, nested objects.
  • Always define clear types with z.infer.
  • Use .transform() to clean inputs before validation.
  • For forms, integrate Zod with a resolver to display user-friendly error messages.
  • Reuse schemas via composition to maintain consistency.

Normal Zod Usage

Zod is a TypeScript-first schema declaration and validation library. It is designed to be as developer-friendly as possible.

import { z } from "zod"; // Define a schema const userSchema = z.object({ name: z.string().min(2, "Name must have at least 2 chars"), email: z.string().email("Invalid email"), age: z.number().int().min(0, "Age must be positive").optional(), role: z.enum(["user", "admin", "superadmin"]), preferences: z.object({ newsletter: z.boolean().default(false), }).optional(), }); // Infer TypeScript type type User = z.infer<typeof userSchema>; // Validate data const data = { name: "Alice", email: "alice@example.com", age: 25, role: "user" }; const result = userSchema.safeParse(data); if (!result.success) { console.log(result.error.issues); // Array of errors } else { const user: User = result.data; console.log(user); // { name: 'Alice', email: 'alice@example.com', age: 25, role: 'user', preferences: { newsletter: false } } }

Advanced Patterns / Tips

PatternDescriptionKey Use Case
.optional() / .nullable()Fields can be missing or null.Handling optional user input or API fields.
.default(value)Auto-fill default values if undefined.Ensuring data integrity when a field is optional but requires a fallback.
.refine()Simple, custom validation rules on a single field or object.Custom business logic checks (e.g., date ranges, list length limits).
.transform()Transform values before validation or output.Cleaning/normalizing data (e.g., trimming strings, converting numbers).
.superRefine()Cross-field validation with detailed, granular error path control.Complex validation rules where .refine’s single error path is insufficient.
ArraysDefines an array of items (e.g., strings, numbers, or objects).Lists of tags, multiple choice answers, or collections of sub-records.
Schema CompositionCombining schemas for reusability.z.intersection(schema1, schema2) (merges fields)
schema1.merge(schema2) (merges, with schema2 overwriting)
EnumRestricting a string to a specific set of values.z.enum(["user", "admin", "superadmin"]) ensures exact values.

Nested Objects

Handling objects within the main form schema. Use dot notation in the field name for form libraries.

// Schema definition const formSchema = z.object({ profile: z.object({ firstName: z.string().min(2), lastName: z.string().min(2), age: z.number().int().min(0), }), settings: z.object({ darkMode: z.boolean().default(false), notifications: z.boolean().default(true), }).optional(), }); // For forms: Use dot notation in field names // MUI: <Controller name="profile.firstName" ... /> // Shadcn: <FormField name="profile.firstName" ... />

Conditional Validation

A field is required/validated based on another field’s value. Use .refine() on the entire object and specify the path for the error message.

// Conditional validation example const formSchema = z.object({ email: z.string().email().optional(), subscribe: z.boolean(), }).refine(data => !data.subscribe || !!data.email, { message: "Email is required when subscribing", path: ["email"], // Attaches the error to the 'email' field }); // Another example: Age requirement based on account type const accountSchema = z.object({ accountType: z.enum(["standard", "premium"]), age: z.number().int().min(0).optional(), }).refine(data => data.accountType !== "premium" || (data.age !== undefined && data.age >= 18), { message: "Premium accounts require age 18+", path: ["age"], });

Cross-field Validation

Enforcing rules that compare two or more fields (e.g., matching passwords). Use .refine() on the entire object and specify the path for the error message.

// Cross-field validation (Password Match) const passwordSchema = z.object({ password: z.string().min(6), confirmPassword: z.string(), }).refine(data => data.password === data.confirmPassword, { message: "Passwords must match", path: ["confirmPassword"], // Attaches the error to the 'confirmPassword' field }); // Date range validation const dateRangeSchema = z.object({ startDate: z.date(), endDate: z.date(), }).refine(data => data.endDate >= data.startDate, { message: "End date must be after start date", path: ["endDate"], });

Note: Optional Fields with Conditional Validation

Sometimes some fields are optional, but if they have a value, then they need to be in a specific shape. For example, if you have location_url, it’s optional, but if the user fills it, then it should be a valid URL - it cannot be just any string.

In this case, we use .or() from Zod to create a union type that allows either undefined (optional) or a validated value.

// Optional URL field - must be valid URL if provided const formSchema = z.object({ location_url: z.string().url().or(z.undefined()), // Or more commonly written as: (recommended) // location_url: z.string().url().optional(), }); // Example: Optional email that must be valid if provided const contactSchema = z.object({ email: z.string().email().or(z.undefined()), // Or simply: email: z.string().email().optional(), });

Zod + MUI (Material-UI)

Zod is commonly used with React Hook Form via the @hookform/resolvers/zod package to handle form validation in React applications, especially with component libraries like MUI.

Setup: Use React Hook Form + zodResolver.

import React from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { TextField, Button } from "@mui/material"; // Schema const formSchema = z.object({ username: z.string().min(2, "Username must be at least 2 characters"), email: z.string().email("Invalid email"), password: z.string().min(6, "Password must be at least 6 characters"), }); type FormValues = z.infer<typeof formSchema>; export function MUIForm() { const { handleSubmit, control } = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { username: "", email: "", password: "" }, }); const onSubmit = (data: FormValues) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> {/* Use Controller to integrate React Hook Form with MUI TextField */} <Controller name="username" control={control} render={({ field, fieldState }) => ( <TextField {...field} label="username" error={!!fieldState.error} // Pass error state to MUI helperText={fieldState.error?.message} // Pass error message fullWidth margin="normal" /> )} /> {/* other fields */} <Button type="submit" variant="contained"> Submit </Button> </form> ); }

Zod + Shadcn UI Form Example

Shadcn UI forms typically leverage the Form component wrapper pattern built on top of React Hook Form.

import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; // Assuming form components (Form, FormField, FormLabel, etc.) are imported // from a standard shadcn/ui setup. import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; const formSchema = z.object({ username: z.string().min(2, "Username must be at least 2 characters."), email: z.string().email("Invalid email format."), password: z.string().min(6, "Password must be at least 6 characters."), }); type FormValues = z.infer<typeof formSchema>; export function ShadcnForm() { const form = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { username: "", email: "", password: "" }, }); const onSubmit = (values: FormValues) => console.log(values); return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="username" // The field name render={({ field }) => ( <FormItem> <FormLabel>Username</FormLabel> <FormControl> <Input {...field} type="text" /> </FormControl> <FormMessage /> {/* Displays the Zod error message */} </FormItem> )} /> {/* ... other fields ... */} <button type="submit">Submit</button> </form> </Form> ); }

For the most up-to-date and detailed information, please refer to the official documentation: