JSONPath はなぜツールごとに書き方が違うのか ― RFC 9535 と jq / JavaScript パスの実務整理
DDEVELOPER
開発(更新: 2026-10-13)11 分で読める

JSONPath はなぜツールごとに書き方が違うのか ― RFC 9535 と jq / JavaScript パスの実務整理

JSON の中から items[0].name のような値を取り出したい場面はよくあります。ところが、ツールによって $.items[0].name、.items[0].name、items[0].name、["items",0,"name"] のように表記が変わり、同じものに見えてもそのまま貼り替えると動かないことがあります。

この記事では、RFC 9535 で標準化された JSONPath を中心に、jq のフィルタ、JavaScript のプロパティアクセス、lodash の path との違いを整理します。ポイントは、これらを「JSON の住所」として似たものに見すぎず、評価するエンジンが違う別言語として扱うことです。

#JSONPath#JSON#jq#RFC 9535#JavaScript#API

JSONPath は JSON の中のノードを選ぶクエリ言語

JSONPath は、JSON 文書の中から値を選択するためのクエリ表記です。たとえば次の JSON から最初の商品名を取りたいなら、JSONPath では $.items[0].name のように書きます。

{
  "items": [
    { "name": "USB-C cable", "price": 1200 },
    { "name": "charger", "price": 2980 }
  ]
}

先頭の $ はルートノード、.items はオブジェクトのメンバー、[0] は配列の 0 番目、.name はその中の名前です。ファイルパスのように見えますが、実際には JSON の木構造に対する問い合わせです。

RFC 9535 で標準化された範囲

JSONPath は長いあいだ実装ごとの差が大きい記法でしたが、2024 年 2 月に RFC 9535 として標準化されました。RFC 9535 は、JSONPath を「JSON 値の中から値を選択し抽出する文字列構文」として定義しています。

表が収まらない場合は、左右にスクロールできます。

表記意味例
$ルートノード$
.name名前による子の選択$.items
['name']引用符つき名前選択$['user.name']
[0]配列インデックス$.items[0]
[*]子要素のワイルドカード$.items[*].name
..name子孫方向の探索$..price
[?expr]条件に合う要素の選択$.items[?@.price < 2000]

RFC 9535 の重要な点は、単に書き方を並べたことではなく、結果を「ノードリスト」として扱うこと、フィルタ式の現在ノードを @ で表すこと、配列スライスやワイルドカードの意味を定義したことです。

$ と @ を混同しない

JSONPath で最初に混乱しやすいのが $ と @ です。$ は常に文書全体のルートを表します。一方、@ はフィルタの中で評価中の現在ノードを表します。

$.items[?@.price < 2000].name

この例では、$.items で配列まで進み、[?@.price < 2000] で各要素を 1 つずつ見ます。フィルタ内の @.price は「いま見ている商品オブジェクトの price」です。ここで $.price と書くと、ルート直下の price を見に行く別の意味になります。

jq の .items[0].name は JSONPath ではない

jq でも .items[0].name のような表記を使いますが、これは JSONPath ではなく jq のフィルタです。jq では . が入力そのものを表す identity filter で、.foo はオブジェクトのキー foo を取り出すフィルタです。

似ているため混同しやすいのですが、jq は値の抽出だけでなく、変換、計算、パイプ、配列生成、複数出力などを持つプログラミング言語に近い道具です。JSONPath は基本的に「どのノードを選ぶか」を表すクエリ言語です。

表が収まらない場合は、左右にスクロールできます。

目的JSONPathjq
最初の商品名$.items[0].name.items[0].name
全商品名$.items[*].name.items[].name
2000円未満$.items[?@.price < 2000].items[] | select(.price < 2000)

この比較表は、各商品の price が数値である冒頭のデータを前提にしています。jqには型をまたぐ大小順序があり、欠損や null も .price < 2000 を満たします。数値だけを対象にするなら .items[] | select(.price | numbers) | select(.price < 2000) として型を絞ります。JSONPathの数値との大小比較は同じ挙動ではありません。

jq の .items[].name は、各要素を個別の出力として流すイメージです。JSONPath の $.items[*].name は、該当ノードのリストを返します。これは重複を取り除く集合ではありません。たとえば配列 ["a","b"] に $[0,0] を適用すると、同じノードが2回選ばれ、値を配列で表せば ["a","a"] です。配列の [*] は配列順ですが、オブジェクトの .* のメンバー順はRFCでは保証されません。

JavaScript のプロパティアクセスとも違う

JavaScript では obj.items[0].name のように、オブジェクトのプロパティをドット記法やブラケット記法で参照します。これは実行中の JavaScript 式であり、JSONPath のような問い合わせ文字列ではありません。

たとえばキー名にドットが含まれる JSON では、違いがはっきり出ます。

{ "user.name": "Aki", "user": { "name": "Bun" } }
  • $.user.name: user の中の name
  • $['user.name']: user.name という 1 つのキー
  • obj.user.name: JavaScript のプロパティアクセス
  • obj["user.name"]: ドットを含むキーを JavaScript で参照

見た目が似ていても、文字列として保存するパスなのか、JavaScript として実行する式なのかで扱いが変わります。ユーザー入力をそのまま JavaScript 式として評価するのは危険なので、ツールでは構文を限定したパスとして扱うのが安全です。

lodash の path は便利だが標準JSONPathではない

lodash の _.get(object, path) は、"a[0].b.c" のような文字列や ["a", 0, "b", "c"] のような配列で深い値を取り出せます。これは実務で便利ですが、RFC 9535 の JSONPath と同じ構文ではありません。

lodash 系の path は、単一の値へたどる「プロパティパス」として使われることが多い一方、JSONPath はワイルドカード、子孫探索、フィルタなどにより複数ノードを選べます。つまり items[0].name のような単純ケースでは似ていても、items[*].name や [?@.price < 2000] に進むと別物です。

実務でハマりやすい5つのケース

  1. ルート記号の有無: RFC 9535 のトップレベルクエリは $ が必須です。フィルタ内では @ を起点にできます。jq の入力へのアクセスは . を使いますが、すべてのjqフィルタがドットから始まるわけではありません。
  2. 配列の全要素: JSONPath は [*]、jq は [] を使う。
  3. ドットを含むキー: user.name というキーは ['user.name'] のように引用する。
  4. 存在しないキー: RFC準拠のJSONPathで存在しない名前を選ぶ結果は空ノードリストです。値が null のノードを1件選ぶこととは異なります。jq・JavaScript・lodashの結果は次の例のように区別します。
  5. フィルタ構文: JSONPath の [?@.x] は存在検査です。x の値が false や null でも選びます。jq の select(.x) は false と null を除外しますが、0 と空文字列は通します。JavaScriptの真偽値判定とも異なります。
{ "x": null }

上のオブジェクトを入力にすると、JSONPathの $.missing は0件、$.x は値がnullの1件です。以下の [] と [null] は、選んだ値を配列で表したものです。APIによって結果の包み方は異なります。

表が収まらない場合は、左右にスクロールできます。

処理系存在しないmissing存在するx
JSONPath$.missing → []$.x → [null]
jq.missing → null.x → null
JavaScriptobj.missing → undefinedobj.x → null
lodash_.get(obj, ["missing"], "fallback") → "fallback"_.get(obj, ["x"], "fallback") → null

JavaScriptで obj.missing.name と続けるとTypeErrorになります。obj.missing?.name ならundefinedです。lodashの既定値は解決結果がundefinedの場合に使われ、nullを置き換えません。

存在条件の例として [{"x":false},{"x":null},{"x":0},{"x":""},{}] にJSONPathの $[?@.x] を適用すると先頭4件、jqの .[] | select(.x) では0と空文字列を持つ2件が残ります。

特に API レスポンス確認、ログ解析、テスト自動化では、別ツールのパスをそのまま移植して壊れることがよくあります。パスを共有するときは「これは JSONPath」「これは jq」「これは JavaScript/lodash path」と明記するだけで、かなり事故を減らせます。

人間が読むパスと機械が評価する式を分ける

画面上で「この値は items[0].name にあります」と示すだけなら、必ずしも完全な JSONPath である必要はありません。人間が読む説明用パスなら、短く読みやすい表記のほうが役に立つこともあります。

一方、実際に検索・抽出・テストに使う式は、評価エンジンに合わせた厳密な構文が必要です。JSONPath エンジンに渡すなら RFC 9535 の範囲を意識し、jq に渡すなら jq のフィルタとして書き、JavaScript で使うなら安全なプロパティアクセスや path 配列に変換します。

NanToo の JSONPath Finder のようなツールでは、JSON ツリーをクリックしてパスを確認できます。選択ノードに対するJSONPath、JavaScript、Python、jq、lodashの5形式が並び、個別にコピーできます。数値比較をこのツールで試すときは $.items[?(@.price < 2000)].name のように条件を丸括弧で囲みます。RFCではこの丸括弧を省略できますが、本ツールでは必要です。検索欄は限定実装であり、RFC 9535全体の検証器ではありません。この記事の存在フィルタ、重複選択 $[0,0]、配列sliceや複合条件は未対応です。例を試す処理系の対応範囲を先に確認してください。

参考文献・ソース

記事作成に関する注記

本記事は AI(大規模言語モデル)を編集補助として活用して作成しています。 公開前に編集者が内容を確認していますが、事実誤認・仕様の解釈ミス・最新情報との齟齬が含まれる可能性があります。 重要な判断を行う際は、本文中の一次ソースや公式ドキュメントを必ずご自身でご確認ください。 誤りにお気づきの場合は、お問い合わせフォームよりご連絡いただけると助かります。

🔧 関連ツール

📚 関連記事