JSONPath 構文ガイド:JSON をクエリ・フィルターする実例集
JSONPath は JSON のためのクエリ言語であり、XPath が XML のクエリ言語であるのとちょうど同じ関係にある。パス式を書けば、評価器がマッチするすべての値を返してくれる。bookstore ドキュメントから著者名をすべて取り出したいなら、$.store.book[*].author と書く。これだけで著者の一覧が返ってきて、走査コードを書く必要はない。
このガイドでは、JSONPath 構文のすべてのセレクターを、読みながら実行できるコピペ可能な例とともに解説していく。先に一つだけはっきりさせておきたいのは、方言が二つあるという点だ。2007 年の Goessner 方言が事実上の古典であり、RFC 9535 は 2024 年 2 月に公開された IETF の正式な標準である。両者は一般的なパスでは一致するが、エッジケースで食い違うので、このガイドではその違いが出てくるたびに指摘していく。以下のすべての式は JSONPath Tester で試せるし、両エンジンを切り替えて比較することもできる。
まずはセレクターのチートシートから始めよう。記事の残りでは、一つの共有 JSON ドキュメントに対して各行を実例つきで展開していく。
| セレクター | 意味 | 例 |
|---|---|---|
$ | ドキュメントのルート | $ |
@ | 現在の要素(フィルター内) | [?(@.price < 10)] |
.name / ['name'] | 子メンバー | $.store.book |
.. | 再帰下降 | $..author |
* | すべての要素/メンバー | $.store.book[*] |
[0] | 配列インデックス | $.store.book[0] |
[start:end:step] | 配列スライス(半開区間) | $.store.book[0:2] |
[a,b] | 名前/インデックスの和集合 | $.store.book[0,2] |
[?()] | フィルター式 | $.store.book[?(@.price < 10)] |
length() count() match() search() value() | RFC 9535 関数(フィルター内のみ) | [?length(@.title) > 15] |
JSONPath とは何か
JSONPath は、JSON ドキュメントからノードを選び出すための宣言的なクエリ言語だ。オブジェクトや配列を歩き回るループを書く代わりに、欲しい場所をパスで記述すれば、評価器がマッチする値を返してくれる。メンタルモデルは、XPath が XML に対して与えてくれるものと同じ。構造をたどっていくセレクターから成るパスである。
これは開発者が JSON に触れるあらゆる場面に登場する。API レスポンスから 1 つのフィールドを取り出すとき、結合テストで値をアサートするとき、Kubernetes・AWS Step Functions・Azure Logic Apps のパイプライン設定でフィールドを指定するとき、そして大きく不規則な JSON から走査ロジックを手書きせずにデータを抽出するときに使う。
歴史について少し触れておくと、これが方言の分裂を説明してくれる。Stefan Goessner が 2007 年に JSONPath を提案した。急速に広まって事実上の標準になったが、正式に仕様化されることはなかった。そのため実装ごとに細部がばらついていった。IETF は 2024 年 2 月に RFC 9535、すなわち初めての正式な JSONPath 仕様でそのギャップを埋めた。今日では両方の方言が生きている。だからこそ、同じ式でもどのライブラリが実行するかによって挙動が変わりうるのだ。
クエリを始める前に、構造を読み解いておくと役立つ。乱れた入力は JSON Formatter で整形すれば、ネストが見えるようになる。
サンプルドキュメント
以下のすべての例は、古典的な Goessner の bookstore JSON に対して実行する。一度貼り付けて使い回そう。
{
"store": {
"book": [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
{ "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
],
"bicycle": { "color": "red", "price": 19.95 }
}
}
title・author・price を持つ 4 冊の本に、自転車が 1 台。これを覚えておこう。価格は 8.95、12.99、8.99、22.99 で、これが後のフィルター結果を左右する。
ルート、子、再帰下降($ . ..)
あらゆる式はルートから始まり、これは $ と書く。そこからドット記法またはブラケット記法で子に入っていく。この二つは等価だ。
$.store.book → the book array
$['store']['book'] → identical result
ブラケット記法が必要になるのは、キーにスペース・ドット・その他の特殊文字が含まれる場合だ。$['first name'] は機能するが、$.first name は機能しない。
.. 演算子は再帰下降である。直接の子だけでなくドキュメントのあらゆる階層を検索し、続くセレクターにマッチするものをどの深さからでも集めてくる。
$..author
→ ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
.. を使うべきときと、フルパスを書くべきとき
再帰下降は便利だが大雑把でもある。$..price はツリー内のどこにある price にもマッチする。store.bicycle.price も含まれてしまうが、それは意図していなかったかもしれない。構造が分かっているなら、クエリを正確に保つためにパスを明示的に書こう。
$..price → [8.95, 12.99, 8.99, 22.99, 19.95] (includes the bicycle)
$.store.book[*].price → [8.95, 12.99, 8.99, 22.99] (only books)
.. は本当に不規則だったり未知だったりする構造のために取っておく。トレードオフは便利さと制御性の間にある。データについて知っていることが多いほど、明示的なパスを選ぶべきだ。
ワイルドカード、インデックス、配列スライス(* [0] [start:end:step])
ワイルドカード * は、配列のすべての要素またはオブジェクトのすべてのメンバーを選択する。
$.store.book[*].title
→ ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]
配列インデックスは 0 始まりで、負のインデックスは末尾から数える。
$.store.book[0].title → ["Sayings of the Century"]
$.store.book[-1].title → ["The Lord of the Rings"]
スライスは Python や JavaScript と同じ半開区間の [start:end:step] 規約を使う。start は含まれ、end は除外される。
$.store.book[0:2].title → ["Sayings of the Century", "Sword of Honour"]
これは 3 冊ではなく2 冊を返す。インデックス 0 と 1 で、インデックス 2 の手前で止まる。終端を含めないこの境界は、JSONPath で最もよくあるバグなので、頭に焼き付けておく価値がある。
[0:2] → first TWO elements (indices 0, 1) ← correct
[0:3] → first THREE elements (indices 0, 1, 2)
境界を省略すれば端まで走り、N 個ごとに 1 つ取りたいなら step を加える。
$.store.book[2:].title → ["Moby Dick", "The Lord of the Rings"]
$.store.book[:3].title → first three titles
$.store.book[::2].title → ["Sayings of the Century", "Moby Dick"] (every other)
フィルター式 [?()]
フィルターこそ JSONPath が本領を発揮するところだ。フィルター [?()] は述語が真である要素だけを残し、フィルター内では @ がテスト対象の現在の要素を指す。
10 より安い本を選ぶには次のように書く。
$.store.book[?(@.price < 10)].title
→ ["Sayings of the Century", "Moby Dick"]
bookstore の価格(8.95、12.99、8.99、22.99)に照らすと、2 冊が条件を満たす。フィルターの述語を一歩ずつ組み立てる方法は次のとおりだ。
- リテラルと比較する。
==、!=、<、<=、>、>=を使う。たとえば@.price > 10。 - 文字列をマッチする。 文字列リテラルはシングルクォートを使う。
@.author == 'Nigel Rees'。 - 存在をテストする。 メンバー参照を裸で書くと、それを持つ要素を選ぶ。
[?(@.isbn)]はisbnを持つ本だけを残す。 - 条件を組み合わせる。
&&と||で述語をつなぐ。[?(@.price < 10 && @.author == 'Herman Melville')]。
最も頻繁なフィルターのミスは @ のスコープだ。述語の内側では、現在の要素は @ であって $ ではない。$.price と書くとドキュメントのルートを指してしまい、テスト対象の本を指さない。
$.store.book[?($.price < 10)] → wrong scope, matches nothing useful
$.store.book[?(@.price < 10)] → correct: each book's own price
フィルターにおける RFC 9535 と Classic の違い
二つの方言は空白とクォートの扱いで分かれる。Classic は寛容で、空白なしの [?(@.price<10)] も問題なくパースされる。RFC 9535 はその文法に厳密に従い、フィルターの書き方についてより厳格だ。どこかで動いていたフィルターが失敗したら、空白とエンジンを確認しよう。フィルターをきれいに保てば(演算子の前後に空白、文字列はシングルクォート)、最終的にどのライブラリが実行しても同じように評価される。
和集合セレクター:複数のキーを一度に選ぶ([a,b])
和集合セレクターは、一つのブラケット内に複数の名前またはインデックスを並べ、それらをすべて集める。
$.store.book[0]['title','author']
→ ["Sayings of the Century", "Nigel Rees"]
和集合はインデックスでも機能し、固定的な射影のために他のセレクターと組み合わせることもできる。
$.store.book[0,2].title → ["Sayings of the Century", "Moby Dick"]
$.store.book[*]['title','price'] → title and price of every book
オブジェクト全体やワイルドカードの一掃ではなく、いくつかの特定のフィールドだけが欲しいときには、和集合が適切な道具だ。
RFC 9535 関数:length, count, match, search, value
RFC 9535 は 5 つの標準的な関数拡張を定義している。ほとんどの人がつまずき、競合するガイドが絶えず間違えるルールがこれだ。
これらの関数はフィルター
[?...]の内側でのみ呼び出せる。独立したパスセグメントとしては決して呼べない。
$.store.book.length() と書くのは有効な RFC 9535 ではなく、標準の文法はこれを拒否する。そのセグメント呼び出しの形式は jsonpath-plus の拡張であって、仕様の一部ではない。長さでフィルターするには、述語の内側で関数を呼ぶ。
$.store.book[?length(@.title) > 15]
→ [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
]
選ばれた 2 つの title はどちらも 15 文字より長い。「Moby Dick」(9)と「Sword of Honour」(15 で、15 を超えてはいない)は除外される。
各関数がフィルター内で何をするかは次のとおりだ。
length()— 文字列・配列・オブジェクトの長さ:[?length(@.title) > 15]count()— nodelist 内のノード数:[?(count(@.authors) > 1)]match()— 文字列全体の正規表現テスト(I-Regexp パターン):[?match(@.author, 'J.*')]search()— 部分文字列の正規表現テスト:[?search(@.title, 'the')]value()— 単一ノードの nodelist を比較のためにその値へ変換する
5 つすべてが RFC 9535 の機能だ。Classic(Goessner)方言はこれらを実装していないので、関数ベースの式が失敗したら、フィルターの内側で呼んでいるか、そしてエンジンが RFC 9535 に設定されているかを確認しよう。
RFC 9535 と Classic Goessner:なぜ同じ式で結果が違うのか
JSONPath 式が二つのツールで異なる結果を返すとき、たいていは方言が原因だ。両者を比較するとこうなる。
| 観点 | Classic Goessner (2007) | RFC 9535 (2024) |
|---|---|---|
| 標準化 | 事実上の標準だが正式化されず | 初めての正式な IETF 仕様 |
| フィルターの空白/クォート | 寛容([?(@.price<10)] でも可) | 厳格、文法に正確に従う |
| 欠落メンバーの比較 | 実装依存 | 明確に定義、例外を投げない |
| 標準関数 | 方言の一部ではない | length count match search value |
| 正規化パス | 正準形なし | 正準のシングルクォート・ブラケット形式 |
| 和集合の順序 | ライブラリによって異なる | 規定済み |
実践的なアドバイス。下流のシステムが RFC 9535 準拠をうたっているなら、標準エンジンに対して書いて検証しよう。jsonpath.com・jsonpath-plus・Jayway ベースのサービスからコピーした式を保守しているなら、結果を再現するために Classic を使おう。JSONPath Tester は一つのトグルの背後で両エンジンを動かすので、式を一度貼り付けるだけで、各方言がそれをどう扱うかを並べて確認できる。この二重エンジン比較が、食い違いを診断する最速の方法だ。
JSONPath と XPath と jq:どれを使うか
この三つはよく混同されるので、手短にまとめておく。
- JSONPath は JSON のための宣言的なパスクエリだ。コードを書かずに場所を名指ししたい設定やテストアサーションに埋め込むのに最も向いている。
- XPath は XML 世界の対応物だ。JSONPath はその記法の一部(
*、..、[])を借用しており、だからこそこの類比が成り立つ。ただし両言語は互換ではなく、関数セットも異なる。 - jq はコマンドラインの JSON プロセッサだ。パス選択をはるかに超えて、変換・集約・再構成までこなし、シェルのパイプラインに住んでいる。
判断はたいてい明快だ。埋め込みのアサーションやパイプライン設定のフィールドなら JSONPath を選ぶ。シェル駆動の変換やデータ整形なら jq を選ぶ。jq チートシート がそのワークフローを詳しく扱っている。そして、フィールドがどこにあるかではなく、ペイロードが期待する形に適合しているかが問題なら、JSON Schema Validator とその完全な検証ガイドで検証しよう。
JSONPath のよくある間違い 7 つ
- ルート
$を忘れる。store.bookはほとんどのエンジンで拒否される。あらゆる式は$から始まる。 - スライスの off-by-one。
[0:2]は 3 つではなく 2 つの要素だ。終端の境界は含まれない。 - 方言の取り違え。 Classic の式を RFC 9535 で(あるいはその逆で)実行すると、パースエラーになったり、別のノードにマッチしたりする。エンジンを合わせて切り替えよう。
- 関数を独立したセグメントとして使う。
$.store.book.length()は無効な RFC 9535 だ。length()はフィルターの内側で呼ぶ。 - フィルター内で
@を忘れる。[?($.price < 10)]はルートを指す。[?(@.price < 10)]を使う。 - ブラケットのクォート間違い。
$[store]はエラーだ。キーをクォートする。$['store']。 ..が第 1 階層で止まると思い込む。 再帰下降は直接の子だけでなく、あらゆる深さでマッチする。
JSONPath をオンラインで、プライベートにテストする
JSONPath 構文を学ぶ最速の方法は、実際に実行することだ。JSONPath Tester はこのガイドのすべての式をライブで評価する。RFC 9535 と Classic の二重エンジン、Values / Paths / Both の結果ビュー、デバッグ用の正規化パス、そして 100% ブラウザ内実行。アップロードもサインアップも eval もないので、専有のペイロードでも安全に使える。ここでパスを組み立て、欲しいノードを正確に選んでいることを確認したら、検証済みの式をそのままコード・テスト・パイプラインに貼り付けよう。
残りの JSON ワークフローについては、サンプルレスポンスを JSON to TypeScript で型付きインターフェースに変換したり、二つのドキュメントを JSON Diff でフィールドごとに比較したりできる。
よくある質問
JSONPath は何に使うのか
JSONPath は命令的なコードなしで JSON をクエリする。開発者は API レスポンスからフィールドを取り出したり、結合テストで値をアサートしたり、Kubernetes・AWS Step Functions・Azure Logic Apps の設定でフィールドを指定したりするのに使う。走査を手書きするのが面倒な、大きく不規則な構造からデータを抽出する場面でこそ真価を発揮する。
RFC 9535 と古典的な JSONPath の違いは何か
Classic は Stefan Goessner の 2007 年の事実上の方言だ。広く実装されたが正式に仕様化されることはなく、ライブラリごとに分岐した。RFC 9535 は IETF の 2024 年 2 月の正式な仕様で、精密な文法、結果のための正規化パス、5 つの標準関数を定義している。両者はフィルター・和集合・欠落メンバーの比較といった境界で異なる。
JSONPath のフィルター式はどう動くのか
フィルター [?()] は述語が真である要素だけを残し、@ が現在の要素だ。たとえば $.store.book[?(@.price < 10)] は価格が 10 未満の本を選ぶ。&& や || で条件を組み合わせ、メンバーが存在するかをテストし、文字列や数値のリテラルと比較できる。
length() を $.store.book.length() のように使えるか
いいえ。RFC 9535 では、length() と他の 4 つの関数はフィルターの内側でのみ呼び出せる。たとえば $.store.book[?length(@.title) > 15] のように。独立したセグメントの形式 $.store.book.length() は jsonpath-plus の拡張であって標準の JSONPath ではなく、RFC 9535 の文法はこれを拒否する。
JSONPath は XPath と同じものか
いいえ、ただし考え方は似ている。XPath は XML を、JSONPath は JSON をクエリし、どちらもパスセレクターでノードを特定する。JSONPath は XPath の記法の一部(*、..、[])を意図的に借用しており、だからこそこの類比が役立つ。だが構文・意味論・関数セットは異なり、互換ではない。
JSONPath の再帰下降(..)は何をするのか
.. 演算子は直接の子だけでなく、ドキュメントのあらゆる階層を検索する。$..author は、どの深さに現れるかにかかわらず、すべての author メンバーを集める。深くネストした構造や不規則な構造から単一のフィールドを取り出す最速の方法だが、想定よりはるかに多くのノードにマッチしうる。可能なら絞り込もう。