Перейти к основному содержимому

Введение в JsonPath

· 10 мин. чтения

1. Обзор

Одним из преимуществ XML является доступность обработки, включая XPath, которая определена как стандарт W3C . Для JSON появился аналогичный инструмент под названием JSONPath.

В этом руководстве вы познакомитесь с Jayway JsonPath , Java-реализацией спецификации JSONPath . В нем описываются установка, синтаксис, распространенные API и демонстрация вариантов использования.

2. Настройка

Чтобы использовать JsonPath, нам просто нужно включить зависимость в Maven pom:

<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
</dependency>

3. Синтаксис

Мы будем использовать следующую структуру JSON для демонстрации синтаксиса и API JsonPath:

{
"tool":
{
"jsonpath":
{
"creator":
{
"name": "Jayway Inc.",
"location":
[
"Malmo",
"San Francisco",
"Helsingborg"
]
}
}
},

"book":
[
{
"title": "Beginning JSON",
"price": 49.99
},

{
"title": "JSON at Work",
"price": 29.99
}
]
}

3.1. Обозначение

JsonPath использует специальные обозначения для представления узлов и их соединений с соседними узлами в пути JsonPath. Существует два стиля записи: точка и квадратная скобка.

Оба следующих пути относятся к одному и тому же узлу из приведенного выше документа JSON, который является третьим элементом в поле местоположения узла- создателя , который является дочерним элементом объекта jsonpath , принадлежащего инструменту в корневом узле.

Сначала мы увидим путь с точечной записью:

$.tool.jsonpath.creator.location[2]

Теперь давайте посмотрим на запись в скобках:

$['tool']['jsonpath']['creator']['location'][2]

Знак доллара ($) представляет объект корневого члена.

3.2. Операторы

У нас есть несколько полезных операторов в JsonPath:

  • Корневой узел ($) обозначает корневой член структуры JSON, будь то объект или массив. Мы включили примеры использования в предыдущий подраздел.
  • Текущий узел (@) представляет обрабатываемый узел. В основном мы используем его как часть входных выражений для предикатов. Предположим, мы имеем дело с массивом книг в приведенном выше документе JSON; выражение book[?(@.price == 49,99)] относится к первой книге в этом массиве.
  • Подстановочный знак (*) обозначает все элементы в указанной области. Например, book[*] указывает на все узлы внутри массива book .

3.3. Функции и фильтры

JsonPath также имеет функции, которые мы можем использовать в конце пути для синтеза выходных выражений этого пути: min() , max() , avg() , stddev() и length() .

Наконец, у нас есть фильтры. Это логические выражения для ограничения возвращаемых списков узлов только теми, которые нужны вызывающим методам.

Вот несколько примеров: равенство ( == ), сопоставление с регулярным выражением ( =~ ), включение ( in ) и проверка на пустоту ( пусто ). В основном мы используем фильтры для предикатов.

Полный список и подробные объяснения различных операторов, функций и фильтров см. в проекте JsonPath GitHub .

4. Операции

Прежде чем мы перейдем к операциям, небольшое примечание: в этом разделе используется структура примера JSON, которую мы определили ранее.

4.1. Доступ к документам

JsonPath имеет удобный способ доступа к документам JSON. Мы делаем это через статические API чтения :

<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);

API- интерфейсы чтения могут работать со статическими API-интерфейсами Fluent для обеспечения большей гибкости:

<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);

Мы можем использовать другие перегруженные варианты чтения для различных типов источников JSON, включая Object , InputStream , URL и File .

Для простоты тест для этой части не включает предикаты в список параметров (пустые varargs ). Но мы обсудим предикаты в последующих подразделах.

Давайте начнем с определения двух примеров путей для работы:

String jsonpathCreatorNamePath = "$['tool']['jsonpath']['creator']['name']";
String jsonpathCreatorLocationPath = "$['tool']['jsonpath']['creator']['location'][*]";

Далее мы создадим объект DocumentContext , проанализировав данный источник JSON jsonDataSourceString . Затем вновь созданный объект будет использоваться для чтения содержимого с использованием путей, определенных выше:

DocumentContext jsonContext = JsonPath.parse(jsonDataSourceString);
String jsonpathCreatorName = jsonContext.read(jsonpathCreatorNamePath);
List<String> jsonpathCreatorLocation = jsonContext.read(jsonpathCreatorLocationPath);

Первый API чтения возвращает строку , содержащую имя создателя JsonPath, а второй возвращает список его адресов.

И мы будем использовать JUnit Assert API, чтобы убедиться, что методы работают должным образом:

assertEquals("Jayway Inc.", jsonpathCreatorName);
assertThat(jsonpathCreatorLocation.toString(), containsString("Malmo"));
assertThat(jsonpathCreatorLocation.toString(), containsString("San Francisco"));
assertThat(jsonpathCreatorLocation.toString(), containsString("Helsingborg"));

4.2. Предикаты

