Files
gruperly/.agents/skills/zod/references/compose-lazy-recursive.md
2026-09-04 16:49:24 -03:00

144 lines
3.2 KiB
Markdown

---
title: Use z.lazy() for Recursive Schemas
impact: MEDIUM
impactDescription: Recursive types reference themselves before definition; z.lazy() defers evaluation to enable self-referential schemas
tags: 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):**
```typescript
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):**
```typescript
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:**
```typescript
// 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):**
```typescript
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:**
```typescript
// 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](https://zod.dev/api#recursive-types)