JSONPath-Filter im Detail: Rekursion, Slices und Mehrfachbedingungen
Fazit: Rekursiver Abstieg, Slices und Mehrfachbedingungs-Filter sind der Bereich, in dem JSONPath mächtig und missbraucht wird. Behandelt Filtersemantik, Ergebnisformen und Bibliotheksunterschiede.

Kurz gesagt: Zuerst eine bekannte Tiefe lokalisieren und dann Slices und Filter ergänzen, statt zu $.. zu greifen; ein nackter Pfad in einem Filter prüft nur die Existenz, also einen Boolean mit @.stock == true prüfen.
Jeder kann
$.store.book[0].titleschreiben. Was JSONPath tatsächlich bricht, ist$..mit Slices und mehreren Bedingungen zu kombinieren und dann zu viele Ergebnisse, zu wenige Ergebnisse oder eine unerwartete Reihenfolge zu bekommen.
1. Ein Beispieldokument
{
"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"
}
Jeder Ausdruck unten verwendet dieses Dokument als Eingabe. Behalten Sie zwei Prämissen im Kopf: Das Ergebnis ist eine Knotenmenge, und ihre Reihenfolge folgt der Dokumentreihenfolge, nicht der Reihenfolge, in der Bedingungen geschrieben wurden. Innerhalb eines Filters verweist @ nur auf den aktuellen Kandidatenknoten.
2. Rekursiver Abstieg $..: bequem, aber teuer
.. bedeutet „von der aktuellen Position aus rekursiv in beliebiger Tiefe suchen“. RFC 9535 definiert es als Descendant-Segment, und es lässt sich mit Namen, Platzhaltern, Indizes, Slices und Filtern kombinieren:
$..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
Drei Dinge sind zu verstehen:
$..bookfindet jeden Knoten namensbook, unabhängig von seiner Tiefe;- Die Traversierung ist tiefenorientiert, und die Ausgabereihenfolge hängt davon ab, wie die Implementierung Kinder besucht;
..kann keinen Index nutzen, seine Kosten skalieren also mit der Gesamtzahl der Knoten.
Folglich können sich $..price und $.store.book[*].price in ihrer Ausgabe nur um den Fahrradpreis unterscheiden, in den Kosten aber um eine Größenordnung. Kennen Sie die Tiefe, schreiben Sie immer den ausdrücklichen Pfad. Wollen Sie nur die Grenzen der beiden Pfadsprachen vergleichen, siehe JSONPath vs. JSON Pointer.
3. Array-Slices und Platzhalter: drei Fallen
Slice-Syntax ist [start:end:step], einschließlich des Anfangs und ausschließlich des Endes, negative Zahlen zählen von hinten:
| Ausdruck | Bedeutung | Ergebnis (mit vier Büchern) |
|---|---|---|
[0:2] |
Erste zwei Bücher | Bücher 1 und 2 |
[1:] |
Vom zweiten Buch bis zum Ende | Bücher 2, 3 und 4 |
[:2] |
Erste zwei Bücher | Bücher 1 und 2 |
[-2:] |
Letzte zwei Bücher | Bücher 3 und 4 |
[::2] |
Jedes zweite Buch | Bücher 1 und 3 |
[::-1] |
Umgekehrte Reihenfolge | Bücher 4, 3, 2 und 1 |
Drei Fallen:
- Eine Schrittweite von null ist ungültig. RFC 9535 verlangt eine Schrittweite ungleich null, manche Implementierungen liefern stillschweigend eine leere Menge;
- Negative Schrittweiten werden nicht überall unterstützt. Die ursprüngliche Goessner-Implementierung unterstützt sie, manche Minimalimplementierungen akzeptieren nur positive Schrittweiten;
- Filtern nach einem Slice erweitert den Bereich nicht.
[0:2][?(@.stock)]filtert nur die zwei Bücher aus dem Slice; es geht nicht zum ganzen Array zurück.
Der Platzhalter [*] lässt sich mit Slices kombinieren, aber [*] liefert „alle Kindknoten“. Sind diese Kinder Objekte, ergibt ein angehängtes .title alle Titel. Ist ein Kind selbst ein Array, faltet .title keine weitere Ebene implizit. Schreiben Sie [*] erneut, wenn Sie expandieren müssen; erwarten Sie keine implizite Faltung.
4. Mehrfachbedingungs-Filter: Vergleiche, Logik und Existenz
Ein Filter [?(...)] kann vier Arten von Konstrukten enthalten: Vergleiche, logische Operatoren, Existenzprüfungen und Funktionen. Eine häufige Mehrfachbedingungsabfrage sieht so aus:
$..book[?(@.price < 10 && @.stock)]
Sie bedeutet „Preis unter 10 und stock ist wahrheitswertig“. Beachten Sie einige semantische Details:
@.stockist eine Existenzprüfung, nicht „der Wert ist true“. Existiert das Feld, sein Wert ist aberfalse, gilt die Bedingung dennoch. Um einen Boolean-Wert zu prüfen, müssen Sie@.stock == trueschreiben;&&bindet stärker als||. Setzen Sie Klammern beim Mischen, statt sich auf das Gedächtnis zu verlassen;- Beide Seiten eines Vergleichs müssen denselben Typ haben. Zahlen und Strings werden nicht implizit konvertiert;
@.price < "10"ist in strengen Implementierungen falsch oder ein Fehler; - Ein fehlendes Feld macht einen Vergleich immer falsch.
@.missing < 10löst keinen Fehler aus; es passt einfach nichts.
Das ist die leichteste Stelle, um zu stolpern, wenn man Filter wie JavaScript schreibt: if (obj.stock) prüft in JavaScript den Wahrheitswert, während JSONPath die Existenz prüft. Zur vollständigen Standardsyntax und zu Funktionen siehe Die JSONPath-Abfragesprache in der Praxis.
5. Skriptausdrücke: mehr Macht, weniger Portabilität
Die ursprüngliche Goessner-Implementierung und jsonpath-plus erlauben Vergleiche über Knoten hinweg wie [?(@.price < $.max)] und sogar beliebige Skripte. RFC 9535 erlaubt ausdrücklich nur den Zugriff auf den aktuellen Knoten @ und verbietet $ innerhalb eines Filters.
// 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 })
Skriptausdrücke erzeugen drei Probleme:
- Die Portabilität sinkt auf null. Ein Bibliothekswechsel heißt, jeden Ausdruck neu zu schreiben;
- Die Sicherheitsgrenze verschwindet. Kommen Ausdrücke von Nutzern, übergibt die Skriptauswertung Codeausführung;
- Statische Optimierung wird unmöglich. Die Implementierung kann nur Knoten für Knoten interpretieren, was langsam und schwer zu cachen ist.
Brauchen Sie wirklich einen Vergleich über Knoten hinweg, übergeben Sie den Schwellenwert als Abfrageparameter und filtern Sie im Anwendungscode ein zweites Mal. Das ist meist stabiler, als sich auf eine Erweiterungssyntax zu verlassen.
6. Ergebnisformen: Sie denken, Sie bekommen Werte, aber Sie bekommen Knoten
Derselbe Ausdruck kann über Bibliotheken hinweg mindestens drei Formen zurückgeben:
["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
Das beeinflusst drei Dinge direkt: wie das Frontend Ergebnisse liest, wie Ergebnisse dedupliziert werden und wie Abfrageergebnisse zu Schreiboperationen werden. Die von RFC 9535 definierte Nodelist ist eine Menge von Knotenreferenzen, aber JavaScript-Implementierungen geben meist nur Werte zurück, weil Referenzen Prozessgrenzen nicht überqueren können.
Praktische Ratschläge:
- Lesen Sie einen einzelnen Wert immer über
[0]odervalue(); nehmen Sie nie an, das Ergebnis sei ein Skalar; - Müssen Sie zurückschreiben, wählen Sie eine Implementierung, die JSON Pointer zurückgibt, oder bauen Sie den Pointer selbst;
- Ein leeres Ergebnis und ein Ergebnis
[null]sind verschieden; prüfen Sie die Länge statt den Wahrheitswert.
7. Bibliotheksunterschiede: vor der Migration die Konformitätssuite ausführen
| Fähigkeit | Original Goessner | jsonpath-plus | Strenges RFC 9535 |
|---|---|---|---|
Filterklammern ?() |
Erforderlich | Kompatibel | Optional |
Referenz über die Wurzel $.x |
Unterstützt | Unterstützt | Verboten |
| Skriptausdrücke | Unterstützt | Unterstützt | Verboten |
| Negative Indizes und Schrittweiten | Unterstützt | Unterstützt | Unterstützt |
| Pfade zurückgeben | Konfigurierbar | Konfigurierbar | Nicht spezifiziert |
Funktionen wie length() |
Keine | Erweiterungen | Standardfunktionen |
Prüfen Sie vor einer Migration mindestens vier Dinge: ob Klammern erforderlich sind, ob $ innerhalb eines Filters vorkommen darf, wie sich negative Schrittweiten verhalten und die Ergebnisstruktur. Die offizielle Konformitätstestsuite deckt die meisten Syntaxunterschiede ab und verhindert stille Fehlschläge wie „nach dem Bibliothekswechsel sind drei Zeilen verschwunden“.
8. Performance und Missbrauchs-Checkliste
Performance lässt sich in einem Satz zusammenfassen: JSONPath ist ein vollständiger Durchlauf, und .. ist die teuerste Art des vollständigen Durchlaufs.
- Bei Dokumenten mit mehr als 100.000 Knoten zuerst den Bereich mit einem ausdrücklichen Pfad verengen und dann die Teilmenge filtern;
- Einen Ausdruck einmal kompilieren und wiederverwenden; parsen Sie den String nicht in einer Schleife neu;
- Für Aggregation (Summieren, Gruppieren, Sortieren) auf
jqoder Code umstellen; JSONPath wählt nur aus; - Bedingungen so früh wie möglich setzen:
$.store.book[?(@.price < 10)]scannt weit weniger irrelevante Knoten als$..[?(@.price < 10)].
Missbrauchs-Checkliste:
- [ ] Eine Existenzprüfung als Boolean-Wahrheitsprüfung behandeln;
- [ ]
$..statt einer bekannten Tiefe verwenden; - [ ] Für Vergleiche über Knoten hinweg von Skriptausdrücken abhängen;
- [ ] Annehmen, das Ergebnis sei immer ein Array von Werten;
- [ ] Von Nutzern kontrollierte Ausdrücke ohne Längen- und Verschachtelungsgrenzen zulassen.
FAQ
Warum passt @.stock noch, wenn das Feld false ist?
Weil ein nackter Pfad innerhalb eines Filters „das Feld existiert“ bedeutet, nicht „der Wert ist true“. Um den Boolean zu prüfen, vergleichen Sie ausdrücklich mit @.stock == true. Das ist der größte semantische Unterschied zwischen JSONPath und JavaScript-Wahrheitswerten.
Ist die Reihenfolge des rekursiven Abstiegs stabil?
Nein. RFC 9535 spezifiziert Knotenmengen-Semantik, und die Traversierungsreihenfolge hängt davon ab, wie die Implementierung Kinder besucht. Brauchen Sie eine feste Reihenfolge, lokalisieren Sie das Array und lesen Sie nach Index oder sortieren Sie die Ergebnisse im Anwendungscode.
Können Slices und Filter kombiniert werden?
Ja, aber beachten Sie den Bereich. $.store.book[0:2][?(@.stock)] filtert nur die zwei Bücher aus dem Slice. Um zuerst zu filtern und dann zu schneiden, schreiben Sie $.store.book[?(@.stock)][0:2]; eine andere Reihenfolge ergibt ein anderes Ergebnis.
Warum unterstützt jsonpath-plus $.expensive, obwohl der Standard es verbietet?
Weil Filter nur über den aktuellen Knoten @ definiert sind. Eine Referenz über die Wurzel macht einen Ausdruck vom gesamten Dokumentkontext abhängig, was statisch schwer zu analysieren und sprachübergreifend schwer zu übertragen ist. Übergeben Sie den Schwellenwert als Parameter, statt ihn in den Ausdruck einzubetten.
Wie verhindere ich, dass rekursiver Abstieg irrelevante Knoten scannt?
Schreiben Sie den ausdrücklichen Pfad, wann immer Sie die Tiefe kennen, verengen Sie zuerst den Bereich und filtern Sie nur die Teilmenge, statt $.. anstelle einer bekannten Ebene zu verwenden. Setzen Sie Bedingungen so früh wie möglich: $.store.book[?(@.price < 10)] scannt weit weniger irrelevante Knoten als $..[?(@.price < 10)]. Bei Dokumenten mit mehr als 100.000 Knoten tun Sie das zuerst.