--- 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 = 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 = 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 = 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 = 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 = 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)