在Java中比较两个JSON对象的相等性,忽略子项的顺序测试。

294
我正在寻找一个JSON解析库,支持比较两个JSON对象时忽略子级顺序,特别是用于对来自Web服务的JSON返回值进行单元测试。
主要的JSON库中是否有任何一个支持此功能?org.json库仅执行引用比较。

1
无法将两个对象序列化为字符串表示并进行比较?我猜所有的库都支持 toString() 将对象转换为 JSON 字符串。 - Teja Kantamneni
58
假定序列化到字符串和反序列化回来的顺序总是相同的,但我不确定这种假设是否成立。 - Jeff
你说得对,Jeff,这真的不安全。这个测试展示了一种情况,其中映射是相同的,但toString()返回的输出并不相同:https://gist.github.com/anonymous/5974797。这是因为底层的HashMap可能会增长,如果你删除键,HashMap内部数组不会缩小。 - Guillaume Perrot
Json Compare json-compare - Slev Florin
28个回答

195

尝试使用Skyscreamer的JSONAssert

它的非严格模式具有两个主要优点,使其更加灵活:

  • 对象可扩展性(例如,对于预期值为{id:1}的情况,这条测试也可以通过:{id:1,moredata:'x'}
  • 松散的数组排序(例如,['dog','cat']==['cat','dog'])

在严格模式下,它的行为更像json-lib的测试类。

测试案例如下所示:

@Test
public void testGetFriends() {
    JSONObject data = getRESTData("/friends/367.json");
    String expected = "{friends:[{id:123,name:\"Corby Page\"}"
        + ",{id:456,name:\"Solomon Duskis\"}]}";
    JSONAssert.assertEquals(expected, data, false);
}

JSONAssert.assertEquals()的参数包括expectedJSONStringactualDataStringisStrict

结果消息非常清晰,这在比较非常大的JSON对象时非常重要。


29
我一直在使用这个解决方案,但我刚刚发现你还可以提供一个由你选择的JSON比较模式。其中之一是NON_EXTENSIBLE。因此,你需要像这样做: JSONAssert.assertEquals(expected, data, JSONCompareMode.NON_EXTENSIBLE); NON_EXTENSIBLE模式意味着任何新字段或缺失字段会导致失败,但顺序不会。使用false会启动宽松模式,该模式不会报告任何额外或缺少的子元素。 - Dan Temple
1
非可扩展的比较模式正是我在寻找的。感谢你,丹。 - ThoughtCrhyme
3
在我兴奋之前,这是否也支持嵌套的JSON对象和数组呢? :) - Christian
2
在使用该库之前,人们可能需要考虑此JSONassert问题,它表明该库的一个依赖项目前存在潜在的许可问题。 - Chriki
3
JSONAssert问题#44已经通过清洁室库替换在PR 67中得到修复,今天发布了JSONAssert 1.4.0版本。 - Carter Page
显示剩余7条评论

104
作为一般的架构设计建议,我通常不建议让特定序列化格式的依赖超出存储/网络层; 因此,我首先建议您考虑测试您自己应用程序对象之间的相等性,而不是它们的JSON表现形式。
话虽如此,我目前非常喜欢Jackson,根据我快速阅读他们的ObjectNode.equals()实现,它似乎执行了你所需的集合成员比较。
public boolean equals(Object o)
{
    if (o == this) return true;
    if (o == null) return false;
    if (o.getClass() != getClass()) {
        return false;
    }
    ObjectNode other = (ObjectNode) o;
    if (other.size() != size()) {
        return false;
    }
    if (_children != null) {
        for (Map.Entry<String, JsonNode> en : _children.entrySet()) {
            String key = en.getKey();
            JsonNode value = en.getValue();

            JsonNode otherValue = other.get(key);

            if (otherValue == null || !otherValue.equals(value)) {
                return false;
            }
        }
    }
    return true;
}

