Le langage de requête JSONPath en pratique
L’essentiel : En 2024, JSONPath a enfin obtenu une norme formelle, la RFC 9535. Cet article explique la syntaxe, la sémantique des filtres, les fonctions et les différences historiques entre implémentations.

L'essentiel : localisez d'abord avec un chemin explicite puis filtrez par-dessus, et écrivez vos expressions conformément à la RFC 9535 ; utilisez l'accès aux propriétés ou JSON Pointer pour un champ fixe unique, et jq ou du code pour l'agrégation.
Vous avez probablement déjà vu des expressions comme
$.store.book[?(@.price < 10)].title, mais il n'est devenu une norme internationale qu'en 2024.
1. Du standard de fait à la norme formelle
JSONPath a été proposé par Stefan Gössner en 2007, et pendant longtemps la seule « spécification » fut un unique billet de blog, chaque implémentation de langage faisant sa propre cuisine :
- faut-il des parenthèses pour les filtres (
?(@.a)contre?@.a) ; - l'index de tableau
[-1]est-il pris en charge ; - le résultat renvoie-t-il des « valeurs » ou des « valeurs et chemins » ;
- les expressions de script (
?(@.price < $.max)) sont-elles autorisées.
La RFC 9535, publiée en février 2024, a fait converger ces divergences en une syntaxe et une sémantique uniques, et fournit une suite de tests de conformité officielle. Les nouveaux projets devraient suivre la RFC 9535 ; lors de la migration d'anciens projets, confirmez d'abord la version d'implémentation.
2. Aide-mémoire de la syntaxe
| Syntaxe | Signification | Exemple |
|---|---|---|
$ |
Nœud racine | $.store |
@ |
Nœud courant dans un filtre | $[?@.price < 10] |
.name |
Nœud enfant (le nom doit correspondre à [A-Za-z_][A-Za-z0-9_]*) |
$.store.book |
['name'] |
Nœud enfant (nom quelconque, y compris non ASCII et points) | $['store']['书'] |
[0] |
Index de tableau ; les nombres négatifs comptent depuis la fin | $.book[0], $.book[-1] |
[0,2] |
Index multiples / noms multiples | $.book[0,2] |
[1:3] |
Tranche, début inclus et fin exclue | $.book[1:3] |
[::2] |
Tranche avec un pas | $.book[::2] |
[*] |
Joker pour tous les nœuds enfants | $.book[*].title |
..name |
Descente récursive, recherche à toute profondeur | $..price |
..[*] |
Prendre récursivement tous les nœuds | $..* |
[?<expr>] |
Sélecteur de filtre | $.book[?@.price < 10] |
Une sémantique à retenir absolument : JSONPath renvoie un ensemble de nœuds (nodelist), pas une valeur unique. Même lorsqu'un seul nœud correspond, le résultat est une liste de longueur un.
3. Parcourir la syntaxe avec un échantillon
{
"store": {
"book": [
{ "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
{ "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
{ "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
| Expression | Résultat |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
4 prix (y compris celui du vélo) |
$.store.book[*].title |
3 titres |
$.store.book[?@.price < 10].title |
Titres des livres 1 et 3 |
$.store.book[?@.tags] |
Tous les livres qui ont tags (test d'existence) |
$.store.book[-1].title |
Titre du dernier livre |
$..book[0,1] |
Les deux premiers livres |
$.store.book[?@.price < $.expensive] |
Syntaxiquement dépendant de l'implémentation ; la RFC 9535 n'autorise pas les références inter-racines |
La dernière ligne est un piège historique : de nombreuses implémentations autorisent $.expensive dans un filtre pour une comparaison entre nœuds, ce que la RFC 9535 interdit explicitement —
un filtre ne peut accéder qu'au nœud courant @. Lorsqu'il faut comparer à un seuil global, passez le seuil comme argument à la fonction de requête.
4. Ce que les filtres peuvent exprimer
Les expressions de filtre de la RFC 9535 prennent en charge quatre types de constructions :
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
Quelques exemples pratiques :
// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// More than one tag
$.store.book[?length(@.tags) > 1]
// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()
match() effectue une correspondance sur la chaîne entière et search() une recherche de sous-chaîne ; le dialecte d'expression régulière sous-jacent est I-Regexp (un sous-ensemble de la RFC 9485),
qui ne prend en charge ni les références arrière ni les assertions avant/arrière — c'est à la fois une limite et un bouclier pratique contre les attaques DoS par expression régulière.
5. Différences d'implémentation : vérifiez avant de migrer
| Implémentation | Langage | Prise en charge RFC 9535 | Remarques |
|---|---|---|---|
jsonpath-plus |
JavaScript | Largement compatible | Prend en charge les fonctions d'extension et les expressions de script ; plus puissant mais pas strictement standard |
jsonpath (dchester) |
JavaScript | Partielle | Bibliothèque ancienne avec la syntaxe ?() et une structure de résultat différente |
ojg / gjson |
Go | ojg la prend bien en charge |
gjson utilise une autre syntaxe de chemin et n'est pas JSONPath |
jsonpath-ng |
Python | Partielle | Prend en charge les extensions et est assez permissive |
Jayway JsonPath |
Java | Partielle | Écosystème large ; comportement historique différent de la RFC |
Trois vérifications avant de migrer :
- Les parenthèses de filtre sont-elles obligatoires ;
- Les index négatifs comme
-1sont-ils pris en charge ; - Le résultat est-il un « tableau de valeurs » ou un « tableau d'objets
{path, value}».
Le troisième est le plus facile à heurter : le frontend lit result[0], et après changement de bibliothèque il reçoit un objet, laissant la page vide.
6. Performances et sécurité
Performances
- JSONPath est essentiellement une seule traversée de complexité O(nombre de nœuds), et il ne peut pas utiliser d'index ;
- La descente récursive avec
..est la plus coûteuse ; si vous pouvez localiser d'abord avec$.store.bookpuis filtrer, n'écrivez pas$..book; - Compilez les expressions une fois et réutilisez-les (la plupart des bibliothèques offrent
compileou un cache) au lieu de les réanalyser à chaque requête ; - Pour les documents de plus de 100 000 nœuds, envisagez d'abord une extraction exacte avec JSON Pointer, ou un prétraitement côté serveur avec
jq.
Sécurité
- Les expressions issues de l'entrée utilisateur doivent avoir des limites de longueur et de profondeur d'imbrication ;
- Ne laissez pas JSONPath gérer l'autorisation — il ne peut que lire, et ne peut pas exprimer « quels champs sont invisibles » ;
- Lorsque l'expression régulière utilisée par
match()vient des utilisateurs, limitez sa complexité ; bien qu'elle soit plus sûre que PCRE, un plafond de longueur reste recommandé.
7. Quand ne pas utiliser JSONPath
- Vous n'avez besoin que d'un champ fixe → utilisez directement
data.store.bicycle.color; - Vous devez réécrire après modification → utilisez JSON Pointer ou un patch ; JSONPath ne réécrit pas ;
- Vous avez besoin d'agrégation (somme, regroupement) → utilisez
jq, SQL ou du code ; JSONPath ne fait que sélectionner ; - Le document est énorme et vous n'avez besoin que de quelques champs → utilisez un analyseur en flux au lieu de charger tout l'arbre en mémoire d'abord.
8. Liste de vérification
- [ ] Les expressions suivent la RFC 9535 et ne reposent pas sur des références inter-racines ;
- [ ] Il est clair que le résultat est un ensemble de nœuds ; utilisez
[0]ouvalue()pour obtenir une valeur unique ; - [ ] Les expressions sont précompilées et réutilisées ;
- [ ] Les expressions contrôlées par l'utilisateur ont des limites de longueur et de complexité ;
- [ ] Les cas de test de conformité officiels sont exécutés lors du changement d'implémentation.
FAQ
Comment choisir entre JSON Pointer et JSONPath ?
Utilisez un simple accès aux propriétés ou un JSON Pointer lorsque vous n'avez besoin que d'un champ fixe ; recourez à JSONPath lorsque vous avez besoin de filtres, de jokers ou de descente récursive. JSONPath ne fait que sélectionner et ne réécrit jamais, la réécriture appartient donc à JSON Pointer ou à un patch.
Pourquoi un filtre ne peut-il pas référencer $.expensive ?
La RFC 9535 interdit explicitement les références inter-racines ; un filtre ne peut accéder qu'au nœud courant @. Passez plutôt le seuil comme argument à la fonction de requête. Les anciennes implémentations acceptent la syntaxe, mais elle casse lorsque vous changez ou mettez à niveau de bibliothèque.
Faut-il se fier à result[0] pour lire une valeur ?
Seulement après avoir confirmé que l'implémentation actuelle renvoie un tableau de valeurs. Certaines bibliothèques renvoient des objets {path, value}, si bien que result[0] devient un objet et que la page reste vide. Lisez les valeurs uniques via [0] ou value(), et exécutez la suite de conformité officielle avant de changer d'implémentation.