测试两个JSON对象是否相等,忽略Java中的子顺序


233

我正在寻找一个JSON解析库,该库支持比较两个JSON对象而忽略子顺序,特别是对从Web服务返回的JSON进行单元测试。

任何主要的JSON库都支持吗?org.json库仅进行参考比较。


1
无法将两个对象都序列化为字符串表示形式并进行比较?我猜所有库都支持toString()将对象转换为JSON字符串。
Teja Kantamneni

47
假定与字符串的串行化顺序始终相同。我对此假设不满意。
杰夫

杰夫,您说得对,一点都不安全。该测试显示了一个映射是相同的场景,但是toString()没有返回相同的输出:gist.github.com/anonymous/5974797。这是因为基础HashMap可以增长,并且如果删除键,则HashMap内部数组不会缩小。
Guillaume Perrot

Answers:


84

作为一般的体系结构要点,我通常建议不要让对特定序列化格式的依赖关系超出存储/网络层的范围。因此,我首先建议您考虑测试自己的应用程序对象之间的相等性,而不是它们的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 2010年

23
@Yoni:不对,因为有尺寸比较。他们必须有完全相同数量的孩子以及相同的孩子。@Jolly Roger:在这种情况下,我没有将对象从JSON序列化回POJO,但是在将系统发送给JSON时,我不能依靠它以与发送时完全相同的格式将其发送回它。
杰夫

1
@Jeff对您有用吗?在junit中,assertEquals对我而言失败。该项目使用的是旧版本(1.5)。
ronnyfm 2015年

1
如果您不声明JSON,我认为您缺少一些高级测试。您可以进行一个瘦测试,该测试发出一个http请求并以一些期望的JSON响应-这是Web服务的主要功能行为。
mogronalol's

只是关于性能的一点说明:将“值”与“ otherValue”进行比较的代码可以简化为if(value.equals(other.get(key))返回false;因为“ value”保证为非null且JsonNode的equals()方法应该接受空参数
Tillmann

154

尝试使用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对象时非常重要。


18
我一直在使用此解决方案,但我发现您还可以提供自己选择的JSONCompareMode。其中之一是NON_EXTENSIBLE。因此,您JSONAssert.assertEquals(expected, data, JSONCompareMode.NON_EXTENSIBLE);将遇到类似这样的情况: NON_EXTENSIBLE模式意味着任何新字段或缺失字段都会导致失败,但顺序不会导致失败。使用false会激发宽大模式,该模式不会报告任何额外的或丢失的子元素。
丹·

1
NON_EXTENSIBLE比较模式正是我想要的。谢谢你,丹。
ThoughtCrhyme 2013年

在我激动之前:这是否也支持嵌套的JSON对象和数组?:)
基督教徒

2
人们可能想在使用该库之前考虑这个JSONassert问题:它表明当前存在与库相关的潜在许可问题。
Chriki

3
JSONAssert问题#44已通过PR 67中的无尘室库替换修复,今天作为JSONAssert 1.4.0发布。
Carter Page

49

使用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中工作。
汤姆·萨利巴

1
如果jsonArray元素顺序不正确,则无法使用。版本2.8.2
Naveen Kumar RB

1
如果对象/元素的顺序不同,则对我
不起作用

它对我有用,因为我只想将它用于两个jsons属性具有相同顺序的单元测试
GabrielBB

@GabrielBB您应该编辑问题,并指出不建议使用哪个版本的API,以及新版本的代码将在哪个版本中开始工作。
clearlight

30

我会做以下事情,

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

如果您已经在使用Jackson,这是恕我直言的最佳答案。
Christoph Dietze

18
但这是我的严格比较。对于两个具有不同元素顺序的相等json,它将返回false。
死侍,

@deadpool的原理是合理的,您只需要更改比较就可以使它更复杂(例如,检查关键字段,而不是简单的等号树)。
jwenting

这是有史以来最好的答案。它不仅回答了我的问题,而且还回答了我们需要做的几乎每个对象比较。谢谢,乔树
LuizFeijãoVeronesi

2
@deadpool,可能是严格的比较,但现在不严格。
Andrei Damian-Fekete

18

您可以尝试使用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 2013年

2
尽管此解决方案适用于JSON中数据项的顺序,但如果数组中元素的顺序不匹配,它将失败。例如,如果您的代码使用Set转换为JSON。以下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 2016年

15

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

Pom:

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

IGNORING_ARRAY_ORDER-忽略数组中的顺序

assertJsonEquals("{\"test\":[1,2,3]}",
  "{\"test\":  [3,2,1]}",
  when(IGNORING_ARRAY_ORDER)
);

您能添加更多评论吗,我们如何使用它,我将其添加到我的pom中,但我们需要文档才能理解如何使用它
Ran Adler 2014年

