JSONPath-Tester

JSONPath-Abfragen nach RFC 9535 gegen JSON testen: Selektoren, Filter, alle fünf Funktionen und den normalisierten Pfad jedes Treffers — alles im Browser.

JSONPath-Abfrage
JSON
Treffer: 2
  • Pfad$['store']['book'][0]['title']
    "Sayings of the Century"
  • Pfad$['store']['book'][2]['title']
    "Moby Dick"

Eine Abfragesprache, die endlich einen Standard hat

JSONPath ist für JSON, was XPath für XML ist: eine kleine Sprache, um auf Teile eines Dokuments zu zeigen. Sie schreiben einen Ausdruck wie $.store.book[0].title, und er wählt die passenden Knoten aus. Die Idee stammt aus einem Blogbeitrag von Stefan Goessner aus dem Jahr 2007, und siebzehn Jahre lang war dieser Beitrag die einzige Referenz — was bedeutete, dass jede Bibliothek die Lücken anders füllte. Was wählt ein bloßes $..? Kommt $[1,2] garantiert in der Reihenfolge zurück? Wie vergleicht ein Filter einen Wert, der nicht da ist? Fragen Sie drei JSONPath-Bibliotheken, und Sie erhalten vielleicht drei Antworten.

RFC 9535, veröffentlicht 2024, hat all das endlich festgelegt. Dieser Tester zielt auf diesen Standard, nicht auf die Folklore. Er führt die Abfrage so aus, wie die RFC es sagt, zeigt Ihnen jeden Treffer samt seinem genauen Ort und — wichtig — weist eine Abfrage ab, die in einem älteren Dialekt gültig ist, aber nicht in RFC 9535, und sagt Ihnen, wo das Problem liegt, statt stillschweigend etwas Nicht-Standardisiertes zu tun, das ein anderes Werkzeug anders täte.

Segmente und Selektoren

Eine Abfrage ist der Wurzel-Bezeichner $ gefolgt von einer Folge von Segmenten, und jedes Segment wendet einen oder mehrere Selektoren auf die Knoten an, die es erhält. Es gibt fünf Selektoren:

  • Name — $.store oder $["store"] wählt den Wert eines Members. Die Punktform ist eine Abkürzung; die Klammer-und-Anführungszeichen-Form funktioniert für jeden Schlüssel, auch für einen mit Leerzeichen oder Satzzeichen.
  • Wildcard — * wählt jeden Member eines Objekts oder jedes Element eines Arrays.
  • Index — [0] wählt ein Array-Element, und ein negativer Index zählt vom Ende, also ist [-1] das letzte.
  • Slice — [start:end:step] wählt einen Bereich, genau wie in Python: [1:3] sind die Elemente 1 und 2, [::-1] kehrt um, [::2] nimmt jedes zweite.
  • Filter — [?<Ausdruck>] behält nur die Elemente oder Member, für die der Ausdruck wahr ist (siehe unten).

Eine Klammer kann mehrere Selektoren zugleich enthalten: [0, 2, "title"] wählt drei Dinge in einem Segment. Und ein Segment kann ein Kindsegment (ein einzelner Punkt oder eine Klammer) oder ein Nachfahren-Segment (..) sein, das den Knoten und alle seine Nachfahren durchsucht — $..author findet jeden author irgendwo im Dokument.

Filter und die vier Arten, das Nichts zu vergleichen

Ein Filterselektor prüft jedes Element mit einem logischen Ausdruck, in dem @ auf das aktuelle Element und $ auf das ganze Dokument verweist. Der Ausdruck kann Werte vergleichen (==, !=, <, <=, >, >=), Tests mit && und || und ! verknüpfen und einfach auf Existenz prüfen: $..book[[email protected]] behält die Bücher, die eine ISBN haben, denn @.isbn wählt einen Knoten nur, wenn der Schlüssel vorhanden ist.

Der heikle Teil ist der Vergleich von etwas, das nicht da ist. Eine Abfrage links oder rechts eines Vergleichs ergibt einen Wert oder nichts. Die RFC definiert das genau: nichts gleicht nichts, nichts gleicht keinem echten Wert, und jeder Ordnungstest (<, >) gegen nichts ist einfach falsch. So überspringt @.price < 10 stillschweigend ein Element ohne price, statt einen Fehler zu werfen — was meist gewünscht ist und immer das, was der Standard sagt.

Die fünf Funktionen

RFC 9535 fügt fünf Funktionen hinzu, die Sie innerhalb eines Filters aufrufen können:

  • length() — die Länge einer Zeichenkette (in Zeichen gezählt), eines Arrays oder eines Objekts.
  • count() — wie viele Knoten eine Abfrage wählt, sodass Sie nach Kardinalität filtern können: [?count(@.chapters) > 3].
  • value() — der einzelne Wert, den eine Abfrage wählt, oder nichts, wenn sie null oder viele wählt.
  • match() — ob eine Zeichenkette vollständig auf einen regulären Ausdruck passt.
  • search() — ob ein regulärer Ausdruck irgendwo in einer Zeichenkette vorkommt.

Der Standard ist streng darin, wie sie verwendet werden, und dieser Tester erzwingt das beim Parsen: length() nimmt genau einen Wert, also ist length(@.*) — eine Abfrage, die viele Knoten wählen könnte — ein Syntaxfehler und keine Abfrage, die sich stillschweigend falsch verhält. Ebenso gibt match() wahr oder falsch zurück, sodass match(@.a, "x") == true abgelehnt wird, denn ein logisches Ergebnis vergleicht man nicht.

