Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Mon livre « Cyberstructure »

Ève

RFC 9535: JSONPath: Query Expressions for JSON

Date de publication du RFC : Février 2024
Auteur(s) du RFC : S. Gössner (Fachhochschule Dortmund), G. Normington, C. Bormann (Universität Bremen TZI)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF jsonpath
Première rédaction de cet article le 14 février 2025


Le langage JSONPath sert à désigner une partie d'un document JSON. Il est donc utile pour, par exemple, extraire des valeurs qui nous intéressent.

Il existe de nombreux langages pour cela (une liste partielle figure à la fin de l'article), pas forcément normalisés mais mis en œuvre dans tel ou tel logiciel (dont, évidemment, jq). Ce RFC est, je crois, la première normalisation formelle d'un tel langage. Voici tout de suite un exemple, utilisant le logiciel jpq, sur la la base des arbres parisiens :

% jpq '$[*].libellefrancais' arbres.json
[
  "Marronnier",
  "Aubépine",
  "Cerisier à fleurs",
…
  

Cela se lit « affiche les noms d'espèce [libellefrancais, dans le fichier] de tous [vous avez vu l'astérisque] les arbres ». (Avec jq, on aurait écrit jq '.[].libellefrancais' arbres.json.) Voyons maintenant les détails.

JSONPath travaille sur des documents JSON, JSON étant normalisé dans le RFC 8259. Il n'est sans doute pas utile de présenter JSON, qui est utilisé partout aujourd'hui. Notons simplement que l'argument d'une requête JSONPath, écrit en JSON, est ensuite modélisé comme un arbre, composé de nœuds. Le JSON {"foo": 3, "bar": 0} a ainsi une racine de l'arbre sous laquelle se trouvent deux nœuds, avec comme valeur 3 et 0 (les noms des membres JSON, foo et bar, ne sont pas sélectionnables par JSONPath et donc pas considérés comme nœuds).

Vous êtes impatient·e de vous lancer ? On va utiliser le logiciel jpq cité plus haut. Pensez à installer un Rust très récent puis :

cargo install jpq
  

Ou bien, si vous voulez travailler à partir des sources :

git clone https://codeberg.org/KMK/jpq.git
cd jpq
cargo install --path .
  

Puis pensez à modifier votre chemin de recherche d'exécutables si ce n'est pas déjà fait (notez que l'excellente Julia Evans vient justement de faire un très bon article sur ce sujet). Testez :

% echo '["bof"]' | jpq '$[0]'
[
  "bof"
]
  

Les choses utiles à savoir pour écrire des expressins JSONPath :

  • Le dollar indique la racine de l'arbre JSON.
  • L'expression JSONPath est faite de segments, chaque segment permettant de filtrer une partie du document JSON (dans l'exemple immédiatement au-dessus, [0] est un segment sélectionnant le premier élément d'un tableau, ici, le seul).
  • Des sélecteurs sont utilisés dans les segments pour sélectionner via le nom d'un membre JSON ou via un indice (0 dans l'exemple plus haut).

La grammaire complète (en ABNF, RFC 5234) figure dans l'annexe A du RFC.

Allez, passons aux exemples, avec les arbres parisiens comme document JSON. Le quatrième arbre du document (on part de zéro) :

% jpq '$[3]' arbres.json
[
  {
    "idbase": 143057,
    "typeemplacement": "Arbre",
    "domanialite": "CIMETIERE",
    "arrondissement": "PARIS 20E ARRDT",
    "complementadresse": null,
    "numero": null,
    "adresse": "CIMETIERE DU PERE LACHAISE / DIV 41",
    "idemplacement": "D00000041050",
    "libellefrancais": "Erable",
…
  

Tous les genres (dans la classification de Linné) :

% jpq '$[*].genre' arbres.json  
[
  "Aesculus",
  "Crataegus",
  "Prunus",
…
  

Les sélecteurs par nom peuvent s'écrire avec un point, comme ci-dessus, ou bien entre crochets avec des apostrophes (notez comme c'est moins pratique pour le shell Unix) :

% jpq "\$[*]['genre']" arbres.json 
[
  "Aesculus",
  "Crataegus",
  "Prunus",
…
  

La forme avec les crochets est la forme canonique pour une expression JSONPath. La notation avec un point n'est acceptée que pour les noms, pas pour les indices :

% echo '[1, 2, 3]' | jpq '$[1]'
[
  2
]

% echo '[1, 2, 3]' | jpq '$.1' 
Error:   × Invalid JSONPath: at position 2, in dot member name, must start with lowercase alpha or '_'
   ╭────
 1 │ $.1
   ·   ▲
   ·   ╰── in dot member name, must start with lowercase alpha or '_'
   ╰────
  

Tous les arbres qui font plus de 50 mètres de haut :

% jpq '$[?@.hauteurenm > 50].hauteurenm' < arbres.json          
[
  120,
  69,
  100,
  67,
  58,
  74,
  911
]
  

L'arobase indique le nœud en cours d'évaluation. Et, oui, l'arbre de 911 mètres de haut est une erreur, chose courante dans les données ouvertes.

Comme vous avez vu, la requête JSONPath renvoie toujours une liste de nœuds, affichée par jpq dans la syntaxe des tableaux JSON.

Vous avez vu, dans l'exemple où on cherchait les plus grands arbres, que JSONPath permet de faire des comparaisons. Vous avez droit à tous les opérateurs booléens classiques. Ici, l'identificateur de tous les arbres situés dans un cimetière :

% jpq '$[?@.domanialite == "CIMETIERE"].idbase' < arbres.json 
[
  143057,
  165296,
  166709,
…
  

JSONPath permet d'ajouter des fonctions, qui ne sont utilisables que pour les comparaisons (je ne crois pas qu'on puisse mettre leur résultat dans la valeur retournée par JSONPath). Plusieurs d'entre elles sont définies par ce RFC, comme length(), mais pas forcément mises en œuvre dans tous les programmes. Et pour compliquer les choses, des implémentations de JSONPath ajoutent leurs propres fonctions non enregistrées et qui rendent l'expression non portable.

Deux de ces fonctions, search() et match(), prennent en paramètre une expression rationnelle, à la syntaxe du RFC 9485.

Les expressions JSONPath, quand on les envoie via l'Internet, peuvent être marquées avec le type application/jsonpath, désormais enregistré (section 3 du RFC). En plus, un nouveau registre IANA est créé, pour stocker les fonctions mentionnées au paragraphe précédent. Pour ajouter des fonctions à ce registre, il faut passer par la procédure « Examen par un expert » du RFC 8126.

La section 4, sur la sécurité, est très détaillée. JSONPath était souvent mis en œuvre en passant directement l'expression ou une partie d'entre elle au langage sous-jacent (typiquement JavaScript). Inutile de détailler les énormes problèmes de sécurité que cela pose si l'expression évaluée est, en tout ou en partie, fournie par un client externe (par exemple via un formulaire Web), permettant ainsi l'injection de code. Une validation de ces expressions avant de les passer étant irréaliste, le RFC insiste sur le fait qu'une mise en œuvre de JSONPath doit avoir son propre code et ne pas compter sur une évaluation par le langage de programmation sous-jacent.

Même ainsi, une expression JSONPath peut permettre une attaque par déni de service, si une expression mène à une consommation de ressources excessive. (Cf. la section 10 du RFC 8949 et la section 8 du RFC 9485.)

La section 1.2 de notre RFC décrit l'histoire un peu compliquée de JSONPath. (Une des raisons pour lesquelles il existe plusieurs langages de requête dans JSON est que JSONPath a mis du temps à être terminé.) L'origine de JSONPath date de 2007, dans cet article de Gössner (un des auteurs du RFC). L'interprétation de ce premier JSONPath était souvent déléguée au langage sous-jacent (le eval() de JavaScript…) avec les problèmes de sécurité qu'on imagine. Le JSONPath actuel se veut portable, au sens où il ne dépend pas d'un langage d'implémentation particulier, ni d'un moteur d'expressions rationnelles particulier. L'inconvénient est qu'il n'est pas forcément compatible avec l'ancien JSONPath.

JSONPath a aussi été inspiré par le XPath de XML (cf. annexe B de notre RFC, qui détaille cette inspiration, et compare les deux langages). On peut aussi comparer JSONPath à JSON Pointer (RFC 6901), ce qui est fait dans l'annexe C.

Il existe bien sûr d'autres mises en œuvre de JSONPath que jpq. Mais attention : certaines sont très anciennes (comme Python JSONPath RW) et ne correspondent pas à ce qui est normalisé dans le RFC. Essayons Python JSONPath Next-Generation :

% python
Python 3.13.2 (main, Feb  5 2025, 01:23:35) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from  jsonpath_ng.ext import filter, parse
>>> import json
>>> data =  json.load(open("arbres.json"))
>>> expr = parse("$[?@.hauteurenm > 50].hauteurenm")
>>> result = expr.find(data)
>>> for r in result:
...     print(r.value)
...     
120
69
100
67
58
74
911

D'autres langages de requête dans le JSON existent, chacun avec sa syntaxe :


Téléchargez le RFC 9535

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)