【问题标题】:How to compare JSON documents and return the differences with Jackson or Gson?如何比较 JSON 文档并返回与 Jackson 或 Gson 的差异?
【发布时间】:2018-06-21 11:07:19
【问题描述】:

我正在使用 spring-boot 开发后端服务。有一个场景来比较 2-beans(一个是 DB 对象,另一个是客户端请求的对象)并返回“新元素”、“修改的元素”,如果没有变化则返回 false。 2-bean 采用以下格式

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

我已经在互联网上浏览了有关 Java 中此场景的 bean 比较,但我找不到任何更简单的解决方案,但找到了一些很酷的 JSON 解决方案。我可以看到 GSON 的一些解决方案,但它不会返回包含“新元素”和“更改元素”的客户端对象。有没有办法在 JSON 或 JAVA 中返回更新和修改的元素?你的帮助应该是可观的。对我来说,即使是一个提示也是一个很好的开始。

【问题讨论】:

    标签: java json jackson gson


    【解决方案1】:

    将 JSON 文档读取为 Maps 并进行比较

    您可以将这两个 JSON 文档读取为Map<K, V>。请参阅以下有关 Jackson 和 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);
    

    然后使用 Guava 的Maps.difference(Map&lt;K, V&gt;, Map&lt;K, V&gt;) 进行比较。它返回一个MapDifference&lt;K, V&gt; 实例:

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

    如果您对结果不满意,可以考虑展平地图,然后进行比较。它将提供更好的比较结果,尤其是对于嵌套对象和数组。

    为比较创建平面Maps

    要平整地图,您可以使用:

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

    它使用在RFC 6901 中定义的 JSON 指针表示法 作为键,因此您可以轻松找到值。

    示例

    考虑以下 JSON 文档:

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

    下面的代码来比较它们并显示差异:

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

    它将产生以下输出:

    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)
    

    【讨论】:

    • 非常感谢您的清晰解释卡西欧...我会检查并通知您
    • 这里抛出 nullPointerException "return Stream.of(entry);"如果条目为空。
    • 啊,异常来自“Map.Entry::getValue”。所以我们应该在那里进行空检查,因为值可以为空
    • @VelNaga 如果它回答了您的原始问题,请接受我的回答。而且,是的,您可以根据需要自定义代码。您可以使用.filter() 删除在处理流时不想比较的键,或者只是从比较结果中删除键。但这超出了您最初问题的范围。
    • @cassiomolin 感谢一个了不起的答案,写得很清楚。在第一次运行时为我工作
    【解决方案2】:

    创建 JSON 补丁文档

    除了other answer 中描述的方法之外,您还可以使用JSR 374 中定义的Java API for JSON Processing(它不适用于Gson 或Jackson)。需要以下依赖项:

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

    然后您可以从 JSON 文档创建 JSON 差异。它将生成RFC 6902 中定义的 JSON Patch 文档:

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

    当应用于源文档时,JSON 补丁会生成目标文档。 JSON Patch 可以通过以下方式应用于源文档:

    JsonObject patched = diff.apply(source);
    

    创建 JSON 合并补丁文档

    根据您的需要,您可以创建一个在RFC 7396 中定义的 JSON 合并补丁文档:

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

    当应用于源文档时,JSON 合并补丁会生成目标文档。要修补源,请使用:

    JsonValue patched = mergeDiff.apply(source);
    

    漂亮的打印 JSON 文档

    要漂亮地打印 JSON 文档,您可以使用:

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

    示例

    考虑以下 JSON 文档:

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

    以下代码生成 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()));
    

    它将产生以下输出:

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

    现在考虑以下代码来生成 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()));
    

    它将产生以下输出:

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

    应用补丁时的不同结果

    应用补丁文档时,上述方法的结果略有不同。考虑以下将 JSON Patch 应用于文档的代码:

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

    它产生:

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

    现在考虑以下将 JSON Merge Patch 应用于文档的代码:

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

    它产生:

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

    在第一个示例中,occupation 属性为null。在第二个例子中,它被省略了。这是由于 JSON Merge Patch 上的 null 语义。来自RFC 7396

    如果目标确实包含该成员,则替换该值。合并补丁中的空值具有特殊含义,以指示删除目标中的现有值。 [...]

    这种设计意味着合并补丁文档适用于描述对主要使用对象作为其结构并且不使用显式空值的 JSON 文档的修改。合并补丁格式不适用于所有 JSON 语法。

    【讨论】:

    • 看起来您的解决方案无法处理数组的不同顺序,就像这两个相同:[ {f:"a"},{f:"b"}] == [ { f:"b"},{f:"a"}],但你的代码说不一样
    • @Justin 根据RFC 8259(定义JSON格式的文档),数组元素的顺序很重要(重点是我的):一个对象 是零个或多个名称/值对的无序 集合,其中名称是字符串,值是字符串、数字、布尔值、null、对象或数组。 数组是零个或多个值的有序序列。所以[{"f":"a"},{"f":"b"}][{"f":"b"},{"f":"a"}]不同的。跨度>
    猜你喜欢
    • 2021-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-14
    • 1970-01-01
    • 2014-02-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多