Jeśli kiedykolwiek wpatrywałeś się w głęboko zagnieżdżoną odpowiedź API i myślałeś „potrzebuję tylko tego jednego pola ukrytego gdzieś w środku" — JSONPath to narzędzie, którego szukasz. To język zapytań dla JSON, podobny duchem do tego, jak XPath działa dla XML. Piszesz wyrażenie ścieżki, a w zamian dostajesz pasujące wartości. Żadnych pętli, żadnego ręcznego przechodzenia. Stefan Goessner wprowadził go w 2007 roku, a po latach nieznacznie niekompatybilnych implementacji krążących w sieci, został formalnie ustandaryzowany jako RFC 9535 w 2024 roku. Wyrażenia JSONPath możesz też eksplorować interaktywnie za pomocą narzędzia JSON Path.

Zbiór danych, którego będziemy używać

Zamiast abstrakcyjnych przykładów, pracujmy z czymś realistycznym — odpowiedzią e-commerce z zamówieniem. To rodzaj JSON, który otrzymasz z API zarządzania zamówieniami:

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
    }
  }
}

Operator root $ i podstawowa nawigacja

Każde wyrażenie JSONPath zaczyna się od $, które odnosi się do korzenia dokumentu. Stamtąd poruszasz się za pomocą kropek. Chcesz poznać status zamówienia? To proste:

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

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

$.order.totals.total
// → 166.88

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

Możesz też używać notacji nawiasowej, która jest przydatna, gdy klucz zawiera spacje lub znaki specjalne, albo gdy odwołujesz się do elementów tablicy po indeksie. Oba style są zgodne z zasadami składni RFC 9535:

text
// Notacja kropkowa
$.order.customer.email

// Notacja nawiasowa — równoważna
$['order']['customer']['email']

// Indeks tablicy — pierwszy element zamówienia
$.order.lineItems[0].name
// → "Wireless Headphones"

// Ostatni element zamówienia (ujemne indeksowanie — obsługiwane w RFC 9535)
$.order.lineItems[-1].name
// → "Phone Stand Pro"
Uwaga dotycząca ujemnych indeksów: $[-1:] (składnia slice) i $.array[-1] (bezpośredni ujemny indeks) są oba poprawne w RFC 9535, ale niektóre starsze implementacje ich nie obsługują. Przetestuj swoją konkretną bibliotekę, jeśli używasz ujemnych indeksów.

Wieloznaczniki — jednoczesne zapytanie do wszystkich dzieci

Wieloznacznik * dopasowuje wszystkie elementy na danym poziomie. Zamiast pytać o lineItems[0], lineItems[1] itd., możesz pobrać wszystko naraz:

text
// Wszystkie nazwy pozycji zamówienia
$.order.lineItems[*].name
// → ["Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]

// Wszystkie ceny jednostkowe pozycji zamówienia
$.order.lineItems[*].unitPrice
// → [89.99, 12.49, 34.00]

// Wszystkie kody SKU pozycji zamówienia
$.order.lineItems[*].sku
// → ["WH-1042", "CB-USB-C", "SC-PRO-7"]

To wzorzec, który będziesz najczęściej stosować przy pracy z tablicami w odpowiedziach API. Zamiast mapować po tablicy w kodzie aplikacji, możesz wyodrębnić dokładnie potrzebne pole zanim dotrze ono do logiki biznesowej.

Rekurencyjne przeszukiwanie — znajdowanie wartości na każdej głębokości

Operator .. przeprowadza rekurencyjne przeszukiwanie przez cały drzewo dokumentu. To jak przejście w głąb, które zbiera każdy węzeł pasujący do nazwy klucza, niezależnie od tego, gdzie się znajduje:

text
// Znajdź każde pole "name" gdziekolwiek w dokumencie
$..name
// → ["Maria Chen", "Wireless Headphones", "USB-C Charging Cable", "Phone Stand Pro"]

