JSON Toolkit

Filtres JSONPath en profondeur : récursion, tranches et conditions multiples

L’essentiel : La descente récursive, les tranches et les filtres à conditions multiples sont là où JSONPath devient puissant et où on l'utilise à mauvais escient. Sémantique des filtres, forme des résultats et différences entre bibliothèques.

Intermédiaire~10 min de lecturePublié le 2026-10-31Mis à jour le 2026-10-31
Illustration de l'article Filtres JSONPath en profondeur

L'essentiel : localisez d'abord une profondeur connue, puis ajoutez tranches et filtres au lieu de recourir à $.. ; un chemin nu dans un filtre teste seulement l'existence, donc testez un booléen avec @.stock == true.

Tout le monde sait écrire $.store.book[0].title. Ce qui casse vraiment JSONPath, c'est de combiner $.. avec des tranches et plusieurs conditions, puis d'obtenir trop de résultats, trop peu, ou un ordre inattendu.

1. Un document d'exemple

{
  "store": {
    "book": [
      { "title": "JSON: The Definitive Guide", "price": 8.95, "stock": true, "tags": ["json", "spec"] },
      { "title": "A History of Data Formats", "price": 12.99, "stock": false, "tags": ["history"] },
      { "title": "How Parsers Work", "price": 8.99, "stock": true, "tags": ["parser", "json"] },
      { "title": "Streaming JSON in Practice", "price": 9.5, "stock": true, "tags": [] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "updatedAt": "2026-10-31"
}

Toutes les expressions ci-dessous utilisent ce document en entrée. Gardez deux prémisses en tête : le résultat est un ensemble de nœuds, et son ordre suit l'ordre du document plutôt que l'ordre dans lequel les conditions ont été écrites. Dans un filtre, @ ne désigne que le nœud candidat courant.

2. Descente récursive $.. : pratique mais coûteuse

.. signifie « chercher récursivement à n'importe quelle profondeur depuis la position courante ». La RFC 9535 le définit comme un segment descendant, et il peut se combiner avec des noms, des jokers, des indices, des tranches et des filtres :

$..price                  // every member named price at any depth
$..book[?(@.price < 10)]  // any book at any depth, then filter
$..[?(@.stock)]           // any node at any depth where stock exists

Trois points à comprendre :

  1. $..book trouve tous les nœuds nommés book, quelle que soit leur profondeur ;
  2. La traversée est en profondeur d'abord, et l'ordre de sortie dépend de la manière dont l'implémentation visite les enfants ;
  3. .. ne peut pas utiliser d'index, donc son coût croît avec le nombre total de nœuds.

Par conséquent, $..price et $.store.book[*].price peuvent ne différer que par le prix du vélo dans leur sortie, tout en différant d'un ordre de grandeur en coût. Quand vous connaissez la profondeur, écrivez toujours le chemin explicite. Pour comparer les frontières des deux langages de chemins, voir JSONPath vs JSON Pointer.

3. Tranches de tableau et jokers : trois pièges

La syntaxe de tranche est [start:end:step], borne de début incluse et borne de fin exclue, les nombres négatifs comptant depuis la fin :

Expression Signification Résultat (avec quatre livres)
[0:2] Les deux premiers livres Livres 1 et 2
[1:] Du deuxième livre à la fin Livres 2, 3 et 4
[:2] Les deux premiers livres Livres 1 et 2
[-2:] Les deux derniers livres Livres 3 et 4
[::2] Un livre sur deux Livres 1 et 3
[::-1] Ordre inversé Livres 4, 3, 2 et 1

Trois pièges :

  • Un pas nul est invalide. La RFC 9535 exige un pas non nul, tandis que certaines implémentations renvoient silencieusement un ensemble vide ;
  • Les pas négatifs ne sont pas universellement pris en charge. L'implémentation originale de Goessner les accepte, mais certaines implémentations minimales n'acceptent que des pas positifs ;
  • Filtrer après une tranche n'élargit pas la portée. [0:2][?(@.stock)] ne filtre que les deux livres produits par la tranche ; il ne revient pas à l'ensemble du tableau.

Le joker [*] se combine avec les tranches, mais [*] produit « tous les nœuds enfants ». Si ces enfants sont des objets, l'ajout de .title donne tous les titres. Si un enfant est lui-même un tableau, .title n'aplatit pas implicitement un niveau supplémentaire. Réécrivez [*] quand il faut étendre ; n'attendez pas un aplatissement implicite.

4. Filtres à conditions multiples : comparaisons, logique et existence

Un filtre [?(...)] peut contenir quatre types de constructions : comparaisons, opérateurs logiques, tests d'existence et fonctions. Une requête multi-condition courante ressemble à ceci :

$..book[?(@.price < 10 && @.stock)]

Elle signifie « prix inférieur à 10 et stock présent ». Attention à plusieurs nuances sémantiques :

  • @.stock est un test d'existence, pas « la valeur est vraie ». Si le champ existe mais vaut false, la condition est quand même vraie. Pour tester un booléen, il faut écrire @.stock == true ;
  • && est plus prioritaire que ||. Mettez des parenthèses quand vous les mélangez plutôt que de compter sur votre mémoire ;
  • Les deux côtés d'une comparaison doivent avoir le même type. Nombres et chaînes ne sont pas convertis implicitement ; @.price < "10" est faux ou une erreur dans les implémentations strictes ;
  • Un champ absent rend toujours la comparaison fausse. @.missing < 10 ne lève pas d'erreur ; il ne correspond simplement à rien.

C'est l'endroit le plus facile où trébucher en écrivant des filtres comme s'ils étaient du JavaScript : if (obj.stock) teste la véracité en JavaScript, alors que JSONPath teste l'existence. Pour la syntaxe standard complète et les fonctions, revenez à Le langage de requête JSONPath en pratique.

5. Expressions de script : plus de puissance, moins de portabilité

L'implémentation originale de Goessner et jsonpath-plus autorisent des comparaisons entre nœuds comme [?(@.price < $.max)], et même des scripts arbitraires. La RFC 9535 n'autorise explicitement que l'accès au nœud courant @ et interdit $ dans un filtre.

// Works in older implementations; not allowed by RFC 9535
$..book[?(@.price < $.expensive)]

// Standard approach: pass the threshold from the caller
query(document, '$..book[?(@.price < threshold)]', { threshold: 10 })

Les expressions de script créent trois problèmes :

  1. La portabilité tombe à zéro. Changer de bibliothèque signifie réécrire chaque expression ;
  2. La frontière de sécurité disparaît. Quand les expressions viennent des utilisateurs, l'évaluation de script revient à céder l'exécution de code ;
  3. L'optimisation statique devient impossible. L'implémentation ne peut qu'interpréter nœud par nœud, ce qui est lent et difficile à mettre en cache.

Si vous avez vraiment besoin d'une comparaison entre nœuds, passez le seuil comme paramètre de requête et filtrez une seconde fois côté application. C'est généralement plus stable que de dépendre d'une syntaxe d'extension.

6. Forme des résultats : vous croyez recevoir des valeurs, vous recevez des nœuds

La même expression peut renvoyer au moins trois formes selon les bibliothèques :

["JSON: The Definitive Guide", "How Parsers Work", "Streaming JSON in Practice"] // value array
[{ "path": "$['store']['book'][0]['title']", "value": "..." }]                 // path + value
[{ "pointer": "/store/book/0/title", "value": "..." }]                          // JSON Pointer + value

Cela influence directement trois choses : la façon dont le frontend lit les résultats, la façon dont les résultats sont dédupliqués, et la façon dont les résultats de requête deviennent des opérations d'écriture. L'ensemble de nœuds défini par la RFC 9535 est un ensemble de références à des nœuds, mais les implémentations JavaScript renvoient souvent seulement des valeurs, car les références ne franchissent pas les frontières de processus.

Conseils pratiques :

  • Lisez toujours une valeur unique via [0] ou value() ; ne supposez jamais que le résultat est un scalaire ;
  • Quand il faut réécrire, choisissez une implémentation qui renvoie des JSON Pointers ou construisez le pointeur vous-même ;
  • Un résultat vide et un résultat [null] sont différents ; vérifiez la longueur plutôt que la véracité.

7. Différences entre bibliothèques : lancez la suite de conformité avant de migrer

Capacité Goessner original jsonpath-plus RFC 9535 strict
Parenthèses de filtre ?() Obligatoires Compatibles Facultatives
Référence à la racine $.x Pris en charge Pris en charge Interdite
Expressions de script Prises en charge Prises en charge Interdites
Indices et pas négatifs Pris en charge Pris en charge Pris en charge
Renvoi des chemins Configurable Configurable Non spécifié
Fonctions comme length() Aucune Extensions Fonctions standard

Avant de migrer, vérifiez au moins quatre points : si les parenthèses sont obligatoires, si $ peut apparaître dans un filtre, comment se comportent les pas négatifs et la structure du résultat. La suite de tests de conformité officielle couvre la plupart des différences de syntaxe et évite les échecs silencieux du type « trois lignes ont disparu après le changement de bibliothèque ».

8. Performances et liste de vérification des mauvais usages

Les performances se résument en une phrase : JSONPath est une traversée complète, et .. est le type de traversée complète le plus coûteux.

  • Pour les documents de plus de 100 000 nœuds, réduisez d'abord la portée avec un chemin explicite, puis filtrez le sous-ensemble ;
  • Compilez une expression une fois et réutilisez-la ; ne reparsez pas la chaîne dans une boucle ;
  • Passez à jq ou à du code pour l'agrégation (somme, regroupement, tri) ; JSONPath ne fait que sélectionner ;
  • Placez les conditions le plus tôt possible : $.store.book[?(@.price < 10)] scanne bien moins de nœuds non pertinents que $..[?(@.price < 10)].

Liste de vérification des mauvais usages :

  • [ ] Traiter un test d'existence comme un test de vérité booléenne ;
  • [ ] Utiliser $.. au lieu d'une profondeur connue ;
  • [ ] Dépendre d'expressions de script pour des comparaisons entre nœuds ;
  • [ ] Supposer que le résultat est toujours un tableau de valeurs ;
  • [ ] Autoriser des expressions contrôlées par l'utilisateur sans limites de longueur ni d'imbrication.

FAQ

Pourquoi @.stock correspond-il encore quand le champ vaut false ?

Parce qu'un chemin nu dans un filtre signifie « le champ existe », et non « la valeur est vraie ». Pour tester le booléen, comparez explicitement avec @.stock == true. C'est la plus grande différence sémantique entre JSONPath et la véracité JavaScript.

L'ordre de la descente récursive est-il stable ?

Non. La RFC 9535 spécifie une sémantique d'ensemble de nœuds, et l'ordre de traversée dépend de la manière dont l'implémentation visite les enfants. Quand vous avez besoin d'un ordre fixe, localisez le tableau et lisez par index, ou triez les résultats dans le code applicatif.

Peut-on combiner tranches et filtres ?

Oui, mais souvenez-vous de la portée. $.store.book[0:2][?(@.stock)] ne filtre que les deux livres produits par la tranche. Pour filtrer d'abord puis trancher, écrivez $.store.book[?(@.stock)][0:2] ; un ordre différent donne un résultat différent.

Pourquoi jsonpath-plus prend-il en charge $.expensive alors que le standard l'interdit ?

Parce que les filtres ne sont définis qu'en fonction du nœud courant @. Une référence à la racine rend une expression dépendante de tout le contexte du document, ce qui est difficile à analyser statiquement et à porter entre langages. Passez le seuil comme paramètre au lieu de l'incorporer dans l'expression.

Comment empêcher la descente récursive de scanner des nœuds non pertinents ?

Écrivez le chemin explicite dès que vous connaissez la profondeur, réduisez d'abord la portée, puis filtrez seulement le sous-ensemble au lieu d'utiliser $.. à la place d'un niveau connu. Placez les conditions le plus tôt possible : $.store.book[?(@.price < 10)] scanne bien moins de nœuds non pertinents que $..[?(@.price < 10)]. Pour les documents de plus de 100 000 nœuds, faites-le en premier.