该方法不对称,因为它仅测试子级的“子集”关系,而不是相等性。 “其他”对象可能具有比_children更多的子级,但此方法仍将返回true。 - Yoni
24
@Yoni:不准确,因为存在尺寸比较。他们必须有相同数量的孩子以及相同的孩子。@Jolly Roger:在这种情况下,我没有将对象从JSON串行化回POJO,但是当将JSON发送到一个执行此操作的系统时,我不能依赖它以与我发送的格式完全相同的格式发送回来。 - Jeff
1
@Jeff,这个对你有用吗?在junit中,assertEquals对我失败了。该项目使用的是旧版本(1.5),供参考。 - ronnyfm
1
如果您没有断言 JSON,则可能缺少一些高级测试。您可以编写一个简单的测试,进行 HTTP 请求并响应一些预期的 JSON,这是 Web 服务的主要功能行为。 - mogronalol
1
关于性能的一个小注:比较“value”和“otherValue”的代码可以简化为 if (value.equals(other.get(key)) return false; 因为“value”保证不为空,而JsonNode的equals()方法应该接受空参数。 - Tillmann

61

使用 GSON

JsonParser parser = new JsonParser();
JsonElement o1 = parser.parse("{a : {a : 2}, b : 2}");
JsonElement o2 = parser.parse("{b : 2, a : {a : 2}}");
assertEquals(o1, o2);

编辑:自从GSON v2.8.6开始,实例方法JsonParser.parse已被弃用。您必须使用静态方法JsonParser.parseString

JsonElement o1 = JsonParser.parseString("{a : {a : 2}, b : 2}");
JsonElement o2 = JsonParser.parseString("{b : 2, a : {a : 2}}");
assertEquals(o1, o2);

对我来说,在GSON 2.8.2中可以工作。 - Tom Saleeba
2
如果jsonArray元素不按顺序排列,则它将无法工作。版本2.8.2。 - Naveen Kumar R B
1
如果对象/元素的顺序不同,对我来说无法工作。 - jknair0
它对我有效,因为我只是想在一个单元测试中使用它,其中两个 JSON 属性具有相同的顺序。 - GabrielBB
@GabrielBB 你应该编辑问题,指明API在哪个版本中被弃用,并且新的代码风格在哪个版本中开始生效。 - clearlight
@GabrialBB,我已经作为建议编辑队列中的审核人员批准了您之前的编辑。当您的新答案再次出现在队列中时,我相信它会得到投票者的批准。谢谢。 - clearlight

43

我会做以下事情:

JSONObject obj1 = /*json*/;
JSONObject obj2 = /*json*/;

ObjectMapper mapper = new ObjectMapper();

JsonNode tree1 = mapper.readTree(obj1.toString());
JsonNode tree2 = mapper.readTree(obj2.toString());

return tree1.equals(tree2);

26
但我认为这是一种严格的比较。对于两个相等的JSON,如果元素顺序不同,则会返回false。 - deadpool
这是有史以来最好的答案。它不仅回答了我的问题,还回答了我们几乎需要做的每个对象比较。谢谢,Joshu。 - Luiz Feijão Veronesi
2
@deadpool,可能以前是严格比较,但现在不再是了。 - Andrei Damian-Fekete
3
当您将数组作为嵌套对象并且数组中存在顺序差异时,此方法将不起作用。 - sourabh
3
这将忽略大多数的顺序更改,除了必须相同的列表顺序更改。请参见此处链接:https://www.baeldung.com/jackson-compare-two-json-objects - thedrs
显示剩余4条评论

23

使用这个库:https://github.com/lukas-krecan/JsonUnit

Pom:

<dependency>
    <groupId>net.javacrumbs.json-unit</groupId>
    <artifactId>json-unit-assertj</artifactId>
    <version>2.24.0</version>
    <scope>test</scope>
</dependency>

IGNORING_ARRAY_ORDER - 忽略数组中的顺序

assertThatJson("{\"test\":[1,2,3]}")
  .when(Option.IGNORING_ARRAY_ORDER)
  .isEqualTo("{\"test\": [3,2,1]}");

你能否添加更多的注释,以便我们知道如何使用它?我已经将它添加到我的POM文件中,但我们需要文档来理解如何使用它。 - Ran Adler
使用非常简单: 将以下内容添加到您的pom文件中。<dependency> <groupId>net.javacrumbs.json-unit</groupId> <artifactId>json-unit</artifactId> <version>1.5.0</version> <scope>test</scope> </dependency> - chethu
我已经看了链接中的指示十次,最终找到了它 :) - Ran Adler

20

你可以尝试使用json-lib库中的JSONAssert类:

JSONAssert.assertEquals(
  "{foo: 'bar', baz: 'qux'}",
  JSONObject.fromObject("{foo: 'bar', baz: 'xyzzy'}")
);

输出结果:

junit.framework.ComparisonFailure: objects differed at key [baz]; expected:<[qux]> but was:<[xyzzy]>

