How to Validate React Forms with Zod

Validate React forms with Zod schemas, read field errors from safeParse, and connect the same schema to React Hook Form.

7 min read

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.

bashbash
npm install zod

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

index.jsindex.js
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.

index.jsindex.js
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.

index.jsindex.js
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.

App.jsxApp.jsx
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.

RuleSchema
Required textz.string().min(1)
Valid emailz.email()
Minimum lengthz.string().min(8)
URLz.url()
Number in a rangez.number().min(1).max(100)
Optional fieldz.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.

bashbash
npm install @hookform/resolvers

The resolver runs your schema and translates the issues into React Hook Form's errors object, so the messages appear under errors.field.message automatically.

App.jsxApp.jsx
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 component

The return block renders a registered email input and shows errors.email.message on failure, exactly like the built-in rules from the earlier sections.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Do I need React Hook Form to use Zod?

No. Zod validates data on its own with parse or safeParse. React Hook Form only wires the schema into a form through zodResolver when you want library-managed errors.

What is the difference between parse and safeParse?

parse throws a ZodError when input is invalid. safeParse returns a result object with success, data, and error fields instead of throwing, which is easier to branch on.

Is z.string().email() still supported in Zod 4?

It still works but is deprecated. Use the top-level z.email() instead, which is the current recommended API.

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.