How to compare JSON documents and return the differences with Jackson or Gson?

后端 未结 2 1419
悲哀的现实
悲哀的现实 2020-12-24 08:35

I am using spring-boot to develop backend services. There is a scenario to compare 2-beans(one is the DB object and another one is the client requested object) and return th

相关标签:
2条回答
  • 2020-12-24 08:53

    Creating a JSON Patch document

    Alternatively to the approach described in the other answer, you could use the Java API for JSON Processing defined in the JSR 374 (it doesn't use on Gson or Jackson). The following dependencies are required:

    <!-- 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>
    

    Then you can create a JSON diff from the JSON documents. It will produce a JSON Patch document as defined in the RFC 6902:

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

    When applied to the source document, the JSON Patch yields the target document. The JSON Patch can be applied to the source document using:

    JsonObject patched = diff.apply(source);
    

    Creating a JSON Merge Patch document

    Depending on your needs, you could create a JSON Merge Patch document as defined in the RFC 7396:

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

    When applied to the source document, the JSON Merge Patch yields the target document. To patch the source, use:

    JsonValue patched = mergeDiff.apply(source);
    

    Pretty printing JSON documents

    To pretty print the JSON documents, you can use:

    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);
        }
    }
    

    Example

    Consider the following JSON documents:

    {
      "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"
      ]
    }
    

    And the following code to produce a JSON Patch:

    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()));
    

    It will produce the following output:

    [
        {
            "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"
            ]
        }
    ]
    

    Now consider the following code to produce a JSON Merge Patch:

    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()));
    

    It will produce the following output:

    {
        "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"
        ]
    }
    

    Different results when applying the patches

    When the patch document is applied, the results are slightly different for the approaches described above. Consider the following code that applies JSON Patch to a document:

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

    It produces:

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

    Now consider the following code that applies JSON Merge Patch to a document:

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

    It produces:

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

    In the first example, the occupation property is null. In the second example, it's omitted. It's due to the null semantics on JSON Merge Patch. From the RFC 7396:

    If the target does contain the member, the value is replaced. Null values in the merge patch are given special meaning to indicate the removal of existing values in the target. [...]

    This design means that merge patch documents are suitable for describing modifications to JSON documents that primarily use objects for their structure and do not make use of explicit null values. The merge patch format is not appropriate for all JSON syntaxes.

    0 讨论(0)
  • 2020-12-24 09:13

    Reading the JSON documents as Maps and comparing them

    You could read both JSON documents as Map<K, V>. See the below examples for Jackson and 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);
    

    Then use Guava's Maps.difference(Map<K, V>, Map<K, V>) to compare them. It returns a MapDifference<K, V> instance:

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

    If you are not happy with the result, you can consider flattening the maps and then compare them. It will provide better comparison results especially for nested objects and arrays.

    Creating flat Maps for the comparison

    To flat the map, you can use:

    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);
        }
    }
    

    It uses the JSON Pointer notation defined in the RFC 6901 for the keys, so you can easily locate the values.

    Example

    Consider the following JSON documents:

    {
      "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"
      ]
    }
    

    And the following code to compare them and show the differences:

    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));
    

    It will produce the following output:

    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)
    
    0 讨论(0)
提交回复
热议问题