如何使用Jackson或Gson比较JSON文档并返回差异?

42

我正在使用spring-boot开发后端服务。有一个场景需要比较两个bean(一个是数据库对象,另一个是客户端请求的对象),并返回“新元素”,“修改的元素”,如果没有变化,则返回false。 两个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中返回新的和修改后的元素?您的帮助应该会受到赞赏,即使是一点提示也将是我开始的好开端。

3个回答

82

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

您可以将两个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<K, V>, Map<K, V>) 来进行比较。它会返回一个 MapDifference<K, V> 实例:
MapDifference<String, Object> difference = Maps.difference(leftMap, rightMap);

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

创建用于比较的平面Map

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

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)

1
非常感谢你清晰的解释,Cassio。我会检查并让你知道。 - VelNaga
如果输入的entry为null,这里会抛出 nullPointerException 异常:"return Stream.of(entry);" - VelNaga
1
@VelNaga 如果我的回答解决了你的问题,请接受我的答案。是的,你可以根据自己的需求自定义代码。在处理流时,你可以使用.filter()来删除你不想比较的键,或者直接从比较结果中删除该键。但这超出了你原始问题的范围。 - cassiomolin
1
看起来你的解决方案无法处理数组的不同顺序,例如这两个数组是相同的:[{f:“a”},{f:“b”}] == [{f:“b”},{f:“a”}],但是你的代码却认为它们是不同的。 - Justin
1
@cassiomolin 谢谢你的精彩回答,写得非常清晰明了。第一次就为我解决了问题。 - kunal
显示剩余14条评论

44

创建一个JSON Patch文档

除了在其他答案中描述的方法,您还可以使用在JSR 374中定义的Java JSON处理API(它不使用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差异。它将生成一个JSON Patch文档,如RFC 6902所定义:

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

在应用到源文档时,JSON Patch 将产生目标文档。可以使用以下方法将 JSON Patch 应用于源文档:

JsonObject patched = diff.apply(source);

创建 JSON Merge Patch 文档

根据您的需求,您可以创建一个符合 RFC 7396 定义的 JSON Merge Patch 文档:

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 补丁至文档的代码:

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合并补丁到文档的代码:

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

如果目标包含该成员,则该值会被替换。合并补丁中的null值具有特殊含义,可用于指示删除目标中现有值。[...]

此设计意味着合并补丁文档适用于描述主要使用对象进行结构化的JSON文档的修改,并且不使用显式null值。合并补丁格式并不适用于所有JSON语法。


看起来你的解决方案无法处理数组的不同顺序,例如这两个数组是相同的:[{f:“a”},{f:“b”}] == [{f:“b”},{f:“a”}],但是你的代码却认为它们是不同的。 - Justin
8
根据定义JSON格式的文档RFC 8259所述,数组中元素的顺序很重要(高亮部分为我的翻译):一个对象是零个或多个名称/值对的无序集合,其中名称是字符串,值可以是字符串、数字、布尔值、null、对象或数组。一个数组是零个或多个值的有序序列。 因此,[{"f":"a"},{"f":"b"}][{"f":"b"},{"f":"a"}]是不同的。 - cassiomolin
@cassiomolin 假设一个数组包含5个JSON对象,并且只有第一个位置的对象的一个键的值被更新。合并补丁将返回整个数组,包括未更改的对象。 - undefined

1
你可以尝试我的库 - json-delta
它基于Gson,可以配置特定的目的,如忽略字段或考虑/不考虑丢失/意外的字段。 示例
expected:
{
  "type": "animal",
  "info": {
    "id": 123,
    "subtype": "Cat",
    "timestamp": 1684852390
  }
}

actual:
{
  "type": "animal",
  "info": {
    "id": 123,
    "subtype": "Tiger",
    "timestamp": 1684852399
  }
}

比较和打印出结果:

// Third parameter 'ignoredFields' is vararg  
// Here 'timestamp' field is ignored because dynamic
JsonDeltaReport report = new JsonDelta().compare(expected, actual, "root.info.timestamp");
System.out.println(report);

输出结果将如下所示:

Status: failed
Mismatches:
"root.info.subtype": Value mismatch. Expected: "Cat"; Actual: "Tiger"

JsonDeltaReport 对象具有以下字段:

  1. success(布尔值):比较结果(如果 JSON 相等,则为成功)
  2. mismatches(列表):所有不匹配项的列表

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接