Zod validates React forms against a schema you define. You describe each field with a rule, validate the submitted values with safeParse, and turn the returned issues into field messages. Zod 4 is the current stable version, and npm install zod is the only package you need.
Zod also infers a TypeScript type from the schema, so the validated data is typed without writing a separate interface.
Install Zod and define a schema
Install Zod, then describe your form as an object schema. Each key mirrors a form field, and each rule returns a new schema instance so the chain stays readable.
npm install zodThe schema below requires a name and a valid email. Zod 4 uses the top-level z.email() for the email format, which replaced the older z.string().email() call.
import { z } from "zod";
const signupSchema = z.object({
name: z.string().min(1, { error: "Enter your name." }),
email: z.email(),
});The min rule rejects an empty name, and the error option supplies the message a user will see. z.email() accepts a single valid address and rejects anything else.
Validate with safeParse
Call safeParse on the submitted values. It returns a result object instead of throwing, so one branch handles success and the other reads the failures.
const result = signupSchema.safeParse({
name: "",
email: "not-an-email",
});
if (!result.success) {
console.log(result.error.issues);
}The result has a success flag. When it is false, result.error.issues holds one entry per failed rule, and each issue carries a path array and a message. The path tells you which field failed, and the message is ready to display.
Because a schema is plain data, you can run the exact same rules on the server and share one source of truth between the client check and the authoritative one.
Map issues to field messages
Your form state wants an object shaped like { field: message }. Loop over the issues and copy each message under the field name from the first path entry.
function issuesToErrors(result) {
const errors = {};
for (const issue of result.error.issues) {
errors[issue.path[0]] = issue.message;
}
return errors;
}Call this helper from the submit handler and store the returned object in state. The render side stays exactly like any other validation, with a message under each failed field.
function handleSubmit(e) {
e.preventDefault();
const result = signupSchema.safeParse(values);
setErrors(result.success ? {} : issuesToErrors(result));
}Add this handler inside a component that already holds values and errors state in useState, the same shape covered in the earlier hand-written validation guide. When the submit handler runs, the errors state becomes empty on success or a field-to-message map on failure. The full display pattern with aria-invalid and aria-describedby is covered in React form validation without a library.
Common Zod rules for forms
Most form rules map to one short Zod call. Start with these and compose more specific checks with refine when you need custom logic.
| Rule | Schema |
|---|---|
| Required text | z.string().min(1) |
| Valid email | z.email() |
| Minimum length | z.string().min(8) |
| URL | z.url() |
| Number in a range | z.number().min(1).max(100) |
| Optional field | z.string().optional() |
Each rule reads almost like plain English, which keeps the schema close to the form it validates. The main zod package is the right default for browser apps.
Wire the schema into React Hook Form
If you use React Hook Form, the same schema plugs in through a resolver. Install the resolver package alongside Zod and pass it to useForm.
npm install @hookform/resolversThe resolver runs your schema and translates the issues into React Hook Form's errors object, so the messages appear under errors.field.message automatically.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
export default function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(signupSchema),
});
function onSubmit(data) {
alert(JSON.stringify(data));
}
// the return below continues this same componentThe return block renders a registered email input and shows errors.email.message on failure, exactly like the built-in rules from the earlier sections.
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register("email")} />
{errors.email && <p role="alert">{errors.email.message}</p>}
<button type="submit">Sign up</button>
</form>
);For the full React Hook Form setup, see the React Hook Form tutorial. For the server side, where validation is repeated, see how to show server validation errors in React forms.
When to reach for Zod
Reach for Zod when a form has more than a few rules, or when you want the same validation on both client and server. A hand-written validate function stays simpler for one or two quick checks, and it keeps an extra dependency out of small forms.
Rune AI
Key Insights
- Install zod and import { z } from "zod".
- Define a schema with z.object and z.string rules.
- Use safeParse to validate without throwing.
- Map error.issues to per-field messages.
- Wire the schema into React Hook Form with zodResolver.
Frequently Asked Questions
Do I need React Hook Form to use Zod?
What is the difference between parse and safeParse?
Is z.string().email() still supported in Zod 4?
Conclusion
Define one Zod schema, validate with safeParse, and map the returned issues to field messages. The same schema can drive React Hook Form through zodResolver.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.