确保用法非常简单:将其添加到pom中。<dependency> <groupId> net.javacrumbs.json-unit </ groupId> <artifactId> json-unit </ artifactId> <version> 1.5.0 </ version> <scope> test </ scope> </ dependency>
chethu 2014年

10倍我查看了链接上的说明,然后找到了它:)
Ran Adler 2014年

12

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

有一个名为hamcrest-jsonJSON感知匹配项的小型开源库。它有充分的文档记录,测试和支持。以下是一些有用的链接:

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

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

(可选)您可以(1)允许“任意顺序”数组,并且(2)忽略多余的字段。

由于有各种JSON库的Java(的JacksonGSONjson-lib等),它是有用的hamcrest-json支持JSON文本(如java.lang.String),从道格拉斯Crockford的JSON库以及原生支持对象org.json

最后,如果您不使用JUnit,则可以直接使用Hamcrest进行断言。(我在这里写过。


使用hamcrast匹配器而不是直接使用JSONAssert有什么优势?
Johannes

2
@Johannes:仅风格。
kevinarpe 2015年

hamcrest-json源当前的最后提交日期为2012年。它可能不再受到很好的支持。
托尔比约恩Ravn的安德森

11

您可以尝试JsonUnit。它可以比较两个JSON对象并报告差异。它建在Jackson的顶部。

例如

assertJsonEquals("{\"test\":1}", "{\n\"test\": 2\n}");

结果是

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

6

我做过的一件事,它的工作原理是将两个对象读入HashMap,然后与常规的assertEquals()比较。它将调用hashmaps的equals()方法,该方法将递归比较内部的所有对象(它们将是其他hashmap或某个单值对象,例如字符串或整数)。这是使用Codehaus的Jackson JSON解析器完成的。

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

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


6

我正在使用它,并且对我来说很好(使用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;
        }
    }
}

4

对于org.json,我已经推出了自己的解决方案,该方法可以与JSONObject实例进行比较。我没有在该项目中使用复杂的JSON对象,所以我不知道这是否在所有情况下都适用。另外,考虑到我在单元测试中使用了它,所以我没有花精力进行优化。这里是:

public static boolean jsonObjsAreEqual (JSONObject js1, JSONObject js2) throws JSONException {
    if (js1 == null || js2 == null) {
        return (js1 == js2);
    }

    List<String> l1 =  Arrays.asList(JSONObject.getNames(js1));
    Collections.sort(l1);
    List<String> l2 =  Arrays.asList(JSONObject.getNames(js2));
    Collections.sort(l2);
    if (!l1.equals(l2)) {
        return false;
    }
    for (String key : l1) {
        Object val1 = js1.get(key);
        Object val2 = js2.get(key);
        if (val1 instanceof JSONObject) {
            if (!(val2 instanceof JSONObject)) {
                return false;
            }
            if (!jsonObjsAreEqual((JSONObject)val1, (JSONObject)val2)) {
                return false;
            }
        }

        if (val1 == null) {
            if (val2 != null) {
                return false;
            }
        }  else if (!val1.equals(val2)) {
            return false;
        }
    }
    return true;
}

1
由于您提到了优化:),如果val1为null,则将从此代码中获取NullPointerExceptionif (!val1.equals(val2)) {
JohnDoDo 2012年

它也发生在我们当中最好的:)。+1以解决您的问题。
JohnDoDo 2012年

要点:)希望它不会得到太多的选票,否则会得到很多支持。
Victor Ionescu 2012年

1
你没有考虑,如果js2null当与否js1不是null

此代码不适用于嵌套的对象/序列。
FabienB 2014年


2

我将使用位于http://json.org/java/的库,并修改equalsJSONObject和JSONArray 的方法以进行深度相等性测试。为确保它在不影响子级的情况下起作用,您所需要做的就是将内部映射替换为TreeMap,或使用Collections.sort()


4
它不是很好-它确实应该带有代码来进行json比较。
CHII

但是,想象一下编写该代码,其中JSON在任何结构中都可以是任何东西...在上面编写比较!就像为所有类型的HTML页面编写比较。
JPM

2

试试这个:

public static boolean jsonsEqual(Object obj1, Object obj2) throws JSONException

    {
        if (!obj1.getClass().equals(obj2.getClass()))
        {
            return false;
        }

        if (obj1 instanceof JSONObject)
        {
            JSONObject jsonObj1 = (JSONObject) obj1;

            JSONObject jsonObj2 = (JSONObject) obj2;

            String[] names = JSONObject.getNames(jsonObj1);
            String[] names2 = JSONObject.getNames(jsonObj1);
            if (names.length != names2.length)
            {
                return false;
            }

            for (String fieldName:names)
            {
                Object obj1FieldValue = jsonObj1.get(fieldName);

                Object obj2FieldValue = jsonObj2.get(fieldName);

                if (!jsonsEqual(obj1FieldValue, obj2FieldValue))
                {
                    return false;
                }
            }
        }
        else if (obj1 instanceof JSONArray)
        {
            JSONArray obj1Array = (JSONArray) obj1;
            JSONArray obj2Array = (JSONArray) obj2;

            if (obj1Array.length() != obj2Array.length())
            {
                return false;
            }

            for (int i = 0; i < obj1Array.length(); i++)
            {
                boolean matchFound = false;

                for (int j = 0; j < obj2Array.length(); j++)
                {
                    if (jsonsEqual(obj1Array.get(i), obj2Array.get(j)))
                    {
                        matchFound = true;
                        break;
                    }
                }

                if (!matchFound)
                {
                    return false;
                }
            }
        }
        else
        {
            if (!obj1.equals(obj2))
            {
                return false;
            }
        }

        return true;
    }

在jsonArrays中,如果某些元素重合(与所有元素重合相反),则返回true 。
matiasg 2014年

@matiasg-是否 if (obj1Array.length() != obj2Array.length())不确保所有元素都重合?

3
@kwah:不。考虑以下示例:obj1Array = [1,1,1],obj2Array = [1,2,3]。这将返回true。同样,即使元素重合,它们也应以相同的顺序排列。对于[1,2,3]和[2,3,1]也将返回true,这是错误的
matiasg 2014年


2

空手道正是您想要的。这是一个例子:

* def myJson = { foo: 'world', hey: 'ho', zee: [5], cat: { name: 'Billie' } }
* match myJson = { cat: { name: 'Billie' }, hey: 'ho', foo: 'world', zee: [5] }

(免责声明:此处为dev)


2

为了比较json,我建议使用JSONCompare:https : //github.com/fslev/json-compare

// Compare by regex
String expected = "{\"a\":\".*me.*\"}";
String actual = "{\"a\":\"some text\"}";
JSONCompare.assertEquals(expected, actual);  // True

// Check expected array has no extra elements
String expected = "[1,\"test\",4,\"!.*\"]";
String actual = "[4,1,\"test\"]";
JSONCompare.assertEquals(expected, actual);  // True

// Check expected array has no numbers
String expected = "[\"\\\\\\d+\"]";
String actual = "[\"text\",\"test\"]";
JSONCompare.assertEquals(expected, actual);  // True

// Check expected array has no numbers
String expected = "[\"\\\\\\d+\"]";
String actual = "[2018]";
JSONCompare.assertNotEquals(expected, actual);  // True

0

对于像我这样想与Jackson一起使用的人,可以使用json-unit

JsonAssert.assertJsonEquals(jsonNode1, jsonNode2);

错误会为不匹配的类型提供有用的反馈:

java.lang.AssertionError: JSON documents have different values:
Different value found in node "heading.content[0].tag[0]". Expected 10209, got 10206.

0

似乎没有其他方法可以正常工作,所以我这样写:

private boolean jsonEquals(JsonNode actualJson, JsonNode expectJson) {
    if(actualJson.getNodeType() != expectJson.getNodeType()) return false;

    switch(expectJson.getNodeType()) {
    case NUMBER:
        return actualJson.asDouble() == expectJson.asDouble();
    case STRING:
    case BOOLEAN:
        return actualJson.asText().equals(expectJson.asText());
    case OBJECT:
        if(actualJson.size() != expectJson.size()) return false;

        Iterator<String> fieldIterator = actualJson.fieldNames();
        while(fieldIterator.hasNext()) {
            String fieldName = fieldIterator.next();
            if(!jsonEquals(actualJson.get(fieldName), expectJson.get(fieldName))) {
                return false;
            }
        }
        break;
    case ARRAY:
        if(actualJson.size() != expectJson.size()) return false;
        List<JsonNode> remaining = new ArrayList<>();
        expectJson.forEach(remaining::add);
        // O(N^2)   
        for(int i=0; i < actualJson.size(); ++i) {
            boolean oneEquals = false;
            for(int j=0; j < remaining.size(); ++j) {
                if(jsonEquals(actualJson.get(i), remaining.get(j))) {
                    oneEquals = true;
                    remaining.remove(j);
                    break;
                }
            }
            if(!oneEquals) return false;
        }
        break;
    default:
        throw new IllegalStateException();
    }
    return true;
}

0

以下代码对比较两个JsonObject,JsonArray,JsonPrimitive和JasonElements会更有帮助。

private boolean compareJson(JsonElement json1, JsonElement json2) {
        boolean isEqual = true;
        // Check whether both jsonElement are not null
        if (json1 != null && json2 != null) {

            // Check whether both jsonElement are objects
            if (json1.isJsonObject() && json2.isJsonObject()) {
                Set<Entry<String, JsonElement>> ens1 = ((JsonObject) json1).entrySet();
                Set<Entry<String, JsonElement>> ens2 = ((JsonObject) json2).entrySet();
                JsonObject json2obj = (JsonObject) json2;
                if (ens1 != null && ens2 != null) {
                    // (ens2.size() == ens1.size())
                    // Iterate JSON Elements with Key values
                    for (Entry<String, JsonElement> en : ens1) {
                        isEqual = isEqual && compareJson(en.getValue(), json2obj.get(en.getKey()));
                    }
                } else {
                    return false;
                }
            }

            // Check whether both jsonElement are arrays
            else if (json1.isJsonArray() && json2.isJsonArray()) {
                JsonArray jarr1 = json1.getAsJsonArray();
                JsonArray jarr2 = json2.getAsJsonArray();
                if (jarr1.size() != jarr2.size()) {
                    return false;
                } else {
                    int i = 0;
                    // Iterate JSON Array to JSON Elements
                    for (JsonElement je : jarr1) {
                        isEqual = isEqual && compareJson(je, jarr2.get(i));
                        i++;
                    }
                }
            }

            // Check whether both jsonElement are null
            else if (json1.isJsonNull() && json2.isJsonNull()) {
                return true;
            }

            // Check whether both jsonElement are primitives
            else if (json1.isJsonPrimitive() && json2.isJsonPrimitive()) {
                if (json1.equals(json2)) {
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        } else if (json1 == null && json2 == null) {
            return true;
        } else {
            return false;
        }
        return isEqual;
    }


0

查看答案,我尝试了JSONAssert,但失败了。所以我将Jackson与zjsonpatch一起使用。我在此处的SO答案中张贴了详细信息。


-5

这个解决方案对我来说,工作非常好:

try {           
                // Getting The Array "Courses" from json1 & json2   
                Courses1 =json1.getJSONArray(TAG_COURSES1);
                Courses2 = json2.getJSONArray(TAG_COURSES);

                //LOOP FOR JSON1
                for(int i = 0; i < Courses1.length(); i++){
                    //LOOP FOR JSON2
                    for(int ii = 0; ii < Courses2.length(); ii++){
                        JSONObject courses1 = Courses1.getJSONObject(i);
                        JSONObject courses2 = Courses2.getJSONObject(ii);

                        // Storing each json1 item in variable
                        int courseID1 = courses1.getInt(TAG_COURSEID1);
                        Log.e("COURSEID2:", Integer.toString(courseID1));
                        String Rating1 = courses1.getString(TAG_RATING1);
                        int Status1 = courses1.getInt(TAG_STATUS1);
                        Log.e("Status1:", Integer.toString(Status1));      //Put the actual value for Status1 in log.             

                        // Storing each json2 item in variable
                        int courseID2 = courses2.getInt(TAG_COURSEID);
                        Log.e("COURSEID2:", Integer.toString(courseID));   //Put the actual value for CourseID in log
                        String Title2 = courses2.getString(TAG_TITLE);                      
                        String instructor2 = courses2.getString(TAG_INSTRUCTOR);
                        String length2 = courses2.getString(TAG_LENGTH);
                        String rating2 = courses2.getString(TAG_RATING);
                        String subject2 = courses2.getString(TAG_SUBJECT);
                        String description2 = courses2.getString(TAG_DESCRIPTION);

                        //Status1 = 5 from json1; Incomplete, Status1 =-1 Complete 
                        if(Status1 == 5 && courseID2 == courseID1){                                  

                        // creating new HashMap
                        HashMap<String, String> map = new HashMap<String, String>();         
                        //Storing the elements if condition is true.
                        map.put(TAG_COURSEID, Integer.toString(courseID2)); //pend for compare
                        map.put(TAG_TITLE, Title2);
                        map.put(TAG_INSTRUCTOR, instructor2);
                        map.put(TAG_LENGTH, length2);
                        map.put(TAG_RATING, rating2);
                        map.put(TAG_SUBJECT, subject2); //show it
                        map.put(TAG_DESCRIPTION, description2);

                        //adding HashList to ArrayList
                        contactList.add(map);
                        }//if
                    }//for2 (json2)
                } //for1 (json1)                
            }//Try

希望这对别人有帮助。


当然,在这种情况下,只需陈述您的价值观和条件以及看法。列表视图上的哈希图。
路易斯(JLouis)

1
这是一个很好的例子,说明如何不做:)
PetrÚjezdský18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.