Как сравнить два JsonNode с Джексоном?

У меня есть метод, который сравнивает два объекта, но я не знаю, как сравнить JsonNode по библиотеке Джексона.

Я хочу получить что-то подобное:

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

3 ответа

Решение

Этого достаточно, чтобы использовать JsonNode.equals:

Равенство для узловых объектов определяется как полное (глубокое) значение равенства. Это означает, что можно сравнивать полные деревья JSON на равенство, сравнивая равенство корневых узлов.

Может также добавить нулевую проверку как test != null

Еще одно решение (особенно подходящее для модульных тестов, поскольку оно настраиваемое) — использовать собственный компаратор, который игнорирует порядок дочерних узлов. Сравнение JSON не такая тривиальная задача, как кажется; существуют различные предостережения, и вам, вероятно, придется выполнить настройку в соответствии с вашим случаем и подходом к сериализации.

Вот пример, который работает с Jackson и AssertJ (но его легко переписать для других фреймворков).

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

В первую очередь следует настроитьObjectMapperдля сериализации дат некоторым предсказуемым образом, например:

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

В противном случае вы закончите сериализацию некоторыхInstantполя какTextNodeи некоторые поля какDecimalNodeв зависимости от того, как вы создаете JsonNode.

Кроме того, могут возникнуть проблемы с такими полями, какvalue: null, они могут присутствовать в одном JSON и просто отсутствовать в другом JSON. Компаратор, упомянутый ниже, просто игнорирует поля сnullзначение, отсутствующее в другом JSON

Еще одно предостережение заключается в том, что вы сериализуете поля типа, вы получите массивы JSON (с некоторым порядком), и для обработки этого случая необходимы дополнительные усилия. Это пример метаинформации, которая просто теряется при сериализации из Java.Setв массив JSON.

Вот пример компаратора. Можно настроить игнорировать порядок элементов внутри массивов JSON или обращать внимание на порядок элементов (см.ignoreElementOrderInArraysпеременная)

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

Ваш текущий код выглядит нормально, JsonNode класс обеспечивает JsonNode.equals(Object) метод проверки:

Равенство для узловых объектов определяется как полное (глубокое) значение равенства.

Начиная с версии 2.6 также имеется перегруженная версия, в которой используется пользовательский компаратор:

public boolean equals(Comparator<JsonNode> comparator, JsonNode other){
    return comparator.compare(this, other) == 0;
}
Другие вопросы по тегам