Introduction
Zod has become the go-to validation library for TypeScript developers. It lets you define schemas that validate data at runtime while automatically inferring TypeScript types. This guide shows you how to generate Zod schemas from JSON data, so you can quickly add validation to any API response or data source without writing schemas by hand.
What is Zod?
Zod is a TypeScript-first schema validation library. Unlike Joi or Yup, Zod was designed from the ground up for TypeScript, providing perfect type inference. Key features:
- TypeScript-first - Full type inference with zero manual type annotations
- Runtime validation - Catches invalid data that TypeScript can't
- Composable - Build complex schemas from simple primitives
- Zero dependencies - Small bundle size, no external deps
- SSR compatible - Works in Node.js and browsers
Generating Zod Schemas from JSON
The fastest way to create a Zod schema from JSON data is using ourJSON to TypeScript & Zod tool. Paste your JSON, and it generates both TypeScript interfaces and Zod schemas.
Given this JSON:
{
"id": 1,
"name": "Alice",
"email": "[email protected]",
"isActive": true,
"address": {
"street": "123 Main St",
"city": "San Francisco"
}
}The tool generates this Zod schema:
import { z } from "zod";
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
});
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
isActive: z.boolean(),
address: AddressSchema,
});Deriving TypeScript Types from Zod
The biggest advantage of Zod is that you never write TypeScript types manually. Use z.infer to derive types from your schemas:
// Define the schema once
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// TypeScript type is automatically inferred
type User = z.infer<typeof UserSchema>;
// Equivalent to: { id: number; name: string; email: string }
// Use it in your code
function updateUser(user: User) {
// TypeScript knows the exact shape
console.log(user.name); // string
console.log(user.email); // string
}This is the "single source of truth" pattern: define your schema once, and both runtime validation and compile-time types come from it automatically.
Advanced Zod Patterns
Optional and Default Values
const ConfigSchema = z.object({
port: z.number().default(3000),
host: z.string().default("localhost"),
debug: z.boolean().default(false),
apiKey: z.string().optional(), // string | undefined
timeout: z.number().nullable(), // number | null
});Validation Rules
const UserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().min(0).max(150),
website: z.string().url().optional(),
role: z.enum(["admin", "user", "moderator"]),
});Coercion (String to Number)
// When data comes from forms or query params (always strings)
const SearchParamsSchema = z.object({
page: z.coerce.number().default(1), // "2" -> 2
limit: z.coerce.number().default(20), // "50" -> 50
sort: z.string().default("created_at"),
active: z.coerce.boolean().default(true), // "true" -> true
});Discriminated Unions
const TextMessage = z.object({
type: z.literal("text"),
content: z.string(),
});
const ImageMessage = z.object({
type: z.literal("image"),
url: z.string().url(),
width: z.number(),
height: z.number(),
});
const Message = z.discriminatedUnion("type", [TextMessage, ImageMessage]);
// Zod validates based on the "type" field
Message.parse({ type: "text", content: "Hello" }); // OK
Message.parse({ type: "image", url: "...", width: 100, height: 100 }); // OK
Message.parse({ type: "text", url: "..." }); // Error!Transform Data
const UserSchema = z.object({
id: z.number(),
name: z.string(),
created_at: z.string().transform(s => new Date(s)),
metadata: z.string().transform(s => JSON.parse(s)),
});
type User = z.infer<typeof UserSchema>;
// created_at is Date, metadata is parsed objectReal-World Usage Patterns
Validating API Responses
async function fetchUsers(): Promise<User[]> {
const res = await fetch("/api/users");
const data = await res.json();
// Validate the entire response
const users = z.array(UserSchema).parse(data);
return users;
}
// With error handling
async function safeFetchUsers() {
try {
const users = await fetchUsers();
return { success: true, data: users };
} catch (error) {
if (error instanceof z.ZodError) {
console.error("API response doesn't match schema:", error.errors);
return { success: false, error: "Invalid data format" };
}
throw error;
}
}Form Validation with React
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const LoginFormSchema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Password must be 8+ characters"),
remember: z.boolean().default(false),
});
type LoginForm = z.infer<typeof LoginFormSchema>;
function LoginForm() {
const { register, handleSubmit } = useForm<LoginForm>({
resolver: zodResolver(LoginFormSchema),
});
const onSubmit = (data: LoginForm) => {
// data is fully validated and typed
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} type="email" />
<input {...register("password")} type="password" />
<input {...register("remember")} type="checkbox" />
<button type="submit">Login</button>
</form>
);
}Next.js API Routes
// app/api/users/route.ts
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user"]).default("user"),
});
export async function POST(request: Request) {
const body = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ errors: result.error.flatten() },
{ status: 400 }
);
}
// result.data is fully typed and validated
const user = await createUser(result.data);
return Response.json(user);
}Key Takeaways
- Use Zod for runtime validation at API boundaries - TypeScript types alone aren't enough
- Generate Zod schemas from JSON using our JSON to TypeScript & Zod tool
- Always use
z.infer<typeof Schema>to derive types - single source of truth - Use
z.coercewhen data comes from forms or query parameters - Use
safeParse()instead ofparse()when you want to handle errors gracefully - Combine with React Hook Form via
@hookform/resolvers/zodfor form validation