// Znajdź każde pole "city" gdziekolwiek
$..city
// → ["Portland"]

// Znajdź każde pole o nazwie "id" na dowolnej głębokości
$..id
// → ["ORD-9182", "CUST-441"]

Zauważ, jak $..name znajduje zarówno imię klienta, jak i wszystkie nazwy produktów — nie obchodzi go głębokość. Jest to potężne narzędzie do eksploracji schematu: gdy masz do czynienia z nieznanym obiektem JSON i chcesz znaleźć wszystkie wartości dla konkretnego klucza bez znajomości struktury. Jeśli chcesz najpierw sprawdzić sam JSON, JSON Validator i JSON Formatter są przydatnymi punktami startowymi.

Wycinanie tablic

JSONPath zapożycza notację wycinków w stylu Pythona dla tablic. Format to [start:end:step], gdzie każda część może być pominięta. Jest to udokumentowane w RFC 9535 § 2.3.5:

text
// Pierwsze dwie pozycje zamówienia
$.order.lineItems[0:2]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]

// Od indeksu 1 do końca
$.order.lineItems[1:]
// → [{ sku: "CB-USB-C", ... }, { sku: "SC-PRO-7", ... }]

// Tylko ostatni element (składnia slice — szeroko obsługiwana)
$.order.lineItems[-1:]
// → [{ sku: "SC-PRO-7", ... }]

// Co drugi element (krok 2)
$.order.lineItems[0::2]
// → [{ sku: "WH-1042", ... }, { sku: "SC-PRO-7", ... }]

Wyrażenia filtrujące — zapytania według warunku

Tu JSONPath naprawdę pokazuje swoją wartość. Składnia filtra [?(...)] pozwala na zapytania o elementy tablicy według wartości ich pól. Symbol @ odnosi się do aktualnie testowanego elementu. Oryginalny artykuł Stefana Goessnera z 2007 roku na stronie goessner.net wprowadził tę składnię, a RFC 9535 ją sformalizował:

text
// Pozycje poniżej 50 USD
$.order.lineItems[?(@.unitPrice < 50)]
// → [{ sku: "CB-USB-C", unitPrice: 12.49, ... }, { sku: "SC-PRO-7", unitPrice: 34.00, ... }]

// Tylko produkty aktualnie dostępne w magazynie
$.order.lineItems[?(@.inStock == true)]
// → [{ sku: "WH-1042", ... }, { sku: "CB-USB-C", ... }]

// Pozycje, gdzie qty jest większe niż 1
$.order.lineItems[?(@.qty > 1)]
// → [{ sku: "CB-USB-C", qty: 2, ... }]

// Pozycje niedostępne w magazynie
$.order.lineItems[?(@.inStock == false)].name
// → ["Phone Stand Pro"]

Możesz też łączyć warunki. Większość implementacji obsługuje && i || wewnątrz wyrażeń filtrujących, co pozwala pisać rzeczy takie jak [?(@.inStock == true && @.unitPrice < 30)]. Sprawdź dokumentację swojej biblioteki — różnice w obsłudze operatorów były jedną z głównych niespójności między implementacjami sprzed RFC, które standaryzacja RFC 9535 miała naprawić.

Sprawdzanie istnienia pola: Aby filtrować elementy, które mają określone pole (niezależnie od wartości), użyj składni sprawdzania istnienia: [?(@.fieldName)]. Na przykład, jeśli niektóre pozycje miały pole discountCode, a inne nie, $.order.lineItems[?(@.discountCode)] zwróci tylko te ze zniżką.

Używanie JSONPath w JavaScript

JavaScript nie ma jeszcze wbudowanej obsługi JSONPath (w przeciwieństwie do XPath, który ma document.evaluate() w przeglądarce). Będziesz potrzebować biblioteki. Dwie najpopularniejsze to jsonpath i jsonpath-plus. Pakiet jsonpath-plus jest aktywniej utrzymywany i lepiej zgodny z 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

