Zodの実務パターン — スキーマの置き場所と設計の勘所
この章の目次開く
ここまでは「どう書くか」でした。最後に「どこに置くか・どこまでやるか」という、コードの見た目には出にくい判断をまとめます。
スキーマの置き場所
境界ごとに分けて、共通部分は派生させる
同じリソースでも、用途によって必要なスキーマは違います。
// src/schemas/user.ts
export const userSchema = z.object({
id: z.number(),
name: z.string().trim().min(1),
email: z.email(),
});
export const createUserSchema = userSchema.omit({ id: true });
export const updateUserSchema = createUserSchema.partial();.omit() や .partial() で派生させる。項目が増えたときに直す場所が1か所で済みます。
クライアントとサーバーで共有する
TypeScriptでフロントもバックエンドも書いているなら、スキーマファイルを両方から読み込みます。同じルールを2回書かなければ、ずれようがありません。
src/schemas/user.ts
├─ components/UserForm.tsx から import(画面の検証)
└─ app/api/users/route.ts から import(サーバーの検証)
どこまで検証するか
Zodに慣れると、あらゆる場所にスキーマを書きたくなります。しかし検証はタダではありません。実行時のコストと、コードの読みにくさが増えます。
| 場所 | 検証するか |
|---|---|
| 外部APIのレスポンス | する |
| リクエストボディ・フォーム | する |
| 環境変数(起動時) | する |
JSON.parse / localStorage | する |
| 自分のコード内で組み立てたオブジェクト | しない |
| 検証済みデータを渡す内部関数 | しない |
学習者関数の入り口ごとに検証しておけば安心じゃない?
先生気持ちは分かるけど、それは型が既に保証していることの二度手間なんだ。内部関数まで parse だらけにすると、本当に危ないのはどこかが読み取れなくなる。信頼境界を1本引いて、そこだけ守る方が結果的に安全だよ。

大きなデータの検証コスト
数万件の配列をそのまま parse すると、要素の数だけ検証が走ります。体感できる遅さが出ることがあります。対処の考え方です。
- 必要な項目だけスキーマに書く。 使わないフィールドまで検証しない(余分なキーは既定で取り除かれます)
- ページングされた単位で検証する。 全件をまとめて検証しない
- 信頼できる内部データは検証しない。 自前のDBから型付きで取れているなら不要な場合がある
パフォーマンスは推測せず、実際に測ってから判断してください。多くのアプリでは、Zodの検証がボトルネックになる前にネットワークやDBが先に効いてきます。
他のライブラリとの関係
| ライブラリ | 特徴 |
|---|---|
| Zod | TypeScript前提。型推論が強く、情報量が多い |
| Valibot | 関数を組み合わせる形。バンドルサイズが小さい |
| Yup | 歴史が長い。JavaScript時代からのフォーム検証 |
| JSON Schema | 言語非依存の標準仕様。Fastifyなどが採用 |
Zod自体も、バンドルサイズを気にする場合向けに zod/mini を同梱しています。
実務で効く小さなパターン
1. 環境変数は起動時に1回だけ
変換の章のとおり、env.ts で parse して、アプリからはその戻り値だけを使います。process.env を直接読む場所をなくすのが目的です。
2. スキーマから選択肢を作る
const status = z.enum(['draft', 'published']);
// 画面のセレクトボックスは status.options から生成する選択肢の定義がスキーマ1か所になり、画面と検証がずれません。
3. エラーメッセージは書くときに添える
後からまとめて日本語化しようとすると、必ず漏れます。.min(1, 'メッセージ') を書く癖をつけるのが結局いちばん早いです。
4. 検証結果は必ず戻り値を使う
.trim() や .default() の結果は戻り値にしか反映されません。result.data を使ってください。
よくあるハマりどころ(総まとめ)
| 症状 | 原因 |
|---|---|
| 必須項目が空文字で通る | z.string() は "" を通す。.trim().min(1) にする |
| 送った項目が保存されない | z.object() が未知のキーを黙って捨てている |
.optional() なのに null で落ちる | null は .nullable() の担当。両方なら .nullish() |
フォームの空欄が 0 になる | z.coerce.number() が "" を 0 にする |
FLAG=false が効かない | z.coerce.boolean() は "false" を true にする。z.stringbool() を使う |
| 日本語にしたのに英語のまま | v3の required_error などは無視される |
| エラーが画面に出ない | refine に path を指定していない |
| 検証したのに変換が効かない | 戻り値ではなく元の変数を使っている |
ちゃんと使うためのポイント
- スキーマは境界ごとに用意し、共通部分から
.omit()/.partial()で派生させる - 境界で1回止めたら、その先は普通の型で扱う。内部まで検証で埋めない
- 検証コストは推測せず測る。まず「使う項目だけ書く」のが効く
- エラーメッセージは後回しにせず、書くときに添える
次の章は、本書の内容を引ける形に圧縮した Zodチートシート です。作業中に開く前提でまとめています。
参考リンク
- Zod 公式ドキュメント — APIリファレンス(英語)
- Standard Schema — バリデーションライブラリ共通仕様の公式サイト(英語)