Zod + React Hook Form: Schema Setup and Validation Guide
Table of Contents
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.
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 ConverterFrequently 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

