Blog
Wild & Free Tools

Zod + React Hook Form: Schema Setup and Validation Guide

Last updated: April 2026 6 min read

Table of Contents

  1. Installation and Basic Setup
  2. Displaying Field-Level Errors
  3. Typed Submit Handler
  4. Complex Schemas: Optional Fields and Defaults
  5. Frequently Asked Questions

Connecting Zod to React Hook Form takes three steps: define a z.object() schema, pass zodResolver(schema) to useForm(), and use formState.errors to display validation messages. Field errors are typed by field name, and the form submission data is fully typed.

This guide covers the complete setup, common patterns, and how to generate schemas from existing form data quickly.

Installation and Basic Setup

Install the required packages:

npm install react-hook-form @hookform/resolvers zod

Create a schema and derive the form type:

import { z } from 'zod';

const ContactSchema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Please enter a valid email'),
  message: z.string().min(10, 'Message must be at least 10 characters')
});

type ContactFormData = z.infer<typeof ContactSchema>;

Connect to React Hook Form:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const { register, handleSubmit, formState: { errors } } = useForm<ContactFormData>({
  resolver: zodResolver(ContactSchema)
});

Displaying Field-Level Errors

Error messages are available via formState.errors, keyed by field name:

<form onSubmit={handleSubmit(onSubmit)}>
  <div>
    <input {...register('name')} placeholder="Name" />
    {errors.name && <p className="error">{errors.name.message}</p>}
  </div>

  <div>
    <input {...register('email')} placeholder="Email" />
    {errors.email && <p className="error">{errors.email.message}</p>}
  </div>

  <div>
    <textarea {...register('message')} placeholder="Message" />
    {errors.message && <p className="error">{errors.message.message}</p>}
  </div>

  <button type="submit">Send</button>
</form>

Validation runs on submit by default. Set mode: 'onChange' in useForm for real-time validation.

Sell Custom Apparel — We Handle Printing & Free Shipping

Typed Submit Handler

The data passed to your submit handler is fully typed as the inferred schema type:

const onSubmit = (data: ContactFormData) => {
  // TypeScript knows:
  // data.name: string
  // data.email: string
  // data.message: string
  console.log(data);
};

No casting, no manual validation. If the user submits invalid data, React Hook Form + Zod prevents the submit handler from running at all.

Complex Schemas: Optional Fields and Defaults

Real forms have optional fields and default values:

const ProfileSchema = z.object({
  username: z.string().min(3).max(20),
  bio: z.string().max(200).optional(),
  website: z.string().url().optional().or(z.literal('')),
  notifications: z.boolean().default(true),
  role: z.enum(['user', 'admin', 'moderator'])
});

const { register, handleSubmit } = useForm({
  resolver: zodResolver(ProfileSchema),
  defaultValues: {
    notifications: true,
    role: 'user'
  }
});

To quickly generate a complex schema from a sample form payload, paste the JSON into the JSON to Zod converter and get the base structure in one step.

Try It Free — No Signup Required

Runs 100% in your browser. No data is collected, stored, or sent anywhere.

Open Free JSON to Zod Converter

Frequently Asked Questions

How do I connect Zod to React Hook Form?

Install @hookform/resolvers and pass zodResolver(YourSchema) as the resolver option in useForm. The schema handles all validation, and errors appear in formState.errors by field name.

Does zodResolver validate on submit or on change?

By default it validates on submit. Add mode: "onChange" to useForm options for real-time validation as users type. Use mode: "onBlur" to validate when a field loses focus.

Can I define custom error messages in Zod schemas for forms?

Yes. Pass an error message string as the second argument to validators: z.string().min(2, "Name must be at least 2 characters") or z.string().email("Invalid email format"). These messages appear in errors.fieldName.message.

Do I need to define a TypeScript interface separately for React Hook Form with Zod?

No. Use z.infer to extract the type. Pass it as a generic to useForm: useForm>. One schema, zero duplication.

Launch Your Own Clothing Brand — No Inventory, No Risk