skyscreamer / JSONassert

Write JSON unit tests in less code. Great for testing REST interfaces.
http://jsonassert.skyscreamer.org
Apache License 2.0
1k stars 197 forks source link

Allow field values to be ignored by specifying a JSON path #15

Open hertzsprung opened 12 years ago

hertzsprung commented 12 years ago

We often have a requirement where we're not interested in the value of a field, but we do care that the field exists. This might be an autogenerated ID or a timestamp for example.

It would be useful to specify the field(s) as a JSON path expression so that we can be selective in which fields get ignored.

carterpage commented 12 years ago

Any idea what this might look like to a programmer?

hertzsprung commented 12 years ago

Thinking some more, we probably want to make this feature more flexible than just "ignore a field's value". From a hamcrest point-of-view, it would be good if we could say to JSONassert, "compare these two documents, but if you see a node matching , ignore it". That way, I can make my assertions on particular paths separately. Something like this:

assertThat(myActualDocument, both(
    sameJSONAs(myExpectedDocument).ignoringPath("$.library.book.id"))
   .and(path("$.library.book").hasField("id"))) // I'm making this syntax up here ;-)

Or we could maybe have

assertThat(myActualDocument, both(
    sameJSONAs(myExpectedDocument).ignoringValueAtPath("$.library.book.id"))

When I get some time, I'm going to play with https://github.com/jayway/JsonPath and see what might work.

carterpage commented 12 years ago

I get it now. I think the concept is useful. My main question now, is what is the proper syntax? Your example almost reminds me of JPA, which is not a bad convention. Still, it is pretty different from the straight-forward "compare X with Y" of JUnit.

One other thing, JSONAssert should already be able to figure out whether a node matches something like "$.library.book.id" without requiring a 3rd-party library. If you want to get more sophisticated with the JSON queries, you might need something else, but integration may prove tricky.

hertzsprung commented 12 years ago

Right, my example was written in a hamcrest style (since I'm writing the hamcrest-json library). I'm not yet sure how to surface this feature in JSONassert itself but you're right, we should probably stick with the existing style. If we /do/ want to integrate a third party JSON path library, we might want to make it an optional dependency. One of the good things about JSONassert is that it needs almost no dependencies!

ibrahimover commented 11 years ago

Hi,

I need this ignore field functionality, but it seems like it might take a while to agree and finalize this issue, meanwhile i would like to get benefit of using JSONCompareResult,

Is there any date set for 1.1.2 release?

hertzsprung commented 11 years ago

You're right, this feature isn't done yet but it's good to know I'm not the only one that needs it ;-) What in particular are you looking for from JSONCompareResult?

ibrahimover commented 11 years ago

I was hoping to get list of failure from _fieldFailures and exclude some fields manually but after a bit of investigation currently it only holds the failures on value comparison, all the missing, and unexpected messages are being appended to actual message String so i can not have those fields as a list.

hertzsprung commented 11 years ago

Yes, my idea for JSONCompareResult is that it should provide you with a model of the failures (including missing fields, unexpected fields and mismatched fields). Doing that was higher up my TODO list, so I will try and get round to that one :smiley:

carterpage commented 11 years ago

How about something like adding this signature to JSONAssert:

JSONAssert.assertEquals(String exp, String act, JSONCompareMode mode, JSONConstraint constraint)

It could work something like this:

JSONConstraint constraint = JSONConstraint.ignoreValueAtPath("$.library.book.id");
JSONAssert.assertEquals(expected, actual, STRICT, constraint);

Constraint gives us theoretically infinite flexibility in terms of what we want to throw into the calculation.


To take it further, we could even consider making JSONCompareMode a child of JSONConstraint to reduce the number of variables. In that case the signature could look like this (I've renamed some classes, but we'd make sure we do this in a way to maintain backwards compatibility with previous releases):

JSONAssert.assertEquals(String exp, String act, JAConstraint constraint)

And it could work like this in a simple case:

JSONAssert.assertEquals(expected, actual, JACompareMode.STRICT);

Or:

JAConstraint constraint = JACompareMode.STRICT.ignoreValueAtPath("$.library.book.id");
JSONAssert.assertEquals(expected, actual, constraint);

I imagine the ability to chain constraints like this to make them easier to write:

JAConstraint constraint = JAConstraint.ignoreValueAtPath("$.library.book.id")
                                      .ignoreValueAtPath("$.library.book.title");
btiernay commented 11 years ago

I too need this feature for the same reason (generated id fields). Any progress on this feature?

hertzsprung commented 11 years ago

@carterpage At the risk of sounding like a fanboy, I think the API would start to look a lot like Hamcrest's, only Hamcrest is already very composable and has lots of useful features that already exist ;-) (see my earlier example)

@btiernay As a workaround, perhaps you could set the values in the actual and expected documents to a known magic value such as ""?

btiernay commented 11 years ago

@hertzsprung I really don't see a difference between (a) ignoring added fields, (b) ignoring array element ordering, and (c) ignoring specific properties. That is to say, if you can make a case for one, you can make a case for all. I could do as you suggest, but then again, I could also do that for (a) and (b).

I could also fork the project and add this feature myself, but I'd much rather contribute to this one as it is already established. Since the need for this feature seems common with many practical applications, why not support it?

The need for (c) comes from the fact that JSON objects do not have an encapsulated equals() method as do classes in languages such as Java. In Java, for instance, one can implement the equals() method to ignore certain fields. In JSON, this must be extrinsic (and thus customizable in assertEquals). This is the motivation here and it seems a reasonable request.

I should also add, FEST assertions FTW :).

