John
John

Reputation: 1601

How to compare two JsonNodes with Jackson?

I have a method which compares two objects, but I don't know how to compare JsonNode by Jackson library.

I want get something like that:

private boolean test(JsonNode source) {
    JsonNode test = compiler.process(file);
    return test.equals(source);
}

Upvotes: 13

Views: 18891

Answers (3)

Dmitriusan
Dmitriusan

Reputation: 12399

Yet another solution (especially suitable for unit tests because it is customizable) is to use a custom comparator that ignores the order of child nodes. Comparing JSONs is not as trivial a task as it sounds; there are various caveats, and you will likely have to make customizations according to your case and serialization approach.

Here is a sample that works with Jackson and AssertJ (but it is easy to re-write it for other frameworks).

  assertThat(json1)
      .usingComparator(new ComparatorWithoutOrder(true))
      .isEqualTo(json2);

First of all, you should set up ObjectMapper to serialize dates in some predictable way, for example:

  mapper = new ObjectMapper();
  mapper.registerModule(new JavaTimeModule());
  mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

Otherwise, you will end up serializing some Instant fields as TextNode and some fields as DecimalNode depending on how you create a JsonNode.

Also, the may be a trouble with fields like value: null, they may be present at one JSON and just missing at another JSON. The comparator mentioned below just ignores fields with null value that are missing at other JSON

Yet another caveat is that you are serializing fields of Set type, you will get JSON arrays (with some order), and an additional effort is needed to handle this case. It is an example of metainformation that is just lost during when serializing from Java Set to JSON array.

Here is a sample comparator. It is configurable to ignore element order inside JSON arrays, or to pay attention to element order (see ignoreElementOrderInArrays variable)

import static java.util.Spliterators.spliteratorUnknownSize;
import static java.util.stream.StreamSupport.stream;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.Comparator;
import java.util.Optional;
import java.util.Spliterator;

class ComparatorWithoutOrder implements Comparator<Iterable<? extends JsonNode>> {

  private boolean ignoreElementOrderInArrays;

  public ComparatorWithoutOrder(boolean ignoreElementOrderInArrays) {
    this.ignoreElementOrderInArrays = ignoreElementOrderInArrays;
  }

  @Override
  public int compare(Iterable<? extends JsonNode> o1, Iterable<? extends JsonNode> o2) {
    if (o1 == null || o2 == null) {
      return -1;
    }
    if (o1 == o2) {
      return 0;
    }
    if (o1 instanceof JsonNode && o2 instanceof JsonNode) {
      return compareJsonNodes((JsonNode) o1, (JsonNode) o2);
    }
    return -1;
  }

  private int compareJsonNodes(JsonNode o1, JsonNode o2) {
    if (o1 == null || o2 == null) {
      return -1;
    }
    if (o1 == o2) {
      return 0;
    }
    if (!o1.getNodeType().equals(o2.getNodeType())) {
      return -1;
    }
    switch (o1.getNodeType()) {
      case NULL:
        return o2.isNull() ? 0 : -1;
      case BOOLEAN:
        return o1.asBoolean() == o2.asBoolean() ? 0 : -1;
      case STRING:
        return o1.asText().equals(o2.asText()) ? 0 : -1;
      case NUMBER:
        double double1 = o1.asDouble();
        double double2 = o2.asDouble();
        return Math.abs(double1 - double2) / Math.max(double1, double2) < 0.999 ? 0 : -1;
      case OBJECT:
        // ignores fields with null value that are missing at other JSON
        var missingNotNullFields = Sets
            .symmetricDifference(Sets.newHashSet(o1.fieldNames()), Sets.newHashSet(o2.fieldNames()))
            .stream()
            .filter(missingField -> isNotNull(o1, missingField) || isNotNull(o2, missingField))
            .toList();
        if (!missingNotNullFields.isEmpty()) {
          return -1;
        }
        Integer reduce1 = stream(spliteratorUnknownSize(o1.fieldNames(), Spliterator.ORDERED), false)
            .map(key -> compareJsonNodes(o1.get(key), o2.get(key)))
            .reduce(0, (a, b) -> a == -1 || b == -1 ? -1 : 0);
        return reduce1;
      case ARRAY:
        if (o1.size() != o2.size()) {
          return -1;
        }
        if (o1.isEmpty()) {
          return 0;
        }
        var o1Iterator = o1.elements();
        var o2Iterator = o2.elements();
        var o2Elements = Sets.newHashSet(o2.elements());
        Integer reduce = stream(spliteratorUnknownSize(o1Iterator, Spliterator.ORDERED), false)
            .map(o1Next -> ignoreElementOrderInArrays ?
                lookForMatchingElement(o1Next, o2Elements) : compareJsonNodes(o1Next, o2Iterator.next()))
            .reduce(0, (a, b) -> a == -1 || b == -1 ? -1 : 0);
        return reduce;
      case MISSING:
      case BINARY:
      case POJO:
      default:
        return -1;
    }
  }

  private int lookForMatchingElement(JsonNode elementToLookFor, Collection<JsonNode> collectionOfElements) {
    // Note: O(n^2) complexity
    return collectionOfElements.stream()
        .filter(o2Element -> compareJsonNodes(elementToLookFor, o2Element) == 0)
        .findFirst()
        .map(o2Element -> 0)
        .orElse(-1);
  }

  private static boolean isNotNull(JsonNode jsonObject, String fieldName) {
    return Optional.ofNullable(jsonObject.get(fieldName))
        .map(JsonNode::getNodeType)
        .filter(nodeType -> nodeType != JsonNodeType.NULL)
        .isPresent();
  }
}

Upvotes: 3

Ori Marko
Ori Marko

Reputation: 58772

That's good enough to use JsonNode.equals:

Equality for node objects is defined as full (deep) value equality. This means that it is possible to compare complete JSON trees for equality by comparing equality of root nodes.

Maybe also add a null check as test != null

Upvotes: 13

Karol Dowbecki
Karol Dowbecki

Reputation: 44952

You current code looks ok, the JsonNode class provides JsonNode.equals(Object) method for checking:

Equality for node objects is defined as full (deep) value equality.

Since version 2.6 there is also overloaded version which uses a custom comparator:

public boolean equals(Comparator<JsonNode> comparator, JsonNode other){
    return comparator.compare(this, other) == 0;
}

Upvotes: 3

Related Questions