JSONPath est un langage de requêtes pour JSON, de la même manière qu'XPath en est un pour XML. Il a été esquissé par Stefan Goessner en 2007, implémenté différemment par tous ceux qui en avaient besoin, et finalement standardisé en RFC 9535 en 2024. C'est cette histoire qui fait que deux implémentations JSONPath sont souvent en désaccord, et pourquoi une fiche est plus utile avec des résultats joints qu'en tant que liste de symboles.
Chaque requête ci-dessous s'exécute contre ce document. Vous pouvez le coller dans le testeur JSONPath, qui charge exactement ceci comme exemple.
{
"store": {
"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 }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
Les sélecteurs
Racine. $ est l'entier document. Chaque requête commence là.
Enfant par nom. $.store.bicycle.color retourne "red". Un point suivi d'un nom,
autant de fois que nécessaire.
Nom entre guillemets. $['store']['bicycle'] est la même chose écrite d'une autre
manière. Vous avez besoin des crochets quand une clé contient un espace, un point ou un tiret, ou
quand elle commence par un chiffre : $['first name'].
Joker. $.store.bicycle.* retourne "red" et 19.95, chaque enfant de
cet objet. $.store.book[*] retourne les trois livres.
Indice. $.store.book[0] est le premier livre. $.store.book[-1] est le
dernier, en comptant depuis la fin.
Tranche. $.store.book[0:2] retourne les deux premiers livres. La fin est
exclusive, exactement comme en Python. $.store.book[::2] prend chaque deuxième livre,
et un pas négatif inverse : $.store.book[::-1].
Union. $.store.book[0, 2] retourne le premier et le troisième livres.
$.store.bicycle['color', 'price'] sélectionne deux enfants nommés à la fois.
Descendant. $..price retourne chaque prix à n'importe quelle profondeur, ce qui est
8.95, 12.99, 8.99 et 19.95 ici. C'est le sélecteur vers lequel les gens se tournent le plus
et celui qui coûte le plus sur un grand document, car il visite chaque
valeur.
Filtre. $.store.book[?@.price < 10] retourne les deux livres moins chers. Le
@ se réfère à la valeur testée. La RFC 9535 a supprimé les parenthèses que
les anciens tutoriels utilisent, donc [?@.price < 10] et [?(@.price < 10)] apparaissent tous
deux dans le milieu. Le testeur ici accepte les deux.
Existence. $.store.book[?@.isbn] retourne uniquement le livre qui a un champ isbn,
sans le comparer à quoi que ce soit.
Combiner des tests. &&, || et ! fonctionnent comme vous l'attendriez :
$..book[?@.category == 'fiction' && @.price < 10].title retourne
"Moby Dick".
Pourquoi un filtre qui semble correct ne retourne rien
Presque toujours, c'est un défaut de type. La spécification dit que des valeurs de
types différents ne sont jamais égales et jamais ordonnées, donc un prix stocké en "8.95" avec
des guillemets ne correspond pas à @.price < 10, et ne correspond pas non plus à @.price == 8.95.
Si un filtre ne retourne rien, vérifiez les types dans le document avant de
vérifier la requête.
La deuxième cause est d'attacher le filtre au mauvais niveau. Un filtre choisit parmi
les enfants de la valeur à laquelle il est attaché, donc il va après le tableau :
book[?@.price < 10], pas book.price[?...]. Attaché à un objet, il teste
chaque valeur de propriété de cet objet à la place.
Troisièmement, une valeur manquante n'est pas null. @.isbn == null correspond à un livre dont
l'isbn est littéralement null, pas à un livre qui n'a pas du tout d'isbn. Pour tester
l'absence, écrivez !@.isbn.
Où les implémentations sont en désaccord
L'article de 2007 était une proposition, pas une spécification, donc les bibliothèques ont divergé pendant dix-sept ans. Si vous déplacez une requête entre des outils, ce sont les endroits à vérifier :
- Les indices négatifs et les pas négatifs sont largement supportés maintenant et manquent dans certaines anciennes bibliothèques.
- La forme de filtre avec parenthèses est l'ancienne orthographe. La RFC 9535 utilise
[?...]nu, et beaucoup d'outils n'acceptent qu'une des deux. - La RFC définit les fonctions
length(),count(),match()etsearch(). Beaucoup d'implémentations les manquent ou fournissent un ensemble différent sous des noms différents. - La RFC donne aux résultats un ordre de document. Certaines implémentations ne garantissent pas, en particulier pour les segments descendants.
Le testeur sur ce site implémente les sélecteurs listés ci-dessus et l'indique clairement sur la page, y compris un tableau de ce qu'il supporte. Les extensions de fonctions, les chemins absolus dans un filtre, et la comparaison de deux chemins entre eux ne sont pas implémentés, et l'utilisation de l'un d'eux produit un message nommant la fonctionnalité plutôt qu'une erreur de syntaxe. C'est un choix délibéré : un outil qui prétend une conformité totale et échoue ensuite silencieusement sur un cas limite est pire qu'un qui vous dit où se trouve sa limite.
JSONPath ou Pointeur JSON
Ils répondent à des questions différentes.
Un Pointeur JSON nomme exactement une valeur et résout ou
ne résout pas. Il n'a pas de jokers ni de filtres, et c'est précisément pourquoi JSON
Schema, JSON Patch et la plupart des messages d'erreur de validation l'utilisent : une adresse
sans ambiguïté, c'est ce dont ils ont besoin. /store/book/0/title désigne un seul endroit.
Une requête JSONPath est une recherche. Elle peut retourner de nombreuses valeurs ou aucune, et elle peut filtrer. Utilisez-la quand vous ne savez pas déjà où se trouve ce que vous voulez.
Si vous connaissez l'adresse, utilisez un pointeur. Si vous recherchez quelque chose, utilisez une requête. Les deux outils sur ce site indiquent la ligne où se trouve chaque résultat, donc de toute manière vous finissez de retour dans le document plutôt que dans une liste de résultats abstraite.