carterpage commented 11 years ago

@btiernay It's agreed we need this functionality. We just need to figure out the "right" way to express the functionality. We've been a little slow over the holidays, but we'll be ramping up on features and bugs in the coming weeks. Barring any surprises we'll get this out in a 1.2 in the next month.

btiernay commented 11 years ago

@carterpage Sorry, I guess I misread this thread :) Looking forward to it!

cepage commented 11 years ago

I wonder if a more flexible approach might be to allow the developer to specify a wildcard sequence in the character string. The wildcard sequence could be an optional final argument to the assertEquals() method, or a component of the proposed JSONConstraint class.

So let's say you want to check the contents of a response, but you're not interested in the exact value of the (DB-generated) user id:

String expected = "{id:**,name:\"Joe\",friends:[{id:**,name:\"Pat\",pets:[\"dog\"]},{id:**,name:\"Sue\",pets:[\"bird\",\"fish\"]}],pets:[]}";
String wildcard = "**";

JSONAssert.assertEquals( expected, actual, false, wildcard );

The advantages of this approach are:

carterpage commented 11 years ago

Again, I still learning towards a generic way to extend the asserts to keep the interface as simple and immutable as possible. If we do the wildcard, the same would be written as:

JAConstraint jaConstraint = JSONCompareMode.STRICT.treatAsWildcard("**");
JSONAssert.assertEquals(expected, actual, jaConstraint);
btiernay commented 11 years ago

If anything, I would recommend using JSON Path, e.g. https://github.com/nebhale/JsonPath

hertzsprung commented 11 years ago

I'm back looking at this again. I'm just going to spike this in its own branch for now.

btiernay commented 11 years ago

Btw, https://github.com/nebhale/JsonPath now supports Jackson 2, Java 6 and is deployed to Maven Central. I'm currently using it for filtering JsonNodes based on json path expressions. Works quite well. Could be useful here.

albx79 commented 11 years ago

Has there been any progress on this issue? This feature is really important for a project I'm working on. I may even be able to persuade the boss and let me work a few days on it.

I have looked at the jsonpath branch and the new API looks quite usable. The only change I would suggest is to use a different Matcher interface, where the "matches" method takes both the expected value and the actual value. This would be useful because e.g. we are storing numbers as json strings (too long to explain why, and too late to change it), and I'd need my custom matcher to parse those strings into numbers when comparing, so that "0.5" and "0.50" actually match.

carterpage commented 11 years ago

If you'd like to take a stab at this, go ahead. @hertzsprung was going to try but got tied up with other projects.

Please submit unit tests with any code. Thanks!

On Fri, May 24, 2013 at 11:42 AM, alberto notifications@github.com wrote:

Has there been any progress on this issue? This feature is really important for a project I'm working on. I may even be able to persuade the boss and let me work a few days on it.

I have looked at the jsonpath branch and the new API looks quite usable. The only change I would suggest is to use a different Matcher interface, where the "matches" method takes both the expected value and the actual value. This would be useful because e.g. we are storing numbers as json strings (too long to explain why, and too late to change it), and I'd need my custom matcher to parse those strings into numbers when comparing, so that "0.5" and "0.50" actually match.

— Reply to this email directly or view it on GitHubhttps://github.com/skyscreamer/JSONassert/issues/15#issuecomment-18412747 .

albx79 commented 11 years ago

I have a very preliminary implementation here: https://github.com/albx79/JSONassert/commit/f49da4b61b75b0dfa0760f8b1d970309e4c1b612

It uses the full key instead of a proper jpath (no initial $, no support for wildcards or selectors etc). Opinions welcome. Would it be OK to bring in JsonPath as a dependency, in order to provide full jpath support?

carterpage commented 11 years ago

I'll look at it this weekend. Thanks.

On Thu, May 30, 2013 at 8:07 AM, alberto notifications@github.com wrote:

I have a very preliminary implementation here: albx79@f49da4bhttps://github.com/albx79/JSONassert/commit/f49da4b61b75b0dfa0760f8b1d970309e4c1b612

It uses the full key instead of a proper jpath (no initial $, no support for wildcards or selectors etc). Opinions welcome. Would it be OK to bring in JsonPath as a dependency, in order to provide full jpath support?

— Reply to this email directly or view it on GitHubhttps://github.com/skyscreamer/JSONassert/issues/15#issuecomment-18686314 .

theon commented 11 years ago

+1 for this feature - we also have a requirement for this.

carterpage commented 11 years ago

Could use some additional functionality, but the hooks are in place now for overriding path-specific comparison behavior. Merged in #31.

carterpage commented 11 years ago

Just noticed a couple of things including changes to signatures and a required hamcrest dependency. It's going to require quite a bit of cleanup before I incorporate it into master.

albx79 commented 11 years ago

The dependency on hamcrest was added in c6fbb122, and it's used only to get the Matcher interface. But I had to replace that interface anyway (https://github.com/skyscreamer/JSONassert/issues/15#issuecomment-18412747), so now we can remove the hamcrest dependency. I can do it this Friday.

I'll also have a look at what signatures have changed. I think I was careful not to make any incompatible changes to public methods, but I better doublecheck.

carterpage commented 11 years ago

Thanks. I don't have a problem putting test dependencies into the project. Might even be worthwhile to show people how to integrate with hamcrest if they want. But I can't have the main jar depend on it. If at some point you want to build out some richer hamcrest/JSONassert functionality, we would create a new module (e.g jsonassert-hamcrest) to do that so it can be included optionally.

On Mon, Jun 17, 2013 at 3:57 AM, alberto notifications@github.com wrote:

The dependency on hamcrest was added in c6fbb12https://github.com/skyscreamer/JSONassert/commit/c6fbb122, and it's used only to get the Matcher interface. But I had to replace that interface anyway (#15https://github.com/skyscreamer/JSONassert/issues/15#issuecomment-18412747), so now we can remove the hamcrest dependency. I can do it this Friday.

I'll also have a look at what signatures have changed. I think I was careful not to make any incompatible changes to public methods, but I better doublecheck.

— Reply to this email directly or view it on GitHubhttps://github.com/skyscreamer/JSONassert/issues/15#issuecomment-19530796 .

albx79 commented 11 years ago

I've sent a pull request with the change that removes hamcrest from main.

I've also had a look at my previous patch looking for signature changes, but I couldn't find any. The only pre-existing class I modified was Customization, which is not in master anyway.

cliviu commented 11 years ago

hi, can the exclusion of some fields when comparing be obtained in some way using the JSONCompareResult ?

albx79 commented 11 years ago

OK, I couldn't merge the jsonpath branch into master, because the APIs had diverged too much. So I ported the change manually, and made it fit in the current JSONComparator API.

https://github.com/skyscreamer/JSONassert/pull/35

albx79 commented 11 years ago

@cliviu when my patch is accepted, then you can create a custom comparator that returns always true, and associate it to the fields you want to ignore.

cliviu commented 11 years ago

thanks @albx79 , i will try it

cliviu commented 11 years ago

@albx79 at the moment, no JSONPath match, only simple paths ? Behavior behavior = STRICT.with(customization("outer.inner.value", comparator)) so the Customization can be initialized with a simple path, not allowing wildcards , like JSONPath syntax?

albx79 commented 11 years ago

@cliviu that is correct. Unfortunately I couldn't find a jsonpath library that supports the org.json library. If one exists, I'd love to implement proper jsonpath support in JSONAssert!

carterpage commented 11 years ago

I'm going to merge the pull request but leave this issue open until we're able to support full JSONPath syntax. One possibility is to create a module for JSONassert, e.g. jsonassert-jsonpath or jsonassert-hamcrest. We could add more complicated dependencies in that case, without breaking compatibility for people who want the stripped down core library.

If one of you wants to take a stab at that, you could create a new branch and keep the code in a new directory. Once it's working there, I could reorganize the project to generate two JARs.

asadhwani commented 10 years ago

Hi,

Wondering if there has been any progress on this?

Below are my requirements if there are any additional ways to achieve this please suggest

  1. I need to compare two JSON bodies, I am able achieve the same via below

JSONAssert.assertEquals(actual response, expected response, true);

But while comparing I need to ignore certain values as they would be auto generated.

All help would be appriciated

carterpage commented 10 years ago

This might be a good question to post to http://stackoverflow.com/.

dmackinder commented 10 years ago

Could you use one or more calls to JSONObject remove method to remove unwanted values from object you would compare against, then use JSONAssert in lenient mode?

Duncan

On Monday, 7 July 2014, asadhwani notifications@github.com wrote:

Hi,

Wondering if there has been any progress on this?

Below are my requirements if there are any additional ways to achieve this please suggest

  1. I need to compare two JSON bodies, I am able achieve the same via below

JSONAssert.assertEquals(actual response, expected response, true);

But while comparing I need to ignore certain values as they would be auto generated.

All help would be appriciated

— Reply to this email directly or view it on GitHub https://github.com/skyscreamer/JSONassert/issues/15#issuecomment-48146291 .

lysu commented 10 years ago

Hi I'm using old edition and try to use fieldFailures to do some ignore, too...

but I found that fieldFailures doesn't contain any not exists filed

e.g. {"a":1, "b", 1} vs {"b":1}, a is miss...but fieldFailures doesn't contain any information about this..

lysu commented 10 years ago

At last I use result and split string to work around...but I think fieldFailures better contain this information in STRICT or NON_EXTENSIBLE mode.

ittiel commented 9 years ago

Here is a simple example to ignore ALL values It uses the default comparator with a single change: overriding the compareValues method to do nothing

` import org.json.JSONException; import org.skyscreamer.jsonassert.JSONCompareMode; import org.skyscreamer.jsonassert.JSONCompareResult; import org.skyscreamer.jsonassert.comparator.DefaultComparator;

// // Created by Ittiel on 3/24/15. // Json comparator, validates JSON formation, ignores values //

public class JSONAssertCompareIgnoreValues extends DefaultComparator {

public JSONAssertCompareIgnoreValues(JSONCompareMode mode) {
    super(mode);
}

@Override
public void compareValues(String s, Object o, Object o1, JSONCompareResult jsonCompareResult) throws JSONException {
    //don't verify anything here...ignore all values
}

} `

and in test:

` // // This test will fail (just an example) // reason: id1 is expected but does not appear in the actual json. // @Test public void testIgnoreFieldsComparator() {

    String expected = "{id1:1,name:\"Joe\",friends:[{id:2,name:\"Pat1\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"bird\",\"fish\"]}],pets:[]}";
    String actual = "{id:1,name:\"Joe\",friends:[{id:2,name:\"Pat\",pets:[\"dog\"]},{id:3,name:\"Sue\",pets:[\"cat\",\"fish\"]}],pets:[]}";
    JSONAssert.assertEquals(expected, actual, new JSONAssertCompareIgnoreValues(JSONCompareMode.LENIENT));
}

`

sudhak271 commented 9 years ago

The above would not apply for arrays isnt it? In the example above, what if I want friends[0].id to be ignored?

ittiel commented 9 years ago

@sudhak271, You are right, this is an 'all or nothing' solution Meaning that in the example above - you can either ignore ALL values or not, but not partials compares

You can always override JSONAssertCompareIgnoreValues.compareValues in any way that fits you.

sudhak271 commented 9 years ago

Thank you for the reply...but still confused. :-) What are my options, if I want to ignore a field from an array? Eg : friends[0].id. For simple fields, I was able to get by fine, by doing something like this : customization.add(new Customization(fieldToIgnore, ignoreFieldsComparator)); static ValueMatcher ignoreFieldsComparator = new ValueMatcher() {

    @Override
    public boolean equal(Object o1, Object o2) {
        comparatorCallCount++;
                    return true;
    }
};

For array values, this isnt working. Should I be using syntax similar to Json-Path?

ittiel commented 9 years ago

@sudhak271 - I'm just a visitor here who wanted to share a solution that worked for me I'm sorry that I didn't provide a generic solution.

shahnaazr commented 9 years ago

where are we with the implementation of the field ignore feature

javierseixas commented 9 years ago

+1 to the solution proposed by @cepage , something close to what is available in Json Expressions or PHP-Matcher. With the option of including a kind of type tags for values, we would be able to keep checking the JSON structure in a very readable and intuitive way.

khoubyari commented 8 years ago

+1 for a partial ignore feature

fndg87 commented 8 years ago

+1 for a partial ignore feature