JSON Toolkit

Язык запросов JSONPath на практике

Главное: В 2024 году JSONPath наконец получил формальный стандарт, RFC 9535. Эта статья объясняет синтаксис, семантику фильтров, функции и исторические различия между реализациями.

Средний~12 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка языка запросов JSONPath на практике

Главное: сначала локализуйте явным путём и фильтруйте поверх него, и пишите выражения по RFC 9535; используйте доступ к свойствам или JSON Pointer для одного фиксированного поля, и jq или код для агрегации.

Вы, вероятно, видели выражения вроде $.store.book[?(@.price < 10)].title, но международным стандартом он стал только в 2024 году.

1. От де-факто стандарта к формальному стандарту

JSONPath был предложен Стефаном Гёсснером в 2007 году, и долгое время единственной «спецификацией» был один пост в блоге, а каждая языковая реализация делала своё:

  • нужны ли скобки в фильтрах (?(@.a) против ?@.a);
  • поддерживается ли индекс массива [-1];
  • возвращает ли результат «значения» или «значения и пути»;
  • разрешены ли скриптовые выражения (?(@.price < $.max)).

RFC 9535, опубликованный в феврале 2024 года, свёл эти расхождения в единый синтаксис и семантику и предоставил официальный набор тестов соответствия. Новые проекты должны следовать RFC 9535; при миграции старых проектов сначала подтвердите версию реализации.

2. Шпаргалка по синтаксису

Синтаксис Значение Пример
$ Корневой узел $.store
@ Текущий узел внутри фильтра $[?@.price < 10]
.name Дочерний узел (имя должно соответствовать [A-Za-z_][A-Za-z0-9_]*) $.store.book
['name'] Дочерний узел (любое имя, включая не-ASCII и точки) $['store']['书']
[0] Индекс массива; отрицательные числа считаются с конца $.book[0], $.book[-1]
[0,2] Несколько индексов / несколько имён $.book[0,2]
[1:3] Срез, включая начало и исключая конец $.book[1:3]
[::2] Срез с шагом $.book[::2]
[*] Подстановочный знак для всех дочерних узлов $.book[*].title
..name Рекурсивный спуск, поиск на любой глубине $..price
..[*] Рекурсивно взять все узлы $..*
[?<expr>] Селектор фильтра $.book[?@.price < 10]

Одну семантику вы должны запомнить: JSONPath возвращает набор узлов (nodelist), а не одно значение. Даже когда совпадает только один узел, результат — список длины один.

3. Проход по синтаксису на одном образце

