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:
- Import
zod
andzodResolver
- Define your schema
- Infer off the schema to create your custom type
- Pass in
zodResolver
touseForm
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.