Теперь, когда у нас есть основы, давайте определим новый пример JSON для работы и проиллюстрируем, как создавать и использовать предикаты:

{
"book":
[
{
"title": "Beginning JSON",
"author": "Ben Smith",
"price": 49.99
},

{
"title": "JSON at Work",
"author": "Tom Marrs",
"price": 29.99
},

{
"title": "Learn JSON in a DAY",
"author": "Acodemy",
"price": 8.99
},

{
"title": "JSON: Questions and Answers",
"author": "George Duckett",
"price": 6.00
}
],

"price range":
{
"cheap": 10.00,
"medium": 20.00
}
}

Предикаты определяют истинные или ложные входные значения для фильтров, чтобы сузить возвращаемые списки до только соответствующих объектов или массивов. Мы можем легко интегрировать предикат в фильтр , используя его в качестве аргумента для его статического фабричного метода. Затем запрошенный контент можно прочитать из строки JSON с помощью этого фильтра :

Filter expensiveFilter = Filter.filter(Criteria.where("price").gt(20.00));
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensiveFilter);
predicateUsageAssertionHelper(expensive);

Мы также можем определить наш настраиваемый предикат и использовать его в качестве аргумента для API чтения :

Predicate expensivePredicate = new Predicate() {
public boolean apply(PredicateContext context) {
String value = context.item(Map.class).get("price").toString();
return Float.valueOf(value) > 20.00;
}
};
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensivePredicate);
predicateUsageAssertionHelper(expensive);

Наконец, предикат можно применять напрямую для чтения API без создания каких-либо объектов, что называется встроенным предикатом:

List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?(@['price'] > $['price range']['medium'])]");
predicateUsageAssertionHelper(expensive);

Все три приведенных выше примера Predicate проверяются с помощью следующего вспомогательного метода утверждения:

private void predicateUsageAssertionHelper(List<?> predicate) {
assertThat(predicate.toString(), containsString("Beginning JSON"));
assertThat(predicate.toString(), containsString("JSON at Work"));
assertThat(predicate.toString(), not(containsString("Learn JSON in a DAY")));
assertThat(predicate.toString(), not(containsString("JSON: Questions and Answers")));
}

5. Конфигурация

5.1. Опции

Jayway JsonPath предоставляет несколько вариантов настройки конфигурации по умолчанию:

  • Параметр Option.AS_PATH_LIST возвращает пути результатов оценки вместо их значений.
  • Option.DEFAULT_PATH_LEAF_TO_NULL возвращает null для отсутствующих листьев.
  • Option.ALWAYS_RETURN_LIST возвращает список, даже если путь определен.
  • Опция Option.SUPPRESS_EXCEPTIONS гарантирует, что при оценке пути не будут распространяться исключения.
  • Для параметра Option.REQUIRE_PROPERTIES требуются свойства, определенные в пути, когда оценивается неопределенный путь.

Вот как применить Option с нуля:

Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();

и как добавить его в существующую конфигурацию:

Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);

5.2. SPI

Конфигурации JsonPath по умолчанию с помощью Option должно хватить для большинства задач. Однако пользователи с более сложными вариантами использования могут изменить поведение JsonPath в соответствии со своими конкретными требованиями, используя три разных SPI:

  • JsonProvider SPI позволяет нам изменить способы, с помощью которых JsonPath анализирует и обрабатывает документы JSON.
  • MappingProvider SPI позволяет настраивать привязки между значениями узлов и возвращаемыми типами объектов.
  • CacheProvider SPI регулирует способы кэширования путей, что может помочь повысить производительность.

6. Примеры использования

Теперь у нас есть хорошее понимание функциональности JsonPath. Итак, давайте рассмотрим пример.

В этом разделе показано, как работать с данными JSON, возвращаемыми веб-службой.

Предположим, у нас есть служба информации о фильмах, которая возвращает следующую структуру:

[
{
"id": 1,
"title": "Casino Royale",
"director": "Martin Campbell",
"starring":
[
"Daniel Craig",
"Eva Green"
],
"desc": "Twenty-first James Bond movie",
"release date": 1163466000000,
"box office": 594275385
},

{
"id": 2,
"title": "Quantum of Solace",
"director": "Marc Forster",
"starring":
[
"Daniel Craig",
"Olga Kurylenko"
],
"desc": "Twenty-second James Bond movie",
"release date": 1225242000000,
"box office": 591692078
},

{
"id": 3,
"title": "Skyfall",
"director": "Sam Mendes",
"starring":
[
"Daniel Craig",
"Naomie Harris"
],
"desc": "Twenty-third James Bond movie",
"release date": 1350954000000,
"box office": 1110526981
},

{
"id": 4,
"title": "Spectre",
"director": "Sam Mendes",
"starring":
[
"Daniel Craig",
"Lea Seydoux"
],
"desc": "Twenty-fourth James Bond movie",
"release date": 1445821200000,
"box office": 879376275
}
]

где значение поля даты выхода — миллисекунды с Эпохи, а кассовые сборы — выручка фильма в кинотеатре в долларах США.

