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 .