Nouveauté Symfony 7.3 : Le Composant JsonPath
Publié le 26 mai 2025
Après quatre ans de travail, une spécification officielle décrivant un langage de requête pour JSON a été publiée en février 2024. La récente RFC 9535 vous explique tout ce qu’il faut savoir à son sujet. Ce qu’il faut retenir, c’est que si XML a son langage XPath pour requêter des éléments précis d’un document XML/HTML, le format JSON dispose désormais d’un équivalent. Ce standard a déjà été adopté par beaucoup de langages (comme C#, Go ou Python pour ne citer que ceux-là), nativement ou grâce à des bibliothèques tierces. C’est désormais au tour de PHP de bénéficier d’une implémentation prenant en charge cette syntaxe !

La syntaxe JSON Path
La syntaxe JSON Path est similaire au format JSON : concise et sans un nombre incalculable de fonctionnalités. Nous pouvons même en faire presque le tour ici sans que cet article ne devienne un livre. Voici ce qu’il y a à savoir :
$
représente la racine du document, et tous les paths commencent avec ce symbole ;.
et[]
permettent d’accéder à des enfants, selon que l’on se trouve dans un objet ou un tableau ;..
permet de descendre récursivement dans tous les enfants, quelque soit leur nom ;*
sélectionne tous les enfants disponibles au niveau courant ;- finalement, il est possible d’utiliser des filtres pour ne sélectionner qu’une partie des nœuds en utilisant
@
suivi du filtre.
Avant d’aller plus loin, détaillons deux subtilités. Tout d’abord, la notation []
s’inspire grandement de la syntaxe utilisée en Python. En effet, trois éléments peuvent être renseignés entre crochets. Voici quelques exemples qui permettront de comprendre exactement ce qui est décrit :
[0]
sélectionne le premier élément ;[-1]
sélectionne le dernier élément ;[2:-2]
sélectionne tous les éléments à partir du troisième jusqu’à l’avant dernier ;[-3:]
sélectionne les trois derniers éléments ;[::2]
sélectionne tous les éléments avec un pas de deux ;[0:9:3]
sélectionne les éléments correspondant aux clés 0, 3, 6 et 9.
En résumé, il est possible de donner un indice de début, optionnellement de fin, et optionnellement un pas.
La deuxième chose à détailler est l’utilisation des filtres. Pour cela, imaginons que nous ayons une chaîne JSON qui représente une bibliothèque contenant des livres ayant chacun un prix. Nous pouvons par exemple filtrer les résultats avec leur prix en utilisant le JSON Path suivant :
$.store.book[?(@.price < 10)]
Les opérateurs de comparaison que vous connaissez déjà sont pris en charge. Les filtres ne sont cependant pas limités à ces opérateurs ! En effet, la RFC prévoit plusieurs fonctions built-in, à savoir :
length
, retournant la taille d’un tableau ou la longueur d’une chaîne ;count
, retournant la taille d’un tableau ou 0 si l’élément n’est pas un tableau ;match
, vérifiant qu’une chaîne correspond à l’expression régulière donnée ;search
, vérifiant qu’une sous-chaîne d’une chaîne plus grande correspond à l’expression régulière donnée ;value
, retournant la valeur d’un nœud.
À savoir que la RFC autorise aussi les fonctions personnalisées. Cette fonctionnalité n’est pas encore implémentée dans le composant que nous allons voir dans la suite de l’article à l’heure actuelle, mais elle pourrait arriver rapidement !
Quoiqu’il en soit, nous voilà au point sur la syntaxe JSON Path. Nous allons pouvoir entrer dans le vif du sujet, à savoir comment utiliser cette syntaxe avec le nouveau composant JsonPath de Symfony !
Le composant JsonPath
La première chose à rappeler est que les composants Symfony sont des bibliothèques PHP comme les autres, c’est-à-dire que vous n’avez pas besoin d’utiliser Symfony (le framework full-stack) dans votre projet pour utiliser ce composant. Il est autonome et peut être installé dans n’importe quel projet PHP. Et comme tous les composants, il s’installe avec Composer :
composer require symfony/json-path
C’est tout, le composant est prêt à être utilisé !
Pour les exemples qui suivent, nous utiliserons le jeu de données suivant :
$json = <<<'JSON'
{"store": {"book": [
{"category": "reference", "author": "Nigel Rees", "title": "Sayings", "price": 8.95},
{"category": "fiction", "author": "Evelyn Waugh", "title": "Sword", "price": 12.99}
]}}
JSON;
À partir de là, un crawler peut être créé et utilisé pour requêter des données grâce à la syntaxe JSON Path :
use Symfony\Component\JsonPath\JsonCrawler;
$crawler = new JsonCrawler($json);
$result = $crawler->find('$.store.book[0].title'); // retourne ['Sayings']
Comme nous l’avons vu précédemment, il est possible d’utiliser toutes sortes de requêtes complexes grâce aux filtres :
$crawler = new JsonCrawler($json);
$result = $crawler->find('$.store.book[?(@.price < 10)]'); // retourne seulement le premier livre
$result = $crawler->find('$.store.book[?length(@.author) > 11]'); // retourne le deuxième livre
$result = $crawler->find('$.store.book[?match(@.author, "[A-Z].*el.+")]'); // retourne tous les livres
Dans ces exemples, la chaîne JSON que nous donnons au crawler est une chaîne de caractères. Mais le crawler accepte aussi des ressources, permettant le traitement très efficace des gros volumes de données. En effet, grâce au composant Symfony JsonStream (qui est une dépendance optionnelle), le support des ressources est ajouté à la classe JsonCrawler
. Un mécanisme interne permet notamment de deviner, selon le chemin donné, la plus petite sous-chaîne de la ressource ou de la chaîne JSON à décoder. Cela permet d’économiser de la mémoire et du processeur en ne décodant que ce qui est réellement nécessaire.
Une autre subtilité du composant est l’existence de la classe `JsonPath`. Cette classe permet d’encapsuler le path dans un objet, permettant ainsi de décrire un chemin comme vous le feriez avec un query builder dans Doctrine. Voici un exemple de son utilisation :
use Symfony\Component\JsonPath\JsonPath;
$path = new JsonPath();
$path = $path
->key('store')
->key('book')
->slice(start: 1, end: 5, step: 2)
->filter('match(@.author, "[A-Z].*el.+")');
$result = $crawler->find($path);
L’utilisation du builder est vraiment à la préférence de chacun et les fonctionnalités sont identiques à l’utilisation d’une chaîne de caractères simple.
Les assertions JsonPath
L’un des grands intérêts de ce composant est son utilisation dans les tests. En effet, il permet, de manière compréhensible et standardisée, de récupérer des données précises d’une chaîne JSON sans devoir jongler avec des tableaux.
Pour une expérience développeur (DX) optimale, le composant met à disposition un trait contenant des assertions PHPUnit utilisables dans les tests. Voici un exemple dans un test fictif :
use PHPUnit\Framework\TestCase;
use Symfony\Component\JsonPath\Test\JsonPathAssertionsTrait;
class MyApiTest extends TestCase
{
use JsonPathAssertionsTrait;
public function testItFetchesAllUsers(): void
{
$json = self::fetchUserCollection();
$this->assertJsonPathCount(3, '$.users[*]', $json);
$this->assertJsonPathSame(['Melchior'], '$.users[0].username', $json);
$this->assertJsonPathEquals(['30'], '$.users[0].age', $json, 'should return the age as string or int');
}
public function testItFetchesOneUser(): void
{
$json = self::fetchOneUser();
$this->assertJsonPathSame(['Melchior'], '$.user.username', $json);
$this->assertJsonPathEquals(['30'], '$.user.age', $json, 'should return the age as string or int');
}
private static function fetchUserCollection(): string
{
return <<<JSON
{
"users": [
{
"username": "Melchior",
"age": 30
},
{
"username": "Gaspard",
"age": 50
},
{
"username": "Balthazar",
"age": 55
}
]
}
JSON;
}
private static function fetchOneUser(): string
{
return <<<JSON
{
"user": {
"username": "Melchior",
"age": 30
}
}
JSON;
}
}
Nous vous recommandons vivement d’aller lire directement le fichier source pour voir comment cela fonctionne, ainsi que toutes les possibilités offertes par le trait.
Ces nouvelles assertions viendront parfaitement compléter celles dédiées aux tests d’API fournies par notre framework API Platform !
Et ensuite ?
Le composant a encore de belles évolutions à proposer. On peut noter l’ajout du support des fonctions personnalisées évoquées en début d’article (le travail est déjà commencé à ce niveau-là). Cela viendra, entre autres, avec une intégration au FrameworkBundle de Symfony. Il est également à noter que le composant, qui va être introduit dans Symfony 7.3, se trouve actuellement dans le statut expérimental. Cela signifie que son API publique (comprendre : les signatures des méthodes publiques et protégées, ainsi que les noms de classes) est sujette à modification et n’est pas encore couverte par la promesse de rétrocompatibilité qui fait la force de Symfony. Attention à ne pas coupler trop fortement votre code au composant durant sa phase de stabilisation.
Si tout se passe bien, le composant sera considéré comme stable avec Symfony 7.4/8.0, fin novembre 2025. Si des changements majeurs sont effectués ou si de nouvelles fonctionnalités importantes voient le jour au sein de ce composant, vous pouvez être à peu près sûr qu’un nouvel article sera publié sur notre blog pour l’occasion !