37

I want to compare two JSON strings which is a huge hierarchy and want to know where they differ in values. But some values are generated at runtime and are dynamic. I want to ignore those particular nodes from my comparison.

I am currently using JSONAssert from org.SkyScreamer to do the comparison. It gives me nice console output but does not ignore any attributes.

for ex.

java.lang.AssertionError messageHeader.sentTime
expected:null
got:09082016 18:49:41.123

Now this comes dynamic and should be ignored. Something like

JSONAssert.assertEquals(expectedJSONString, actualJSONString,JSONCompareMode, *list of attributes to be ignored*)

It would be great if someone suggests a solution in JSONAssert. However other ways are also welcome.

Alex Shesterov
  • 26,085
  • 12
  • 82
  • 103
Prateik
  • 545
  • 1
  • 5
  • 12
  • what happens if you provide JSONCompareMode as false? is it not ignoring the nodes? – Rishal Aug 09 '16 at 15:01
  • @rishal-dev-singh No, it doesn't ignore specific attribute – Prateik Aug 09 '16 at 17:38
  • @Prateik As an alternative, you can remove the attributes before comparison. As another alternative, you can do it yourself, e.g. http://stackoverflow.com/questions/2253750/compare-two-json-objects-in-java – icyrock.com Aug 10 '16 at 00:33
  • @icyrock.com both can work for me, short term solution to remove the field from comparison itself or the other way, implement something on the grounds of the mentioned post. Thanks. – Prateik Aug 10 '16 at 05:50

4 Answers4

63

You can use Customization for this. For example, if you need to ignore a top-level attribute named "timestamp" use:

JSONAssert.assertEquals(expectedResponseBody, responseBody,
            new CustomComparator(JSONCompareMode.LENIENT,
                new Customization("timestamp", (o1, o2) -> true)));

It's also possible to use path expressions like "entry.id". In your Customization you can use whatever method you like to compare the two values. The example above always returns true, no matter what the expected value and the actual value are. You could do more complicated stuff there if you need to.

It is perfectly fine to ignore that values of multiple attributes, for example:

@Test
public void ignoringMultipleAttributesWorks() throws JSONException {
    String expected = "{\"timestamp\":1234567, \"a\":5, \"b\":3 }";
    String actual = "{\"timestamp\":987654, \"a\":1, \"b\":3 }";

    JSONAssert.assertEquals(expected, actual,
            new CustomComparator(JSONCompareMode.LENIENT,
                    new Customization("timestamp", (o1, o2) -> true),
                    new Customization("a", (o1, o2) -> true)
            ));
}

There is one caveat when using Customizations: The attribute whose value is to be compared in a custom way has to be present in the actual JSON. If you want the comparison to succeed even if the attribute is not present at all you would have to override CustomComparator for example like this:

@Test
public void extendingCustomComparatorToAllowToCompletelyIgnoreCertainAttributes() throws JSONException {
    // AttributeIgnoringComparator completely ignores some of the expected attributes
    class AttributeIgnoringComparator extends CustomComparator{
        private final Set<String> attributesToIgnore;

        private AttributeIgnoringComparator(JSONCompareMode mode, Set<String> attributesToIgnore, Customization... customizations) {
            super(mode, customizations);
            this.attributesToIgnore = attributesToIgnore;
        }

        protected void checkJsonObjectKeysExpectedInActual(String prefix, JSONObject expected, JSONObject actual, JSONCompareResult result) throws JSONException {
            Set<String> expectedKeys = getKeys(expected);
            expectedKeys.removeAll(attributesToIgnore);
            for (String key : expectedKeys) {
                Object expectedValue = expected.get(key);
                if (actual.has(key)) {
                    Object actualValue = actual.get(key);
                    compareValues(qualify(prefix, key), expectedValue, actualValue, result);
                } else {
                    result.missing(prefix, key);
                }
            }
        }
    }

    String expected = "{\"timestamp\":1234567, \"a\":5}";
    String actual = "{\"a\":5}";

    JSONAssert.assertEquals(expected, actual,
            new AttributeIgnoringComparator(JSONCompareMode.LENIENT,
                    new HashSet<>(Arrays.asList("timestamp")))
            );
} 