Мы собираемся обработать пять различных рабочих сценариев, связанных с запросами GET, предполагая, что приведенная выше иерархия JSON была извлечена и сохранена в переменной String с именем jsonString .

6.1. Получение данных объекта с заданными идентификаторами

В этом случае клиент запрашивает подробную информацию о конкретном фильме, предоставляя серверу точный идентификатор фильма . В этом примере показано, как сервер ищет запрошенные данные, прежде чем вернуться к клиенту.

Скажем, нам нужно найти запись с id равным 2.

Первый шаг — подобрать правильный объект данных:

Object dataObject = JsonPath.parse(jsonString).read("$[?(@.id == 2)]");
String dataString = dataObject.toString();

JUnit Assert API подтверждает наличие нескольких полей:

assertThat(dataString, containsString("2"));
assertThat(dataString, containsString("Quantum of Solace"));
assertThat(dataString, containsString("Twenty-second James Bond movie"));

6.2. Получение названия фильма с учетом главной роли

Допустим, мы хотим найти фильм с участием актрисы по имени Ева Грин . Сервер должен вернуть название фильма, в котором Ева Грин входит в список главных ролей .

Последующий тест проиллюстрирует, как это сделать, и подтвердит возвращаемый результат:

@Test
public void givenStarring_whenRequestingMovieTitle_thenSucceed() {
List<Map<String, Object>> dataList = JsonPath.parse(jsonString)
.read("$[?('Eva Green' in @['starring'])]");
String title = (String) dataList.get(0).get("title");

assertEquals("Casino Royale", title);
}

6.3. Расчет общего дохода

В этом сценарии используется функция JsonPath с именем length() для определения количества записей о фильмах, чтобы рассчитать общий доход от всех фильмов.

Посмотрим на реализацию и тестирование:

@Test
public void givenCompleteStructure_whenCalculatingTotalRevenue_thenSucceed() {
DocumentContext context = JsonPath.parse(jsonString);
int length = context.read("$.length()");
long revenue = 0;
for (int i = 0; i < length; i++) {
revenue += context.read("$[" + i + "]['box office']", Long.class);
}

assertEquals(594275385L + 591692078L + 1110526981L + 879376275L, revenue);
}

6.4. Фильм с самым высоким доходом

Этот пример использования иллюстрирует использование параметра конфигурации JsonPath, отличного от значения по умолчанию, а именно Option.AS_PATH_LIST , чтобы найти фильм с наибольшим доходом.

Во-первых, нам нужно извлечь список кассовых сборов всех фильмов. Затем преобразуем его в массив для сортировки:

DocumentContext context = JsonPath.parse(jsonString);
List<Object> revenueList = context.read("$[*]['box office']");
Integer[] revenueArray = revenueList.toArray(new Integer[0]);
Arrays.sort(revenueArray);

Мы можем легко выбрать переменную наивысшего дохода из отсортированного массива доходов , а затем использовать ее для определения пути к записи фильма с наибольшим доходом:

int highestRevenue = revenueArray[revenueArray.length - 1];
Configuration pathConfiguration =
Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(pathConfiguration).parse(jsonString)
.read("$[?(@['box office'] == " + highestRevenue + ")]");

На основе этого рассчитанного пути мы определим и вернем название соответствующего фильма:

Map<String, String> dataRecord = context.read(pathList.get(0));
String title = dataRecord.get("title");

Весь процесс проверяется Assert API:

assertEquals("Skyfall", title);

6.5. Последний фильм режиссера

Этот пример иллюстрирует, как вычислить последний фильм, снятый режиссером по имени Сэм Мендес .

Для начала составим список всех фильмов режиссера Сэма Мендеса :

DocumentContext context = JsonPath.parse(jsonString);
List<Map<String, Object>> dataList = context.read("$[?(@.director == 'Sam Mendes')]");

Затем мы используем этот список для извлечения дат выпуска. Эти даты будут сохранены в массиве, а затем отсортированы:

List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : dataList) {
Object date = item.get("release date");
dateList.add(date);
}
Long[] dateArray = dateList.toArray(new Long[0]);
Arrays.sort(dateArray);

Мы используем переменную lastestTime (последний элемент отсортированного массива) в сочетании со значением поля Director для определения названия запрошенного фильма:

long latestTime = dateArray[dateArray.length - 1];
List<Map<String, Object>> finalDataList = context.read("$[?(@['director']
== 'Sam Mendes' && @['release date'] == " + latestTime + ")]");
String title = (String) finalDataList.get(0).get("title");

Следующее утверждение доказывает, что все работает так, как ожидалось:

assertEquals("Spectre", title);

7. Заключение

В этой статье были рассмотрены основные возможности Jayway JsonPath — мощного инструмента для просмотра и анализа документов JSON.

Хотя у JsonPath есть некоторые недостатки, такие как отсутствие операторов для доступа к родительским или дочерним узлам, он может быть очень полезен во многих сценариях.

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub .