JSON Toolkit

JSONPath クエリ言語の実践

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

中級約 12 分で読めます2026-10-30 公開2026-10-30 更新
JSONPath クエリ言語の実践のカバー画像

要点:まず明示的なパスで位置を特定し、その上にフィルタを重ね、式は 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. フィルタの括弧が必須か。
  2. -1 のような負のインデックスをサポートするか。
  3. 結果が「値の配列」か、「{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() で読み、実装を切り替える前に公式の適合スイートを実行してください。