Filtry JSONPath w głąb: rekurencja, wycinki i dopasowanie wielu warunków
Podsumowanie: Zejście rekurencyjne, wycinki i filtry wielowarunkowe to miejsce, w którym JSONPath staje się potężny i nadużywany. Omawia semantykę filtrów, kształty wyników i różnice bibliotek.

Najważniejsze: najpierw zlokalizuj znaną głębokość, a dopiero potem dodaj wycinki i filtry, zamiast sięgać po $..; naga ścieżka w filtrze tylko testuje istnienie, więc wartość logiczną testuj przez @.stock == true.
Każdy potrafi napisać
$.store.book[0].title. JSONPath naprawdę łamie się na łączeniu$..z wycinkami i wieloma warunkami, a potem dostajesz za dużo wyników, za mało albo kolejność, której się nie spodziewałeś.
1. Przykładowy dokument
{
"store": {
"book": [
{ "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
{ "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
{ "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
{ "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"updatedAt": "2026-10-31"
}
Każde poniższe wyrażenie używa tego dokumentu jako wejścia. Pamiętaj o dwóch założeniach: wynik to zbiór węzłów, a jego kolejność podąża za kolejnością dokumentu, a nie za kolejnością zapisanych warunków. Wewnątrz filtra @ odnosi się wyłącznie do obecnego węzła kandydata.
2. Zejście rekurencyjne $..: wygodne, ale drogie
.. znaczy „szukaj rekurencyjnie na dowolnej głębokości od obecnej pozycji”. RFC 9535 definiuje je jako segment potomny, który można łączyć z nazwami, symbolami wieloznacznymi, indeksami, wycinkami i filtrami:
$..price // every member named price at any depth
$..book[?(@.price < 10)] // any book at any depth, then filter
$..[?(@.stock)] // any node at any depth where stock exists
Trzy rzeczy do zrozumienia:
$..bookznajduje każdy węzeł o nazwiebook, niezależnie od głębokości;- Przejście jest w głąb, a kolejność wyjścia zależy od tego, jak implementacja odwiedza dzieci;
..nie może użyć żadnego indeksu, więc jego koszt skaluje się z całkowitą liczbą węzłów.
W rezultacie $..price i $.store.book[*].price mogą różnić się w wyniku tylko ceną roweru, a kosztem o rząd wielkości. Gdy znasz głębokość, zawsze pisz jawną ścieżkę. Jeśli chcesz tylko porównać granice dwóch języków ścieżek, zobacz JSONPath vs JSON Pointer.
3. Wycinki tablic i symbole wieloznaczne: trzy pułapki
Składnia wycinka to [start:end:step], początek włącznie, koniec wyłącznie, liczby ujemne liczone od ogona:
| Wyrażenie | Znaczenie | Wynik (przy czterech książkach) |
|---|---|---|
[0:2] |
Pierwsze dwie książki | Książki 1 i 2 |
[1:] |
Od drugiej książki do końca | Książki 2, 3 i 4 |
[:2] |
Pierwsze dwie książki | Książki 1 i 2 |
[-2:] |
Ostatnie dwie książki | Książki 3 i 4 |
[::2] |
Co druga książka | Książki 1 i 3 |
[::-1] |
Odwrotna kolejność | Książki 4, 3, 2 i 1 |
Trzy pułapki:
- Krok zero jest niepoprawny. RFC 9535 wymaga niezerowego kroku, a niektóre implementacje po cichu zwracają pusty zbiór;
- Ujemne kroki nie są wszędzie wspierane. Oryginalna implementacja Goessnera je wspiera, ale niektóre minimalne implementacje akceptują tylko kroki dodatnie;
- Filtrowanie po wycinku nie poszerza zakresu.
[0:2][?(@.stock)]filtruje tylko dwie książki wyprodukowane przez wycinek; nie wraca do całej tablicy.
Symbol wieloznaczny [*] można łączyć z wycinkami, ale [*] daje „wszystkie węzły podrzędne”. Jeśli te dzieci są obiektami, dopisanie .title daje wszystkie tytuły. Jeśli dziecko samo jest tablicą, .title nie spłaszczy niejawnie kolejnego poziomu. Napisz [*] ponownie, gdy potrzebujesz rozwinięcia; nie oczekuj niejawnego spłaszczania.
4. Filtry wielowarunkowe: porównania, logika i istnienie
Filtr [?(...)] może zawierać cztery rodzaje konstrukcji: porównania, operatory logiczne, testy istnienia i funkcje. Typowe zapytanie wielowarunkowe wygląda tak:
$..book[?(@.price < 10 && @.stock)]
Znaczy „cena poniżej 10 i stan magazynowy jest prawdziwy”. Zwróć uwagę na kilka szczegółów semantycznych:
@.stockto test istnienia, a nie „wartość jest prawdziwa”. Jeśli pole istnieje, ale jego wartość tofalse, warunek nadal obowiązuje. Aby przetestować wartość logiczną, musisz zapisać@.stock == true;&&wiąże mocniej niż||. Dodaj nawiasy przy mieszaniu, zamiast polegać na pamięci;- Obie strony porównania muszą mieć ten sam typ. Liczby i łańcuchy nie są niejawnie konwertowane;
@.price < "10"jest fałszywe albo błędem w ścisłych implementacjach; - Brakujące pole zawsze czyni porównanie fałszywym.
@.missing < 10nie zgłasza błędu; po prostu nic nie dopasowuje.
To najłatwiejsze miejsce do potknięcia przy pisaniu filtrów tak, jakby były JavaScriptem: if (obj.stock) testuje prawdziwość w JavaScript, podczas gdy JSONPath testuje istnienie. Pełną standardową składnię i funkcje znajdziesz w Języku zapytań JSONPath w praktyce.
5. Wyrażenia skryptowe: więcej mocy, mniej przenośności
Oryginalna implementacja Goessnera i jsonpath-plus pozwalają na porównania między węzłami, takie jak [?(@.price < $.max)], a nawet na dowolne skrypty. RFC 9535 wyraźnie pozwala tylko na dostęp do obecnego węzła @ i zabrania $ wewnątrz filtra.
// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]
// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })
Wyrażenia skryptowe tworzą trzy problemy:
- Przenośność spada do zera. Zmiana biblioteki oznacza przepisanie każdego wyrażenia;
- Granica bezpieczeństwa znika. Gdy wyrażenia pochodzą od użytkowników, ewaluacja skryptu oddaje wykonanie kodu;
- Statyczna optymalizacja staje się niemożliwa. Implementacja może tylko interpretować węzeł po węźle, co jest wolne i trudne do cache’owania.
Jeśli naprawdę potrzebujesz porównania między węzłami, przekaż próg jako parametr zapytania i filtruj drugi raz w kodzie aplikacji. To zwykle stabilniejsze niż poleganie na składni rozszerzenia.
6. Kształty wyników: myślisz, że dostajesz wartości, a dostajesz węzły
To samo wyrażenie może zwrócić co najmniej trzy kształty w różnych bibliotekach:
["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }] // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }] // JSON Pointer + value
To bezpośrednio wpływa na trzy rzeczy: jak frontend czyta wyniki, jak wyniki są deduplikowane i jak wyniki zapytań stają się operacjami zapisu. Nodelist zdefiniowany przez RFC 9535 to zbiór referencji do węzłów, ale implementacje JavaScript zwykle zwracają tylko wartości, bo referencje nie przekraczają granic procesów.
Praktyczna rada:
- Zawsze czytaj pojedynczą wartość przez
[0]lubvalue(); nigdy nie zakładaj, że wynik jest skalarem; - Gdy musisz zapisać z powrotem, wybierz implementację zwracającą JSON Pointers albo zbuduj wskaźnik samodzielnie;
- Pusty wynik i wynik
[null]to różne rzeczy; sprawdzaj długość, a nie prawdziwość.
7. Różnice bibliotek: uruchom zestaw zgodności przed migracją
| Możliwość | Oryginalny Goessner | jsonpath-plus | Ścisły RFC 9535 |
|---|---|---|---|
Nawiasy filtrów ?() |
Wymagane | Zgodne | Opcjonalne |
Referencja między korzeniami $.x |
Wspierana | Wspierana | Zabroniona |
| Wyrażenia skryptowe | Wspierane | Wspierane | Zabronione |
| Indeksy i kroki ujemne | Wspierane | Wspierane | Wspierane |
| Zwracanie ścieżek | Konfigurowalne | Konfigurowalne | Nieokreślone |
Funkcje takie jak length() |
Brak | Rozszerzenia | Standardowe funkcje |
Przed migracją zweryfikuj co najmniej cztery rzeczy: czy nawiasy są wymagane, czy $ może wystąpić w filtrze, jak zachowują się kroki ujemne i jaka jest struktura wyniku. Oficjalny zestaw testów zgodności pokrywa większość różnic składni i zapobiega cichym awariom, takim jak „po zmianie biblioteki zniknęły trzy wiersze”.
8. Wydajność i lista kontrolna nadużyć
Wydajność sprowadza się do jednego zdania: JSONPath to pełne przejście, a .. to najdroższy rodzaj pełnego przejścia.
- Dla dokumentów z ponad 100 000 węzłów najpierw zawęź zakres jawną ścieżką, potem filtruj podzbiór;
- Kompiluj wyrażenie raz i używaj ponownie; nie parsuj łańcucha wewnątrz pętli;
- Przełącz się na
jqlub kod dla agregacji (sumowanie, grupowanie, sortowanie); JSONPath tylko wybiera; - Umieszczaj warunki tak wcześnie, jak to możliwe:
$.store.book[?(@.price < 10)]skanuje znacznie mniej nieistotnych węzłów niż$..[?(@.price < 10)].
Lista kontrolna nadużyć:
- [ ] Traktowanie testu istnienia jako testu prawdziwości wartości logicznej;
- [ ] Używanie
$..zamiast znanej głębokości; - [ ] Poleganie na wyrażeniach skryptowych do porównania między węzłami;
- [ ] Zakładanie, że wynik zawsze jest tablicą wartości;
- [ ] Dopuszczanie wyrażeń kontrolowanych przez użytkownika bez limitów długości i zagnieżdżenia.
FAQ
Dlaczego @.stock nadal pasuje, gdy pole ma wartość false?
Bo naga ścieżka wewnątrz filtra znaczy „pole istnieje”, a nie „wartość jest prawdziwa”. Aby przetestować wartość logiczną, porównaj jawnie przez @.stock == true. To największa różnica semantyczna między JSONPath a prawdziwością w JavaScript.
Czy kolejność zejścia rekurencyjnego jest stabilna?
Nie. RFC 9535 określa semantykę zbioru węzłów, a kolejność przejścia zależy od tego, jak implementacja odwiedza dzieci. Gdy potrzebujesz stałej kolejności, zlokalizuj tablicę i czytaj po indeksie albo posortuj wyniki w kodzie aplikacji.
Czy wycinki i filtry można łączyć?
Tak, ale pamiętaj o zakresie. $.store.book[0:2][?(@.stock)] filtruje tylko dwie książki wyprodukowane przez wycinek. Aby filtrować najpierw, a wycinkować potem, napisz $.store.book[?(@.stock)][0:2]; inna kolejność daje inny wynik.
Dlaczego jsonpath-plus wspiera $.expensive, gdy standard tego zabrania?
Bo filtry są definiowane wyłącznie w kategoriach obecnego węzła @. Referencja między korzeniami sprawia, że wyrażenie zależy od kontekstu całego dokumentu, co jest trudne do statycznej analizy i przenoszenia między językami. Przekaż próg jako parametr, zamiast osadzać go w wyrażeniu.
Jak uchronić zejście rekurencyjne przed skanowaniem nieistotnych węzłów?
Pisz jawną ścieżkę, gdy znasz głębokość, najpierw zawęź zakres i filtruj tylko podzbiór, zamiast używać $.. w miejsce znanego poziomu. Umieszczaj warunki tak wcześnie, jak to możliwe: $.store.book[?(@.price < 10)] skanuje znacznie mniej nieistotnych węzłów niż $..[?(@.price < 10)]. Dla dokumentów z ponad 100 000 węzłów zrób to przede wszystkim.