Zod: The Ultimate Validation Library for TypeScript
Zod is a TypeScript-first schema validation library. Unlike static type checking, Zod helps you to validate data dynamically at runtime. Whether user input, API response, or any data you want to validate, structure custom errors, Zod is the ultimate tool.
I’ll go through the discussion on schema structure, custom validation, asynchronous checks, and demonstrate how to integrate Zod into a React/Next.js project for form validation, state management, and prop type validation.
Zod Use Cases
1. Schema Definitions
Defining a complex schema using Zod is fun. Array, Object, nested data, nothing is as complex as we may think. Imagine an address schema:
import {z} from 'zod';
const addressSchema = z.object({
area: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/, "Invalid zip code"),
country: z.string(),
});
This schema will validate the address and ensure type safety. We can also set custom errors on validation.
import {z} from 'zod';
const addressSchema = z.object({
area: z.string({ required_error: "Area is required" }),
city: z.string({ required_error: "City is required" }),
zipCode: z.string({ required_error: "Zip code is required" }).regex(/^\d{5}$/, "Invalid zip code"),
country: z.string({ required_error: "Country is required" }),
});
This schema will validate the data types and return an error if any field is missing or empty. We can also chain the validation methods. And here are a few examples
// validate both undefined and empty field or data
z.string().nonempty()
// validate undefined, nonempty, minimum characters
z.string().nonempty().min(100);
// validate undefined, nonempty, minimum and max characters
z.string().nonempty().min(100).max(500);
// setting up custom error message
z.string({ required_error: "String is required" })
.nonempty({message: "String can't be empty"})
.min(100, {message: "A minimum of 100 characters is required"})
.max(500, {message: "String can't be more than 500 characters"});
Note: In the validation chain, if multiple validations fail, the last error message will be assigned to the error data. For instance, for a blank text field, both ‘nonempty’ and ‘min’ are correct. And the error defined in the min() method will be added to the error data. This is how Zod prioritizes errors when multiple rules fail.
2. Advance validations
All the default validation methods can be used in different contexts. Like, you can use the ‘min()’ method in array data to ensure a minimum of data is present. Get back to the address schema
import {z} from 'zod';
const addressSchema = z.array(
z.object({
area: z.string({ required_error: "Area is required" }),
city: z.string({ required_error: "City is required" }),
zipCode: z.string({ required_error: "Zip code is required" }).regex(/^\d{5}$/, "Invalid zip code"),
country: z.string({ required_error: "Country is required" }),
})
).min(1, {message: "A minimum of one address is required"});
This will prevent form submission if there is no address in the submitted data. The address object will be in an array, as this is wrapped in Zod array data, to clarify. We can also use other methods to validate our data or a collection of data.
3. Custom Validations
In addition to the default validation methods, our project may require custom validations. Calculating age based on DOB input, comparing dates, validating a field’s data that depends on a different field, etc. Zod has refine
and superrefine
methods to implement custom validation. The refine
is a syntactic sugar of superrefine
. Here is how we can use refine:
jobDescription: z.string().refine((value) => {
if (value.trim().split(" ").length > 500) {
return false;
}
return true;
},
{ message: "Job description can't be more than 500 words" }
).optional(),
The above Zod schema checks if the job description words are longer than 500 words. If it is, it will return false. However, it’s an optional data as we added Zod’s optional() method.
The refine method should always return a boolean. If it’s untrue, the validation fails, and the error state updates with the message passed as the second argument to the refine function. To clarify the entire scenario, the refine method takes two arguments. The first one is a validation function. You can check the value and run your refinement as you want to validate your data. The second argument should be an Object containing a message for the error state. The default Zod’s max() method checks characters. Our goal was to validate words. So, we used refine() here.
As I discussed in “Advance Validations”, how we can chain validation methods, we can chain the refine() method as well and validate our data with different conditions. We can also run the refine() method on a whole object data group or on the array. For a data group, we’ll get all the data in the value parameter. Here is an example.
z
.array(
z
.object({
company: z
.string({ required_error: "Company name is required" })
.nonempty({ message: "Company name can't be empty" }),
jobTitle: z
.string({ required_error: "Job title is required" })
.nonempty({
message: "Job title can't be empty",
}),
startingDate: z.date({ required_error: "Starting date is required" }),
endDate: z
.date({ required_error: "End date is required" })
.optional()
.nullable(),
currentlyWorking: z.boolean().optional(),
jobDescription: z
.string()
.refine(
(value) => {
if (value.trim().split(" ").length > 500) {
return false;
}
return true;
},
{ message: "Job description can't be more than 500 words" }
)
.optional(),
})
.refine(
(date) => {
if (date.currentlyWorking) return true;
return compareDesc(date.startingDate, date.endDate ?? new Date()) ==
0 ||
compareDesc(date.startingDate, date.endDate ?? new Date()) == -1
? false
: true;
},
{
message:
"Star and End date can't be same or End date is before the Start date",
path: ["endDate"],
}
)
)
.optional()
.default([]);
The compareDesc function is from date-fns to check date equality.
We can also pass an async function as our validator and validate data.
const stringSchema = z.string().refine(async (val) => val.length <= 8);
Zod’s parse and parseAsync are the methods to parse a Zod schema and check if the data is valid. The parseAsync method is used to validate data asynchronously for an asynchronous validation function passed to the refine method.
const stringSchema = z.string();
stringSchema.parse("fish"); // => returns "fish"
stringSchema.parse(12); // throws error
The parse and parseAsync throw an error on failing validation. If we want to get an object containing parsed data or an error, we can use safeParse and safeParseAsync.
Integrating Zod into a React Project
1. Form validation with React Hook Form
Zod pairs beautifully with form libraries like React Hook Form for validating user inputs. Here’s a login form example:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
const onSubmit = (data) => {
console.log("Valid data:", data);
};
return (
<div>
<input {...register("email")} placeholder="Email" />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register("password")} placeholder="Password" />
{errors.password && <p>{errors.password.message}</p>}
<button onClick={handleSubmit(onSubmit)}>Login</button>
</div>
);
}
Zod validates the form data, and React Hook Form manages the UI state, displaying errors as needed.
2. Prop Types Validation
const propsSchema = z.object({
title: z.string(),
count: z.number().int().positive(),
});
function MyComponent(props) {
const validatedProps = propsSchema.parse(props);
return <div>{validatedProps.title}: {validatedProps.count}</div>;
}
Zod provides detailed errors if props don’t match the schema, enhancing debugging.
Zod Documentations:
Zod offers comprehensive documentation for various use cases. Check it out at Zod’s official documentation to dive deeper.
Conclusion
Zod is a compact yet powerful tool that enhances your development workflow. With clear error messages and a straightforward API, it’s an excellent choice for TypeScript applications needing robust data validation. Whether validating forms, managing state, or ensuring prop safety, Zod simplifies the process.
If you found this article helpful, feel free to share it with your network and leave feedback or suggestions in the comments below.