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.

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ą:
- Czy nawiasy filtrów są wymagane;
- Czy indeksy ujemne takie jak
-1są wspierane; - 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
compilelub 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]lubvalue()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.