Zod Schema Definition
Define runtime-validated TypeScript schemas with z.object, primitives, enums, literals, and schema composition
When to Use
- You need runtime validation that also produces TypeScript types (no manual interface duplication)
- You are receiving untrusted data: API responses, form submissions, environment variables, query params
- You want a single source of truth for both runtime validation and static typing
- You are starting a new data model and want to define it as a Zod schema first
Instructions
- Install Zod:
npm install zod - Import the
znamespace:import { z } from 'zod' - Define primitive fields — choose the right primitive for each field:
z.string(),z.number(),z.boolean(),z.date(),z.bigint(),z.symbol()z.undefined(),z.null(),z.void(),z.any(),z.unknown(),z.never()
- Use
z.object()to compose fields into a schema object — each key maps to a Zod type:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(['admin', 'editor', 'viewer']),
createdAt: z.date(),
});
- Use
z.enum()for a fixed set of string literals — prefer overz.union([z.literal(...)])for simple cases:
const StatusSchema = z.enum(['active', 'inactive', 'pending']);
type Status = z.infer<typeof StatusSchema>; // 'active' | 'inactive' | 'pending'
- Use
z.literal()for exact value matching:
const TrueSchema = z.literal(true);
const FortyTwoSchema = z.literal(42);
const HelloSchema = z.literal('hello');
- Make fields optional or nullable explicitly:
const ProfileSchema = z.object({
bio: z.string().optional(), // string | undefined
avatar: z.string().nullable(), // string | null
website: z.string().nullish(), // string | null | undefined
});
- Compose schemas by nesting them — use schema references, not re-definition:
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/),
});
const OrderSchema = z.object({
id: z.string().uuid(),
shippingAddress: AddressSchema,
billingAddress: AddressSchema.optional(),
});
- Parse input data — use
.parse()to throw on failure,.safeParse()to handle errors gracefully:
// Throws ZodError if invalid
const user = UserSchema.parse(rawData);
// Returns { success: true, data } or { success: false, error }
const result = UserSchema.safeParse(rawData);
if (result.success) {
console.log(result.data); // fully typed
} else {
console.error(result.error.issues);
}
- Export both the schema and its inferred type together:
export const UserSchema = z.object({ ... })
export type User = z.infer<typeof UserSchema>
Details
Zod schemas are composable objects — they are not just validators, they are type constructors. Every schema carries both a runtime validator and a TypeScript type. The z.infer<typeof Schema> pattern eliminates the dual-maintenance problem of keeping an interface and a validator in sync.
Schema composition strategies:
- Flat schemas — all fields in one
z.object(). Good for simple models. - Nested schemas — reference child schemas by variable. Better for shared sub-shapes.
- Extended schemas — use
.extend()to add fields without re-defining the base. Seezod-object-patterns.
Primitive coercion:
Zod does not coerce by default. z.number().parse("42") throws. Use z.coerce.number() when input may be a string (e.g., URL query params, form fields):
const PageSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
});
Default values:
const ConfigSchema = z.object({
timeout: z.number().default(5000),
retries: z.number().default(3),
debug: z.boolean().default(false),
});
Recursive schemas:
For tree structures, use z.lazy():
type Category = {
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema),
})
);
When NOT to use Zod schema definition directly:
- For large union types with many variants — prefer
z.discriminatedUnion()(seezod-union-discriminated) - For complex string patterns — use dedicated string validators (see
zod-string-validation) - For full object manipulation — use object-level methods (see
zod-object-patterns)
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.