Zod & Runtime Validation
Zod is the X-ray machine for your data — one schema, two guarantees.
TypeScript's type system is incredibly powerful, but it has one fundamental limitation: it operates at COMPILE TIME. Once your code compiles, all types are erased. The actual data flowing through your application at runtime might not match the types you declared — an API might change, a user might submit invalid input, or a third-party service might send unexpected data. This is where RUNTIME validation becomes essential, and this is where Zod shines.
Think of a hospital's X-ray machine at Osmania General Hospital. The machine serves TWO purposes. First, it helps the doctor DIAGNOSE the patient at the moment (runtime validation — checking the actual data right now). Second, the machine's design and capabilities define the TYPE of diagnosis possible (compile-time type — knowing what to expect based on the machine's specifications). A single machine provides instant visibility AND sets expectations.
Zod is your X-ray machine for data. You define a schema — a description of what the data should look like — ONCE. That schema does TWO things:
- At compile time: TypeScript infers the type from your schema using
z.infer. This gives you full type safety in your editor. - At runtime: Zod validates incoming data against the schema. If the data doesn't match, Zod throws a detailed error telling you exactly what's wrong.
This is called the Single Source of Truth pattern. Instead of writing an interface AND writing validation logic separately (which always drifts apart as your code evolves), you write the schema ONCE. The type and the validation are ALWAYS in sync. No duplication, no drift, no surprises.
Zod is a TypeScript-first schema validation library. Let's start with the basics.
Installation
npm install zod
Basic Schemas
import { z } from "zod";
// Primitive schemas
const nameSchema = z.string();
const ageSchema = z.number();
const isActiveSchema = z.boolean();
// Object schema
const UserSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
isActive: z.boolean().default(true),
});
// Parsing — throws on failure
const user = UserSchema.parse({
name: "Imran",
age: 25,
email: "imran@example.com",
});
// user is typed as:
// { name: string; age: number; email: string; isActive: boolean; }
Safe Parsing (Recommended for most cases)
const result =
UserSchema.safeParse(inputData);
if (result.success) {
// result.data is typed!
console.log(result.data.name);
} else {
// result.error is ZodError
console.error(result.error.errors);
}
Common Schema Types
z.string() // String
z.number() // Number
z.boolean() // Boolean
z.null() // Null
z.undefined() // Undefined
z.nullable(z.string()) // string | null
z.optional(z.string()) // string | undefined
z.array(z.string()) // string[]
z.object({}) // Object
z.union([z.string(), z.number()]) // string | number
z.literal("admin") // Literal "admin"
z.enum(["a", "b"]) // Enum
z.date() // Date
z.any() // any
z.unknown() // unknownThe real power of Zod is the z.infer utility type, which extracts the TypeScript type from your schema. This eliminates duplication entirely.
// Step 1: Define the schema ONCE
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().positive(),
role: z.enum(["admin", "user"]),
tags: z.array(z.string()).default([]),
createdAt: z.date().default(() => new Date()),
});
// Step 2: Derive the type from schema
type User = z.infer<typeof UserSchema>;
// User is equivalent to:
// {
// id: string;
// name: string;
// email: string;
// age: number;
// role: "admin" | "user";
// tags: string[];
// createdAt: Date;
// }
Nested Schemas
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zip: z.string().length(6),
});
const OrderSchema = z.object({
id: z.string().uuid(),
user: UserSchema,
items: z.array(
z.object({
productId: z.string(),
quantity: z.number().int().min(1),
price: z.number().positive(),
})
),
total: z.number().positive(),
status: z.enum([
"pending", "shipped", "delivered"
]),
});
type Order = z.infer<typeof OrderSchema>;
Schema Composition & Reuse
// Partial schema (all fields optional)
const PartialUser =
UserSchema.partial();
// Pick specific fields
const UserName =
UserSchema.pick({ name: true });
// Omit fields
const UserWithoutEmail =
UserSchema.omit({ email: true });
// Extend a schema
const AdminSchema =
UserSchema.extend({
permissions: z.array(z.string()),
});
// Merge schemas
const FullProfile =
UserSchema.merge(ProfileSchema);
With z.infer, your TypeScript types and your runtime validation are ALWAYS in sync. Change the schema, and both the type and validation update automatically. No more remembering to update an interface when you change a validation rule!
Zod is powerful, but there are common patterns and pitfalls you should know.
Trap 1: Forgetting safeParse for External Data
Always use safeParse for data from external sources (APIs, user input, files). parse() is for data you trust. safeParse is for data you don't.
Trap 2: Creating Duplicate Types
Don't write both an interface AND a Zod schema. Use z.infer to derive the type from the schema. One source of truth only.
Trap 3: Not Refining After Parsing
// Use refinements for complex rules
const PasswordSchema = z.string()
.min(8, "Too short")
.refine(
(val) => /[A-Z]/.test(val),
"Must have uppercase"
);
Trap 4: Ignoring Type Performance
Very large Zod schemas (100+ fields) can slow down TypeScript compilation. For massive schemas, split them into smaller, composable schemas.
Here's your complete cheatsheet for Zod!
Installation:
npm install zod
Basic Usage:
const Schema = z.object({ name: z.string() });
type T = z.infer<typeof Schema>; // compile-time type
Schema.parse(data); // throws on failure
Schema.safeParse(data); // { success, data | error }
Common Schema Methods:
z.string()
z.string().email().min(2).max(100)
z.number().int().positive()
z.boolean()
z.array(z.string())
z.object({})
z.union([...])
z.enum(["a", "b"])
.z.nullable() .optional() .default(val)
.partial() .pick({}) .omit({}) .extend({})
.refine(fn, msg)
Key Rules:
- Use z.infer to derive types — never duplicate type definitions
- Use safeParse for external/untrusted data, parse for trusted data
- Compose schemas using extend, merge, pick, omit for reuse
- Use .refine() for custom validation logic beyond simple types
- Use .default() to provide fallback values for missing fields
The Golden Rule: "Zod is the X-ray machine for your data — one schema provides TWO guarantees: compile-time types with z.infer and runtime validation with .parse(). Never duplicate. Never drift. Single source of truth, bhai!"
Key Takeaways
- Zod provides runtime validation AND compile-time types from a single schema definition
- Use z.infer
to derive TypeScript types — eliminates duplication - safeParse returns a discriminated union: { success: true, data } | { success: false, error }
- parse() throws ZodError on failure — use try/catch or prefer safeParse for external data
- Compose schemas with .extend(), .pick(), .omit(), .merge() for reusability
- Use .refine() for custom validation and .default() for fallback values
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login