Blog Details
SOHIDUL ISLAM
12 Oct 2024
5 min read
Imagine you're working on an application that collects user information, and you want to ensure that every piece of data is correct. Zod is a tool that helps you with exactly that. When you're coding with TypeScript, Zod makes sure that the data coming in fits the rules you define.
In TypeScript, while types provide safety, they only work at compile time (when you're writing the code). Zod takes it a step further by actually validating the data at runtime, so when the app is running, you can be confident that your data is correct. Let’s explore how Zod helps you build more reliable applications! [documentation]
Requirement
TypeScript 4.5+!
strict
mode in your tsconfig.json
. This is a best practice for all TypeScript projects.// tsconfig.json
{
// ...
"compilerOptions": {
// ...
"strict": true
}
}
npm install zod # npm
yarn add zod # yarn
bun add zod # bun
pnpm add zod # pnpm
Creating a simple string schema:
import { z } from "zod";
// creating a schema for strings
const mySchema = z.string();
// parsing
mySchema.parse("tuna"); // => "tuna"
mySchema.parse(12); // => throws ZodError
// "safe" parsing (doesn't throw error if validation fails)
mySchema.safeParse("tuna"); // => { success: true; data: "tuna" }
mySchema.safeParse(12); // => { success: false; error: ZodError }
Number Validation:
import { z } from "zod";
// Creating a schema for numbers
const numberSchema = z.number();
// parsing
numberSchema.parse(42); // => 42
numberSchema.parse("42"); // => throws ZodError
// "safe" parsing
numberSchema.safeParse(42); // => { success: true; data: 42 }
numberSchema.safeParse("42"); // => { success: false; error: ZodError }
Boolean Validation:
import { z } from "zod";
// Creating a schema for booleans
const booleanSchema = z.boolean();
// parsing
booleanSchema.parse(true); // => true
booleanSchema.parse("true"); // => throws ZodError
// "safe" parsing
booleanSchema.safeParse(false); // => { success: true; data: false }
booleanSchema.safeParse("true"); // => { success: false; error: ZodError }
Array Validation:
import { z } from "zod";
// Creating a schema for an array of numbers
const numberArraySchema = z.array(z.number());
// parsing
numberArraySchema.parse([1, 2, 3]); // => [1, 2, 3]
numberArraySchema.parse([1, "two", 3]); // => throws ZodError
// "safe" parsing
numberArraySchema.safeParse([1, 2, 3]); // => { success: true; data: [1, 2, 3] }
numberArraySchema.safeParse([1, "two", 3]); // => { success: false; error: ZodError }
Object Validation:
import { z } from "zod";
// Creating a schema for an object with defined properties
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
// parsing
userSchema.parse({ name: "Alice", age: 30 }); // => { name: "Alice", age: 30 }
userSchema.parse({ name: "Alice", age: "30" }); // => throws ZodError
// "safe" parsing
userSchema.safeParse({ name: "Alice", age: 30 }); // => { success: true; data: { name: "Alice", age: 30 } }
userSchema.safeParse({ name: "Alice", age: "30" }); // => { success: false; error: ZodError }
Enum Validation:
import { z } from "zod";
// Creating a schema for an enum of string values
const roleSchema = z.enum(["admin", "user", "guest"]);
// parsing
roleSchema.parse("admin"); // => "admin"
roleSchema.parse("superadmin"); // => throws ZodError
// "safe" parsing
roleSchema.safeParse("user"); // => { success: true; data: "user" }
roleSchema.safeParse("superadmin"); // => { success: false; error: ZodError }
Optional Validation:
import { z } from "zod";
// Creating a schema where the value can be either string or undefined
const optionalSchema = z.string().optional();
// parsing
optionalSchema.parse("Hello"); // => "Hello"
optionalSchema.parse(undefined); // => undefined
optionalSchema.parse(42); // => throws ZodError
// "safe" parsing
optionalSchema.safeParse("Hello"); // => { success: true; data: "Hello" }
optionalSchema.safeParse(undefined); // => { success: true; data: undefined }
optionalSchema.safeParse(42); // => { success: false; error: ZodError }
Default Value for Undefined:
import { z } from "zod";
// Creating a schema where the default value is set if undefined
const defaultSchema = z.string().default("default value");
// parsing
defaultSchema.parse("Hi"); // => "Hi"
defaultSchema.parse(undefined); // => "default value"
// "safe" parsing
defaultSchema.safeParse(undefined); // => { success: true; data: "default value" }
More About Primitives Type Validation
import { z } from "zod";
// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();
// empty types
z.undefined();
z.null();
z.void(); // accepts undefined
// catch-all types
// allows any value
z.any();
z.unknown();
// never type
// allows no values
z.never();
In this section, let's walk through how you can use Zod and React Hook Form together to make form validation more powerful and flexible. We'll also see how to add some advanced validation like using refinement and regex for extra control.
React Hook Form (RHForm) is a library that helps manage forms in React. It handles input validation, form state, and error messages in a super simple way. By combining RHForm with Zod, we can create strong, easy-to-read validation logic.
Setting Up React Hook Form with Zod:
To integrate Zod with React Hook Form, you’ll need to install both libraries:
npm install react-hook-form zod @hookform/resolvers
Let’s create a simple registration form that includes fields for name, email, and age. We’ll define a basic schema for our form:
import React from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
// Define your Zod schema
const schema = z.object({
name: z.string().min(2, "Name must be at least 2 characters long."),
email: z.string().email("Please enter a valid email."),
age: z.number().min(18, "You must be at least 18 years old.")
});
function RegistrationForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema)
});
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name</label>
<input {...register("name")} />
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>Email</label>
<input {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>Age</label>
<input type="number" {...register("age")} />
{errors.age && <p>{errors.age.message}</p>}
</div>
<button type="submit">Register</button>
</form>
);
}
export default RegistrationForm;
Step 2: Adding Refinement and Regex Validation
Now, let’s enhance our form validation using refinement and regex patterns.
We’ll ensure that the name only contains alphabetic characters (no numbers or special characters). Here’s how we can use regex for that:
const schema = z.object({
name: z.string()
.min(2, "Name must be at least 2 characters long.")
.regex(/^[A-Za-z\s]+$/, "Name should only contain letters and spaces."),
email: z.string()
.email("Please enter a valid email."),
age: z.number()
.min(18, "You must be at least 18 years old.")
});
regex(/^[A-Za-z\s]+$/): This regular expression checks that the name contains only letters (both upper and lower case) and spaces. If it contains anything else, the error message "Name should only contain letters and spaces." will be displayed.
Next, let’s refine our email validation. We’ll restrict it to only accept emails ending with a specific domain, like @example.com
:
const schema = z.object({
name: z.string()
.min(2, "Name must be at least 2 characters long.")
.regex(/^[A-Za-z\s]+$/, "Name should only contain letters and spaces."),
email: z.string()
.email("Please enter a valid email.")
.refine((value) => value.endsWith('@example.com'), {
message: "Email must end with @example.com",
}),
age: z.number()
.min(18, "You must be at least 18 years old.")
});
refine(): This method allows you to add custom logic for validation. Here, we check if the email ends with @example.com
. If it doesn’t, the user will see the message "Email must end with @example.com."
Age Validation with Refinement:
Lastly, let’s add a rule to ensure the age is an even number:
const schema = z.object({
name: z.string()
.min(2, "Name must be at least 2 characters long.")
.regex(/^[A-Za-z\s]+$/, "Name should only contain letters and spaces."),
email: z.string()
.email("Please enter a valid email.")
.refine((value) => value.endsWith('@example.com'), {
message: "Email must end with @example.com",
}),
age: z.number()
.min(18, "You must be at least 18 years old.")
.refine((value) => value % 2 === 0, {
message: "Age must be an even number."
})
});
refine(value % 2 === 0): This line checks if the age is even by checking if it’s divisible by 2. If the user enters an odd number, they will receive the message "Age must be an even number."
Now that we’ve added these additional validation rules, we need to ensure errors are displayed appropriately in the form. Here’s how you can display error messages for each field:
{errors.name && <p>{errors.name.message}</p>}
{errors.email && <p>{errors.email.message}</p>}
{errors.age && <p>{errors.age.message}</p>}
By integrating Zod with React Hook Form, you create a powerful validation system that can handle complex rules:
This combination makes your forms not only more robust but also more user-friendly, ensuring that the data collected meets your requirements. Happy coding!
Don’t worry, we don’t spam!