JSONPath, die Fassung, die tatsächlich spezifiziert ist
JSONPath war siebzehn Jahre lang ein Blogbeitrag. RFC 9535 sagt endlich, was es bedeutet.
Jede Aussage auf dieser Seite ist entweder gemessen oder belegt. Wo sie keines von beidem ist, steht das dabei.
JSONPath begann 2007 als Blogbeitrag von Stefan Goessner, der auf etwa zwei Bildschirmseiten Prosa und mit einer Referenzimplementierung in JavaScript eine XPath-artige Abfragesprache für JSON skizzierte. Er war gut genug, dass ihn alle implementierten, und vage genug, dass ihn alle unterschiedlich implementierten. Berücksichtigt der Nachfahren-Operator .. den Wurzelknoten selbst oder nur seine Kinder? Ist [-1] das letzte Element oder ein Fehler? Darf eine Klammer mehrere Selektoren enthalten? Was tut ein Filter, wenn der geprüfte Schlüssel nicht existiert? Was wählt ein Slice mit Schrittweite null? Auf jede dieser Fragen gab es draußen mindestens zwei Antworten, und das Originaldokument klärte keine davon, weil Teile davon an das gerade verfügbare eval() delegiert waren.
RFC 9535 hat das im Februar 2024 behoben. Es ist ein Proposed Standard der IETF, die erste Reifestufe des Standardisierungswegs: eine echte, stabile, normativ formulierte Spezifikation, und kein Internet Standard. Behandeln Sie es wie jeden Proposed Standard: als das, wogegen man neuen Code schreibt, im Wissen, dass viel ausgelieferter Code älter ist.
Das Dokument
Alles Folgende läuft gegen dies:
{
"store": {
"name": "Corner Books",
"book": [
{ "category": "reference", "author": "Nigel Rees",
"title": "Sayings of the Century", "price": 8.95 },
{ "category": "fiction", "author": "Evelyn Waugh",
"title": "Sword of Honour", "price": 12.99 },
{ "category": "fiction", "author": "Herman Melville",
"title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 },
{ "category": "fiction", "author": "J.R.R. Tolkien",
"title": "The Lord of the Rings", "isbn": "0-395-19395-8" }
]
}
}
Beachten Sie das letzte Buch: kein price. In dieser Abwesenheit steckt das meiste interessante Verhalten.
Segmente und Selektoren
Eine Abfrage ist $, gefolgt von einer Folge von Segmenten. $ ist die Wurzel. Jedes Segment wendet einen oder mehrere Selektoren auf jeden gerade vorliegenden Knoten an und erzeugt eine neue Knotenliste. Das Ergebnis einer Abfrage ist immer eine Liste von Knoten, auch wenn sie einen oder keinen enthält, weshalb eine JSONPath-Bibliothek ein Array zurückgibt, wo eine JSON-Pointer-Bibliothek einen Wert liefert.
$.store.book[0].title Punktnotation, Kurzform für Namensselektoren
$['store']['book'][0] Klammernotation, gleiche Bedeutung
$["store"]["book"][0] doppelte Anführungszeichen gehen auch
Die Klammernotation ist keine optionale Verzierung. $.first-name ist kein gültiger Namensselektor, ein Schlüssel mit Bindestrich, Leerzeichen, Punkt oder führender Ziffer muss also $['first-name'] geschrieben werden. Die Klammerform mit einfachen Anführungszeichen ist zugleich die Gestalt eines normalisierten Pfads, des eindeutigen Bezeichners, den RFC 9535 für einen einzelnen Knoten definiert: $['store']['book'][0]['title'].
Die Selektoren:
- Name:
'title'oder"title", wählt ein Objektmitglied. Auf einem Array nichts. - Platzhalter
*: jeder Mitgliedswert eines Objekts, jedes Element eines Arrays.$.store.book[*]und$.store.book.*sind dieselbe Abfrage. - Index: eine ganze Zahl, nullbasiert. Negative zählen vom Ende,
[-1]ist also das letzte Element. Das ist nun spezifiziert und keine Nettigkeit einzelner Bibliotheken. - Slice
start:ende:schritt: halboffen, Ende ausgeschlossen, und ein negativer Schritt läuft rückwärts. Ein Schritt von0wählt nichts, statt einen Fehler zu werfen, die eine Stelle, an der die RFC bewusst von Python abweicht. - Filter
?ausdruck: siehe unten.
Ein Kindsegment kann mehrere kommagetrennte Selektoren enthalten, und sie müssen nicht derselben Art sein. $.store.book[0, -1] sind das erste und das letzte Buch; $.store.book[0, 2:4] mischt einen Index mit einem Slice. Ergebnisse kommen in Selektorreihenfolge zurück, eine Vereinigung darf also legitim denselben Knoten zweimal liefern.
Ein Nachfahrensegment schreibt man mit zwei Punkten: $..author, $..['author'], $..*, $..[0]. Es besucht den Eingangsknoten und jeden seiner Nachfahren und wendet dann seine Selektoren auf jeden an. Die alte Mehrdeutigkeit ist weg: $..store trifft auf dem obigen Dokument sehr wohl $.store, weil das Nachfahrensegment bei der Wurzel selbst beginnt.
Filter, und dort sitzen die Fragen
Innerhalb eines Filters ist @ der gerade geprüfte Knoten, und $ bleibt die Wurzel des gesamten Dokuments, ein Filter kann also einen Wert mit etwas an anderer Stelle im Dokument vergleichen.
Eine nackte Abfrage als Filterausdruck ist ein Existenztest: Sie ist wahr, wenn die Abfrage mindestens einen Knoten wählt. $.store.book[[email protected]] wählt die beiden Bücher mit ISBN. Die Vergleichsoperatoren sind ==, !=, <, <=, >, >=; die logischen sind &&, || und das Präfix !, mit Klammern zur Gruppierung. Klammern um den gesamten Ausdruck sind erlaubt, aber nicht mehr erforderlich, [?(@.price < 10)] und [[email protected] < 10] sind also beide gültig und bedeuten dasselbe.
Vergleichsoperanden sind eingeschränkt. Jede Seite muss ein Literal, eine singuläre Abfrage (nur aus Namens- und Indexselektoren aufgebaut, kann also höchstens einen Knoten wählen) oder ein Funktionsaufruf sein. @.price erfüllt das. @..price und @.book[*].price nicht, und eine Implementierung sollte die Abfrage ablehnen, statt zu raten.
Nun die Regel, über die man stolpert. Eine Abfrage, die nichts wählt, liefert den besonderen Wert Nothing, und Nothing ist weder null noch null als Zahl noch false. Es ist gleich Nothing und sonst nichts, und jeder Ordnungsvergleich, an dem es beteiligt ist, ist falsch. Folgen:
$.store.book[[email protected] < 10] schließt das Tolkien-Buch aus (kein price)
$.store.book[[email protected] >= 10] schließt es ebenfalls aus
$.store.book[[email protected] == null] schließt es ebenfalls aus: Nothing ist nicht null
$.store.book[[email protected]] wählt es, und nur es
Abwesenheit prüft man also, indem man den Existenztest negiert, und == null prüft auf ein vorhandenes Mitglied, das null enthält. Das Spiegelbild ist eine echte Überraschung: $.store.book[[email protected] == @.discount] wählt das Tolkien-Buch, weil beide Seiten Nothing sind und Nothing gleich Nothing ist.
Ein Vergleich zwischen zwei verschiedenen Typen ist nie ein Fehler. Gleichheit über Typgrenzen ist schlicht falsch, und eine Ordnung ist nur zwischen zwei Zahlen oder zwei Zeichenketten definiert, @.price > "10" ist also für jedes Buch falsch.
Funktionserweiterungen
Fünf sind definiert, und sie sind typisiert, length(@.book[*]) ist also ein Typfehler statt einer Laufzeitüberraschung.
| Funktion | Nimmt | Gibt |
|---|---|---|
length() |
einen Wert | Unicode-Skalarwerte in einer Zeichenkette, Elemente in einem Array, Mitglieder in einem Objekt, sonst Nothing |
count() |
eine Knotenliste | wie viele Knoten sie gewählt hat |
match() |
eine Zeichenkette und eine Regex | wahr, wenn die Regex auf die gesamte Zeichenkette passt |
search() |
eine Zeichenkette und eine Regex | wahr, wenn die Regex irgendwo in der Zeichenkette passt |
value() |
eine Knotenliste | den Wert, wenn die Liste genau einen Knoten enthält, sonst Nothing |
count() gibt es, weil eine nicht singuläre Abfrage kein Vergleichsoperand sein kann; count(@.book[[email protected]]) == 2 ist also die Art zu sagen „hat genau zwei Bücher mit ISBN“. value() löst dasselbe Problem von der anderen Seite: $[?value(@..name) == 'Corner Books'] funktioniert, weil value() eine Mehrknotenabfrage zu einem einzelnen vergleichbaren Wert zusammenzieht oder zu Nothing, wenn sie mehr oder weniger als einen Knoten getroffen hat.
Der Regex-Dialekt ist I-Regexp (RFC 9485), eine bewusst kleine Teilmenge, die sich auf XSD-reguläre Ausdrücke abbildet. Es ist nicht PCRE. Erwarten Sie keine Lookaheads und keine Rückwärtsreferenzen, und denken Sie daran, dass match(@.category, 'fic') für "fiction" falsch ist, während search(@.category, 'fic') wahr ist.
Was nicht in der Sprache ist
Drei Dinge, nach denen man weiterhin greift, existieren nicht. Skriptausdrücke, die Form [(...)] aus dem ursprünglichen Beitrag, sind weg, und mit ihnen die Abhängigkeit von eval(). Es gibt keinen Eltern-Operator: Eine Abfrage läuft nur abwärts; wenn Sie das umschließende Objekt brauchen, wählen Sie das Objekt und filtern über das Kind. Und die Pseudo-Eigenschaft length ist weg, $.store.book[(@.length-1)] ist also keine Abfrage. Schreiben Sie $.store.book[-1].
Durchgerechnete Beispiele
| Ausdruck | Ergebnis |
|---|---|
$.store.name |
"Corner Books" |
$.store.book[*].author |
alle vier Autoren |
$..isbn |
die beiden ISBN-Zeichenketten |
$.store.book[-1].title |
"The Lord of the Rings" |
$.store.book[1:3].title |
"Sword of Honour", "Moby Dick" |
$.store.book[::2].title |
"Sayings of the Century", "Moby Dick" |
$.store.book[0,-1].title |
"Sayings of the Century", "The Lord of the Rings" |
$.store.book[[email protected] < 10].title |
"Sayings of the Century", "Moby Dick" |
$.store.book[[email protected]].title |
"The Lord of the Rings" |
$.store.book[[email protected] > $.store.book[0].price].title |
"Sword of Honour", "Moby Dick" |
$.store.book[?match(@.category, 'fic.*')].author |
Waugh, Melville, Tolkien |
$.store.book[?search(@.author, 'Mel')].title |
"Moby Dick" |
$.store.book[?length(@.title) > 16].title |
"Sayings of the Century", "The Lord of the Rings" |
Fügen Sie das Dokument und einen dieser Ausdrücke in den JSONPath-Tester ein, um die Knotenliste samt normalisiertem Pfad jedes Treffers zu sehen. Das ist der schnellste Weg zu prüfen, ob Ihre Bibliothek bei negativen Indizes und fehlenden Schlüsseln mit der RFC übereinstimmt.
Wann etwas anderes richtig ist
JSONPath wählt Knoten aus. Das ist die ganze Aufgabe, und drei andere Abfragewerkzeuge überschneiden sich damit:
JSON Pointer (RFC 6901) adressiert genau einen Knoten, ohne Platzhalter, ohne Filter und ohne Mehrdeutigkeit: /store/book/0/title, mit ~1 für einen wörtlichen Schrägstrich und ~0 für eine wörtliche Tilde. Damit zeigen die Fehler von JSON Schema und die Operationen von JSON Patch. Wenn Sie die Adresse kennen, nehmen Sie einen Pointer.
jq ist eine vollwertige Stromverarbeitungssprache mit eigenem Wertmodell, Arithmetik, Variablen und Ausgabeformatierung. Sie transformiert; JSONPath wählt nur aus. Wenn Ihr Ausdruck beginnt, neue Objekte zu bauen, wollen Sie jq.
JMESPath liegt dazwischen: eine spezifizierte Abfragesprache, älter als RFC 9535, mit Projektionen und eigener Funktionsbibliothek, und einer Syntax, die JSONPath nahe genug ist, um zu verwirren, und verschieden genug, um zu brechen. Wählen Sie eine pro Codebasis.
Für den Alltagsfall, ein Payload auf die Felder zu kürzen, die Sie interessieren, erledigt das Filterwerkzeug die Sache ganz ohne Ausdruckssprache, und der Viewer zeigt Ihnen die Form, die Sie abfragen. Wenn Sie wissen wollen, was sich zwischen zwei Payloads geändert hat, ist das Diff und keine Abfrage.