Zod Validation in React: Complete Guide from Beginner to Expert
Zod Validation in React: Complete Guide from Beginner to Expert
Zod is a TypeScript-first schema validation library that provides runtime type checking and validation. This comprehensive guide will take you from basic validation to expert-level patterns in React applications.
What is Zod?
Zod is a schema validation library that allows you to:
Why Use Zod?
Installation
1npm install zod2# For React Hook Form integration3npm install react-hook-form @hookform/resolversBasic Validation Types
String Validation
1import { z } from 'zod';2 3// Basic string4const stringSchema = z.string();5 6// String with constraints7const emailSchema = z.string().email('Invalid email address');8const minLengthSchema = z.string().min(5, 'Must be at least 5 characters');9const maxLengthSchema = z.string().max(100, 'Must be at most 100 characters');10const lengthSchema = z.string().length(10, 'Must be exactly 10 characters');11const urlSchema = z.string().url('Invalid URL');12const uuidSchema = z.string().uuid('Invalid UUID');13const regexSchema = z.string().regex(/^[A-Z]/, 'Must start with uppercase letter');14 15// Combining validationsNumber Validation
1// Basic number2const numberSchema = z.number();3 4// Number with constraints5const positiveSchema = z.number().positive('Must be positive');6const negativeSchema = z.number().negative('Must be negative');7const intSchema = z.number().int('Must be an integer');8const minSchema = z.number().min(0, 'Must be at least 0');9const maxSchema = z.number().max(100, 'Must be at most 100');10const multipleOfSchema = z.number().multipleOf(5, 'Must be a multiple of 5');11 12// Combining validations13const ageSchema = z14 .number()15 .int('Age must be an integer')Boolean Validation
1const booleanSchema = z.boolean();2 3// With refinement4const mustBeTrueSchema = z.boolean().refine(val => val === true, {5 message: 'Must be true'6});Date Validation
1// Basic date2const dateSchema = z.date();3 4// Date with constraints5const minDateSchema = z.date().min(new Date('2020-01-01'), 'Date too early');6const maxDateSchema = z.date().max(new Date('2030-12-31'), 'Date too late');7 8// Date from string9const dateStringSchema = z.string().datetime();10const dateISOStringSchema = z.string().date(); // YYYY-MM-DD format11 12// Combining13const birthDateSchema = z14 .date()15 .max(new Date(), 'Birth date cannot be in the future')Array Validation
1// Basic array2const arraySchema = z.array(z.string());3 4// Array with constraints5const minLengthArray = z.array(z.string()).min(1, 'Array cannot be empty');6const maxLengthArray = z.array(z.number()).max(10, 'Array cannot have more than 10 items');7const lengthArray = z.array(z.boolean()).length(5, 'Array must have exactly 5 items');8const nonEmptyArray = z.array(z.string()).nonempty('Array cannot be empty');9 10// Array of objects11const usersArraySchema = z.array(12 z.object({13 id: z.number(),14 name: z.string(),15 email: z.string().email()Object Validation
1// Basic object2const userSchema = z.object({3 name: z.string(),4 age: z.number(),5 email: z.string().email()6});7 8// Optional fields9const userWithOptionalSchema = z.object({10 name: z.string(),11 age: z.number().optional(),12 email: z.string().email(),13 phone: z.string().optional()14});15 Advanced Validation Patterns
Union Types
1// Union of types2const stringOrNumberSchema = z.union([z.string(), z.number()]);3 4// Discriminated union5const statusSchema = z.discriminatedUnion('type', [6 z.object({ type: z.literal('success'), data: z.string() }),7 z.object({ type: z.literal('error'), message: z.string() })8]);9 10// Enum11const roleSchema = z.enum(['admin', 'user', 'guest']);12 13// Native enum14enum UserRole {15 Admin = 'admin',Optional and Nullable
1// Optional (undefined allowed)2const optionalSchema = z.string().optional();3 4// Nullable (null allowed)5const nullableSchema = z.string().nullable();6 7// Both optional and nullable8const flexibleSchema = z.string().optional().nullable();9 10// Nullish (null or undefined)11const nullishSchema = z.string().nullish();Transformations
1// Transform data2const trimmedStringSchema = z.string().trim();3const toLowerCaseSchema = z.string().toLowerCase();4const toUpperCaseSchema = z.string().toUpperCase();5 6// Custom transform7const numberStringSchema = z.string().transform(val => parseInt(val, 10));8 9// Transform with validation10const positiveNumberStringSchema = z11 .string()12 .transform(val => parseInt(val, 10))13 .refine(val => !isNaN(val), 'Must be a valid number')14 .refine(val => val > 0, 'Must be positive');15 Refinements
1// Basic refinement2const evenNumberSchema = z.number().refine(3 (val) => val % 2 === 0,4 { message: 'Must be an even number' }5);6 7// Refinement with context8const passwordMatchSchema = z.object({9 password: z.string(),10 confirmPassword: z.string()11}).refine(12 (data) => data.password === data.confirmPassword,13 {14 message: "Passwords don't match",15 path: ['confirmPassword'] // Error appears on confirmPassword fieldCustom Error Messages
1const schema = z.object({2 email: z.string({3 required_error: 'Email is required',4 invalid_type_error: 'Email must be a string'5 }).email('Invalid email format'),6 7 age: z.number({8 required_error: 'Age is required',9 invalid_type_error: 'Age must be a number'10 }).min(18, 'Must be at least 18 years old')11});React Hook Form Integration
Basic Setup
1import { useForm } from 'react-hook-form';2import { zodResolver } from '@hookform/resolvers/zod';3import { z } from 'zod';4 5// Define schema6const formSchema = z.object({7 name: z.string().min(1, 'Name is required'),8 email: z.string().email('Invalid email'),9 age: z.number().min(18, 'Must be at least 18')10});11 12type FormData = z.infer<typeof formSchema>;13 14function UserForm() {15 const {Advanced Form Validation
1import { useForm } from 'react-hook-form';2import { zodResolver } from '@hookform/resolvers/zod';3import { z } from 'zod';4 5const registrationSchema = z.object({6 username: z7 .string()8 .min(3, 'Username must be at least 3 characters')9 .max(20, 'Username must be at most 20 characters')10 .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),11 12 email: z.string().email('Invalid email address'),13 14 password: z15 .string()Real-World Examples
User Profile Form
1import { z } from 'zod';2import { useForm } from 'react-hook-form';3import { zodResolver } from '@hookform/resolvers/zod';4 5const profileSchema = z.object({6 firstName: z.string().min(1, 'First name is required').max(50),7 lastName: z.string().min(1, 'Last name is required').max(50),8 email: z.string().email('Invalid email'),9 phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number'),10 dateOfBirth: z.string().date(),11 bio: z.string().max(500, 'Bio must be at most 500 characters').optional(),12 website: z.string().url('Invalid URL').optional().or(z.literal('')),13 socialLinks: z.object({14 twitter: z.string().url().optional().or(z.literal('')),15 linkedin: z.string().url().optional().or(z.literal('')),Dynamic Form Validation
1import { z } from 'zod';2 3// Create schema based on form type4function createFormSchema(formType: 'basic' | 'premium') {5 const baseSchema = z.object({6 name: z.string().min(1),7 email: z.string().email()8 });9 10 if (formType === 'premium') {11 return baseSchema.extend({12 company: z.string().min(1),13 phone: z.string().min(10),14 address: z.string().min(1)15 });Conditional Validation
1const conditionalSchema = z.object({2 accountType: z.enum(['personal', 'business']),3 email: z.string().email(),4 // Conditional fields5 businessName: z.string().optional(),6 taxId: z.string().optional()7}).refine(8 (data) => {9 if (data.accountType === 'business') {10 return data.businessName && data.businessName.length > 0;11 }12 return true;13 },14 {15 message: 'Business name is required for business accounts',Error Handling
Custom Error Messages
1const schema = z.object({2 email: z.string({3 required_error: 'Email is required',4 invalid_type_error: 'Email must be a string'5 }).email({6 message: 'Please enter a valid email address'7 })8});Error Formatting
1function formatZodErrors(error: z.ZodError) {2 return error.errors.map(err => ({3 field: err.path.join('.'),4 message: err.message5 }));6}7 8// Usage9const result = schema.safeParse(data);10if (!result.success) {11 const formattedErrors = formatZodErrors(result.error);12 console.log(formattedErrors);13}Async Validation
1const asyncSchema = z.object({2 username: z.string().refine(3 async (username) => {4 const response = await fetch(`/api/check-username?username=${username}`);5 const data = await response.json();6 return data.available;7 },8 { message: 'Username is already taken' }9 )10});Best Practices
1. Reusable Schemas
1// Create reusable schemas2export const emailSchema = z.string().email('Invalid email');3export const passwordSchema = z.string().min(8, 'Password too short');4 5// Compose schemas6export const loginSchema = z.object({7 email: emailSchema,8 password: passwordSchema9});2. Type Inference
1// Infer TypeScript types from schemas2const userSchema = z.object({3 name: z.string(),4 age: z.number()5});6 7type User = z.infer<typeof userSchema>;8// User = { name: string; age: number }3. Schema Composition
1// Base schema2const baseUserSchema = z.object({3 name: z.string(),4 email: z.string().email()5});6 7// Extend schema8const adminSchema = baseUserSchema.extend({9 role: z.literal('admin'),10 permissions: z.array(z.string())11});12 13// Merge schemas14const userWithAddressSchema = baseUserSchema.merge(15 z.object({4. Validation Modes
1// Use appropriate validation modes2const form = useForm({3 resolver: zodResolver(schema),4 mode: 'onChange', // or 'onBlur', 'onSubmit', 'onTouched', 'all'5 reValidateMode: 'onChange'6});Expert Patterns
Schema Factory Pattern
1function createPaginationSchema<T extends z.ZodTypeAny>(itemSchema: T) {2 return z.object({3 page: z.number().int().positive().default(1),4 limit: z.number().int().positive().max(100).default(10),5 items: z.array(itemSchema),6 total: z.number().int().nonnegative()7 });8}9 10// Usage11const userSchema = z.object({ id: z.number(), name: z.string() });12const paginatedUsersSchema = createPaginationSchema(userSchema);Partial and Pick
1const fullSchema = z.object({2 id: z.number(),3 name: z.string(),4 email: z.string().email(),5 age: z.number()6});7 8// Partial (all fields optional)9const partialSchema = fullSchema.partial();10 11// Pick specific fields12const pickSchema = fullSchema.pick({ name: true, email: true });13 14// Omit fields15const omitSchema = fullSchema.omit({ id: true });Recursive Schemas
1// Define recursive schema2type Category = {3 name: string;4 subcategories?: Category[];5};6 7const categorySchema: z.ZodType<Category> = z.lazy(() =>8 z.object({9 name: z.string(),10 subcategories: z.array(categorySchema).optional()11 })12);Zod Effects
1// Super refine for complex validation2const complexSchema = z.object({3 startDate: z.date(),4 endDate: z.date()5}).superRefine((data, ctx) => {6 if (data.endDate < data.startDate) {7 ctx.addIssue({8 code: z.ZodIssueCode.custom,9 message: 'End date must be after start date',10 path: ['endDate']11 });12 }13 14 const daysDiff = Math.ceil(15 (data.endDate.getTime() - data.startDate.getTime()) / (1000 * 60 * 60 * 24)Conclusion
Zod provides a powerful, type-safe way to validate data in React applications. From basic string validation to complex nested schemas, Zod covers all your validation needs. Combined with React Hook Form, it provides an excellent developer experience for building robust forms.
Key takeaways:
Start with simple schemas and gradually build up to more complex validation patterns as your application grows.
Enjoyed this article?
Support our work and help us create more free content for developers.
Stay Updated
Get the latest articles and updates delivered to your inbox.