web-dev-qa-db-fra.com

Comment comparer des documents JSON et renvoyer les différences avec Jackson ou Gson?

J'utilise spring-boot pour développer des services backend. Il existe un scénario pour comparer les 2 beans (l'un est l'objet DB et l'autre est l'objet demandé par le client) et retourner le "nouvel élément", "l'élément modifié" et s'il n'y a pas de changement, retournez false. Les 2 grains sont dans un format ci-dessous

"sampleList":{
     "timeStamp":"Thu, 21 Jun 2018 07:57:00 +0000",
     "id":"5b19441ac9e77c000189b991",
     "sampleListTypeId":"type001",
     "friendlyName":"sample",
     "contacts":[
        {
           "id":"5b05329cc9e77c000189b950",
           "priorityOrder":1,
           "name":"sample1",
           "relation":"Friend",
           "sampleInfo":{
              "countryCode":"91",
              "numberType":"MOBILE",
              "numberRegion":"IN"
           }
        },
        {
           "id":"5b05329cc9e77c000189b950",
           "priorityOrder":1,
           "name":"sample2",
           "relation":"Friend",
           "sampleInfo":{
              "countryCode":"91",
              "numberType":"MOBILE",
              "numberRegion":"IN"
           }
        }
     ]
  }

J'ai parcouru Internet à propos de la comparaison de haricots pour ce scénario dans Java mais je n'ai trouvé aucune solution plus simple mais j'ai trouvé une solution intéressante pour JSON. Je peux voir une solution pour GSON mais elle ne reviendra pas l'objet client contient "nouvel élément" et "élément change". Existe-t-il un moyen de renvoyer l'élément plus récent et modifié dans JSON ou JAVA? Votre aide devrait être appréciable. Même un indice sera un bon début pour moi.

8
VelNaga

Lire les documents JSON en tant que Map et les comparer

Vous pouvez lire les deux documents JSON sous la forme Map<K, V> . Voir les exemples ci-dessous pour Jackson et Gson:

ObjectMapper mapper = new ObjectMapper();
TypeReference<HashMap<String, Object>> type = 
    new TypeReference<HashMap<String, Object>>() {};

Map<String, Object> leftMap = mapper.readValue(leftJson, type);
Map<String, Object> rightMap = mapper.readValue(rightJson, type);
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>(){}.getType();

Map<String, Object> leftMap = gson.fromJson(leftJson, type);
Map<String, Object> rightMap = gson.fromJson(rightJson, type);

Utilisez ensuite Maps.difference(Map<K, V>, Map<K, V>) de Guava pour les comparer. Il retourne une instance MapDifference<K, V> :

MapDifference<String, Object> difference = Maps.difference(leftMap, rightMap);

Si vous n'êtes pas satisfait du résultat, vous pouvez envisager d'aplatir les cartes, puis de les comparer. Il fournira de meilleurs résultats de comparaison, en particulier pour les objets et les tableaux imbriqués.

Création de plats Map pour la comparaison

Pour aplatir la carte, vous pouvez utiliser:

public final class FlatMapUtil {

    private FlatMapUtil() {
        throw new AssertionError("No instances for you!");
    }

    public static Map<String, Object> flatten(Map<String, Object> map) {
        return map.entrySet().stream()
                .flatMap(FlatMapUtil::flatten)
                .collect(LinkedHashMap::new, (m, e) -> m.put("/" + e.getKey(), e.getValue()), LinkedHashMap::putAll);
    }

    private static Stream<Map.Entry<String, Object>> flatten(Map.Entry<String, Object> entry) {

        if (entry == null) {
            return Stream.empty();
        }

        if (entry.getValue() instanceof Map<?, ?>) {
            return ((Map<?, ?>) entry.getValue()).entrySet().stream()
                    .flatMap(e -> flatten(new AbstractMap.SimpleEntry<>(entry.getKey() + "/" + e.getKey(), e.getValue())));
        }

        if (entry.getValue() instanceof List<?>) {
            List<?> list = (List<?>) entry.getValue();
            return IntStream.range(0, list.size())
                    .mapToObj(i -> new AbstractMap.SimpleEntry<String, Object>(entry.getKey() + "/" + i, list.get(i)))
                    .flatMap(FlatMapUtil::flatten);
        }

        return Stream.of(entry);
    }
}

Il utilise la notation JSON Pointer définie dans RFC 6901 pour les clés, afin que vous puissiez facilement localiser les valeurs.

Exemple

Considérez les documents JSON suivants:

{
  "name": {
    "first": "John",
    "last": "Doe"
  },
  "address": null,
  "birthday": "1980-01-01",
  "company": "Acme",
  "occupation": "Software engineer",
  "phones": [
    {
      "number": "000000000",
      "type": "home"
    },
    {
      "number": "999999999",
      "type": "mobile"
    }
  ]
}
{
  "name": {
    "first": "Jane",
    "last": "Doe",
    "nickname": "Jenny"
  },
  "birthday": "1990-01-01",
  "occupation": null,
  "phones": [
    {
      "number": "111111111",
      "type": "mobile"
    }
  ],
  "favorite": true,
  "groups": [
    "close-friends",
    "gym"
  ]
}

Et le code suivant pour les comparer et montrer les différences:

Map<String, Object> leftFlatMap = FlatMapUtil.flatten(leftMap);
Map<String, Object> rightFlatMap = FlatMapUtil.flatten(rightMap);

MapDifference<String, Object> difference = Maps.difference(leftFlatMap, rightFlatMap);

System.out.println("Entries only on the left\n--------------------------");
difference.entriesOnlyOnLeft()
          .forEach((key, value) -> System.out.println(key + ": " + value));

System.out.println("\n\nEntries only on the right\n--------------------------");
difference.entriesOnlyOnRight()
          .forEach((key, value) -> System.out.println(key + ": " + value));

System.out.println("\n\nEntries differing\n--------------------------");
difference.entriesDiffering()
          .forEach((key, value) -> System.out.println(key + ": " + value));

Il produira la sortie suivante:

Entries only on the left
--------------------------
/address: null
/phones/1/number: 999999999
/phones/1/type: mobile
/company: Acme


Entries only on the right
--------------------------
/name/nickname: Jenny
/groups/0: close-friends
/groups/1: gym
/favorite: true


Entries differing
--------------------------
/birthday: (1980-01-01, 1990-01-01)
/occupation: (Software engineer, null)
/name/first: (John, Jane)
/phones/0/number: (000000000, 111111111)
/phones/0/type: (home, mobile)
10
cassiomolin

Création d'un document de patch JSON

Alternativement à l'approche décrite dans le autre réponse , vous pouvez utiliser le API Java pour le traitement JSON défini dans le JSR 374 (il ne utiliser sur Gson ou Jackson). Les dépendances suivantes sont requises:

<!-- Java API for JSON Processing (API) -->
<dependency>
    <groupId>javax.json</groupId>
    <artifactId>javax.json-api</artifactId>
    <version>1.1.2</version>
</dependency>

<!-- Java API for JSON Processing (implementation) -->
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.json</artifactId>
    <version>1.1.2</version>
</dependency>

Ensuite, vous pouvez créer un diff JSON à partir des documents JSON. Il produira un document de patch JSON tel que défini dans le RFC 6902 :

JsonPatch diff = Json.createDiff(source, target);

Lorsqu'il est appliqué au document source, le correctif JSON produit le document cible. Le correctif JSON peut être appliqué au document source en utilisant:

JsonObject patched = diff.apply(source);

Création d'un document de correctif de fusion JSON

Selon vos besoins, vous pouvez créer un document JSON Merge Patch tel que défini dans le RFC 7396 :

JsonMergePatch mergeDiff = Json.createMergeDiff(source, target);

Lorsqu'il est appliqué au document source, le correctif de fusion JSON produit le document cible. Pour patcher la source, utilisez:

JsonValue patched = mergeDiff.apply(source);

Jolie impression de documents JSON

Pour imprimer joliment les documents JSON, vous pouvez utiliser:

System.out.println(format(diff.toJsonArray()));
System.out.println(format(mergeDiff.toJsonValue()));
public static String format(JsonValue json) {
    StringWriter stringWriter = new StringWriter();
    prettyPrint(json, stringWriter);
    return stringWriter.toString();
}

public static void prettyPrint(JsonValue json, Writer writer) {
    Map<String, Object> config =
            Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true);
    JsonWriterFactory writerFactory = Json.createWriterFactory(config);
    try (JsonWriter jsonWriter = writerFactory.createWriter(writer)) {
        jsonWriter.write(json);
    }
}

Exemple

Considérez les documents JSON suivants:

{
  "name": {
    "first": "John",
    "last": "Doe"
  },
  "address": null,
  "birthday": "1980-01-01",
  "company": "Acme",
  "occupation": "Software engineer",
  "phones": [
    {
      "number": "000000000",
      "type": "home"
    },
    {
      "number": "999999999",
      "type": "mobile"
    }
  ]
}
{
  "name": {
    "first": "Jane",
    "last": "Doe",
    "nickname": "Jenny"
  },
  "birthday": "1990-01-01",
  "occupation": null,
  "phones": [
    {
      "number": "111111111",
      "type": "mobile"
    }
  ],
  "favorite": true,
  "groups": [
    "close-friends",
    "gym"
  ]
}

Et le code suivant pour produire un patch JSON:

JsonValue source = Json.createReader(new StringReader(leftJson)).readValue();
JsonValue target = Json.createReader(new StringReader(rightJson)).readValue();

JsonPatch diff = Json.createDiff(source.asJsonObject(), target.asJsonObject());
System.out.println(format(diff.toJsonArray()));

Il produira la sortie suivante:

[
    {
        "op": "replace",
        "path": "/name/first",
        "value": "Jane"
    },
    {
        "op": "add",
        "path": "/name/nickname",
        "value": "Jenny"
    },
    {
        "op": "remove",
        "path": "/address"
    },
    {
        "op": "replace",
        "path": "/birthday",
        "value": "1990-01-01"
    },
    {
        "op": "remove",
        "path": "/company"
    },
    {
        "op": "replace",
        "path": "/occupation",
        "value": null
    },
    {
        "op": "replace",
        "path": "/phones/1/number",
        "value": "111111111"
    },
    {
        "op": "remove",
        "path": "/phones/0"
    },
    {
        "op": "add",
        "path": "/favorite",
        "value": true
    },
    {
        "op": "add",
        "path": "/groups",
        "value": [
            "close-friends",
            "gym"
        ]
    }
]

Considérez maintenant le code suivant pour produire un correctif de fusion JSON:

JsonValue source = Json.createReader(new StringReader(leftJson)).readValue();
JsonValue target = Json.createReader(new StringReader(rightJson)).readValue();

JsonMergePatch mergeDiff = Json.createMergeDiff(source, target);
System.out.println(format(mergeDiff.toJsonValue()));

Il produira la sortie suivante:

{
    "name": {
        "first": "Jane",
        "nickname": "Jenny"
    },
    "address": null,
    "birthday": "1990-01-01",
    "company": null,
    "occupation": null,
    "phones": [
        {
            "number": "111111111",
            "type": "mobile"
        }
    ],
    "favorite": true,
    "groups": [
        "close-friends",
        "gym"
    ]
}

Résultats différents lors de l'application des patchs

Lorsque le correctif est appliqué, les résultats sont légèrement différents pour les approches décrites ci-dessus. Considérez le code suivant qui applique le correctif JSON à un document:

JsonPatch diff = ...
JsonValue patched = diff.apply(source.asJsonObject());
System.out.println(format(patched));

Cela produit:

{
    "name": {
        "first": "Jane",
        "last": "Doe",
        "nickname": "Jenny"
    },
    "birthday": "1990-01-01",
    "occupation": null,
    "phones": [
        {
            "number": "111111111",
            "type": "mobile"
        }
    ],
    "favorite": true,
    "groups": [
        "close-friends",
        "gym"
    ]
}

Considérez maintenant le code suivant qui applique le correctif de fusion JSON à un document:

JsonMergePatch mergeDiff = ...
JsonValue patched = mergeDiff.apply(source);
System.out.println(format(patched));

Cela produit:

{
    "name": {
        "first": "Jane",
        "last": "Doe",
        "nickname": "Jenny"
    },
    "birthday": "1990-01-01",
    "phones": [
        {
            "number": "111111111",
            "type": "mobile"
        }
    ],
    "favorite": true,
    "groups": [
        "close-friends",
        "gym"
    ]
}

Dans le premier exemple, la propriété occupation est null. Dans le deuxième exemple, il est omis. Cela est dû à la sémantique null sur JSON Merge Patch. De la RFC 7396 :

Si la cible contient le membre, la valeur est remplacée. Les valeurs nulles dans le correctif de fusion ont une signification spéciale pour indiquer la suppression des valeurs existantes dans la cible. [...]

Cette conception signifie que les documents de correctif de fusion conviennent pour décrire les modifications apportées aux documents JSON qui utilisent principalement des objets pour leur structure et n'utilisent pas de valeurs Null explicites. Le format du correctif de fusion n'est pas approprié pour toutes les syntaxes JSON.

10
cassiomolin