Руководство по синтаксису JSONPath: запросы и фильтрация JSON с примерами
JSONPath — это язык запросов к JSON, во многом подобный тому, как XPath служит языком запросов к XML. Записывается путевое выражение, и вычислитель возвращает каждое значение, которое ему соответствует. Чтобы извлечь все имена авторов из документа книжного магазина, достаточно написать $.store.book[*].author — и в ответ приходит список авторов, без всякого кода обхода.
Это руководство разбирает каждый селектор синтаксиса JSONPath на готовых примерах, которые можно запускать по ходу чтения. Стоит сразу прояснить одно: существует два диалекта. Диалект Goessner 2007 года — фактическая классика, а RFC 9535 — формальный стандарт IETF, опубликованный в феврале 2024 года. Они совпадают на типичных путях и расходятся в краевых случаях, поэтому руководство отмечает различия по мере их появления. Любое выражение ниже можно попробовать в JSONPath Tester и переключаться между обоими движками для сравнения.
Для начала — шпаргалка по селекторам. Дальше каждая строка раскрывается разобранным примером на одном общем документе JSON.
| Селектор | Значение | Пример |
|---|---|---|
$ | Корень документа | $ |
@ | Текущий элемент (в фильтрах) | [?(@.price < 10)] |
.name / ['name'] | Дочерний член | $.store.book |
.. | Рекурсивный спуск | $..author |
* | Все элементы / члены | $.store.book[*] |
[0] | Индекс массива | $.store.book[0] |
[start:end:step] | Срез массива (полуоткрытый) | $.store.book[0:2] |
[a,b] | Объединение имён / индексов | $.store.book[0,2] |
[?()] | Фильтрующее выражение | $.store.book[?(@.price < 10)] |
length() count() match() search() value() | Функции RFC 9535 (только в фильтрах) | [?length(@.title) > 15] |
Что такое JSONPath?
JSONPath — это декларативный язык запросов для выбора узлов из документа JSON. Вместо цикла, обходящего объекты и массивы, нужное местоположение описывается путём, и вычислитель возвращает совпадающие значения. Ментальная модель та же, что даёт XPath для XML: путь из селекторов, шаг за шагом проходящий по структуре.
Он встречается всюду, где разработчики имеют дело с JSON. Его применяют, чтобы вытащить поле из ответа API, проверить значение в интеграционном тесте, адресовать поля в конфигурациях конвейеров для Kubernetes, AWS Step Functions и Azure Logic Apps, а также извлечь данные из большого или нерегулярного JSON без ручного написания логики обхода.
Краткая историческая заметка — она объясняет раскол диалектов. Stefan Goessner предложил JSONPath в 2007 году. Язык быстро распространился и стал фактическим стандартом, но формально его так и не специфицировали — поэтому реализации разошлись в деталях. IETF закрыл этот пробел в феврале 2024 года выпуском RFC 9535, первой формальной спецификации JSONPath. Оба диалекта живы и сегодня, и именно поэтому одно и то же выражение может вести себя по-разному в зависимости от того, какая библиотека его выполняет.
Прежде чем приступать к запросам, полезно прочитать структуру. Отформатируйте сырой ввод с помощью JSON Formatter, чтобы вложенность стала видна.
Пример документа
Все примеры ниже выполняются на классическом JSON книжного магазина Goessner. Вставьте его один раз и переиспользуйте:
{
"store": {
"book": [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
{ "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
],
"bicycle": { "color": "red", "price": 19.95 }
}
}
Четыре книги с названием, автором и ценой, плюс велосипед. Запомните: цены равны 8,95, 12,99, 8,99 и 22,99 — именно они определяют результаты фильтров дальше.
Корень, дочерние члены и рекурсивный спуск ($ . ..)
Каждое выражение начинается с корня, который записывается как $. Оттуда переход к дочерним членам выполняется через точку или через скобочную нотацию — эти два способа равнозначны:
$.store.book → the book array
$['store']['book'] → identical result
Скобочная нотация нужна, когда в ключе есть пробелы, точки или другие специальные символы: $['first name'] работает там, где $.first name не сработал бы.
Оператор .. — это рекурсивный спуск. Он ищет на каждом уровне документа, а не только среди прямых потомков, и собирает всё, что соответствует следующему за ним селектору на любой глубине:
$..author
→ ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
Когда использовать .., а когда полный путь
Рекурсивный спуск удобен, но груб. $..price совпадает с каждой ценой где угодно в дереве — включая store.bicycle.price, которая, возможно, была не нужна. Когда форма известна, выпишите путь явно, чтобы запрос оставался точным:
$..price → [8.95, 12.99, 8.99, 22.99, 19.95] (includes the bicycle)
$.store.book[*].price → [8.95, 12.99, 8.99, 22.99] (only books)
Приберегите .. для действительно нерегулярных или неизвестных структур. Это компромисс между удобством и контролем: чем больше известно о данных, тем чаще стоит выбирать явный путь.
Подстановки, индексы и срезы массивов (* [0] [start:end:step])
Подстановка * выбирает все элементы массива или всех членов объекта:
$.store.book[*].title
→ ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]
Индексы массива начинаются с нуля, а отрицательные индексы отсчитываются с конца:
$.store.book[0].title → ["Sayings of the Century"]
$.store.book[-1].title → ["The Lord of the Rings"]
Срезы используют ту же полуоткрытую конвенцию [start:end:step], что Python и JavaScript: start включается, end исключается.
$.store.book[0:2].title → ["Sayings of the Century", "Sword of Honour"]
Это возвращает две книги, а не три — индексы 0 и 1, с остановкой перед индексом 2. Исключающая правая граница — самая частая ошибка в JSONPath, поэтому её стоит врезать в память:
[0:2] → first TWO elements (indices 0, 1) ← correct
[0:3] → first THREE elements (indices 0, 1, 2)
Опустите границу, чтобы дойти до края, и добавьте шаг, чтобы брать каждый N-й элемент:
$.store.book[2:].title → ["Moby Dick", "The Lord of the Rings"]
$.store.book[:3].title → first three titles
$.store.book[::2].title → ["Sayings of the Century", "Moby Dick"] (every other)
Фильтрующие выражения [?()] — самая мощная часть
Фильтры — это то, ради чего JSONPath держат в обиходе. Фильтр [?()] оставляет только те элементы, для которых предикат истинен, а внутри фильтра @ ссылается на текущий проверяемый элемент.
Чтобы выбрать книги дешевле 10:
$.store.book[?(@.price < 10)].title
→ ["Sayings of the Century", "Moby Dick"]
При ценах книжного магазина (8,95, 12,99, 8,99, 22,99) подходят две книги. Вот как строить предикаты фильтра шаг за шагом:
- Сравнение с литералом. Используйте
==,!=,<,<=,>,>=— например@.price > 10. - Совпадение со строкой. Строковые литералы берутся в одинарные кавычки:
@.author == 'Nigel Rees'. - Проверка на существование. Голая ссылка на член выбирает элементы, у которых он есть:
[?(@.isbn)]оставляет только книги сisbn. - Комбинирование условий. Соединяйте предикаты через
&&и||:[?(@.price < 10 && @.author == 'Herman Melville')].
Самая частая ошибка фильтра — область видимости @. Внутри предиката текущий элемент — это @, а не $. Запись $.price указывает обратно на корень документа, а не на проверяемую книгу:
$.store.book[?($.price < 10)] → wrong scope, matches nothing useful
$.store.book[?(@.price < 10)] → correct: each book's own price
RFC 9535 против классики в фильтрах
Два диалекта расходятся в пробелах и кавычках. Классика снисходительна — [?(@.price<10)] без пробелов разбирается нормально. RFC 9535 точно следует своей грамматике и строже относится к тому, как записан фильтр. Если фильтр, который работал где-то ещё, даёт сбой, проверьте пробелы и движок. Держите фильтры чистыми (операторы с пробелами, строки в одинарных кавычках) — и они вычисляются одинаково независимо от того, какая библиотека в итоге их выполнит.
Селекторы объединения — выбрать сразу несколько ключей ([a,b])
Селектор объединения перечисляет несколько имён или индексов внутри одной пары скобок и собирает их все:
$.store.book[0]['title','author']
→ ["Sayings of the Century", "Nigel Rees"]
Объединения работают и с индексами, и их можно смешивать с другими селекторами для фиксированной проекции:
$.store.book[0,2].title → ["Sayings of the Century", "Moby Dick"]
$.store.book[*]['title','price'] → title and price of every book
Объединения — это правильный инструмент, когда нужно несколько конкретных полей, а не целый объект или сплошной проход подстановкой.
Функции RFC 9535: length, count, match, search, value
RFC 9535 определяет пять стандартных функциональных расширений. Правило, на котором спотыкается почти каждый — и которое конкурирующие руководства постоянно искажают, — таково:
Эти функции вызываются только внутри фильтра
[?...], никогда как самостоятельный сегмент пути.
Запись $.store.book.length() недопустима в RFC 9535, и стандартная грамматика её отвергает. Эта форма вызова-в-сегменте — расширение jsonpath-plus, а не часть спецификации. Чтобы фильтровать по длине, функция вызывается внутри предиката:
$.store.book[?length(@.title) > 15]
→ [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
]
Оба выбранных названия длиннее 15 символов; «Moby Dick» (9) и «Sword of Honour» (15, а не больше 15) исключаются.
Вот что делает каждая функция внутри фильтра:
length()— длина строки, массива или объекта:[?length(@.title) > 15]count()— число узлов в списке узлов:[?(count(@.authors) > 1)]match()— проверка регулярным выражением на совпадение со всей строкой (шаблон I-Regexp):[?match(@.author, 'J.*')]search()— проверка регулярным выражением на подстроку:[?search(@.title, 'the')]value()— преобразует список из одного узла в его значение для сравнения
Все пять — это возможность RFC 9535. Классический диалект (Goessner) их не реализует, поэтому если выражение на основе функции даёт сбой, убедитесь, что вы вызываете её внутри фильтра и что движок настроен на RFC 9535.
RFC 9535 против классического Goessner — почему одно выражение даёт разный результат
Когда выражение JSONPath возвращает разные результаты в двух инструментах, причина обычно в диалекте. Вот как они соотносятся:
| Аспект | Классический Goessner (2007) | RFC 9535 (2024) |
|---|---|---|
| Стандартизация | Фактическая, формально не закреплена | Первая формальная спецификация IETF |
| Пробелы/кавычки в фильтре | Снисходительно ([?(@.price<10)] нормально) | Строго, точно по грамматике |
| Сравнение с отсутствующим членом | Зависит от реализации | Чётко определено, не выбрасывает ошибку |
| Стандартные функции | Не входят в диалект | length count match search value |
| Нормализованные пути | Нет канонической формы | Каноническая, скобочная форма с одинарными кавычками |
| Порядок в объединении | Зависит от библиотеки | Специфицирован |
Практический совет: если ваша нижестоящая система заявляет соответствие RFC 9535, пишите и проверяйте на стандартном движке. Если вы поддерживаете выражение, скопированное с jsonpath.com, из jsonpath-plus или сервиса на базе Jayway, используйте классику, чтобы результаты воспроизводились. JSONPath Tester запускает оба движка за одним переключателем, так что можно вставить выражение один раз и увидеть, как каждый диалект его обрабатывает, бок о бок — это двухдвижковое сравнение и есть самый быстрый способ диагностировать расхождение.
JSONPath против XPath против jq — что выбрать
Эти три часто путают, поэтому вот короткая версия:
- JSONPath — это декларативный путевой запрос для JSON. Лучше всего встраивается в конфигурации и проверки тестов, где нужно назвать местоположение, не написав кода.
- XPath — эквивалент из мира XML. JSONPath позаимствовал часть его нотации (
*,..,[]), отчего аналогия и держится, но языки не взаимозаменяемы, и наборы функций у них разные. - jq — это командный процессор JSON. Он выходит далеко за рамки выбора по пути — преобразование, агрегация, переформовка — и живёт в конвейере вашей оболочки.
Решение обычно очевидно. Для встроенной проверки или поля конфигурации конвейера берите JSONPath. Для преобразований и обработки данных из оболочки берите jq — шпаргалка по jq подробно разбирает этот сценарий. А когда вопрос в том, соответствует ли payload ожидаемой форме, а не где находится поле, проверьте его с помощью JSON Schema Validator и его полного руководства по валидации.
7 частых ошибок в JSONPath
- Забыли корень
$.store.bookотвергается большинством движков; каждое выражение начинается с$. - Срез с погрешностью на единицу.
[0:2]— это два элемента, а не три: правая граница исключается. - Неверный диалект. Запуск классического выражения под RFC 9535 (или наоборот) может дать ошибку разбора или совпасть с другими узлами. Переключите движок под выражение.
- Функция как самостоятельный сегмент.
$.store.book.length()недопустима в RFC 9535; вызывайтеlength()внутри фильтра. - Забыли
@в фильтре.[?($.price < 10)]указывает на корень; используйте[?(@.price < 10)]. - Неверные кавычки в скобках.
$[store]— это ошибка; берите ключ в кавычки:$['store']. - Думали, что
..останавливается на первом уровне. Рекурсивный спуск совпадает на любой глубине, а не только среди прямых потомков.
Тестируйте JSONPath онлайн и приватно
Самый быстрый способ выучить синтаксис JSONPath — запускать его. JSONPath Tester вычисляет каждое выражение из этого руководства вживую: оба движка — RFC 9535 и классический, представления результата Values / Paths / Both, нормализованные пути для отладки и 100% выполнение в браузере — без загрузки, без регистрации и без eval, поэтому он безопасен для проприетарных payload. Постройте путь здесь, убедитесь, что он выбирает ровно те узлы, что нужны, а затем вставьте проверенное выражение прямо в код, тесты или конвейер.
Для остальной части работы с JSON превратите образец ответа в типизированные интерфейсы через JSON to TypeScript или сравните два документа поле за полем с помощью JSON Diff.
Часто задаваемые вопросы
Для чего используется JSONPath?
JSONPath запрашивает JSON без императивного кода. Разработчики применяют его, чтобы вытаскивать поля из ответов API, проверять значения в интеграционных тестах и адресовать поля в конфигурациях для Kubernetes, AWS Step Functions и Azure Logic Apps. Он силён в извлечении данных из больших или нерегулярных структур, где писать обход вручную было бы утомительно.
В чём разница между RFC 9535 и классическим JSONPath?
Классика — это фактический диалект Stefan Goessner от 2007 года: широко реализован, но формально не специфицирован, отчего библиотеки разошлись. RFC 9535 — формальная спецификация IETF от февраля 2024 года: она определяет точную грамматику, нормализованные пути для результатов и пять стандартных функций. Эти двое различаются на краях — в фильтрах, объединениях и сравнении с отсутствующим членом.
Как работают фильтрующие выражения JSONPath?
Фильтр [?()] оставляет только те элементы, чей предикат истинен, а @ — это текущий элемент. Например, $.store.book[?(@.price < 10)] выбирает книги дешевле 10. Можно комбинировать условия через && и ||, проверять существование члена и сравнивать со строковыми или числовыми литералами.
Можно ли использовать length() как $.store.book.length()?
Нет. В RFC 9535 length() и остальные четыре функции вызываются только внутри фильтра, как $.store.book[?length(@.title) > 15]. Самостоятельная сегментная форма $.store.book.length() — это расширение jsonpath-plus, а не стандартный JSONPath, и грамматика RFC 9535 её отвергает.
JSONPath — это то же самое, что XPath?
Нет, но идея похожа. XPath запрашивает XML; JSONPath запрашивает JSON; оба находят узлы путевыми селекторами. JSONPath намеренно позаимствовал часть нотации XPath — *, .. и [] — отчего аналогия и полезна, но синтаксис, семантика и наборы функций у них разные и невзаимозаменяемые.
Что делает рекурсивный спуск (..) в JSONPath?
Оператор .. ищет на каждом уровне документа, а не только среди прямых потомков. $..author собирает каждый член author, где бы тот ни появился, на любой глубине вложенности. Это самый быстрый способ вытащить одно поле из глубоко вложенной или нерегулярной структуры, но он может совпасть с куда большим числом узлов, чем ожидается — сужайте его, когда можете.