name: zod-schema description: Create data schemas and validate data using Zod v4. Covers schema definition, parsing, error handling, coercion, metadata, JSON Schema conversion, and common patterns. Use when the user asks to define schemas, validate data, create types with Zod, or work with runtime type checking in TypeScript.
Zod Schema & Validation (v4)
Use Zod v4 (zod@^4.0.0) for all schema definitions. Do NOT use Zod v3 patterns.
Installation
npm install zod@^4.0.0
Zod v4 is stable and published as zod@4 on npm. Import from "zod" (full API) or "zod/mini" (tree-shakable functional API, ~1.9kb gzipped):
// Full API (method chaining).
import * as z from "zod";
// Mini — functional, tree-shakable (use in frontend-only code).
import * as z from "zod/mini";
Defining Schemas
Primitives
z.string();
z.number();
z.boolean();
z.bigint();
z.date();
z.undefined();
z.null();
z.void();
z.any();
z.unknown();
z.never();
Numbers
z.number().min(1).max(100);
z.number().positive();
z.number().nonnegative();
z.number().multipleOf(5);
// Fixed-width numeric types.
z.int(); // safe integer range
z.float32();
z.float64();
z.int32();
z.uint32();
z.int64(); // returns ZodBigInt
z.uint64(); // returns ZodBigInt
Strings — use top-level formats (NOT methods)
// Correct (v4).
z.email();
z.url();
z.uuidv4();
z.uuidv7();
z.ipv4();
z.ipv6();
z.cidrv4(); // CIDR range: "192.168.0.0/24"
z.cidrv6(); // CIDR range: "2001:db8::/32"
z.base64();
z.emoji(); // single emoji character
z.jwt();
z.iso.date();
z.iso.datetime();
z.iso.time();
z.iso.duration();
// Deprecated (v3 style) — do NOT use.
// z.string().email()
// z.string().url()
String constraints still use methods:
z.string().min(1).max(255);
z.string().startsWith("https://");
z.string().includes("@");
z.string().regex(/^[a-z]+$/);
z.string().trim();
z.string().toLowerCase();
z.string().toUpperCase();
Objects
const UserSchema = z.object({
name: z.string(),
email: z.email(),
age: z.number().int().positive(),
});
type User = z.infer<typeof UserSchema>;
Object variants:
z.strictObject({ name: z.string() }); // rejects unknown keys
z.looseObject({ name: z.string() }); // passes through unknown keys
Object manipulation:
UserSchema.pick({ name: true });
UserSchema.omit({ age: true });
UserSchema.partial();
UserSchema.required();
// Extend with .extend() or spread (best tsc performance).
UserSchema.extend({ role: z.string() });
z.object({ ...UserSchema.shape, role: z.string() });
Arrays & Tuples
z.array(z.string()).min(1).max(10);
z.tuple([z.string(), z.number()]);
z.tuple([z.string()], z.string()); // [string, ...string[]]
Unions & Discriminated Unions
z.union([z.string(), z.number()]);
z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.string() }),
z.object({ status: z.literal("error"), message: z.string() }),
]);
Enums
// String enums.
z.enum(["admin", "user", "guest"]);
// Native TypeScript enums.
enum Role { Admin = "admin", User = "user" }
z.enum(Role);
Records
// v4 requires two arguments.
z.record(z.string(), z.number());
// Enum keys are exhaustive in v4.
z.record(z.enum(["a", "b"]), z.number()); // { a: number; b: number }
// For optional keys, use partialRecord.
z.partialRecord(z.enum(["a", "b"]), z.number());
Recursive Types
const Category = z.object({
name: z.string(),
get subcategories() { return z.array(Category) },
});
type Category = z.infer<typeof Category>;
Literals, Nullables, Optionals
z.literal("active");
z.literal([200, 201, 204]); // multiple values
z.string().optional(); // string | undefined
z.string().nullable(); // string | null
z.string().nullish(); // string | null | undefined
Files
z.file().min(10_000).max(1_000_000).mime(["image/png", "image/jpeg"]);
Template Literals
const CssValue = z.templateLiteral([z.number(), z.enum(["px", "em", "rem"])]);
// `${number}px` | `${number}em` | `${number}rem`
Parsing & Validation
parse vs safeParse
// Throws ZodError on failure.
const user = UserSchema.parse(input);
// Returns { success, data, error } — never throws.
const result = UserSchema.safeParse(input);
if (result.success) {
result.data; // typed
} else {
result.error; // ZodError
}
Async versions: parseAsync(), safeParseAsync().
Error Handling
Unified error parameter (v4)
// Simple string error.
z.string().min(5, { error: "Too short." });
// Dynamic error function.
z.string({
error: (issue) =>
issue.input === undefined ? "Required" : "Expected a string",
});
// Conditional by issue code.
z.number().min(0, {
error: (issue) => {
if (issue.code === "too_small") {
return `Must be >= ${issue.minimum}`;
}
},
});
Pretty-printing errors
const result = UserSchema.safeParse(badInput);
if (!result.success) {
console.log(z.prettifyError(result.error));
}
Tree-structured errors
const tree = z.treeifyError(result.error);
// Access nested errors via tree.properties.<fieldName>.errors
Coercion
Coerces input before validation. Input type is unknown.
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input) — falsy → false, truthy → true
z.coerce.date(); // new Date(input)
Environment-style boolean coercion:
z.stringbool();
// "true", "1", "yes", "on" → true
// "false", "0", "no", "off" → false
Transforms & Pipes
// Transform changes the output type.
const schema = z.string().transform((val) => val.length);
// input: string → output: number
// Overwrite keeps the same type (allows chaining).
z.number().overwrite((val) => val ** 2).max(100);
Preprocess with pipes:
z.preprocess((val) => String(val), z.string().min(1));
Defaults
// Default applies to the OUTPUT type (short-circuits parsing).
z.string().default("N/A");
// Prefault applies to the INPUT type (runs through parsing).
z.string().transform((v) => v.length).prefault("hello");
Metadata & Registries
// Add metadata via the global registry.
z.string().meta({
id: "user_email",
title: "Email Address",
description: "User's primary email",
examples: ["user@example.com"],
});
// Custom typed registries.
const myRegistry = z.registry<{ label: string; searchable: boolean }>();
myRegistry.add(nameSchema, { label: "Full Name", searchable: true });
JSON Schema Conversion
const jsonSchema = z.toJSONSchema(UserSchema);
// {
// type: "object",
// properties: { name: { type: "string" }, ... },
// required: ["name", "email", "age"]
// }
Metadata from z.globalRegistry is automatically included.
Common Patterns
API response validation
const ApiResponseSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("success"),
data: z.unknown(),
}),
z.object({
status: z.literal("error"),
code: z.number(),
message: z.string(),
}),
]);
Environment variables
const EnvSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.url(),
DEBUG: z.stringbool().default(false),
NODE_ENV: z.enum(["development", "production", "test"]),
});
const env = EnvSchema.parse(process.env);
Form data validation
const FormSchema = z.object({
username: z.string().min(3).max(20),
email: z.email(),
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
error: "Passwords must match",
path: ["confirmPassword"],
});
Critical v4 Differences — Avoid v3 Patterns
| v3 (deprecated) | v4 (correct) |
|---|---|
z.string().email() |
z.email() |
z.string().url() |
z.url() |
z.string().uuid() |
z.uuidv4() |
{ message: "..." } |
{ error: "..." } |
{ invalid_type_error, required_error} |
{ error: (issue) => ... } |
z.nativeEnum(MyEnum) |
z.enum(MyEnum) |
z.record(z.string()) |
z.record(z.string(), z.string()) |
.merge(OtherSchema) |
.extend(OtherSchema.shape) |
.strict() / .passthrough() |
z.strictObject() / z.looseObject() |
z.string().ip() |
z.ipv4() / z.ipv6() |
Zod Mini (zod/mini)
A tree-shakable variant of Zod (~1.9kb gzipped vs ~12kb for standard Zod). Uses standalone functions instead of method chaining — useful in frontend-only bundles where size matters.
import * as z from "zod/mini";
// Methods become wrapper functions.
z.optional(z.string()); // z.string().optional()
z.nullable(z.string()); // z.string().nullable()
z.union([z.string(), z.number()]); // same
z.extend(baseSchema, { age: z.number() }); // baseSchema.extend({ age: ... })
z.pick(UserSchema, { name: true }); // UserSchema.pick(...)
z.partial(UserSchema); // UserSchema.partial()
Parsing methods (.parse(), .safeParse()) work identically. Use .check() instead of .refine() in mini for refinements:
const schema = z.string().check(z.minLength(3));
Use standard "zod" for server-side code or anything where bundle size is not a concern — the API is cleaner. Use "zod/mini" when shipping to a client bundle where every byte counts.
Additional Resources
- For detailed API patterns and advanced usage, see reference.md.