-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix JsonArray contains for non-wrapped JsonArray/JsonObject #5398
Conversation
4b5b6d5
to
fdecd4f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this PR @kristian
if (list.contains(value)) { | ||
return true; | ||
} | ||
|
||
if (value instanceof JsonObject) { | ||
return list.contains(((JsonObject) value).getMap()); | ||
} else if (value instanceof JsonArray) { | ||
return list.contains(((JsonArray) value).getList()); | ||
} | ||
|
||
return false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the worst scenario, we will iterate over the list three times. Besides, we want to be able to check if the JsonArray contains a JsonArray or JsonObject regardless of how it's stored, not to check if a JsonArray contains an arbitrary list or map.
I would suggest to use something like this:
@SuppressWarnings("unchecked")
public boolean contains(Object value) {
if (value == null) {
for (Object entry : list) {
if (entry == null) {
return true;
}
}
} else {
for (Object entry : list) {
if (entry instanceof Map && value instanceof JsonObject) {
return value.equals(new JsonObject((Map<String, Object>) entry));
}
if (entry instanceof List && value instanceof JsonArray) {
return value.equals(new JsonArray((List<String>) entry));
}
if (value.equals(entry)) {
return true;
}
}
}
return false;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion @tsegismont. Yes I was debating back and forth whether to essentially re-implement ArrayList#contains
or whether the better code quality with the downside of iterating over the list a maximum number of 2 times (not 3, because it is either a JsonArray
or JsonObject
you want to check for) is the better solution. I can agree that we should go for the option that is better from a performance point of view. In my solution I did optimize for doing less comparisons, however I agree that the iteration itself is much more time consuming than doing the comparison operations.
Besides, we want to be able to check if the
JsonArray
contains aJsonArray
orJsonObject
regardless of how it's stored, not to check if aJsonArray
contains an arbitrary list or map.
Mhm, as far as I am concerned, I would argue that a Map / List inside of a JsonArray
is virtually equivalent to it's wrapped JsonArray
/ JsonObject
counterpart. Reason for me is the public interface of JsonArray
does wrapping on getting anyways, so classic duck-typing principle: If it walks like a duck and it quacks like a duck, then it must be a duck. An arbitrary Map
/ List
is not supported inside of a JsonArray
, it is just a "vehicle" for representing nested arrays anyways. This is why for your solution I would argue that creating temporary JsonObject
/ JsonArray
depending on the size of the JsonArray
creates a lot of temporary objects and thus a lot of pressure on the heap / garbage collection.
I would maybe suggest to "mix" both solutions and:
a) Keep the assumption that Map
/ List
inside of JsonArray
is equivalent to JsonObject
/ JsonArray
and
b) Re-implement contains
based on ArrayList#contains
as you suggested.
Let me make a proposal to continue our discussion. 👍 Thanks for the good suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tsegismont I made a new suggestion in my latest force push.
I do not think that there is a good way to fix this behavior. I think the best thing you can do is to document this. For example, if you apply this fix, then the following would work: JsonArray jsonArray = JsonArray.of(Map.of("foo", "bar"));
assertTrue(jsonArray.contains(JsonObject.of("foo", "bar")));
assertTrue(jsonArray.contains(Map.of("foo", "bar"))); That is also a behavior that is not expected. When you decided to change the returned value (e.g., Instant now1 = Instant.now();
JsonArray jsonArray = JsonArray.of(now1);
assertTrue(jsonArray.contains(now1));
for (Object o : jsonArray) {
assertTrue(jsonArray.contains(o));
} This test will fail, which is unexpected to me. You can’t easily fix this because I consider the |
@halber with the suggestions made in the review, there would be no weird cases with JsonObject/JsonArray entries. But you're right, we need some Javadoc on the |
fdecd4f
to
857fc15
Compare
857fc15
to
50474c5
Compare
Thanks for the review @vietj, all changes are included in my latest force push |
@@ -658,6 +658,8 @@ public void testContains() { | |||
JsonArray arr = new JsonArray(); | |||
jsonArray.add(obj); | |||
jsonArray.add(arr); | |||
jsonArray.add(Map.of("foo", "bar")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually I don't think this should pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead rewrite it as jsonArray.getList().add(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json array should not return contains true for map/list structures
@vietj I get the point, however I think I need some guidance of how you want to see it fixed: Generally List list = List.of("foobar");
jsonArray.add(list);
jsonArray.contains(list); // false With the current implementation (without any checks), Alternative solution would be, that you could also check Object reverseWrappedValue = value;
if (value instanceof JsonObject) {
reverseWrappedValue = ((JsonObject) value).getMap();
} else if (value instanceof JsonArray) {
reverseWrappedValue = ((JsonArray) value).getList();
} else if (value instanceof Map) {
reverseWrappedValue = new JSONObject(value);
} else if (value instanceof List) {
reverseWrappedValue = new JSONArray(value);
}
// ... I think this "you can use either" approach fits best to the current implementation of |
ok I see your point @kristian , let me think over this and get back to you :-) |
I think it is fine as is actually, the API is lenient when i.e. I think we can support that we add directly a map/list, however it should not be part of the contract In other words, the contract of adding an object is, add whatever object you want but you should not rely on the fact that adding a map will turn it into a valid |
Agreed. Do you still want to also support |
50474c5
to
97b33cb
Compare
@kristian thanks for the contribution, can you back port this to the 4.x branch ? |
Fixes #5397