Featured

Zod Validation in React: Complete Guide from Beginner to Expert

T
Team
·25 min read
#react#zod#validation#typescript#form validation#react-hook-form

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:

  • Define schemas for your data structures
  • Validate data at runtime
  • Infer TypeScript types from schemas
  • Provide excellent error messages
  • Work seamlessly with TypeScript

  • Why Use Zod?


  • Type Safety: Automatic TypeScript type inference
  • Runtime Validation: Catch errors before they cause issues
  • Great DX: Excellent error messages and developer experience
  • Small Bundle: Lightweight and performant
  • Composable: Build complex schemas from simple ones

  • Installation


    bash
    1npm install zod
    2# For React Hook Form integration
    3npm install react-hook-form @hookform/resolvers

    Basic Validation Types


    String Validation


    typescript(31 lines, showing 15)
    1import { z } from 'zod';
    2 
    3// Basic string
    4const stringSchema = z.string();
    5 
    6// String with constraints
    7const 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 validations

    Number Validation


    typescript(20 lines, showing 15)
    1// Basic number
    2const numberSchema = z.number();
    3 
    4// Number with constraints
    5const 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 validations
    13const ageSchema = z
    14 .number()
    15 .int('Age must be an integer')

    Boolean Validation


    typescript
    1const booleanSchema = z.boolean();
    2 
    3// With refinement
    4const mustBeTrueSchema = z.boolean().refine(val => val === true, {
    5 message: 'Must be true'
    6});

    Date Validation


    typescript(19 lines, showing 15)
    1// Basic date
    2const dateSchema = z.date();
    3 
    4// Date with constraints
    5const 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 string
    9const dateStringSchema = z.string().datetime();
    10const dateISOStringSchema = z.string().date(); // YYYY-MM-DD format
    11 
    12// Combining
    13const birthDateSchema = z
    14 .date()
    15 .max(new Date(), 'Birth date cannot be in the future')

    Array Validation


    typescript(23 lines, showing 15)
    1// Basic array
    2const arraySchema = z.array(z.string());
    3 
    4// Array with constraints
    5const 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 objects
    11const usersArraySchema = z.array(
    12 z.object({
    13 id: z.number(),
    14 name: z.string(),
    15 email: z.string().email()

    Object Validation


    typescript(45 lines, showing 15)
    1// Basic object
    2const userSchema = z.object({
    3 name: z.string(),
    4 age: z.number(),
    5 email: z.string().email()
    6});
    7 
    8// Optional fields
    9const 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


    typescript(19 lines, showing 15)
    1// Union of types
    2const stringOrNumberSchema = z.union([z.string(), z.number()]);
    3 
    4// Discriminated union
    5const 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// Enum
    11const roleSchema = z.enum(['admin', 'user', 'guest']);
    12 
    13// Native enum
    14enum UserRole {
    15 Admin = 'admin',

    Optional and Nullable


    typescript
    1// Optional (undefined allowed)
    2const optionalSchema = z.string().optional();
    3 
    4// Nullable (null allowed)
    5const nullableSchema = z.string().nullable();
    6 
    7// Both optional and nullable
    8const flexibleSchema = z.string().optional().nullable();
    9 
    10// Nullish (null or undefined)
    11const nullishSchema = z.string().nullish();

    Transformations


    typescript(20 lines, showing 15)
    1// Transform data
    2const trimmedStringSchema = z.string().trim();
    3const toLowerCaseSchema = z.string().toLowerCase();
    4const toUpperCaseSchema = z.string().toUpperCase();
    5 
    6// Custom transform
    7const numberStringSchema = z.string().transform(val => parseInt(val, 10));
    8 
    9// Transform with validation
    10const positiveNumberStringSchema = z
    11 .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


    typescript(31 lines, showing 15)
    1// Basic refinement
    2const evenNumberSchema = z.number().refine(
    3 (val) => val % 2 === 0,
    4 { message: 'Must be an even number' }
    5);
    6 
    7// Refinement with context
    8const 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 field

    Custom Error Messages


    typescript
    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


    typescript(41 lines, showing 15)
    1import { useForm } from 'react-hook-form';
    2import { zodResolver } from '@hookform/resolvers/zod';
    3import { z } from 'zod';
    4 
    5// Define schema
    6const 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


    typescript(116 lines, showing 15)
    1import { useForm } from 'react-hook-form';
    2import { zodResolver } from '@hookform/resolvers/zod';
    3import { z } from 'zod';
    4 
    5const registrationSchema = z.object({
    6 username: z
    7 .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: z
    15 .string()

    Real-World Examples


    User Profile Form


    typescript(41 lines, showing 15)
    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


    typescript(23 lines, showing 15)
    1import { z } from 'zod';
    2 
    3// Create schema based on form type
    4function 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


    typescript(29 lines, showing 15)
    1const conditionalSchema = z.object({
    2 accountType: z.enum(['personal', 'business']),
    3 email: z.string().email(),
    4 // Conditional fields
    5 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


    typescript
    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


    typescript
    1function formatZodErrors(error: z.ZodError) {
    2 return error.errors.map(err => ({
    3 field: err.path.join('.'),
    4 message: err.message
    5 }));
    6}
    7 
    8// Usage
    9const result = schema.safeParse(data);
    10if (!result.success) {
    11 const formattedErrors = formatZodErrors(result.error);
    12 console.log(formattedErrors);
    13}

    Async Validation


    typescript
    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


    typescript
    1// Create reusable schemas
    2export const emailSchema = z.string().email('Invalid email');
    3export const passwordSchema = z.string().min(8, 'Password too short');
    4 
    5// Compose schemas
    6export const loginSchema = z.object({
    7 email: emailSchema,
    8 password: passwordSchema
    9});

    2. Type Inference


    typescript
    1// Infer TypeScript types from schemas
    2const 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


    typescript(18 lines, showing 15)
    1// Base schema
    2const baseUserSchema = z.object({
    3 name: z.string(),
    4 email: z.string().email()
    5});
    6 
    7// Extend schema
    8const adminSchema = baseUserSchema.extend({
    9 role: z.literal('admin'),
    10 permissions: z.array(z.string())
    11});
    12 
    13// Merge schemas
    14const userWithAddressSchema = baseUserSchema.merge(
    15 z.object({

    4. Validation Modes


    typescript
    1// Use appropriate validation modes
    2const form = useForm({
    3 resolver: zodResolver(schema),
    4 mode: 'onChange', // or 'onBlur', 'onSubmit', 'onTouched', 'all'
    5 reValidateMode: 'onChange'
    6});

    Expert Patterns


    Schema Factory Pattern


    typescript
    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// Usage
    11const userSchema = z.object({ id: z.number(), name: z.string() });
    12const paginatedUsersSchema = createPaginationSchema(userSchema);

    Partial and Pick


    typescript
    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 fields
    12const pickSchema = fullSchema.pick({ name: true, email: true });
    13 
    14// Omit fields
    15const omitSchema = fullSchema.omit({ id: true });

    Recursive Schemas


    typescript
    1// Define recursive schema
    2type 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


    typescript(25 lines, showing 15)
    1// Super refine for complex validation
    2const 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:

  • Use Zod for runtime validation and TypeScript type inference
  • Integrate with React Hook Form for seamless form handling
  • Create reusable schemas for consistency
  • Leverage refinements for complex validation logic
  • Handle errors gracefully with custom error messages

  • 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.