Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

kristian
Copy link

Fixes #5397

Copy link
Contributor

@tsegismont tsegismont left a 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

Comment on lines 459 to 469
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;
Copy link
Contributor

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

Copy link
Author

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 a JsonArray or JsonObject regardless of how it's stored, not to check if a JsonArray 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.

Copy link
Author

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.

@halber
Copy link

halber commented Nov 20, 2024

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., io.vertx.core.json.impl.JsonUtil#wrapJsonValue) but didn’t apply the same transformation to the stored values, you broke the contains contract. The good thing is that you do not implement the List interface. Therefore, you don’t have to comply with this contract. However, it is still an expected behavior and, of course, a trap. With this fix, the behavior becomes even weirder, in my opinion. Also, you would need to apply this logic for all transformations you perform in JsonUtil#wrapJsonValue. For example:

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 o is, in this example, a String. The same is true for byte[], Buffer, and Enum. You can’t reverse these transformations easily.

I consider the contains method as broken. Possibly consider deprecating the JsonArray#contains method.

@tsegismont
Copy link
Contributor

tsegismont commented Nov 20, 2024

@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 contains method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants