Zod
Library
Zod
Overview
Zod is developed as a "TypeScript-first schema validation with static type inference" validation library. It seamlessly integrates with TypeScript's type system, providing both runtime data validation and static type safety simultaneously. It automatically infers TypeScript types from schema definitions, ensuring type safety at both compile-time and runtime. As of 2025, Zod is the most prominent TypeScript validation library in AI development and modern web applications.
Details
Zod 4 is the latest stable version as of 2025, achieving a "single source of truth for types and validation" through its TypeScript-first approach. While traditional TypeScript only provides static type checking without runtime type safety, Zod solves this problem. It automatically generates TypeScript types from schemas and executes runtime input data validation using the same schema. With 31k+ GitHub stars, it is widely adopted in tRPC, Next.js, and AI development ecosystems.
Key Features
- TypeScript Type Inference: Automatically generates TypeScript types from schema definitions
- Runtime Type Safety: Complements TypeScript type system with runtime data type validation
- Zero Dependencies: Lightweight and fast with no external dependencies
- Functional Approach: Safe operations through immutable schema objects
- Rich Type Support: Comprehensive coverage from primitives to objects, arrays, unions, and functions
- Custom Validation: Flexible definition of advanced validation logic
Pros and Cons
Pros
- Most intuitive API design for TypeScript developers
- Guarantees both compile-time and runtime type safety simultaneously
- Standard choice in AI development and tRPC ecosystem
- Minimal bundle size through tree-shaking
- Rich community ecosystem (adopted by NextJS, Remix, etc.)
- Detailed and customizable error messages
Cons
- TypeScript-only (cannot be used with pure JavaScript)
- Relatively high learning curve (especially for complex schema definitions)
- Performance overhead with large schemas
- Limited cross-language interoperability
- Development environment load due to type inference complexity
- Limited benefits for pure JavaScript projects
Reference Pages
Code Examples
Installation and Basic Setup
# Install Zod
npm install zod
yarn add zod
pnpm add zod
bun add zod
deno add npm:zod
# TypeScript 5.5 or later required
npm install -D typescript
Basic Schema Definition and Validation
import { z } from "zod/v4";
// Basic schema definition
const UserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
// Automatic type inference
type User = z.infer<typeof UserSchema>;
// type User = { name: string; age: number; email: string; }
// Data validation
const userData = { name: "John Doe", age: 30, email: "[email protected]" };
// parse - throws exception on failure
try {
const validUser = UserSchema.parse(userData);
console.log(validUser); // { name: "John Doe", age: 30, email: "[email protected]" }
} catch (error) {
console.error("Validation error:", error);
}
// safeParse - doesn't throw exception on failure
const result = UserSchema.safeParse(userData);
if (result.success) {
console.log("Validation success:", result.data);
} else {
console.log("Validation failed:", result.error);
}
// Primitive type examples
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
stringSchema.parse("Hello"); // "Hello"
numberSchema.parse(42); // 42
booleanSchema.parse(true); // true
Advanced Validation Rules and Custom Validators
// Detailed string validation
const PasswordSchema = z.string()
.min(8, "Password must be at least 8 characters")
.max(50, "Password must be at most 50 characters")
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, "Password must contain uppercase, lowercase, and number");
// Number range specification
const AgeSchema = z.number()
.int("Age must be an integer")
.min(0, "Age must be at least 0")
.max(150, "Age must be at most 150");
// Array and object combinations
const ProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: z.number().positive(),
categories: z.array(z.string()),
metadata: z.record(z.string(), z.any()).optional(),
});
// Union types (one of multiple types)
const StatusSchema = z.union([
z.literal("pending"),
z.literal("completed"),
z.literal("failed"),
]);
// Enum type definition
const RoleSchema = z.enum(["admin", "user", "guest"]);
// Custom validation
const CustomEmailSchema = z.string().refine(
(email) => {
// Custom validation logic
return email.includes("@") && email.endsWith(".com");
},
{
message: "Please enter a valid .com domain email address",
}
);
// Detailed validation with multiple conditions
const UserRegistrationSchema = z.object({
username: z.string()
.min(3, "Username must be at least 3 characters")
.max(20, "Username must be at most 20 characters")
.regex(/^[a-zA-Z0-9_]+$/, "Username can only contain alphanumeric characters and underscores"),
email: z.string().email("Please enter a valid email address"),
password: PasswordSchema,
confirmPassword: z.string(),
agreeToTerms: z.boolean().refine(val => val === true, {
message: "You must agree to the terms and conditions",
}),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
Framework Integration (React, NestJS, FastAPI, etc.)
// Integration with React Hook Form
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const UserFormSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Please enter a valid email address"),
age: z.number().min(18, "Must be at least 18 years old"),
});
function UserForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(UserFormSchema),
});
const onSubmit = (data: z.infer<typeof UserFormSchema>) => {
console.log("Submit data:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} placeholder="Name" />
{errors.name && <span>{errors.name.message}</span>}
<input {...register("email")} placeholder="Email" type="email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register("age", { valueAsNumber: true })} placeholder="Age" type="number" />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
// tRPC integration example
import { z } from "zod";
import { publicProcedure, router } from "./trpc";
const CreateUserInput = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().min(0),
});
export const appRouter = router({
createUser: publicProcedure
.input(CreateUserInput)
.mutation(async ({ input }) => {
// input is automatically type-safe
const user = await createUser(input);
return user;
}),
});
// Express.js middleware example
import express from "express";
const validateRequest = (schema: z.ZodType) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({ error: "Validation error", details: error });
}
};
};
app.post("/users", validateRequest(UserRegistrationSchema), (req, res) => {
// req.body can be accessed type-safely
res.json({ message: "User creation successful" });
});
Error Handling and Custom Error Messages
// Detailed error handling
const schema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
age: z.number().min(0, "Age must be at least 0"),
});
const result = schema.safeParse({ name: "A", age: -1 });
if (!result.success) {
console.log("Error details:", result.error.errors);
/*
[
{
code: "too_small",
minimum: 2,
type: "string",
inclusive: true,
exact: false,
message: "Name must be at least 2 characters",
path: ["name"]
},
{
code: "too_small",
minimum: 0,
type: "number",
inclusive: true,
exact: false,
message: "Age must be at least 0",
path: ["age"]
}
]
*/
}
// Global custom error message configuration
z.setErrorMap((issue, ctx) => {
switch (issue.code) {
case z.ZodIssueCode.invalid_type:
if (issue.expected === "string") {
return { message: "Please enter a string" };
}
break;
case z.ZodIssueCode.too_small:
if (issue.type === "string") {
return { message: `Please enter at least ${issue.minimum} characters` };
}
break;
default:
return { message: ctx.defaultError };
}
return { message: ctx.defaultError };
});
// Form error handling helper
function getFieldErrors(error: z.ZodError): Record<string, string> {
const fieldErrors: Record<string, string> = {};
error.errors.forEach((err) => {
if (err.path.length > 0) {
const fieldName = err.path.join(".");
fieldErrors[fieldName] = err.message;
}
});
return fieldErrors;
}
// Usage example
const validationResult = schema.safeParse(userData);
if (!validationResult.success) {
const fieldErrors = getFieldErrors(validationResult.error);
console.log(fieldErrors); // { "name": "Name must be at least 2 characters", "age": "Age must be at least 0" }
}
Type Safety and TypeScript Integration
// Advanced type inference example
const NestedSchema = z.object({
user: z.object({
profile: z.object({
name: z.string(),
settings: z.record(z.string(), z.boolean()),
}),
posts: z.array(z.object({
id: z.number(),
title: z.string(),
tags: z.array(z.string()),
})),
}),
metadata: z.object({
created: z.date(),
updated: z.date().optional(),
}),
});
type NestedType = z.infer<typeof NestedSchema>;
/*
type NestedType = {
user: {
profile: {
name: string;
settings: Record<string, boolean>;
};
posts: Array<{
id: number;
title: string;
tags: string[];
}>;
};
metadata: {
created: Date;
updated?: Date | undefined;
};
}
*/
// Type-safe function validation
const createUserFunction = z.function()
.args(z.string(), z.number(), z.boolean().optional())
.returns(z.object({ id: z.string(), success: z.boolean() }));
type CreateUserFunction = z.infer<typeof createUserFunction>;
// type CreateUserFunction = (arg_0: string, arg_1: number, arg_2?: boolean) => { id: string; success: boolean; }
// Implementation type safety
const createUser = createUserFunction.implement((name, age, isActive = true) => {
// TypeScript automatically infers argument types
return {
id: `user_${Date.now()}`,
success: true,
};
});
// Conditional schemas (discriminated unions)
const AnimalSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("dog"),
breed: z.string(),
bark: z.boolean(),
}),
z.object({
type: z.literal("cat"),
indoor: z.boolean(),
meow: z.string(),
}),
]);
type Animal = z.infer<typeof AnimalSchema>;
// TypeScript automatically infers union type
// Transform and preprocess
const StringToNumberSchema = z.string()
.transform(val => parseInt(val, 10))
.refine(val => !isNaN(val), "Please enter a valid numeric string");
const PreprocessedSchema = z.preprocess(
(input) => {
// Preprocessing: trim and convert to lowercase
if (typeof input === "string") {
return input.trim().toLowerCase();
}
return input;
},
z.string().min(1)
);
// Lazy evaluation - recursive schemas
interface Category {
id: string;
name: string;
parent?: Category;
children: Category[];
}
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
id: z.string(),
name: z.string(),
parent: CategorySchema.optional(),
children: z.array(CategorySchema),
})
);
API Schema and Documentation Generation
// OpenAPI/JSON Schema generation example
import { zodToJsonSchema } from "zod-to-json-schema";
const UserAPISchema = z.object({
name: z.string().describe("User's name"),
email: z.string().email().describe("User's email address"),
age: z.number().min(0).max(150).describe("User's age"),
role: z.enum(["admin", "user", "guest"]).describe("User's role"),
});
// Convert to JSON Schema
const jsonSchema = zodToJsonSchema(UserAPISchema, "UserAPI");
console.log(JSON.stringify(jsonSchema, null, 2));
// Metadata for API documentation generation
const APIEndpoints = {
createUser: {
method: "POST",
path: "/users",
requestSchema: UserAPISchema,
responseSchema: z.object({
id: z.string().uuid(),
...UserAPISchema.shape,
createdAt: z.date(),
}),
},
getUser: {
method: "GET",
path: "/users/:id",
paramsSchema: z.object({
id: z.string().uuid(),
}),
responseSchema: UserAPISchema.extend({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
}),
},
};
// Type-safe API client generation
type APIClient = {
[K in keyof typeof APIEndpoints]: (
...args: any[]
) => Promise<z.infer<typeof APIEndpoints[K]["responseSchema"]>>;
};