Die regulären Ausdrücke sind I-Regexp

match() und search() verwenden nicht die regulären Ausdrücke von JavaScript; sie verwenden I-Regexp (RFC 9485), eine kleine, portable Teilmenge, die sich in allen Sprachen gleich verhalten soll. Das meiste ist, was Sie erwarten — Zeichenklassen, Quantoren, Alternation, Unicode-Eigenschaften wie \p{Lu} für einen Großbuchstaben. Die eine Falle ist der Punkt: In I-Regexp passt . auf jedes Zeichen außer einem Wagenrücklauf oder Zeilenvorschub, was heißt, dass er sehr wohl auf die Unicode-Zeilentrenner U+2028 und U+2029 passt, die ein JavaScript-Punkt ausschließt. Dieser Tester kompiliert I-Regexp getreu, sodass sich ein Muster hier so verhält, wie es ein konformer Server auswerten würde.

Der Unterschied zwischen den beiden Funktionen ist nur die Verankerung: match verlangt, dass die ganze Zeichenkette passt, während search das Muster irgendwo darin sucht. match(@, "a.*") akzeptiert "abc"; search(@, "b") akzeptiert jede Zeichenkette, die ein b enthält.

Normalisierte Pfade und Ausführung im Browser

Für jeden Treffer zeigt dieser Tester einen Normalized Path — den kanonischen Ort, den die RFC definiert, geschrieben in der Klammer-und-Anführungszeichen-Form: $['store']['book'][0]['author']. Anders als die Abfrage, die viele Knoten wählen kann, zeigt ein normalisierter Pfad auf genau einen, nur mit Namens- und Indexselektoren und einem festen Anführungszeichenstil. Er ist die Antwort auf „woher kam dieser Treffer", und er ist es, der einen Wildcard-Treffer wieder in eine Menge konkreter Orte verwandelt. Die meisten Tester zeigen die Werte und überlassen Ihnen die Pfade; dieser zeigt beides.

Die ganze Engine läuft in Ihrem Browser — das JSON wird geparst und die Abfrage auf Ihrem eigenen Gerät ausgewertet, und nichts, was Sie einfügen, wird hochgeladen, gespeichert oder protokolliert. Sie ist gegen die offizielle JSONPath Compliance Test Suite validiert, sodass ihre Antworten mit dem Standard übereinstimmen und nicht mit der Auslegung einer einzelnen Bibliothek.

Häufig gestellte Fragen

Welchen JSONPath-Dialekt verwendet das?
RFC 9535, den IETF-Standard von 2024, und es ist gegen die offizielle JSONPath Compliance Test Suite validiert. Es akzeptiert bewusst nicht die älteren Goessner-Konventionen, wo sie von der RFC abweichen; eine solche Abfrage wird mit der Position des Problems abgelehnt, damit Sie sie korrigieren können.
Warum wurde meine Abfrage abgelehnt, obwohl sie in einem anderen Werkzeug funktioniert?
Weil jenes Werkzeug den vor-standardisierten Goessner-Konventionen folgt, die sich von RFC 9535 an mehreren Stellen unterscheiden — ein bloßes $.., Skriptausdrücke wie [(@.length-1)], Indizes mit führender Null und nicht in Anführungszeichen gesetzte Namen mit Sonderzeichen sind alle nicht standardisiert. Die RFC ersetzte sie durch wohldefinierte Entsprechungen, und dieser Tester hält sich an die RFC.
Was ist ein normalisierter Pfad?
Der kanonische Ort eines einzelnen Knotens, geschrieben in der Klammer-und-Anführungszeichen-Form, die die RFC definiert, z. B. $['store']['book'][0]['author']. Eine Abfrage kann auf viele Knoten passen; jeder Treffer hat genau einen normalisierten Pfad, weshalb der Tester ihn neben jedem Wert anzeigt.
Wie behandelt ein Filter einen fehlenden Wert?
Als nichts, mit Regeln, die die RFC festlegt: nichts gleicht nichts, nichts gleicht keinem echten Wert, und jeder Ordnungsvergleich (<, >), der nichts einbezieht, ist falsch. So überspringt @.price < 10 einfach ein Element ohne price, statt einen Fehler auszulösen.
Verwenden match() und search() die regulären Ausdrücke von JavaScript?
Nein. Sie verwenden I-Regexp (RFC 9485), eine portable Teilmenge. Der wichtigste praktische Unterschied ist der Punkt, der auf alles außer Wagenrücklauf und Zeilenvorschub passt — einschließlich U+2028 und U+2029, die ein JavaScript-Punkt ausschließt. Dieser Tester kompiliert I-Regexp getreu, damit die Ergebnisse einer konformen Implementierung entsprechen.
Was ist der Unterschied zwischen match und search?
Die Verankerung. match() verlangt, dass die ganze Zeichenkette auf das Muster passt, als wäre sie von Ankern umgeben; search() gelingt, wenn das Muster irgendwo in der Zeichenkette vorkommt. Alles andere ist identisch.
Wird mein JSON an einen Server gesendet?
Nein. Das Dokument wird geparst und die Abfrage vollständig in Ihrem Browser ausgewertet, und nichts, was Sie einfügen, wird hochgeladen oder protokolliert. Es ist sicher, gegen eine echte API-Antwort oder eine Konfigurationsdatei zu testen.