{
  "store": {
    "book": [
      { "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
      { "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
      { "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
    ],
    "bicycle": { "color": "red", "price": 19.95 }
  },
  "expensive": 10
}
Выражение Результат
$.store.bicycle.color ["red"]
$..price 4 цены (включая цену велосипеда)
$.store.book[*].title 3 заголовка
$.store.book[?@.price < 10].title Заголовки книг 1 и 3
$.store.book[?@.tags] Все книги, у которых есть tags (проверка существования)
$.store.book[-1].title Заголовок последней книги
$..book[0,1] Первые две книги
$.store.book[?@.price < $.expensive] Синтаксически зависит от реализации; RFC 9535 не допускает кросс-корневые ссылки

Последняя строка — историческая ловушка: многие реализации допускают $.expensive внутри фильтра для кросс-узлового сравнения, что RFC 9535 явно запрещает — фильтр может обращаться только к текущему узлу @. Когда нужно сравнивать с глобальным порогом, передайте порог как аргумент функции запроса.

4. Что могут выразить фильтры

Выражения фильтра RFC 9535 поддерживают четыре вида конструкций:

Логика:       &&  ||  !          например ?@.price < 10 && @.tags
Сравнение:    ==  !=  <  <=  >  >=
Существование: голый путь        например ?@.tags (проверяет существование)
Функции:      length() count() match() search() value()

Несколько практических примеров:

// Цена между 8 и 10
$.store.book[?@.price >= 8 && @.price <= 10]

// Заголовок содержит "JSON" (сопоставление регулярным выражением; второй аргумент — I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]

// Больше одного тега
$.store.book[?length(@.tags) > 1]

// Получить первое совпавшее значение вместо списка (value() превращает набор из одного узла в значение)
$.store.book[?@.price < 9].title.value()

match() — сопоставление всей строки, а search() — поиск подстроки; лежащий в основе диалект регулярных выражений — I-Regexp (подмножество RFC 9485), который не поддерживает ни обратные ссылки, ни lookaround — это и ограничение, и удобный щит против DoS регулярных выражений.

5. Различия реализаций: проверяйте перед миграцией

Реализация Язык Поддержка RFC 9535 Примечания
jsonpath-plus JavaScript В основном совместима Поддерживает функции расширения и скриптовые выражения; способнее, но не строго стандартна
jsonpath (dchester) JavaScript Частичная Давняя библиотека с синтаксисом ?() и другой структурой результатов
ojg / gjson Go ojg имеет хорошую поддержку gjson использует другой синтаксис путей и не является JSONPath
jsonpath-ng Python Частичная Поддерживает расширения и довольно снисходительна
Jayway JsonPath Java Частичная Широкая экосистема; историческое поведение отличается от RFC

Три проверки перед миграцией:

  1. Обязательны ли скобки фильтра;
  2. Поддерживаются ли отрицательные индексы вроде -1;
  3. Является ли результат «массивом значений» или «массивом объектов {path, value}».

Третье легче всего спотыкает: фронтенд читает result[0], а после смены библиотеки получает объект, оставляя страницу пустой.

6. Производительность и безопасность

Производительность

  • JSONPath по сути — один обход со сложностью O(число узлов), и он не может использовать индексы;
  • Рекурсивный спуск с .. самый дорогой; если вы можете сначала локализовать через $.store.book, а затем фильтровать, не пишите $..book;
  • Компилируйте выражения один раз и переиспользуйте (большинство библиотек предлагают compile или кеширование) вместо повторного разбора при каждом запросе;
  • Для документов с более чем 100,000 узлов рассмотрите точное извлечение через JSON Pointer сначала или предобработку на сервере через jq.

Безопасность

  • Выражения из пользовательского ввода должны иметь ограничения длины и глубины вложенности;
  • Не позволяйте JSONPath обрабатывать авторизацию — он может только читать и не может выразить «какие поля невидимы»;
  • Когда регулярное выражение, используемое match(), приходит от пользователей, ограничьте его сложность; хотя он безопаснее PCRE, ограничение длины всё равно рекомендуется.

7. Когда не использовать JSONPath

  • Вам нужно только одно фиксированное поле → используйте data.store.bicycle.color напрямую;
  • Вам нужно записать обратно после изменения → используйте JSON Pointer или патч; JSONPath не пишет обратно;
  • Вам нужна агрегация (суммирование, группировка) → используйте jq, SQL или код; JSONPath только выбирает;
  • Документ огромен и нужны только несколько полей → используйте потоковый парсер вместо чтения всего дерева в память сначала.

8. Контрольный список

  • [ ] Выражения следуют RFC 9535 и не зависят от кросс-корневых ссылок;
  • [ ] Ясно, что результат — набор узлов; используйте [0] или value() для получения одного значения;
  • [ ] Выражения предварительно компилируются и переиспользуются;
  • [ ] Контролируемые пользователем выражения имеют ограничения длины и сложности;
  • [ ] Официальные тестовые случаи соответствия запускаются при смене реализаций.

Вопросы и ответы

Как выбрать между JSON Pointer и JSONPath?

Используйте обычный доступ к свойствам или JSON Pointer, когда нужно только одно фиксированное поле; обращайтесь к JSONPath, когда нужны фильтры, подстановочные знаки или рекурсивный спуск. JSONPath только выбирает и никогда не пишет обратно, поэтому запись обратно принадлежит JSON Pointer или патчу.

Почему фильтр не может ссылаться на $.expensive?

RFC 9535 явно запрещает кросс-корневые ссылки; фильтр может обращаться только к текущему узлу @. Вместо этого передайте порог как аргумент функции запроса. Старые реализации принимают синтаксис, но он ломается при смене или обновлении библиотеки.

Стоит ли полагаться на result[0] для чтения значения?

Только после подтверждения, что текущая реализация возвращает массив значений. Некоторые библиотеки возвращают объекты {path, value}, поэтому result[0] становится объектом, и страница пустеет. Читайте одиночные значения через [0] или value() и прогоняйте официальный набор соответствия перед сменой реализаций.