JSON Toolkit

Język zapytań JSONPath w praktyce

Podsumowanie: W 2024 roku JSONPath wreszcie doczekał się formalnego standardu — RFC 9535. Ten artykuł omawia składnię, semantykę filtrów, funkcje i historyczne różnice między implementacjami.

Średniozaawansowany~12 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu o języku zapytań JSONPath w praktyce

Najważniejsze: najpierw lokalizuj jawną ścieżką i filtruj na jej podstawie, a wyrażenia pisz według RFC 9535; do pojedynczego stałego pola używaj dostępu do właściwości lub JSON Pointer, a do agregacji jq lub kodu.

Widziałeś pewnie wyrażenia takie jak $.store.book[?(@.price < 10)].title, ale standardem międzynarodowym stało się ono dopiero w 2024 roku.

1. Od standardu de facto do standardu formalnego

JSONPath zaproponował Stefan Gössner w 2007 roku, a przez długi czas jedyną „specyfikacją” był pojedynczy wpis blogowy, przy czym każda implementacja językowa robiła własne rzeczy:

  • czy filtry wymagają nawiasów (?(@.a) vs ?@.a);
  • czy indeks tablicy [-1] jest wspierany;
  • czy wynik zwraca „wartości” czy „wartości i ścieżki”;
  • czy wyrażenia skryptowe (?(@.price < $.max)) są dozwolone.

RFC 9535, opublikowane w lutym 2024 roku, zbiegło te rozbieżności w jedną składnię i semantykę i dostarczyło oficjalny zestaw testów zgodności. Nowe projekty powinny podążać za RFC 9535; migrując stare projekty, najpierw potwierdź wersję implementacji.

2. Ściągawka składni

Składnia Znaczenie Przykład
$ Węzeł główny $.store
@ Obecny węzeł wewnątrz filtra $[?@.price < 10]
.name Węzeł podrzędny (nazwa musi pasować do [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] Węzeł podrzędny (dowolna nazwa, w tym spoza ASCII i z kropkami) $['store']['书']
[0] Indeks tablicy; liczby ujemne liczą od końca $.book[0], $.book[-1]
[0,2] Wiele indeksów / wiele nazw $.book[0,2]
[1:3] Wycinek, początek włącznie, koniec wyłącznie $.book[1:3]
[::2] Wycinek z krokiem $.book[::2]
[*] Symbol wieloznaczny dla wszystkich węzłów podrzędnych $.book[*].title
..name Zejście rekurencyjne, szukanie na dowolnej głębokości $..price
..[*] Rekurencyjnie bierz wszystkie węzły $..*
[?<expr>] Selektor filtrujący $.book[?@.price < 10]

Jedna semantyka, którą musisz zapamiętać: JSONPath zwraca zbiór węzłów (nodelist), a nie pojedynczą wartość. Nawet gdy pasuje tylko jeden węzeł, wynik jest listą o długości jeden.

3. Przejście przez składnię na jednej próbce

{
  "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
}
Wyrażenie Wynik
$.store.bicycle.color ["red"]
$..price 4 ceny (w tym cena roweru)
$.store.book[*].title 3 tytuły
$.store.book[?@.price < 10].title Tytuły książek 1 i 3
$.store.book[?@.tags] Wszystkie książki, które mają tags (test istnienia)
$.store.book[-1].title Tytuł ostatniej książki
$..book[0,1] Pierwsze dwie książki
$.store.book[?@.price < $.expensive] Składniowo zależne od implementacji; RFC 9535 nie pozwala na referencje między korzeniami

Ostatni wiersz to historyczna pułapka: wiele implementacji pozwala na $.expensive wewnątrz filtra do porównania między węzłami, czego RFC 9535 wyraźnie zabrania — filtr może uzyskać dostęp wyłącznie do obecnego węzła @. Gdy musisz porównać z globalnym progiem, przekaż próg jako argument funkcji zapytania.

4. Co potrafią wyrażać filtry

Wyrażenia filtrów RFC 9535 wspierają cztery rodzaje konstrukcji:

Logic:       &&  ||  !          e.g. ?@.price < 10 && @.tags
Comparison:  ==  !=  <  <=  >  >=
Existence:   a bare path        e.g. ?@.tags (tests existence)
Functions:   length() count() match() search() value()

Kilka praktycznych przykładów:

// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]

// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// More than one tag
$.store.book[?length(@.tags) > 1]

// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()

match() to dopasowanie całego łańcucha, a search() to wyszukiwanie podłańcucha; bazowy dialekt regex to I-Regexp (podzbiór RFC 9485), który nie wspiera ani odwołań wstecznych, ani lookaround — to jednocześnie ograniczenie i wygodna tarcza przed regex DoS.

5. Różnice implementacji: sprawdź przed migracją

Implementacja Język Wsparcie RFC 9535 Uwagi
jsonpath-plus JavaScript W większości zgodne Wspiera funkcje rozszerzeń i wyrażenia skryptowe; więcej możliwości, ale nie ścisły standard
jsonpath (dchester) JavaScript Częściowe Długo istniejąca biblioteka ze składnią ?() i inną strukturą wyniku
ojg / gjson Go ojg ma dobre wsparcie gjson używa innej składni ścieżek i nie jest JSONPath
jsonpath-ng Python Częściowe Wspiera rozszerzenia i jest dość permisywne
Jayway JsonPath Java Częściowe Szeroki ekosystem; historyczne zachowanie różni się od RFC

Trzy kontrole przed migracją:

  1. Czy nawiasy filtrów są wymagane;
  2. Czy indeksy ujemne takie jak -1 są wspierane;
  3. Czy wynik to „tablica wartości” czy „tablica obiektów {path, value}”.

Trzecia najłatwiej potknąć: frontend czyta result[0], a po zmianie biblioteki dostaje obiekt, zostawiając pustą stronę.

6. Wydajność i bezpieczeństwo

Wydajność

  • JSONPath to zasadniczo pojedyncze przejście o złożoności O(liczba węzłów) i nie potrafi używać indeksów;
  • Zejście rekurencyjne przez .. jest najkosztowniejsze; jeśli potrafisz najpierw zlokalizować przez $.store.book, a potem filtrować, nie pisz $..book;
  • Kompiluj wyrażenia raz i używaj ponownie (większość bibliotek oferuje compile lub cache’owanie), zamiast parsować przy każdym zapytaniu;
  • Dla dokumentów z ponad 100 000 węzłów rozważ najpierw dokładne wyciągnięcie przez JSON Pointer albo wstępne przetworzenie po stronie serwera przez jq.

Bezpieczeństwo

  • Wyrażenia z danych wejściowych użytkownika muszą mieć limity długości i głębokości zagnieżdżenia;
  • Nie pozwalaj JSONPath zajmować się autoryzacją — potrafi tylko czytać i nie wyrazi „które pola są niewidoczne”;
  • Gdy regex używany przez match() pochodzi od użytkowników, ogranicz jego złożoność; choć jest bezpieczniejszy niż PCRE, limit długości nadal jest zalecany.

7. Kiedy nie używać JSONPath

  • Potrzebujesz tylko jednego stałego pola → użyj bezpośrednio data.store.bicycle.color;
  • Po modyfikacji musisz zapisać z powrotem → użyj JSON Pointer lub patch; JSONPath nie zapisuje z powrotem;
  • Potrzebujesz agregacji (sumowanie, grupowanie) → użyj jq, SQL lub kodu; JSONPath tylko wybiera;
  • Dokument jest ogromny i potrzebujesz tylko kilku pól → użyj parsera strumieniowego, zamiast najpierw wczytywać całe drzewo do pamięci.

8. Lista kontrolna

  • [ ] Wyrażenia podążają za RFC 9535 i nie polegają na referencjach między korzeniami;
  • [ ] Jest jasne, że wynik to zbiór węzłów; użyj [0] lub value() do uzyskania pojedynczej wartości;
  • [ ] Wyrażenia są wstępnie skompilowane i używane ponownie;
  • [ ] Wyrażenia kontrolowane przez użytkownika mają limity długości i złożoności;
  • [ ] Oficjalne przypadki testów zgodności są uruchamiane przy zmianie implementacji.

FAQ

Jak wybrać między JSON Pointer a JSONPath?

Gdy potrzebujesz tylko jednego stałego pola, użyj zwykłego dostępu do właściwości lub JSON Pointer; po JSONPath sięgaj, gdy potrzebujesz filtrów, symboli wieloznacznych lub zejścia rekurencyjnego. JSONPath tylko wybiera i nigdy nie zapisuje z powrotem, więc zapis z powrotem należy do JSON Pointer lub patcha.

Dlaczego filtr nie może odwoływać się do $.expensive?

RFC 9535 wyraźnie zabrania referencji między korzeniami; filtr może uzyskać dostęp tylko do obecnego węzła @. Zamiast tego przekaż próg jako argument funkcji zapytania. Starsze implementacje akceptują składnię, ale psuje się ona przy zmianie lub aktualizacji biblioteki.

Czy polegać na result[0] do odczytu wartości?

Tylko po potwierdzeniu, że obecna implementacja zwraca tablicę wartości. Niektóre biblioteki zwracają obiekty {path, value}, więc result[0] staje się obiektem i strona robi się pusta. Czytaj pojedyncze wartości przez [0] lub value() i uruchom oficjalny zestaw zgodności przed zmianą implementacji.