(With this approach you still could use Customizations to compare other attributes' values in the way you want.)

dknaus
  • 1,133
  • 12
  • 16
  • When I try to use this it just uses the built in JSONComparator and ignores the lambda. It tries to call a function compareJSON but it doesn't exist so it goes to a defaultComparator. – smoosh911 Apr 21 '18 at 02:48
  • 3
    Important note when customizing path expressions - the path for matching customization is in regex form. – maricn May 24 '18 at 12:31
  • is it possible to ignore multiple fields in simple `Customization`? – Nisarg Patil Mar 08 '19 at 12:27
  • @NisargPatilYes, it is perfectly fine to ignore the values of multiple fields. But you cannot do with a single `Customization`. Instead you would use multiple Customizations. I will update my response to reflect that – dknaus Mar 13 '19 at 10:52
  • @smoosh911 I noticed something similar recently updated my response. I suppose that the attribute in question is not present at all in your actual json. Have a look at the end of my updated response – dknaus Mar 13 '19 at 11:04
  • 2
    You can simplify your `checkJsonObjectKeysExpectedInActual` method implemetation to `for (String attribute : attributesToIgnore) { expected.remove(attribute); } super.checkJsonObjectKeysExpectedInActual(prefix, expected, actual, result); ` – TK Gospodinov Jan 16 '20 at 22:22
  • kudos! Very good tip and good improvement as well by @TKGospodinov. Thanks – WinterBoot Feb 15 '23 at 12:58
9

you can use JsonUnit It has the functionality that you are looking for we can ignore fields, paths, and values that are null etc. Check it out for more info. As for the example, you can ignore a path like this

assertJsonEquals(
     "{\"root\":{\"test\":1, \"ignored\": 2}}", 
     "{\"root\":{\"test\":1, \"ignored\": 1}}", 
     whenIgnoringPaths("root.ignored")
);

Sometimes you need to ignore certain values when comparing. It is possible to use ${json-unit.ignore} placeholder like this

assertJsonEquals("{\"test\":\"${json-unit.ignore}\"}",
    "{\n\"test\": {\"object\" : {\"another\" : 1}}}");
Srikar
  • 351
  • 5
  • 16
1

First of all there is open issue for it.

In my tests I compare json from controller with actual object with help of JsonUtil class for serialization/deserialization:

public class JsonUtil {
    public static <T> List<T> readValues(String json, Class<T> clazz) {
        ObjectReader reader = getMapper().readerFor(clazz);
        try {
            return reader.<T>readValues(json).readAll();
        } catch (IOException e) {
            throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e);
        }
    }

    public static <T> T readValue(String json, Class<T> clazz) {
        try {
            return getMapper().readValue(json, clazz);
        } catch (IOException e) {
            throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e);
        }
    }

    public static <T> String writeValue(T obj) {
        try {
            return getMapper().writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
        }
    }

To ignore specific object field I've add new method:

public static <T> String writeIgnoreProps(T obj, String... ignoreProps) {
    try {
        Map<String, Object> map = getMapper().convertValue(obj, new TypeReference<Map<String, Object>>() {});
        for (String prop : ignoreProps) {
            map.remove(prop);
        }
        return getMapper().writeValueAsString(map);
    } catch (JsonProcessingException e) {
        throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
    }
}

and my assert in test now look like this:

            mockMvc.perform(get(REST_URL))
            .andExpect(status().isOk())
            .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
            .andExpect(content().json(JsonUtil.writeIgnoreProps(USER, "registered")))
Grigory Kislin
  • 16,647
  • 10
  • 125
  • 197
1

Thank you @dknaus for the detailed answer. Although this solution will not work in STRICT mode and checkJsonObjectKeysExpectedInActual method code needs to be replaced by following code [As suggested by @tk-gospodinov]:

 for (String attribute : attributesToIgnore) {
     expected.remove(attribute);
     super.checkJsonObjectKeysExpectedInActual(prefix, expected, actual, result);
  }
Roberto Caboni
  • 7,252
  • 10
  • 25
  • 39
bhups
  • 11
  • 1