Skip to main content

7.3 - Validation with Zod

Zod is a validation and schema declaration library in TypeScript. Zod can be used with React Hook Forms. It can replace React Hook Form's validation, having validation taken care of where the form's schema is defined. Zod comes with a few built-in validation functions.


What is Zod?

Zod is a validation and schema declaration library that can be used with React Hook Form. It is written in TypeScript. With Zod, you must define a schema that describes the shape and contraints of your data. Zod replaces parts of React Hook Form's validation, moving it away from register and to the same place as your schema definition. This creates a nice centralized place for validation logic, separate from where your form elements live.

Zod also has built-in validation methods.


Incorporating Zod

Here is an old example using only React Hook Forms:

import { useForm, SubmitHandler } from "react-hook-form"

type Inputs = {
name: string
pet: string
subscribe: boolean
}

export default function App() {
const { register, handleSubmit, formState: { errors }} = useForm<Inputs>()
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data)

return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="name">Name</label>
<input type="text" id="name" {...register("name")} />
{errors.name && <p>errors.name.message</p>}

<label htmlFor="pet">Choose a pet:</label>
<select id="pet" {...register("pet")} />
<option value="">---Choose an option---</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
</select>
{errors.pet && <p>errors.pet.message</p>}

<label htmlFor="subscribe">Subscribe to our email newsletter:</label>
<input {...register("subscribe")} id="subscribe" type="checkbox" />

<button type="submit">Submit</button>
</form>
)
}

To integrate Zod (without validation stuff yet), you will:

  1. Import zod and zodResolver
  2. Define your schema
  3. Infer off the schema to create your custom type
  4. Pass in zodResolver to useForm so that React Hook Form knows to use Zod
import { useForm, SubmitHandler } from "react-hook-form"
// 1. Import zod and zodResolver
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

// 2. Define you schema
const schema = z.object({
name: z.string(),
pet: z.string(),
subscribed: z.boolean()
})

// 3. Infer off the schema to create your custom type, rather than defining it like before
type Inputs = z.infer<typeof schema>

export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Inputs>({
// 4. Pass in zodResolver to useForm so that React Hook Form knows to use Zod
resolver: zodResolver(schema)
})
...
}

Zod Validation

Zod validation can completely replace the validation we covered previously with plain React Hook Form. Before, we would pass in validation rules into register. Now, with Zod, we will handle validation as we define our schema.

Zod has a variety of validation methods. See the documentation for a full list of Zod validation methods.

Required and Optional Fields

Here, we designate the name field to be a required field by using the .min() validation method. While there is no .required() validation method in Zod, using .min() and passing in 1 as the minimum character length serves the same purpose for fields of type string.

const schema = z.object({
name: z.string().min(1, "Name is required"),
pet: z.string().optional(),
subscribed: z.boolean()
})

We also designate the pet field to be optional by calling the .optional() method. By default, Zod will make fields required, unless you explicitly make them optional by calling the .optional() method.

You might wonder why we call .min(1, "Name is required") on the name field if Zod just makes fields required by default. This is because the empty string ("") is technically a valid input for a required field. In React Hook Forms, an empty input field results in an empty string. To prevent empty strings, we tack on .min(). Additionally, this lets us add a custom "Name is required" error message.


Built-in Email Validation

Zod has built-in email validation for string fields:

const schema = z.object({
name: z.string().min(1, "Name is required"),
pet: z.string().optional(),
subscribed: z.boolean(),
email: z.string().email(),
})

Custom Validation Logic

In Zod, the .refine() method is used to apply custom validation logic to a schema. Recall that with plain React Hook Form, this can be done by setting validate to a custom validation function when you register a field. .refine() replaces this old method.

Involving a Single Field

If your custom validation function deals with a single field, call .refine() like you would with the other validation methods so far. Pass in your custom validation function into .refine(). Also pass in your custom error message.

Your custom validation function should take in a field's data, return true if valid or false otherwise.

This example's custom validation function makes sure that the user does not input a date of birth that is in the future.

const schema = z.object({
dateOfBirth: z.string().refine((date) => {
const today = new Date().toISOString().slice(0, 10);
return date <= today;
}, "Date cannot be in the future")
// <input type="date">
// refine placed here because it’s only accessing one field
})

Involving Multiple Fields

If your custom validation function deals with multiple fields, call .refine() after your .object() call. This example's custom validation function checks if the user inputted the same email for both the email and confirmEmail fields.

const schema = z.object({
email: z.string().email(),
confirmEmail: z.string().email(),
}).refine(
(data) => data.email === data.confirmEmail, {
message: "Emails don't match",
path: ["confirmEmail"],
}
) // refine is placed after .object because we are
// accessing multiple fields

Again, pass your custom validation function into .refine() as the first argument. This time, your custom validation function should take in the entire form's data (which is an object). Use dot notation to access an individual field's data.

The second argument to .refine() is an object in which you can pass in your custom error message through the message key, as well as the field(s) which the validation function and error message should pertain to through the path key.

Regex and Chaining Validation Rules

Zod's .regex() validation method applies the given regex pattern onto the form data to see if it is valid.

const schema = z.object({
password: z.
.string()
.min(1, "Password is required")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
})

Like we've seen before, Zod validation methods can be chained.


Wrap Up

  • Now with Zod, all validation on our form data is taken care of at the top of our file as part of the schema definition. It is no longer written inside each field's register.
  • Displaying error messages is still the same as with plain React Hook Form.
  • Zod comes with built-in validation methods (e.g. email)
  • Zod allows us to cleanly chain validation methods and write custom validation logic.