z.objectでオブジェクトを検証する — 未知のキーの扱い
この章の目次開く
実務で検証する対象は、単体の文字列よりもオブジェクトであることがほとんどです。APIのリクエストボディ、フォームの送信値、設定ファイル——どれもキーと値の集まりです。
z.object の基本
構文: z.object(shape)
| 引数 | 渡せるもの | 説明 |
|---|---|---|
shape | キーとスキーマのオブジェクト | どのキーにどのスキーマを適用するか |
戻り値: オブジェクトスキーマ
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.email(),
});値にスキーマを書くだけなので、そのまま入れ子にできます。
const postSchema = z.object({
title: z.string(),
author: z.object({
name: z.string(),
}),
tags: z.array(z.string()),
});オブジェクト以外を渡すと弾かれます。null は expected object, received null、配列は expected object, received array というメッセージになります。JavaScriptでは typeof null === 'object' ですが、Zodは区別してくれます。
定義していないキーは黙って捨てられる
ここが最初につまずくところです。スキーマに書いていないキーが入力に含まれていても、エラーにはなりません。結果から静かに取り除かれます。
const schema = z.object({ id: z.number(), name: z.string() });
schema.parse({ id: 1, name: 'A', extra: 'x' });
// { id: 1, name: 'A' } — extra は消えている「送ったはずのフィールドが保存されていない」という不具合の犯人は、たいていこの挙動です。スキーマにキーを足し忘れると、リクエストには乗っているのに検証後のデータからは消えてしまいます。
学習者エラーにしてくれた方が気づけるのに、どうして黙って捨てるの?
先生安全側に倒しているんだ。知らないキーをそのまま通すと、想定外の値がDBやログに流れ込む。捨てておけば「スキーマに書いたものだけ」が下流に届く、と保証できる。エラーにしたいなら、次の表のように書き方を変えればいい。
4つの書き方
| 書き方 | 余分なキーがあると |
|---|---|
z.object({...}) | 黙って取り除く(デフォルト) |
z.strictObject({...}) | エラーにする(Unrecognized key: "x") |
z.looseObject({...}) | そのまま通す |
z.object({...}).catchall(スキーマ) | 余分なキーの値を指定スキーマで検証する |
z.strictObject({ id: z.number() }).safeParse({ id: 1, x: 2 });
// エラー: Unrecognized key: "x"
z.looseObject({ id: z.number() }).parse({ id: 1, x: 2 });
// { id: 1, x: 2 } — x も残る
z.object({ id: z.number() }).catchall(z.string()).parse({ id: 1, x: 'ok' });
// { id: 1, x: 'ok' } — 追加キーは文字列であることを要求する使い分けの目安です。
- 設定ファイルやCLIの引数 —
strictObject。書き間違えたキーを黙って無視されると、設定が効かない理由が分からなくなる - APIレスポンス — デフォルトの
object。サーバー側がフィールドを追加しても壊れない - フォームやリクエストボディ — デフォルトの
object。余計な値を下流に流さない

エラーはどの項目で起きたか分かる
オブジェクトを検証すると、issues の各要素に path が入ります。どのキーで失敗したかを示す配列です。
const schema = z.object({
user: z.object({ name: z.string() }),
tags: z.array(z.string()),
});
schema.safeParse({ user: { name: 1 }, tags: ['a'] }).error.issues[0].path;
// ['user', 'name'] — ネストした位置も分かる
schema.safeParse({ user: { name: 'a' }, tags: ['a', 2] }).error.issues[0].path;
// ['tags', 1] — 配列の場合はインデックスが入るそして重要なのが、最初のエラーで止まらないことです。
schema.safeParse({ user: { name: 1 }, tags: [2] }).error.issues.length;
// 2 — 両方のエラーが集まる全項目を検証してから、失敗した項目をまとめて返します。だからフォームで「5個の入力ミスを一度に表示する」ことができます。1件ずつ指摘して5回送信させる、という体験にならずに済むのはこの仕組みのおかげです。
スキーマを組み立てる
一度書いたオブジェクトスキーマは、部品として使い回せます。
| メソッド | 引数 | 何をするか |
|---|---|---|
.extend(shape) | 追加するキーとスキーマ | キーを足した新しいスキーマ |
.pick({ キー: true }) | 残すキー | 指定したキーだけのスキーマ |
.omit({ キー: true }) | 除くキー | 指定したキーを外したスキーマ |
.partial() | なし | 全キーを optional にする |
.required() | なし | 全キーを必須に戻す |
.keyof() | なし | キー名のenumスキーマ |
.shape | (プロパティ) | 個々のキーのスキーマを取り出す |
戻り値: .shape 以外はすべて新しいスキーマ。元のスキーマは変わらない
実際に効くのは、同じリソースに対して用途別のスキーマを作る場面です。
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.email(),
});
// 新規作成: idはサーバーが採番するので受け取らない
const createUserSchema = userSchema.omit({ id: true });
// 部分更新: 送られてきた項目だけ更新する
const updateUserSchema = createUserSchema.partial();userSchema を1か所直せば、作成用も更新用も追従します。項目が増えるたびに3つのスキーマを手で直す、という状態を避けられます。
別のスキーマと合成したいときは、.shape を渡します。
const timestamps = z.object({ createdAt: z.iso.datetime() });
const userWithTime = userSchema.extend(timestamps.shape);よくあるハマりどころ
送ったはずの項目が消える
前述のデフォルト挙動です。検証後のデータに項目が無いときは、まずスキーマにそのキーを書いたかを確認してください。
.partial() を使うと空オブジェクトが通る
.partial() は全キーを任意にするため、{} が検証を通ります。部分更新のAPIでは「1項目以上必須」を別途チェックする必要があります。
updateUserSchema.safeParse({}).success; // true — 何も更新しない更新リクエスト配列を渡したのにobjectエラーが出る
z.object() は配列を受け付けません。配列を検証したいなら z.array() を使います。z.array(userSchema) のように、オブジェクトスキーマを中に入れる形になります。
ちゃんと使うためのポイント
-
z.object()は定義していないキーをエラーにせず、黙って取り除く - 厳しくしたいなら
strictObject、通したいならlooseObject strictObjectを外部APIのレスポンスに使うと、相手の項目追加で壊れる- エラーは
pathでどのキーかが分かり、全項目分がまとめて返る .omit()や.partial()で、1つのスキーマから作成用・更新用を派生させる
ここまでで、単体の値からオブジェクトまで検証できるようになりました。値が「通るか通らないか」の基準に迷ったときは、デフォルト挙動の早見表に戻ってきてください。
参考リンク
- Objects - Zod — オブジェクトスキーマの公式リファレンス(英語)