JSONPath est une méthode simple pour extraire des parties spécifiques d'un document JSON. Plusieurs langages de programmation comme JavaScript, Python, PHP et Java implémentent JSONPath.
La bibliothèque JSONPath offre une analyse JSON puissante avec une syntaxe similaire aux expressions régulières, permettant d'extraire pratiquement n'importe quelle information d'un document JSON. Dans cet article, nous allons explorer chaque exrpession documentée sur le site officiel avec des exemples de code pour une meilleure compréhension.
GitHub : https://github.com/json-path/JsonPath
JsonPath est disponible dans le référentiel Central Maven. Les utilisateurs Maven peuvent l'ajouter à leur POM :
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.2.0</version>
</dependency>
Pour obtenir de l'aide, vous pouvez poser des questions sur Stack Overflow en utilisant les tags 'jsonpath' et 'java'.
Les expressions JSONPath référencent toujours la structure JSON de la même manière que les expressions XPath référencent les documents XML.
L'« objet membre racine » dans JSONPath est toujours désigné par $, que ce soit un objet ou un tableau.
Les expressions JSONPath peuvent utiliser la notation par points
$.magasin.livre[0].titre
ou la notation par crochets
$['magasin']['livre'][0]['titre']
Opérateurs
| Opérateur | Description |
|---|---|
$ |
Interroger l'élément racine. Cela initialise toutes les expressions de chemin. |
@ |
Nœud actif traité par un prédicat de filtre. |
* |
Caractère générique, peut remplacer n'importe quel nom ou nombre. |
.. |
Balayage en profondeur. Peut être utilisé avec n'importe quel nom. |
.<nom> |
Point, indique un sous-élément |
['<nom>' (, '<nom>')] |
Crochets pour indiquer des sous-éléments |
[<nombre> (, <nombre>)] |
Index de tableau ou indexes |
[début:fin] |
Opération de tranche de tableau |
[?(<expression>)] |
Expression de filtre. L'expression doit évaluer à une valeur booléenne. |
Fonctions
Les fonctions peuvent être appelées à la fin d'un chemin, leur sortie devient la sortie de l'expression de chemin. La sortie de la fonction dépend de sa propre implémentation.
| Fonction | Description | Sortie |
|---|---|---|
min() |
Fournit la valeur minimale d'un tableau de nombres | Double |
max() |
Fournit la valeur maximale d'un tableau de nombres | Double |
avg() |
Fournit la valeur moyenne d'un tableau de nombres | Double |
stddev() |
Fournit l'écart-type d'un tableau de nombres | Double |
length() |
Fournit la longueur d'un tableau | Integer |
Opérateurs de Filtre
Les filtres sont des expressions logiques utilisées pour filtrer les tableaux. Un filtre typique serait [?(@.age > 18)], où @ représente l'élément en cours de traitement. Des filtres plus complexes peuvent être créés en utilisant les opérateurs logiques && et ||. Les littéraux de chaîne doivent être entourés de simples ou doubles quotes ([?(@.couleur == 'bleu')] ou [?(@.couleur == "bleu")]).
| Opérateur | Description |
|---|---|
== |
Gauche égal à droite (notez que 1 n'est pas égal à '1') |
!= |
Différent de |
< |
Inférieur à |
<= |
Inférieur ou égal à |
> |
Supérieur à |
>= |
Supérieur ou égal à |
=~ |
Correspond à l'expression régulière [?(@.nom =~ /foo.*?/i)] |
in |
La gauche existe dans la droite [?(@.taille in ['S', 'M'])] |
nin |
La gauche n'existe pas dans la droite |
size |
Longueur (tableau ou chaîne) |
empty |
Est vide (tableau ou chaîne) |
Exemples en Java
JSON
{
"magasin": {
"livre": [
{
"categorie": "reference",
"auteur": "Nigel Rees",
"titre": "Proverbes du Siècle",
"prix": 8.95
},
{
"categorie": "fiction",
"auteur": "Evelyn Waugh",
"titre": "L'Épée d'Honneur",
"prix": 12.99
},
{
"categorie": "fiction",
"auteur": "Herman Melville",
"titre": "Moby Dick",
"isbn": "0-553-21311-3",
"prix": 8.99
},
{
"categorie": "fiction",
"auteur": "J. R. R. Tolkien",
"titre": "Le Seigneur des Anneaux",
"isbn": "0-395-19395-8",
"prix": 22.99
}
],
"velo": {
"couleur": "rouge",
"prix": 19.95
}
},
"cher": 10
}
| JSONPath (cliquez pour tester) | Résultat |
|---|---|
$.magasin.livre[*].auteur |
Obtenir toutes les valeurs auteur sous magasin et livre |
$..auteur |
Obtenir toutes les valeurs auteur dans tout le JSON |
$.magasin.* |
Tout, livres et vélos |
$.magasin..prix |
Obtenir toutes les valeurs prix sous magasin |
$..livre[2] |
Obtenir la 3ème valeur du tableau livre |
$..livre[-2] |
L'avant-dernier livre |
$..livre[0,1] |
Les deux premiers livres |
$..livre[:2] |
Tous les livres de l'index 0 (inclus) à l'index 2 (exclu) |
$..livre[1:2] |
Tous les livres de l'index 1 (inclus) à l'index 2 (exclu) |
$..livre[-2:] |
Obtenir les deux dernières valeurs du tableau livre |
$..livre[2:] |
Obtenir les valeurs de la 3ème à la dernière du tableau livre |
$..livre[?(@.isbn)] |
Obtenir toutes les valeurs du tableau livre contenant isbn |
$.magasin.livre[?(@.prix < 10)] |
Obtenir toutes les valeurs du tableau livre où prix < 10 |
$..livre[?(@.prix <= $['cher'])] |
Obtenir toutes les valeurs du tableau livre où prix <= cher |
$..livre[?(@.auteur =~ /.*REES/i)] |
Obtenir tous les livres dont l'auteur se termine par REES (insensible à la casse) |
$..* |
Lister toutes les valeurs du JSON niveau par niveau, de l'extérieur vers l'intérieur |
$..livre.length() |
Obtenir la longueur du tableau livre |
Lecture de Documents
La méthode la plus simple et directe d'utilisation de JSONPath est via l'API de lecture statique.
String json = "...";
List<String> auteurs = JsonPath.read(json, "$.magasin.livre[*].auteur");
Si vous voulez lire plusieurs chemins, la méthode précédente n'est pas optimale car elle nécessite de parser tout le document à chaque fois. Nous pouvons plutôt parser tout le document une seule fois, puis appeler différents chemins.
String json = "...";
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
String auteur0 = JsonPath.read(document, "$.magasin.livre[0].auteur");
String auteur1 = JsonPath.read(document, "$.magasin.livre[1].auteur");
JSONPath propose également une API fluide, qui est la plus flexible.
String json = "...";
ReadContext ctx = JsonPath.parse(json);
List<String> auteursLivreISBN = ctx.read("$.magasin.livre[?(@.isbn)].auteur");
List<Map<String, Object>> livresChers = JsonPath
.using(configuration)
.parse(json)
.read("$.magasin.livre[?(@.prix > 10)]", List.class);
Types de Retour
Lors de l'utilisation de JSONPath en Java, il est important de connaître le type de résultat attendu. JSONPath essaiera automatiquement de convertir le résultat en type attendu par l'appelant.
// Lance une exception java.lang.ClassCastException
List<String> liste = JsonPath.parse(json).read("$.magasin.livre[0].auteur");
// Fonctionne
String auteur = JsonPath.parse(json).read("$.magasin.livre[0].auteur");
Lors de l'évaluation d'un chemin, il est nécessaire de comprendre le concept de chemin indéterminé. Un chemin est indéterminé s'il contient :
..: opération de balayage en profondeur?(<expression>): expression[<nombre>, <nombre> (, <nombre>)]: plusieurs index de tableau
Les chemins indéterminés retournent toujours une liste (représentée par le JsonProvider actuel).
Par défaut, le MappingProvider SPI fournit un simple mappage d'objets. Cela vous permet de spécifier le type de retour souhaité, et le MappingProvider tentera d'effectuer le mappage. Dans l'exemple suivant, nous démontrons le mappage entre Long et Date.
String json = "{\"date_en_long\" : 1411455611975}";
Date date = JsonPath.parse(json).read("$['date_en_long']", Date.class);
Si vous configurez JsonPath pour utiliser JacksonMappingProvider ou GsonMappingProvider, vous pouvez même mapper directement la sortie de JsonPath vers un POJO.
Livre livre = JsonPath.parse(json).read("$.magasin.livre[0]", Livre.class);
Pour obtenir des informations complètes sur les types génériques, utilisez TypeRef.
TypeRef<List<String>> typeRef = new TypeRef<List<String>>(){};
List<String> titres = JsonPath.parse(JSON_DOCUMENT).read("$.magasin.livre[*].titre", typeRef);
Prédicats
Il existe trois manières différentes de créer des prédicats de filtre en JSONPath.
Prédicat en ligne
Un prédicat en ligne est un prédicat défini dans le chemin.
List<Map<String, Object>> livres = JsonPath.parse(json).read("$.magasin.livre[?(@.prix < 10)]");
Vous pouvez combiner plusieurs prédicats en utilisant && et || [?(@.prix < 10 && @.categorie == 'fiction')], [?(@.categorie == 'reference' || @.prix > 10)].
Vous pouvez également utiliser ! pour nier un prédicat [?(!(@.prix < 10 && @.categorie == 'fiction'))].
Prédicat de filtre
Le prédicat peut être construit en utilisant l'API Filter comme suit :
import static com.jayway.jsonpath.JsonPath.parse;
import static com.jayway.jsonpath.Criteria.where;
import static com.jayway.jsonpath.Filter.filter;
...
...
Filter filtreFictionPasCher = filter(
where("categorie").is("fiction").and("prix").lte(10D)
);
List<Map<String, Object>> livres = parse(json).read("$.magasin.livre[?]", filtreFictionPasCher);
Notez le point d'interrogation ? comme placeholder pour le filtre dans le chemin. Lors de la fourniture de plusieurs filtres, ils sont appliqués dans l'ordre correspondant au nombre de points d'interrogation et de filtres fournis. Vous pouvez spécifier plusieurs prédicats dans une opération [?, ?], tous les deux doivent correspondre.
Les filtres peuvent également être combinés avec "OU" et "ET"
Filter fooOuBar = filter(
where("foo").exists(true)).or(where("bar").exists(true)
);
Filter fooEtBar = filter(
where("foo").exists(true)).and(where("bar").exists(true)
);
Personnalisé
La troisième option est d'implémenter votre propre prédicat
Predicate livresAvecISBN = new Predicate() {
@Override
public boolean apply(PredicateContext ctx) {
return ctx.item(Map.class).containsKey("isbn");
}
};
List<Map<String, Object>> livres = reader.read("$.magasin.livre[?].isbn", List.class, livresAvecISBN);
Chemin vs Valeur
Dans l'implémentation de Goessner, JSONPath peut retourner un Chemin ou une Valeur. La valeur est la valeur par défaut, retournée dans tous les exemples ci-dessus. Si vous voulez que le chemin des éléments interrogés soit retourné, vous pouvez le faire avec l'option suivante :
Configuration conf = Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> listeChemins = using(conf).parse(json).read("$..auteur");
assertThat(listeChemins).containsExactly(
"$['magasin']['livre'][0]['auteur']", "$['magasin']['livre'][1]['auteur']",
"$['magasin']['livre'][2]['auteur']", "$['magasin']['livre'][3]['auteur']");
Ajustement de la Configuration
Lors de la création d'une configuration, il existe plusieurs options qui peuvent modifier le comportement par défaut.
DEFAULT_PATH_LEAF_TO_NULL
Cette option configure JSONPath pour retourner null pour les feuilles manquantes. Considérez le JSON suivant :
[
{
"nom" : "jean",
"genre" : "masculin"
},
{
"nom" : "ben"
}
]
Configuration conf = Configuration.defaultConfiguration();
// Fonctionne
String genre0 = JsonPath.using(conf).parse(json).read("$[0]['genre']");
// Lance une exception PathNotFoundException
String genre1 = JsonPath.using(conf).parse(json).read("$[1]['genre']");
Configuration conf2 = conf.addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL);
// Fonctionne
String genre0 = JsonPath.using(conf2).parse(json).read("$[0]['genre']");
// Fonctionne (retourne null)
String genre1 = JsonPath.using(conf2).parse(json).read("$[1]['genre']");
ALWAYS_RETURN_LIST
Cette option configure JSONPath pour toujours retourner une liste
Configuration conf = Configuration.defaultConfiguration();
// Fonctionne
List<String> genres0 = JsonPath.using(conf).parse(json).read("$[0]['genre']");
// Lance une exception PathNotFoundException
List<String> genres1 = JsonPath.using(conf).parse(json).read("$[1]['genre']");
SUPPRESS_EXCEPTIONS
Cette option garantit qu'aucune exception n'est propagée de l'évaluation du chemin. Les règles suivantes sont simples :
- Si l'option ALWAYS_RETURN_LIST est présente, une liste vide sera retournée
- Si l'option ALWAYS_RETURN_LIST n'est pas présente, null sera retourné
SPI JsonProvider
JSONPath est livré avec trois différents JsonProviders :
JsonSmartJsonProvider(par défaut)JacksonJsonProviderJacksonJsonNodeJsonProviderGsonJsonProviderJsonOrgJsonProvider
La modification des valeurs par défaut de la configuration ne peut être effectuée qu'à l'initialisation de l'application. Il est fortement déconseillé de modifier ces valeurs à l'exécution, en particulier dans les applications multithreads.
Configuration.setDefaults(new Configuration.Defaults() {
private final JsonProvider jsonProvider = new JacksonJsonProvider();
private final MappingProvider mappingProvider = new JacksonMappingProvider();
@Override
public JsonProvider jsonProvider() {
return jsonProvider;
}
@Override
public MappingProvider mappingProvider() {
return mappingProvider;
}
@Override
public Set<Option> options() {
return EnumSet.noneOf(Option.class);
}
});
Notez que JacksonJsonProvider nécessite com.fasterxml.jackson.core:jackson-databind:2.4.5, et GsonJsonProvider nécessite com.google.code.gson:gson:2.3.1 dans votre classpath.
SPI Cache
Dans JSONPath 2.1.0, un nouveau Cache SPI a été introduit. Cela permet aux consommateurs de l'API de configurer le cache de chemins de manière adaptée à leurs besoins. Le cache doit être configuré avant la première utilisation, sinon une JsonPathException sera lancée. JSONPath est livré avec deux implémentations de cache :
- com.jayway.jsonpath.spi.cache.LRUCache (par défaut, thread-safe)
- com.jayway.jsonpath.spi.cache.NOOPCache (pas de cache)
Si vous voulez implémenter votre propre cache, l'API est simple :
CacheProvider.setCache(new Cache() {
// Cache simple non thread-safe
private Map<String, JsonPath> map = new HashMap<String, JsonPath>();
@Override
public JsonPath get(String key) {
return map.get(key);
}
@Override
public void put(String key, JsonPath jsonPath) {
map.put(key, jsonPath);
}
});
Exemples d'implémentation Java
JSON
{"magasin":{"livre":[{"categorie":"reference","auteur":"Nigel Rees","titre":"Proverbes du Siècle","prix":8.95},{"categorie":"fiction","auteur":"Evelyn Waugh","titre":"L'Épée d'Honneur","prix":12.99},{"categorie":"fiction","auteur":"Herman Melville","titre":"Moby Dick","isbn":"0-553-21311-3","prix":8.99},{"categorie":"JavaWeb","auteur":"X-rapido","titre":"Top-link","isbn":"0-553-211231-3","prix":32.68},{"categorie":"fiction","auteur":"J. R. R. Tolkien","titre":"Le Seigneur des Anneaux","isbn":"0-395-19395-8","prix":22.99}],"velo":{"couleur":"rouge","prix":19.95}},"cher":10}
Java
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.Iterator;
import java.util.List;
import com.jayway.jsonpath.JsonPath;
public class TestJsonPath {
public static void main(String[] args) {
String jsonDocument = lireFichier();
System.out.println("--------------------------------------extraireValeurJson--------------------------------------");
extraireValeurJson(jsonDocument);
}
private static String lireFichier() {
StringBuilder contenu = new StringBuilder();
try {
FileReader lecteurFichier = new FileReader("D:/livres.txt");
BufferedReader tampon = new BufferedReader(lecteurFichier);
String ligne = "";
while((ligne=tampon.readLine())!=null) {
contenu.append(ligne);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(contenu.toString());
return contenu.toString();
}
private static void extraireValeurJson(String json) {
// Les auteurs de tous les livres
List<String> auteurs1 = JsonPath.read(json, "$.magasin.livre[*].auteur");
// Tous les auteurs
List<String> auteurs2 = JsonPath.read(json, "$..auteur");
// Toutes les choses, livres et vélos
List<Object> auteurs3 = JsonPath.read(json, "$.magasin.*");
// Le prix de tout
List<Object> auteurs4 = JsonPath.read(json, "$.magasin..prix");
// Le troisième livre
List<Object> auteurs5 = JsonPath.read(json, "$..livre[2]");
// Les deux premiers livres
List<Object> auteurs6 = JsonPath.read(json, "$..livre[0,1]");
// Tous les livres de l'index 0 (inclus) à l'index 2 (exclu)
List<Object> auteurs7 = JsonPath.read(json, "$..livre[:2]");
// Tous les livres de l'index 1 (inclus) à l'index 2 (exclu)
List<Object> auteurs8 = JsonPath.read(json, "$..livre[1:2]");
// Les deux derniers livres
List<Object> auteurs9 = JsonPath.read(json, "$..livre[-2:]");
// Livre numéro deux depuis la fin
List<Object> auteurs10 = JsonPath.read(json, "$..livre[2:]");
// Tous les livres avec un numéro ISBN
List<Object> auteurs11 = JsonPath.read(json, "$..livre[?(@.isbn)]");
// Tous les livres en magasin moins chers que 10
List<Object> auteurs12 = JsonPath.read(json, "$.magasin.livre[?(@.prix < 10)]");
// Tous les livres en magasin qui ne sont pas "chers"
List<Object> auteurs13 = JsonPath.read(json, "$..livre[?(@.prix <= $['cher'])]");
// Tous les livres correspondant à l'expression régulière (insensible à la casse)
List<Object> auteurs14 = JsonPath.read(json, "$..livre[?(@.auteur =~ /.*REES/i)]");
// Donnez-moi tout
List<Object> auteurs15 = JsonPath.read(json, "$..*");
// Le nombre de livres
List<Object> auteurs16 = JsonPath.read(json, "$..livre.length()");
afficher("**********auteurs1**********");
afficher(auteurs1);
afficher("**********auteurs2**********");
afficher(auteurs2);
afficher("**********auteurs3**********");
afficherObjets(auteurs3);
afficher("**********auteurs4**********");
afficherObjets(auteurs4);
afficher("**********auteurs5**********");
afficherObjets(auteurs5);
afficher("**********auteurs6**********");
afficherObjets(auteurs6);
afficher("**********auteurs7**********");
afficherObjets(auteurs7);
afficher("**********auteurs8**********");
afficherObjets(auteurs8);
afficher("**********auteurs9**********");
afficherObjets(auteurs9);
afficher("**********auteurs10**********");
afficherObjets(auteurs10);
afficher("**********auteurs11**********");
afficherObjets(auteurs11);
afficher("**********auteurs12**********");
afficherObjets(auteurs12);
afficher("**********auteurs13**********");
afficherObjets(auteurs13);
afficher("**********auteurs14**********");
afficherObjets(auteurs14);
afficher("**********auteurs15**********");
afficherObjets(auteurs15);
afficher("**********auteurs16**********");
afficherObjets(auteurs16);
}
private static void afficher(List<String> liste) {
for(Iterator<String> it = liste.iterator();it.hasNext();) {
System.out.println(it.next());
}
}
private static void afficherObjets(List<Object> liste) {
for(Iterator<Object> it = liste.iterator();it.hasNext();) {
System.out.println(it.next());
}
}
private static void afficher(String s) {
System.out.println("\n"+s);
}
}
Exemple de Filtre
public static void main(String[] args) {
String json = lireFichier();
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);
// Filtre en chaîne (trouver les éléments avec isbn et catégorie fiction ou JavaWeb)
Filtre filtre = Filtre.filtre(Criteres.ou("isbn").existe(true).et("categorie").dans("fiction", "JavaWeb"));
List<Object> livres = JsonPath.read(document, "$.magasin.livre[?]", filtre);
afficherObjets(livres);
System.out.println("\n----------------------------------------------\n");
// Filtre personnalisé
Filtre filtreMap = new Filtre() {
@Override
public boolean appliquer(PredicateContext contexte) {
Map<String, Object> map = contexte.item(Map.class);
if (map.containsKey("isbn")) {
return true;
}
return false;
}
};
List<Object> livres2 = JsonPath.read(document, "$.magasin.livre[?]", filtreMap);
afficherObjets(livres2);
}
Comme on peut le voir, l'utilisation de filtres ou des expressions mentionnées précédemment permet d'obtenir les résultats souhaités.