Zwróć uwagę, że JSONPath() zawsze zwraca tablicę — nawet gdy zapytujesz o pojedynczą wartość. To zamierzone: wyrażenie ścieżki to selektor, a selektor może dopasować zero, jeden lub wiele węzłów. Dlatego zawsze sięgaj po result[0], gdy wiesz, że celujesz w unikalną wartość.

Używanie JSONPath w Pythonie

W Pythonie biblioteka jsonpath-ng jest najbardziej kompletną opcją. Obsługuje podstawową specyfikację plus większość składni wyrażeń filtrujących:

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

Wzorzec parse() + find() to właściwe podejście w zastosowaniach produkcyjnych — skompiluj wyrażenie raz i uruchamiaj je na wielu dokumentach, zamiast parsować ciąg ścieżki za każdym razem. Dokumentacja jsonpath-ng na GitHub zawiera więcej szczegółów na temat rozszerzonej składni filtrów i metody update() do modyfikowania dopasowanych węzłów.

JSONPath vs jq — po które narzędzie sięgnąć?

Ludzie czasem mylą JSONPath i jq — oba „odpytują JSON", ale rozwiązują różne problemy. Oto praktyczne porównanie:

  • JSONPath to język wyrażeń ścieżek zaprojektowany do osadzania w aplikacjach. Odpytuje i wyodrębnia wartości. Używasz go wewnątrz kodu JavaScript, Python, Java lub plików konfiguracyjnych, takich jak selektory Kubernetes.
  • jq to pełny procesor wiersza poleceń z własnym językiem programowania. Może odpytywać, transformować, przekształcać, filtrować i obliczać nowe wartości. To odpowiednie narzędzie do skryptów powłoki i jednorazowego przetwarzania danych w terminalu.
  • Używaj JSONPath, gdy piszesz kod aplikacji, który musi programowo wyodrębniać pola z JSON — szczególnie gdy biblioteka musi być możliwa do osadzenia i lekka.
  • Używaj jq, gdy pracujesz w wierszu poleceń, piszesz skrypty powłoki, debugujesz odpowiedzi API lub potrzebujesz transformacji (zmiana nazw pól, agregowanie, budowanie nowych struktur) wykraczających poza proste wyodrębnianie.
  • Składnia jq jest bardziej rozbudowana, ale też bardziej złożona — jq '.order.lineItems[] | select(.inStock) | .name' vs JSONPath $.order.lineItems[?(@.inStock)].name. W przypadku czystego wyodrębniania JSONPath jest często bardziej czytelny.

Istnieje też złoty środek: jeśli pracujesz już w JavaScript i potrzebujesz tylko okazjonalnych transformacji, coś w stylu _.get() z Lodash obsługuje prosty dostęp do ścieżek bez żadnych zależności. Ale do czegokolwiek obejmującego wieloznaczniki, rekurencję lub wyrażenia filtrujące, właściwa biblioteka JSONPath jest warta zachodu.

Podsumowanie

JSONPath wypełnia prawdziwą lukę między „ręcznym przechodzeniem przez ten obiekt w pętli for" a „uruchamianiem procesu jq". Gdy opanujesz $, ., [*], .. i [?(filter)], będziesz po nie sięgać nieustannie — szczególnie przy pracy z dużymi ładunkami API, gdzie potrzebujesz tylko kilku pól. Standaryzacja RFC 9535 oznacza, że składnia jest teraz stabilna, więc wyrażenia napisane dziś powinny działać spójnie w zgodnych bibliotekach. Wypróbuj swoje wyrażenia za pomocą narzędzia JSON Path — wklej swój JSON, napisz ścieżkę i natychmiast zobacz wyniki. A jeśli Twój JSON wymaga najpierw porządków, JSON Formatter i JSON Validator są zawsze pod ręką.