自己参照JSONから再帰的なZodスキーマを作成する
z.lazy()を使ってツリー構造、コメントスレッド、ネストされたメニューのJSONデータから再帰的なZodスキーマを定義する方法を学びます。
Advanced
詳細な説明
Zodの再帰スキーマ
多くのデータモデルは本質的に再帰的です。ファイルシステム、コメントスレッド、組織図、ナビゲーションメニューはすべて、同じスキーマを参照するノードを含みます。
JSON例
{
"id": 1,
"name": "Root",
"children": [
{
"id": 2,
"name": "Child A",
"children": [
{ "id": 4, "name": "Grandchild", "children": [] }
]
},
{
"id": 3,
"name": "Child B",
"children": []
}
]
}
再帰的なZodスキーマ
import { z } from "zod";
interface TreeNode {
id: number;
name: string;
children: TreeNode[];
}
const treeNodeSchema: z.ZodType<TreeNode> = z.object({
id: z.number().int(),
name: z.string(),
children: z.lazy(() => z.array(treeNodeSchema)),
});
なぜz.lazy()が必要か
JavaScriptは式を先行評価します。z.lazy()がなければ、スキーマは定義される前に自身を参照し、ReferenceErrorが発生します。z.lazy()はパース時まで評価を遅延させます。
コメントスレッド例
interface Comment {
id: number;
author: string;
text: string;
replies: Comment[];
}
const commentSchema: z.ZodType<Comment> = z.object({
id: z.number().int(),
author: z.string(),
text: z.string(),
replies: z.lazy(() => z.array(commentSchema)),
});
型アノテーションの要件
明示的な型アノテーションz.ZodType<TreeNode>に注目してください。自己参照するスキーマの型をTypeScriptが推論できないため、再帰スキーマにはこれが必要です。interfaceを別途定義し、スキーマ変数にアノテーションを付ける必要があります。
深さ制限付きパース
安全のため、カスタムチェックで再帰の深さを制限できます:
function createTreeSchema(maxDepth: number): z.ZodTypeAny {
if (maxDepth <= 0) {
return z.object({
id: z.number(),
name: z.string(),
children: z.array(z.never()),
});
}
return z.object({
id: z.number(),
name: z.string(),
children: z.array(z.lazy(() => createTreeSchema(maxDepth - 1))),
});
}
const shallowTreeSchema = createTreeSchema(5);
相互再帰スキーマ
interface Folder {
name: string;
files: FileNode[];
subfolders: Folder[];
}
interface FileNode {
name: string;
size: number;
}
const fileSchema = z.object({
name: z.string(),
size: z.number(),
});
const folderSchema: z.ZodType<Folder> = z.object({
name: z.string(),
files: z.array(fileSchema),
subfolders: z.lazy(() => z.array(folderSchema)),
});
ユースケース
各コメントが任意の深さでネストされた返信を持てるコメントシステムを構築し、ツリー全体が期待される形状に準拠していることをランタイムで検証する必要がある場合に使用します。