What is a Schema?
In Zod, a schema is an object that represents a data structure and its validation rules. Schemas serve two purposes:
- Runtime validation - Check if data matches expected structure
- Type inference - Automatically generate TypeScript types
Every schema is an instance of ZodType and has methods for parsing, validation, and transformation.
Creating Basic Schemas
Zod provides factory functions for all primitive types:
import { z } from 'zod';
// Primitives
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const bigintSchema = z.bigint();
const dateSchema = z.date();
// Special types
const undefinedSchema = z.undefined();
const nullSchema = z.null();
const anySchema = z.any();
const unknownSchema = z.unknown();
const neverSchema = z.never();
String Schemas
String schemas support validation and transformation methods:
// Length validations
const minFive = z.string().min(5);
const maxTen = z.string().max(10);
const exactlyFive = z.string().length(5);
const nonempty = z.string().min(1);
// Pattern validations
const email = z.string().email();
const url = z.string().url();
const uuid = z.string().uuid();
const regex = z.string().regex(/^[A-Z]+$/);
// Content validations
const includesTest = z.string().includes('test');
const startsWithHello = z.string().startsWith('hello');
const endsWithWorld = z.string().endsWith('world');
// Transformations
const trimmed = z.string().trim();
const lowercase = z.string().toLowerCase();
const uppercase = z.string().toUpperCase();
From the source code at packages/zod/src/v4/classic/schemas.ts:269-318, string schemas have properties like format, minLength, and maxLength that can be inspected.
Number Schemas
Number validation with numeric constraints:
// Comparison validations
const positive = z.number().positive(); // > 0
const nonnegative = z.number().nonnegative(); // >= 0
const negative = z.number().negative(); // < 0
const nonpositive = z.number().nonpositive(); // <= 0
// Range validations
const gtTen = z.number().gt(10); // Greater than
const gteZero = z.number().gte(0); // Greater than or equal
const ltHundred = z.number().lt(100); // Less than
const lteMax = z.number().lte(1000); // Less than or equal
// Aliases
const min = z.number().min(0); // Same as gte
const max = z.number().max(100); // Same as lte
// Special validations
const integer = z.number().int();
const multipleOfFive = z.number().multipleOf(5);
Zod v4 does NOT accept infinite values by default - all numbers must be finite.
Object Schemas
Define complex object structures:
const User = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().optional(),
role: z.enum(['admin', 'user']),
});
// Access shape
const nameSchema = User.shape.name; // ZodString
// Modify objects
const PartialUser = User.partial(); // All fields optional
const RequiredUser = User.required(); // All fields required
const PickedUser = User.pick({ name: true, email: true });
const OmittedUser = User.omit({ id: true });
Array Schemas
Validate arrays with element constraints:
const strings = z.array(z.string());
const numbers = z.number().array(); // Alternative syntax
// Length constraints
const atLeastOne = z.array(z.string()).min(1);
const nonempty = z.array(z.string()).nonempty();
const maxTen = z.array(z.string()).max(10);
const exactlyFive = z.array(z.string()).length(5);
// Unwrap element type
const elementSchema = strings.unwrap(); // Returns z.string()
Union and Intersection
Combine multiple schemas:
// Union - one of several types
const StringOrNumber = z.union([z.string(), z.number()]);
const alt = z.string().or(z.number()); // Alternative syntax
// Intersection - all types simultaneously
const Combined = z.intersection(
z.object({ id: z.number() }),
z.object({ name: z.string() })
);
const alt2 = z.object({ id: z.number() }).and(z.object({ name: z.string() }));
Enum Schemas
// Native enum
const Fruits = z.enum(['apple', 'banana', 'orange']);
type Fruits = z.infer<typeof Fruits>; // 'apple' | 'banana' | 'orange'
// From object keys
const User = z.object({
name: z.string(),
email: z.string(),
});
const UserKeys = z.keyof(User); // 'name' | 'email'
Optional and Nullable
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined
// In objects
const Schema = z.object({
required: z.string(),
optional: z.string().optional(),
nullable: z.string().nullable(),
});
Add descriptions and custom metadata:
const schema = z.string().describe('User email address');
console.log(schema.description); // 'User email address'
// Custom metadata (stored in globalRegistry)
const tagged = schema.meta({ tag: 'email', version: 1 });
const metadata = tagged.meta(); // { tag: 'email', version: 1, description: '...' }
Common Patterns
const LoginForm = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
rememberMe: z.boolean().default(false),
});
const ApiResponse = z.object({
success: z.boolean(),
data: z.unknown(),
error: z.string().optional(),
timestamp: z.date(),
});
const Config = z.object({
port: z.number().int().positive().default(3000),
host: z.string().default('localhost'),
env: z.enum(['development', 'production', 'test']),
database: z.object({
url: z.string().url(),
pool: z.number().int().min(1).max(100).default(10),
}),
});
Schema Chaining
All schema methods return a new schema instance, allowing method chaining:
const schema = z.string()
.trim() // Remove whitespace
.toLowerCase() // Convert to lowercase
.min(3) // Min length 3
.max(50) // Max length 50
.email(); // Must be valid email
Each method call creates a new schema instance. The original schema remains unchanged.
Unknown Keys in Objects
Control how objects handle extra properties:
const strict = z.object({ name: z.string() }).strict();
// Throws on unknown keys
const strip = z.object({ name: z.string() }).strip();
// Silently removes unknown keys (default behavior)
const passthrough = z.object({ name: z.string() }).passthrough();
// Allows unknown keys to pass through
const catchall = z.object({ name: z.string() }).catchall(z.number());
// Unknown keys must be numbers
Next Steps