JSONPath クエリ言語の実践
要点:2024 年、JSONPath はついに正式な標準 RFC 9535 を得ました。本記事では構文、フィルタの意味論、関数、実装間の歴史的な違いを解説します。

要点:まず明示的なパスで位置を特定し、その上にフィルタを重ね、式は RFC 9535 に従って書きましょう。固定の単一フィールドにはプロパティアクセスか JSON Pointer を使い、集計には jq かコードを使います。
$.store.book[?(@.price < 10)].titleのような式を見たことがあるでしょう。しかし国際標準になったのは 2024 年のことです。
1. 事実上の標準から正式標準へ
JSONPath は 2007 年に Stefan Gössner が提唱し、長い間、唯一の「仕様」は一本のブログ記事でした。各言語の実装はそれぞれ勝手に振る舞っていました。
- フィルタに括弧が必要か(
?(@.a)か?@.aか)。 - 配列インデックス
[-1]をサポートするか。 - 結果は「値」を返すのか、「値とパス」を返すのか。
- スクリプト式(
?(@.price < $.max))を許すか。
2024 年 2 月に公開された RFC 9535 は、これらの差異を単一の構文と意味論に収束させ、公式の適合テストスイートを提供しました。 新規プロジェクトは RFC 9535 に従うべきです。古いプロジェクトを移行するときは、まず実装のバージョンを確認してください。
2. 構文チートシート
| 構文 | 意味 | 例 |
|---|---|---|
$ |
ルートノード | $.store |
@ |
フィルタ内の現在ノード | $[?@.price < 10] |
.name |
子ノード(名前は [A-Za-z_][A-Za-z0-9_]* に一致) |
$.store.book |
['name'] |
子ノード(任意の名前。非 ASCII やドットを含む) | $['store']['书'] |
[0] |
配列インデックス。負数は末尾から数える | $.book[0]、$.book[-1] |
[0,2] |
複数のインデックス / 複数の名前 | $.book[0,2] |
[1:3] |
スライス。開始を含み、終了を含まない | $.book[1:3] |
[::2] |
ステップ付きスライス | $.book[::2] |
[*] |
すべての子ノードのワイルドカード | $.book[*].title |
..name |
再帰降下。任意の深さを探索 | $..price |
..[*] |
すべてのノードを再帰的に取得 | $..* |
[?<expr>] |
フィルタセレクタ | $.book[?@.price < 10] |
必ず覚えておくべき意味論が一つあります。JSONPath は単一の値ではなく、ノード集合(nodelist)を返します。 一致するノードが一つだけでも、結果は長さ 1 のリストです。
3. 一つのサンプルで構文を通す
{
"store": {
"book": [
{ "title": "JSON 徹底ガイド", "price": 8.95, "tags": ["json", "spec"] },
{ "title": "データ交換フォーマット史", "price": 12.99, "tags": ["history"] },
{ "title": "パーサー実装の原理", "price": 8.99, "tags": ["parser", "json"] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
| 式 | 結果 |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 つの価格(自転車の価格を含む) |
$.store.book[*].title |
3 つのタイトル |
$.store.book[?@.price < 10].title |
1 冊目と 3 冊目のタイトル |
$.store.book[?@.tags] |
tags を持つすべての本(存在テスト) |
$.store.book[-1].title |
最後の本のタイトル |
$..book[0,1] |
最初の 2 冊 |
$.store.book[?@.price < $.expensive] |
文法的に実装依存。RFC 9535 はルートをまたぐ参照を許さない |
最後の行は歴史的な罠です。多くの実装はフィルタ内で $.expensive を許してノード間比較を行いますが、RFC 9535 は明示的に禁止しています。
フィルタは現在ノード @ にしかアクセスできません。グローバルなしきい値と比較する必要があるときは、そのしきい値をクエリ関数の引数として渡してください。
4. フィルタで表現できること
RFC 9535 のフィルタ式は四種類の構文をサポートします。
論理: && || ! 例: ?@.price < 10 && @.tags
比較: == != < <= > >=
存在: 裸のパス 例: ?@.tags(存在をテスト)
関数: length() count() match() search() value()
実践的な例をいくつか。
// 価格が 8 から 10 の間
$.store.book[?@.price >= 8 && @.price <= 10]
// タイトルに "JSON" を含む(正規表現一致。第 2 引数は I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// タグが複数
$.store.book[?length(@.tags) > 1]
// リストではなく最初の一致値を取得する(value() は単一ノード集合を値に変える)
$.store.book[?@.price < 9].title.value()
match() は文字列全体の一致、search() は部分文字列の探索です。基底の正規表現方言は I-Regexp(RFC 9485 のサブセット)で、
後方参照も先読み・後読みもサポートしません。これは制限であると同時に、正規表現 DoS に対する便利な盾でもあります。
5. 実装差:移行前に確認する
| 実装 | 言語 | RFC 9535 対応 | 備考 |
|---|---|---|---|
jsonpath-plus |
JavaScript | ほぼ互換 | 拡張関数とスクリプト式をサポート。高機能だが厳密な標準ではない |
jsonpath(dchester) |
JavaScript | 部分的 | 長く続くライブラリで ?() 構文と結果構造が異なる |
ojg / gjson |
Go | ojg は良好 |
gjson は別のパス構文で、JSONPath ではない |
jsonpath-ng |
Python | 部分的 | 拡張をサポートし、かなり寛容 |
Jayway JsonPath |
Java | 部分的 | エコシステムが広いが、歴史的な振る舞いが RFC と異なる |
移行前の三つの確認:
- フィルタの括弧が必須か。
-1のような負のインデックスをサポートするか。- 結果が「値の配列」か、「
{path, value}オブジェクトの配列」か。
三番目が最もつまずきやすいです。フロントエンドが result[0] を読んでいるのに、ライブラリを替えたらオブジェクトを受け取り、ページが真っ白になります。
6. 性能とセキュリティ
性能
- JSONPath は本質的に一回の走査で、計算量は O(ノード数) です。インデックスは使えません。
..の再帰降下が最も高コストです。$.store.bookで先に位置を特定できるなら、$..bookと書かないでください。- 式は一度コンパイルして再利用します(多くのライブラリが
compileやキャッシュを提供)。クエリのたびに再パースしないでください。 - ノード数が 10 万を超える文書では、まず JSON Pointer で正確に抽出するか、サーバー側で
jqによる前処理を検討します。
セキュリティ
- ユーザー入力由来の式には、長さとネストの深さの制限が必要です。
- JSONPath に認可を任せないでください。読み取りしかできず、「どのフィールドが見えないか」を表現できません。
match()が使う正規表現がユーザー由来なら、複雑さを制限します。PCRE より安全ではありますが、長さの上限は依然として推奨されます。
7. JSONPath を使うべきでないとき
- 固定の単一フィールドだけが必要 →
data.store.bicycle.colorを直接使う。 - 変更後に書き戻す必要がある → JSON Pointer かパッチを使う。JSONPath は書き戻さない。
- 集計(合計、グループ化)が必要 →
jq、SQL、コードを使う。JSONPath は選択しかできない。 - 文書が巨大で、必要なフィールドがごく少数 → ツリー全体をメモリに読むのではなく、ストリーミングパーサーを使う。
8. チェックリスト
- [ ] 式が RFC 9535 に従い、ルートをまたぐ参照に依存していない。
- [ ] 結果がノード集合であることを理解し、単一値は
[0]かvalue()で取り出す。 - [ ] 式を事前コンパイルして再利用している。
- [ ] ユーザー制御の式に長さと複雑さの制限がある。
- [ ] 実装を切り替えるときに公式の適合テストケースを実行する。
FAQ
JSON Pointer と JSONPath はどう選べばよいですか?
固定の単一フィールドだけが必要なら、素のプロパティアクセスか JSON Pointer を使います。フィルタ、ワイルドカード、再帰降下が必要なら JSONPath に手を伸ばします。JSONPath は選択だけで書き戻さないので、書き戻しは JSON Pointer かパッチの担当です。
フィルタが $.expensive を参照できないのはなぜですか?
RFC 9535 はルートをまたぐ参照を明示的に禁止し、フィルタは現在ノード @ にしかアクセスできません。代わりにしきい値をクエリ関数の引数として渡してください。古い実装はこの構文を受け付けますが、ライブラリを切り替えたり更新したりすると壊れます。
値の読み取りに result[0] を当てにできますか?
現在の実装が値の配列を返すことを確認した後だけです。一部のライブラリは {path, value} オブジェクトを返すので、result[0] がオブジェクトになりページが真っ白になります。単一値は [0] か value() で読み、実装を切り替える前に公式の適合スイートを実行してください。