クエリと検証 FAQ:JSONPath と Schema
要点:パスクエリは JSONPath か Pointer か、Patch と Merge Patch の選び方、Schema が止められるもの、ドラフト版の選び方。一度に解説します。

要点:条件でノードを見つけるには JSONPath、正確な位置を指定・編集するには JSON Pointer か JSON Patch、設定をマージするには Merge Patch を使いましょう。新しい Schema は 2020-12 に固定し、allErrors を有効にしてすべての失敗を一度に返します。
クエリは「見つける」、検証は「正しいと確認する」です。それぞれ独自の標準があるので、混ぜないでください。
1. パスクエリ
JSONPath と JSON Pointer はどう選べばよいですか?
「条件で探す」のか「位置で取得する」のかに依存します。
| 観点 | JSONPath | JSON Pointer |
|---|---|---|
| 標準 | RFC 9535(2024) | RFC 6901(2013) |
| 式 | $.users[?(@.age > 18)].id |
/users/0/id |
| ワイルドカードと再帰 | *、.. に対応 |
非対応 |
| 条件フィルタ | 対応 | 非対応 |
| 結果 | ノード集合(複数のことも) | 単一の位置 |
| 表現力 | 強いが、実装差の重い遺産を持つ | 弱いが完全に決定的 |
| 典型的な用途 | ログ抽出、デバッグ、データ探索 | JSON Patch、設定の位置指定、エラーパス |
選択規則は単純です。
- 「条件に一致するすべてのノードを取得」→ JSONPath。
- 「正確な位置を一つ固定し、追加・削除・変更」→ JSON Pointer。
- 併用する:JSONPath でノードを見つけ、Pointer でその位置を記述する。
API 契約の位置指定に JSONPath を使わないでください。JSONPath は集合を返し、空・単一要素・複数要素の三つの分岐が プロトコルを複雑にします。Pointer は「既存の位置を指すか、存在しないか」で、契約に向きます。
$.. と $.* の違いは?
$.. は再帰降下で全階層を歩き、$.* は現在の階層の直接の子だけを返します。
{
"a": { "id": 1, "child": { "id": 2 } },
"b": { "id": 3 }
}
$.*→ 二つのオブジェクト{id:1, child:{...}}と{id:3}。$..id→ 三つの値1、2、3。$.*.id→1と3。ネストした2は含まない。
再帰降下は便利ですが高コストです。$.. はツリー全体を歩き、大きな文書では O(n) で、意図しない深いフィールドを簡単に掬い上げます。
具体的な階層で動くなら .. を避けてください。
フィルタは文字列を比較できますか?正規表現で一致できますか?
文字列比較は動きます。正規表現は実装に依存します。
標準構文(RFC 9535):
$.items[?(@.status == "active")]
$.items[?(@.price > 10 && @.stock > 0)]
$.items[?(@.name == $.target)] // 別のノードと比較
文字列比較は二重引用符を使います(RFC 9535 では単一引用符も許されますが、一部の古い実装は二重引用符しか受け付けません)。
数値比較は数値の意味論に従います。文字列と数値の混在、たとえば "10" > 9 は標準では未定義の振る舞いなので、当てにしないでください。
正規表現については、RFC 9535 が match() と search() 関数を定義しますが、実装間の対応差はかなり大きいです。
安全な方法:JSONPath では粗いフィルタだけを行い、結果を取得した後にコードで正規表現照合を行います。
JSON Patch と Merge Patch のどちらを使うべきですか?
どちらも「どう変えるか」を記述しますが、表現力と意味論はまったく異なります。
| 観点 | JSON Patch(RFC 6902) | Merge Patch(RFC 7386) |
|---|---|---|
| 形 | 操作の配列 [{"op":"add",...}] |
部分オブジェクト |
| 対応操作 | add / remove / replace / move / copy / test | マージのみ(削除は null 経由) |
| 配列要素を指定できるか | はい。インデックスで | いいえ。配列全体が置換される |
| 原子性 | test が楽観的ロックを可能に |
なし |
| 文書サイズ | 大きいが正確 | 小さいが表現力が限られる |
| 向く場面 | 細かい編集、API の部分更新 | 設定のマージ、フォームの部分送信 |
判断基準:
- 配列の 3 番目の要素のフィールドを変える → JSON Patch。
- 複数の設定項目を既存設定にマージする → Merge Patch。
- 「元の値が X でなければ失敗」という並行性制御 → JSON Patch の
test。
Merge Patch の null 意味論の罠に注意してください。null は「このキーを削除」であり、「値を null に設定」ではありません。
値を null に設定することは Merge Patch では不可能です。
2. Schema 検証
JSON Schema はどんな問題を解きますか?
「データがどう見えるべきか」を口頭の合意から機械が検査できる契約に変えます。実際に止められるエラー:
- 必須フィールドの欠落(
required)。 - 型違い。数値が期待される場所の文字列など(
type)。 - 範囲外の値(
minimum/maximum/minLength)。 - 許可集合の外の値(
enum)。 - 配列要素の型の不整合や誤った配列長(
items/minItems)。 - ネストした構造のフィールド欠落(
propertiesを通じた再帰検証)。 - 余分なフィールド(
additionalProperties: false)。
解けないもの:業務規則(「終了時刻は開始時刻より後でなければならない」)、フィールドをまたぐ条件(一部は if/then で表現できるが
可読性が損なわれる)、データベースの一意性、権限チェック。
ですから Schema は「データの形」の門番であり、業務ロジックの代替ではありません。
draft-07 と 2020-12 のどちらを使うべきですか?
新規プロジェクトには 2020-12 を使い、既存システムではバリデータの対応を確認します。
| バージョン | 状態 | 主な変更 |
|---|---|---|
| draft-04 | 古い | 多くの古いライブラリがなお既定 |
| draft-07 | 事実上の標準 | 最も広い対応、最も豊かなエコシステム |
| 2019-09 | 過渡的 | $defs、unevaluatedProperties を導入 |
| 2020-12 | 現在推奨 | items が prefixItems + items に分割、$dynamicRef 対応 |
移行の二つの罠:
- タプル構文の変更:draft-07 では配列値の
itemsがタプルを表現しました。2020-12 ではそれをprefixItemsに移しました。 $refと兄弟キーワード:draft-07 では$refがあると兄弟キーワードが無視され、2020-12 では共存を許します。
バリデータが draft-07 しか対応していないのに 2020-12 構文を書くと、静かに無視されます。最も危険なのは「何も検証されていないのに検証が通る」場合です。
Schema から TypeScript 型を生成するには?
二つの経路:Schema から型を導出するか、型から Schema を生成するか。
Schema から型へ(バックエンドが契約を定義し、フロントエンドが消費する):
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
age: { type: 'integer' },
},
required: ['id'],
additionalProperties: false,
} as const;
type User = FromSchema<typeof userSchema>;
// { id: string; age?: number }
鍵は as const です。なければ type: 'string' がリテラル 'string' ではなく string と推論され、推論が壊れます。
型から Schema へ(フロントエンドにすでに型があり、実行時検証が必要):
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const userSchema = z.object({ id: z.string(), age: z.number().int().optional() });
const jsonSchema = zodToJsonSchema(userSchema);
この経路の利点は「一つの定義が型と実行時検証の両方を生む」こと。コストは DSL を持ち込むことと、
生成された Schema が実装詳細(たとえば見栄えのしない $ref 構造)を運びうることです。
検証が失敗したとき、特定のフィールドをどう特定しますか?
バリデータが返す instancePath(2020-12)か dataPath(draft-07)を見ます。エラー位置への JSON Pointer です。
[
{
"instancePath": "/items/2/price",
"schemaPath": "#/properties/items/items/properties/price/type",
"keyword": "type",
"message": "must be number"
}
]
instancePath: "/items/2/price" は直接教えます。items 配列の 3 番目の要素の price フィールドの型が誤りです。
実践的な二つの提案:
- すべてのエラーを一度に返す。最初の一つで投げるのではなく。フロントエンドが一度にすべて直す方が、往復より速いです。
- Pointer を平易な言葉に変えてからエンドユーザーに見せる。
/items/2/priceは開発者には有用で、ユーザーには無用です。
allErrors: true が多くのバリデータのスイッチです(Ajv は明示的に有効にする必要があります)。既定は fail-fast モードのことが多いです。
additionalProperties: false を有効にすべきですか?
内部契約では有効にし、公開 API では慎重に。
有効にすると、properties で宣言されていないフィールドが検証に失敗します。利点は綴り間違い
(userNmae)や予期しないパススルーの検出。欠点は、上流のフィールド追加が下流の消費者を壊し、後方互換性を破壊することです。
妥協:
| 場面 | 推奨 |
|---|---|
| 内部サービス間 | 有効。フィールド変更はバージョン管理を通す |
| 公開外部 API | 無効。上流のフィールド追加を許す |
| サードパーティデータの消費 | 無効。相手側を制御できない |
| 設定検証 | 有効。設定の綴り間違いは即座に失敗すべき |
配列要素が一意であることを検証するには?
二つの異なる意味論を混同しないでください。
{ "uniqueItems": true }
これは要素全体の重複排除で、配列に深く等しい要素が二つないことを要求します。
{
"type": "array",
"items": { "type": "object", "properties": { "id": { "type": "string" } } },
"uniqueItems": true
}
上の断片はオブジェクト全体が重複しないことだけを保証し、{"id":"a","name":"x"} と {"id":"a","name":"y"} は重複に数えません。
特定のフィールドで重複排除することを Schema は表現できず、業務層で検証する必要があります。
const ids = items.map((item) => item.id);
if (new Set(ids).size !== ids.length) {
throw new Error('id が重複しています');
}
format は実際にメールや日付を検証しますか?
必ずしもそうではありません。仕様では format は既定で「注釈」であり「表明」ではないので、バリデータは検証せずに収集するだけかもしれません。
- Ajv は別の
ajv-formatsパッケージをインストールして設定する必要があり、さもないとformat: "email"は事実上無効です。 - 一部のバリデータは
formatを既定で有効な表明として扱うので、振る舞いが不統一です。 - 有効にしても
emailの正規表現は非常に緩く、a@bが通ります。
結論:format を強い検証として扱わないでください。メール、URL、日付の厳格な検証は業務コードで行います。
Schema に format を残してドキュメントとするのは価値がありますが、汚れたデータを止めると期待しないでください。
3. クイックリファレンス
| 質問 | 結論 |
|---|---|
| 条件でノードを探す | JSONPath |
| 正確な位置指定、パッチ適用 | JSON Pointer |
| 配列要素を変更 | JSON Patch。Merge Patch にはできない |
| 設定のマージ | Merge Patch |
| 新規プロジェクトの Schema バージョン | 2020-12。prefixItems に注意 |
| Schema から TS 型を生成 | json-schema-to-ts + as const |
| 検証エラーの特定 | instancePath を読み、allErrors を有効にする |
| フィールドで重複排除 | Schema にはできない。業務層で Set を使う |
format: email |
ほとんどのバリデータは既定で検査しないので当てにしない |