甚至更简单:JSONAssert.assertJsonEquals("{foo: 'bar', baz: 'qux'}", {foo: 'bar', baz: 'xyzzy'}"); - Rob Juurlink
3
虽然这种解决方案适用于JSON中数据项的顺序,但如果数组中元素的顺序不匹配,则会失败。例如,如果您的代码使用了一个转换为JSON的Set。以下JSON比较将失败: JSONAssert.assertJsonEquals( "{foo: 'bar', list: [{test: '1'}, {rest: '2'}] }", "{ foo: 'bar', list: [{rest: '2'}, {test: '1'}] }");错误信息为: junit.framework.AssertionFailedError: : : objects differed at key [list];: arrays first differed at element [0];: objects differed at key [test]; - Dan Temple
是的,这是 Json 的一个限制 :(。json.org 显示没有无序集合令牌。{ } 只能包围键值对。这非常令人恼火。 - Merk

14

如果你已经在使用JUnit,最新版本现在采用了Hamcrest。它是一个通用的匹配框架(特别适用于单元测试),可以扩展以构建新的匹配器。

有一个名为hamcrest-json的小型开源库,具有JSON感知匹配功能。 它有很好的文档、测试和支持。以下是一些有用的链接:

一些使用JSON库org.json.simple中对象的示例代码:

Assert.assertThat(
    jsonObject1.toJSONString(),
    SameJSONAs.sameJSONAs(jsonObject2.toJSONString()));

可选地,你可以 (1) 允许 "任意顺序" 的数组和 (2) 忽略额外的字段。

由于 Java 有多种 JSON 库(如 JacksonGSONjson-lib 等),因此 hamcrest-json 支持将 JSON 文本(作为 java.lang.String)以及从 Douglas Crockford 的 JSON 库 org.json 原生支持的对象转换成 JSON 对象。

最后,如果你没有使用 JUnit,你可以直接使用 Hamcrest 进行断言。(我在这里写过相关内容。)


使用Hamcrest匹配器与直接使用JSONAssert相比有什么优势? - Johannes
2
@Johannes:仅限样式。 - kevinarpe
1
hamcrest-json源代码目前的最后提交日期为2012年。它可能不再得到很好的支持。 - Thorbjørn Ravn Andersen

13

你可以尝试使用JsonUnit。它可以比较两个JSON对象并报告差异。这是在Jackson的基础上构建的。

例如:

assertThatJson("{\"test\":1}").isEqualTo("{\n\"test\": 2\n}");

结果为

java.lang.AssertionError: JSON documents are different:
Different value found in node "test". Expected 1, got 2.

8

我正在使用这个(与org.json.*一起),对我来说很好用:

package com.project1.helpers;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class JSONUtils {

    public static boolean areEqual(Object ob1, Object ob2) throws JSONException {
        Object obj1Converted = convertJsonElement(ob1);
        Object obj2Converted = convertJsonElement(ob2);
        return obj1Converted.equals(obj2Converted);
    }

    private static Object convertJsonElement(Object elem) throws JSONException {
        if (elem instanceof JSONObject) {
            JSONObject obj = (JSONObject) elem;
            Iterator<String> keys = obj.keys();
            Map<String, Object> jsonMap = new HashMap<>();
            while (keys.hasNext()) {
                String key = keys.next();
                jsonMap.put(key, convertJsonElement(obj.get(key)));
            }
            return jsonMap;
        } else if (elem instanceof JSONArray) {
            JSONArray arr = (JSONArray) elem;
            Set<Object> jsonSet = new HashSet<>();
            for (int i = 0; i < arr.length(); i++) {
                jsonSet.add(convertJsonElement(arr.get(i)));
            }
            return jsonSet;
        } else {
            return elem;
        }
    }
}

这个方法只是比较对象,但并没有给出合理的解释它们之间的差异。 - LoBo

8

我所做的一件事情,产生了惊人的效果,就是将两个对象都读入HashMap中,然后使用普通的assertEquals()方法进行比较。它会调用hashmaps的equals()方法,该方法将递归地比较内部的所有对象(它们将是其他hashmaps或一些单值对象,如字符串或整数)。这是使用Codehaus' Jackson JSON解析器完成的。

assertEquals(mapper.readValue(expectedJson, new TypeReference<HashMap<String, Object>>(){}), mapper.readValue(actualJson, new TypeReference<HashMap<String, Object>>(){}));

如果JSON对象是数组,则可以使用类似的方法。


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