Guide Complet de l'Utilisation de JSONPath pour l'Analyse de Données JSON

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)
  • JacksonJsonProvider
  • JacksonJsonNodeJsonProvider
  • GsonJsonProvider
  • JsonOrgJsonProvider

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.

Étiquettes: JSONPath analyse JSON Java API query language

Publié le 4 juillet à 03h01