如何比较JSON文档并返回与杰克逊或Gson的差异?



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

"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 中返回更新和修改的元素?您的帮助应该是可观的。即使是一个提示对我来说也是一个很好的开始。

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

然后使用番石榴的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 leftn--------------------------");
difference.entriesOnlyOnLeft()
.forEach((key, value) -> System.out.println(key + ": " + value));
System.out.println("nnEntries only on the rightn--------------------------");
difference.entriesOnlyOnRight()
.forEach((key, value) -> System.out.println(key + ": " + value));
System.out.println("nnEntries differingn--------------------------");
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)

创建 JSON 补丁文档

除了另一个答案中描述的方法之外,您可以使用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 补丁文档:

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

当应用于源文档时,JSON 修补程序将生成目标文档。可以使用以下命令将 JSON 修补程序应用于源文档:

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 补丁:

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 合并补丁上的语义null。从 RFC 7396:

如果目标包含成员,则会替换该值。 合并修补程序中的 null 值具有特殊含义,以指示删除目标中的现有值。[...]

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

你可以试试我的库 - 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. 成功(布尔值(:比较结果(如果 JSON 相等,则成功(
  2. 不匹配
  3. (列表(:所有不匹配的列表

最新更新