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()orintersection()to combine schemas.
Usage Patterns:
- Normal Zod: Define schema → parse/validate data → handle errors via
parse()orsafeParse(). - Form Validation: Integrates with React Hook Form via
zodResolver.- Works with MUI components using
Controller. - Works with Shadcn UI forms via
<FormField>anduseForm.
- Works with MUI components using
- Special Cases: Conditional validation, cross-field checks, arrays, nested objects.
Recommended Practices:
- 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
| Pattern | Description | Key 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. |
| Arrays | Defines an array of items (e.g., strings, numbers, or objects). | Lists of tags, multiple choice answers, or collections of sub-records. |
| Schema Composition | Combining schemas for reusability. | z.intersection(schema1, schema2) (merges fields)schema1.merge(schema2) (merges, with schema2 overwriting) |
| Enum | Restricting 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>
);
}Recommended Docs
For the most up-to-date and detailed information, please refer to the official documentation: