深くネストされたAPIレスポンスを見て「あの奥にある1つのフィールドだけ欲しい」と思ったことがあるなら — JSONPathがまさにその解決策です。 JSONはXMLに対してXPathが機能するのと同じように、 JSONのためのクエリ言語です。 パス式を書けば、マッチする値が返ってきます。ループも手動トラバーサルも不要です。 Stefan Goessnerが2007年に導入し、 微妙に互換性のない実装が長年並存した後、2024年に正式に RFC 9535として標準化されました。 JSON PathツールでJSONPath式をインタラクティブに試すこともできます。

本記事全体で使用するデータセット

抽象的な例ではなく、現実的なもの — ECサイトの注文レスポンスで作業しましょう。 これは注文管理APIから返ってくる典型的なJSONです:

json
{
  "order": {
    "id": "ORD-9182",
    "status": "shipped",
    "customer": {
      "id": "CUST-441",
      "name": "Maria Chen",
      "email": "[email protected]"
    },
    "shippingAddress": {
      "street": "14 Maple Avenue",
      "city": "Portland",
      "state": "OR",
      "zip": "97201",
      "country": "US"
    },
    "lineItems": [
      {
        "sku": "WH-1042",
        "name": "Wireless Headphones",
        "qty": 1,
        "unitPrice": 89.99,
        "inStock": true
      },
      {
        "sku": "CB-USB-C",
        "name": "USB-C Charging Cable",
        "qty": 2,
        "unitPrice": 12.49,
        "inStock": true
      },
      {
        "sku": "SC-PRO-7",
        "name": "Phone Stand Pro",
        "qty": 1,
        "unitPrice": 34.00,
        "inStock": false
      }
    ],
    "totals": {
      "subtotal": 148.97,
      "shipping": 5.99,
      "tax": 11.92,
      "total": 166.88
    }
  }
}

ルート演算子$と基本的なナビゲーション

すべてのJSONPath式は、ドキュメントのルートを参照する$から始まります。 そこからドットを使ってナビゲートします。注文ステータスが欲しい?それは簡単です:

text
$.order.status
// → "shipped"

$.order.customer.name
// → "Maria Chen"

$.order.totals.total
// → 166.88

$.order.shippingAddress.city
// → "Portland"

キーにスペースや特殊文字が含まれている場合や、インデックスで配列要素にアクセスする場合に便利なブラケット記法も使えます。 どちらのスタイルもRFC 9535の構文ルールに従って有効です:

text
// ドット記法
$.order.customer.email

// ブラケット記法 — 同等
$['order']['customer']['email']

// 配列インデックス — 最初の明細項目
$.order.lineItems[0].name
// → "Wireless Headphones"

// 最後の明細項目(負のインデックス — RFC 9535でサポート)
$.order.lineItems[-1].name
// → "Phone Stand Pro"
負のインデックスに注意: $[-1:](スライス構文)と $.array[-1](直接負のインデックス)はどちらもRFC 9535で有効ですが、古い一部の 実装ではサポートされていません。負のインデックスを使う場合は特定のライブラリでテストしてください。

ワイルドカード — 全ての子要素を一度にクエリ

ワイルドカード*は指定したレベルのすべての要素にマッチします。 lineItems[0]lineItems[1]などと個別に指定する代わりに、一度にすべてを取得できます:

text
// 全ての明細品名
$.order.lineItems[*].name
// → ["Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]

// 全ての明細単価
$.order.lineItems[*].unitPrice
// → [89.99, 12.49, 34.00]

// 全ての明細SKU
$.order.lineItems[*].sku
// → ["WH-1042", "CB-USB-C", "SC-PRO-7"]

これはAPIレスポンスで配列を扱う際に最もよく使うパターンです。 アプリケーションコードで配列をマップする代わりに、ビジネスロジックに届く前に必要なフィールドだけを抽出できます。

再帰降下 — 任意の深さで値を見つける

..演算子はドキュメントツリー全体を再帰的に検索します。 どこにあるかに関わらず、キー名にマッチするすべてのノードを収集する深さ優先探索のようなものです:

text
// ドキュメント内のどこにある"name"フィールドも検索
$..name
// → ["Maria Chen", "Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]

// どこにある"city"フィールドも検索
$..city
// → ["Portland"]

// 任意の深さの"id"フィールドをすべて検索
$..id
// → ["ORD-9182", "CUST-441"]

$..nameが顧客名と全製品名の両方を見つけることに注意してください — 深さは関係ありません。これはスキーマ探索に強力です:構造を知らずに特定のキーのすべての値を見つけたい場合、 未知のJSONブロブが渡された時に役立ちます。 JSONそのものを先に確認したい場合は、JSONバリデーターJSONフォーマッターが便利な出発点です。

配列スライシング

JSONPathは配列にPythonスタイルのスライス記法を借用しています。形式は [start:end:step]で、どの部分も省略できます。 これはRFC 9535 § 2.3.5に文書化されています:

text
// 最初の2つの明細項目
$.order.lineItems[0:2]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]

// インデックス1から末尾まで
$.order.lineItems[1:]
// → [{ sku: "CB-USB-C", ... }, { sku: "SC-PRO-7", ... }]

// 最後の項目のみ(スライス構文 — 広くサポート)
$.order.lineItems[-1:]
// → [{ sku: "SC-PRO-7", ... }]

// 1つ置きの項目(ステップ2)
$.order.lineItems[0::2]
// → [{ sku: "WH-1042", ... }, { sku: "SC-PRO-7", ... }]

フィルター式 — 条件でクエリ

ここがJSONPathが真価を発揮するところです。フィルター構文[?(...)]を使うと、 フィールド値で配列項目をクエリできます。@記号は現在テスト中の項目を参照します。Stefan Goessnerの 2007年のオリジナル記事 がこの構文を導入し、RFC 9535が正式化しました:

text
// $50未満の明細項目
$.order.lineItems[?(@.unitPrice < 50)]
// → [{ sku: "CB-USB-C", unitPrice: 12.49, ... }, { sku: "SC-PRO-7", unitPrice: 34.00, ... }]

// 現在在庫ありの項目のみ
$.order.lineItems[?(@.inStock == true)]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]

// qtyが1より大きい項目
$.order.lineItems[?(@.qty > 1)]
// → [{ sku: "CB-USB-C", qty: 2, ... }]

// 在庫なしの項目
$.order.lineItems[?(@.inStock == false)].name
// → ["Phone Stand Pro"]

条件を組み合わせることもできます。ほとんどの実装はフィルター式内で&&||をサポートしており、 [?(@.inStock == true && @.unitPrice < 30)]のようなものを書けます。 ライブラリのドキュメントを確認してください — オペレーターサポートの挙動は、 RFC 9535標準化が修正を目指した、 RFC以前の実装間の主な非一貫性の一つでした。

フィールド存在チェック: 値に関わらず特定のフィールドを持つ項目をフィルタリングするには、 存在チェック構文を使います: [?(@.fieldName)]。例えば、 一部の明細項目にdiscountCodeフィールドがあり他にはない場合、 $.order.lineItems[?(@.discountCode)]は割引された項目のみを返します。

JavaScriptでJSONPathを使う

JavaScriptにはまだネイティブのJSONPathサポートがありません(ブラウザでdocument.evaluate()を持つXPathとは異なり)。 ライブラリが必要です。 最も人気な2つはjsonpathjsonpath-plusです。 jsonpath-plusパッケージはより積極的にメンテナンスされており、RFC 9535への準拠が優れています:

bash
npm install jsonpath-plus
js
import { JSONPath } from 'jsonpath-plus';

const order = {
  order: {
    id: 'ORD-9182',
    status: 'shipped',
    customer: { name: 'Maria Chen', email: '[email protected]' },
    lineItems: [
      { sku: 'WH-1042', name: 'Wireless Headphones', qty: 1, unitPrice: 89.99, inStock: true },
      { sku: 'CB-USB-C', name: 'USB-C Charging Cable', qty: 2, unitPrice: 12.49, inStock: true },
      { sku: 'SC-PRO-7', name: 'Phone Stand Pro', qty: 1, unitPrice: 34.00, inStock: false }
    ],
    totals: { subtotal: 148.97, shipping: 5.99, tax: 11.92, total: 166.88 }
  }
};

// Get all product names
const names = JSONPath({ path: '$.order.lineItems[*].name', json: order });
console.log(names);
// [ 'Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro' ]

// Get in-stock items under $50
const affordable = JSONPath({ path: '$.order.lineItems[?(@.inStock == true && @.unitPrice < 50)]', json: order });
console.log(affordable.map(item => item.name));
// [ 'USB-C Charging Cable' ]

// Get the order total
const total = JSONPath({ path: '$.order.totals.total', json: order });
console.log(total[0]); // 166.88

JSONPath()は単一の値をクエリする場合でも常に配列を返すことに注意してください。 これは設計通りです: パス式はセレクターであり、セレクターはゼロ、1つ、または多くのノードにマッチできます。 ユニークな値を対象としていることがわかっている場合は、常にresult[0]を取得してください。

PythonでJSONPathを使う

Pythonでは、 jsonpath-ng ライブラリが最も完全なオプションです。コアスペックに加え、ほとんどのフィルター式構文をサポートしています:

bash
pip install jsonpath-ng
python
from jsonpath_ng import parse

order = {
    "order": {
        "id": "ORD-9182",
        "status": "shipped",
        "customer": {"name": "Maria Chen", "email": "[email protected]"},
        "lineItems": [
            {"sku": "WH-1042", "name": "Wireless Headphones", "qty": 1, "unitPrice": 89.99, "inStock": True},
            {"sku": "CB-USB-C", "name": "USB-C Charging Cable", "qty": 2, "unitPrice": 12.49, "inStock": True},
            {"sku": "SC-PRO-7", "name": "Phone Stand Pro", "qty": 1, "unitPrice": 34.00, "inStock": False}
        ],
        "totals": {"subtotal": 148.97, "shipping": 5.99, "tax": 11.92, "total": 166.88}
    }
}

# Parse the expression once, then reuse it (more efficient)
expr = parse("$.order.lineItems[*].name")
names = [match.value for match in expr.find(order)]
print(names)
# ['Wireless Headphones', 'USB-C Charging Cable', 'Phone Stand Pro']

# Get all line items and check stock
expr2 = parse("$.order.lineItems[*]")
for item in [m.value for m in expr2.find(order)]:
    status = "in stock" if item["inStock"] else "OUT OF STOCK"
    print(f"{item['name']} (${item['unitPrice']}) — {status}")

# Output:
# Wireless Headphones ($89.99) — in stock
# USB-C Charging Cable ($12.49) — in stock
# Phone Stand Pro ($34.0) — OUT OF STOCK

parse() + find()パターンが本番での正しいアプローチです — 式を一度コンパイルして多くのドキュメントに対して実行し、毎回パス文字列を再パースするのを避けましょう。 GitHubのjsonpath-ngドキュメント には拡張フィルター構文と、マッチしたノードを変更するためのupdate()メソッドの詳細があります。

JSONPath vs jq — どちらを選ぶべきか?

JSONPathとjqを混同する人もいます — どちらも「JSONをクエリ」しますが、 異なる問題を解決します。実用的な分類を以下に示します:

  • JSONPathはアプリケーションに埋め込まれるよう設計されたパス式言語です。値をクエリして抽出します。JavaScript、Python、JavaのコードやKubernetesセレクターのような設定ファイルの中で使います。
  • jqは独自のプログラミング言語を持つ完全なコマンドラインプロセッサーです。クエリ、変換、再構造化、フィルタリング、新しい値の計算ができます。シェルスクリプトやターミナルでの一時的なデータ操作に適したツールです。
  • JSONPathを使うのは、アプリケーションコードがJSONからフィールドをプログラム的に抽出する必要がある時 — 特にライブラリが埋め込み可能で軽量である必要がある時。
  • jqを使うのは、コマンドラインにいる時、シェルスクリプトを書いている時、APIレスポンスをデバッグしている時、または単純な抽出を超えた変換(フィールドのリネーム、集計、新しい構造の構築)が必要な時。
  • jqの構文はより強力ですが複雑でもあります — JSONPathの$.order.lineItems[?(@.inStock)].nameに対してjq '.order.lineItems[] | select(.inStock) | .name'。純粋な抽出なら、JSONPathの方が読みやすいことが多いです。

中間的な選択肢もあります: すでにJavaScriptで作業していて、時折の変換だけが必要なら、 Lodashの_.get() が依存関係なしでシンプルなパスアクセスをカバーします。しかし、ワイルドカード、再帰、 またはフィルター式を含む何かには、適切なJSONPathライブラリが価値を発揮します。

まとめ

JSONPathは「forループでこのオブジェクトを手動トラバース」と「jqプロセスを起動」の間の実際のギャップを埋めます。 $.[*]..[?(フィルター)]に慣れると、常にこれを使うようになるでしょう — 特に数フィールドだけ必要な大きなAPIペイロードを扱う時に。 RFC 9535標準化 により構文が安定しているので、今日書いた式は準拠ライブラリ間で一貫して動作するはずです。 JSON Pathツールで式を試してみてください — JSONを貼り付け、パスを書いて、すぐに結果を確認できます。JSONを先にクリーンアップしたい場合は、 JSONフォーマッターJSONバリデーター も用意されています。