3.2 KiB
3.2 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use z.lazy() for Recursive Schemas | MEDIUM | Recursive types reference themselves before definition; z.lazy() defers evaluation to enable self-referential schemas | compose, lazy, recursive, trees |
Use z.lazy() for Recursive Schemas
TypeScript can't infer recursive Zod schema types automatically. Use z.lazy() to defer schema evaluation and manually provide the type annotation. This enables tree structures, nested comments, and other self-referential data.
Incorrect (direct self-reference):
import { z } from 'zod'
// This fails - categorySchema used before it's defined
const categorySchema = z.object({
id: z.string(),
name: z.string(),
children: z.array(categorySchema), // Error: Block-scoped variable used before declaration
})
Correct (using z.lazy with type annotation):
import { z } from 'zod'
// Define the type manually
interface Category {
id: string
name: string
children: Category[]
}
// Use z.lazy() to defer schema reference
const categorySchema: z.ZodType<Category> = z.object({
id: z.string(),
name: z.string(),
children: z.lazy(() => z.array(categorySchema)),
})
// Now it works
const tree = categorySchema.parse({
id: '1',
name: 'Electronics',
children: [
{
id: '2',
name: 'Phones',
children: [
{ id: '3', name: 'iPhones', children: [] },
{ id: '4', name: 'Android', children: [] },
],
},
],
})
Common recursive patterns:
// Comments with replies
interface Comment {
id: string
content: string
author: string
replies: Comment[]
}
const commentSchema: z.ZodType<Comment> = z.object({
id: z.string(),
content: z.string(),
author: z.string(),
replies: z.lazy(() => z.array(commentSchema)),
})
// Binary tree
interface TreeNode {
value: number
left: TreeNode | null
right: TreeNode | null
}
const treeNodeSchema: z.ZodType<TreeNode> = z.object({
value: z.number(),
left: z.lazy(() => treeNodeSchema.nullable()),
right: z.lazy(() => treeNodeSchema.nullable()),
})
// Nested menu structure
interface MenuItem {
label: string
href?: string
children?: MenuItem[]
}
const menuItemSchema: z.ZodType<MenuItem> = z.object({
label: z.string(),
href: z.string().url().optional(),
children: z.lazy(() => z.array(menuItemSchema)).optional(),
})
JSON Schema (any valid JSON):
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue }
const jsonValueSchema: z.ZodType<JSONValue> = z.lazy(() =>
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(jsonValueSchema),
z.record(jsonValueSchema),
])
)
Performance consideration:
// z.lazy() has minimal overhead - the function is called once
// and the schema is cached. Safe to use in hot paths.
// If validating many recursive structures, the schema itself
// is only built once. Validation performance depends on data depth.
When NOT to use this pattern:
- Non-recursive schemas (lazy adds unnecessary indirection)
- When you can flatten the structure instead
Reference: Zod API - Recursive Types