JEP 540: Simple JSON API (Incubator)
- title
- JEP 540: Simple JSON API (Incubator)
- type
- summary
- summary
- Инкубационный модуль jdk.incubator.json в OpenJDK - нарочито минималистичный, строгий DOM-only JSON API
- tags
- java, json, api-design, openjdk
- sources
- jep-540-json-api
- created
- 2026-07-29
- updated
- 2026-07-29
- lang
- ru
- translation_of
- jep-540-json-api
- source_updated
- 2026-07-29
- translated
- 2026-09-01
- translator
- lllm/antigravity/gemini-3.7-flash-medium
JEP 540 добавляет парсер и генератор JSON в JDK в виде инкубационного модуля jdk.incubator.json. Он заменяет собой JEP 198 (Light-Weight JSON API, 2014), который писался в других обстоятельствах и предлагал иной подход. Самое интересное в документе - не сам API, который намеренно сделан небольшим, а список вещей, от реализации которых авторы отказались, и их аргументация.
Заявленная цель и единственная не-цель
Цель - дать возможность обрабатывать документы по стандарту RFC 8259 без лишних церемоний, чтобы код навигации читался как схема de facto для документа без собственной схемы; чтобы незнакомые структуры можно было быстро исследовать благодаря раннему падению с понятными сообщениями; чтобы отсутствующие и неожиданные значения обрабатывались без сбоев программы, и чтобы сам JDK получил возможность парсить и генерировать JSON.
Единственная не-цель: не планируется создавать API, призванный вытеснить устоявшиеся сторонние библиотеки JSON. Jackson, Gson, Jakarta JSON Processing and Binding, а также Fastjson 2 названы сразу и остаются на своих местах. JEP прямо допускает, что приложение может стартовать на этом API, а затем мигрировать на более функциональную библиотеку, и не считает такой исход неудачей.
Пример для мотивации - вычисление средней температуры прогноза из REST-ответа Национальной метеорологической службы США. Аргумент прост: эквивалентный код на Python или Golang краток, и код на Java должен быть таким же - без внешних зависимостей и без ощущения, что на другом языке это писалось бы быстрее. Это встаёт в один ряд с другими инициативами JDK по снижению многословия: фабричными методами коллекций, var, запуском программ напрямую из файлов исходного кода и компактными исходниками с экземплярными методами main.
Второй мотив - внутренний. JDK не может использовать внешние зависимости, поэтому собственного JSON в нём не было вовсе. Его конфигурационные файлы используют формат properties, не умеющий выражать структуру, что приводит к обходным путям с нумерованными ключами:
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
Встроенный парсер JSON позволил бы превратить это в "providers": [ "SUN", "SunRsaSign", "SunEC" ].
Устройство API
Всё завязано на интерфейс JsonValue - sealed-интерфейс ровно с шестью подинтерфейсами, повторяющими четыре примитива и две структуры JSON: JsonString, JsonNumber, JsonBoolean, JsonNull, JsonObject, JsonArray. Ограничение иерархии (sealing) делает исчерпывающий switch по JSON-значению корректным без ветки default, что, по задумке JEP, и должно использоваться для разбора документов переменной структуры.
Методы доступа get(String) и get(int) объявлены на самом JsonValue, а не на JsonObject и JsonArray, поэтому цепочке навигации не требуется приведение типов:
long tid = threadDump.get("threadContainers").get(0)
.get("threads").get(0).get("tid").asLong();
Преобразование происходит только в конце цепочки через asString(), asInt(), asLong(), asDouble(), asBoolean(), asMap() и asList(). Неверный тип или отсутствующий элемент вызывают JsonValueException. Это непроверяемое исключение (unchecked), благодаря чему небольшие программы и скрипты остаются читаемыми. Сообщение об ошибке содержит путь от корня документа, а также строку и позицию - именно это делает схему работоспособной с учётом того, что упасть может любой шаг навигации:
jdk.incubator.json.JsonValueException: JsonNumber is not a JsonBoolean. Path:
"{threadDump{threadContainers[0{threads[0{tid". Location: line 13, position 19.
Два метода try* закрывают два случая, когда документ преподносит сюрпризы. tryGet(String) возвращает Optional для элемента, которого может не быть. tryValue() возвращает пустой Optional, когда значение равно JSON null, поэтому отдельного asNull() нет - JsonNull обрабатывается через instanceof или через tryValue. Оба варианта взяты из практики собственных JSON thread dump'ов JDK: у объекта потока поле waitingOn появляется, только если поток ждёт, а parent у корневого контейнера потоков равен null, а не строке.
Эволюция схемы документов обрабатывается схожим образом. Дампы потоков в JDK 26 и ранее выдают tid в виде JSON-строки; JDK 27 выдаёт его числом. Код, написанный под один вариант, ломается на другом, и рекомендуемое решение - switch с сопоставлением типов (type pattern), принимающий оба формата.
В чём заключается строгость
Парсинг строго следует RFC 8259 без каких-либо настроек. Запятые в конце списков и комментарии запрещены. Объекты с дублирующимися именами полей также безусловно отклоняются.
Решение по дубликатам имён аргументируется в документе подробнее всего, поскольку RFC лишь указывает, что имена SHOULD (должны) быть уникальными. Эту формулировку сохранили в 2013 году после обсуждений в списке рассылки ECMAScript из опасений, что строгое требование уникальности сделает невалидными существующие документы. Позиция JEP заключается в том, что объект с дубликатами неоднозначен, библиотеки исторически расходились в том, какое значение побеждает, а возникающая непредсказуемость в системе из нескольких независимых JSON-библиотек ведёт к трудноуловимым ошибкам, уязвимостям и проблемам совместимости. Авторы ссылаются на RFC 9413 ("Maintaining Robust Protocols") и делают ставку на то, что документы, которые защищали в 2013 году, с тех пор уже исправлены.
Работа с числами - ещё одно место, где строгость проявляется конкретно. Числа в JSON имеют произвольную точность и диапазон; asDouble() следует рекомендациям RFC по совместимости и округляет значение до ближайшего IEEE 754 double, выбрасывая ошибку при выходе за границы диапазона. asInt() и asLong() требуют точной представимости, но принимают синтаксические дроби, чьё значение целочисленно:
int i1 = Json.parse("123.0").asInt(); // succeeds
int i2 = Json.parse("234.56E2").asInt(); // succeeds
int i3 = Json.parse("345.6").asInt(); // fails, not integral
int i4 = Json.parse("2147483648").asInt(); // fails, out of range
Обработка без потери точности доступна, но не в виде отдельного метода. Метода asBigDecimal() нет; нужно писать new BigDecimal(jn.toString()). Логика JEP в том, что тривиальность реализации сама по себе не повод добавлять метод, а каждый вспомогательный метод - это ещё одно жёстко заданное решение в наборе, который намеренно сделан небольшим и единообразным для всего JsonValue.
Что пошло под нож
Привязка данных (data binding) - самое крупное усечение. JEP признаёт её бесспорную полезность, но отвергает из-за разрастания API и стоимости поддержки, отмечая, что и Jackson, и Jakarta выносят binding в отдельные модули - что само по себе подтверждает наличие массы сценариев, где он не нужен. Потоковая обработка (streaming) отброшена по противоположной причине: она незаменима в узких специализированных задачах, но усложняет даже самое простое извлечение данных.
Отказ от того и другого оставляет модель DOM-дерева, и JEP это вполне устраивает. Из этой же модели вытекает сопутствующее ограничение: входные данные должны целиком помещаться в памяти как String или char[]. Файлы и сетевые соединения в качестве источников не принимаются, так как древовидный парсер при чтении неограниченного источника просто исчерпает память.
Две альтернативы были рассмотрены и полностью отклонены. Включение форка сторонней библиотеки в состав JDK создало бы проблемы с лицензированием и управлением, а также постоянные трения вокруг качества спецификаций, совместимости и графиков релизов - JEP ссылается на прошлый опыт с XML API. Вариант ничего не делать проваливает цель по избавлению от многословия и оставляет приложения, которым JSON пошёл бы на пользу, избегать его из-за накладных расходов и рисков внешней зависимости.
Соответствие стандарту проверяется собственными модульными тестами JDK, а также JSONTestSuite - общепринятым набором краевых случаев. Признаваемые риски носят в основном организационный характер: появление нового API в приложениях, где уже используется сторонняя библиотека, и вероятность того, что новые возможности языка по сопоставлению с образцом (pattern matching) изменят желаемый облик дизайна до выхода из стадии инкубации.
Использование
Модуль отключён по умолчанию, поэтому --add-modules jdk.incubator.json требуется указывать и при компиляции, и во время выполнения, включая jshell:
$ java --add-modules jdk.incubator.json Weather.java
WARNING: Using incubator modules: jdk.incubator.json
53.357142857142854
Заметки
Разделение на JsonParseException и JsonValueException проводит ту же границу, что и json-pass-value-accuracy-gap для вывода LLM: документ может быть синтаксически безупречен, но при этом отдавать строку там, где коду требовалось число. Ответ JEP - сделать такой сбой явным, с точным указанием места и с дешёвым восстановлением: switch с сопоставлением типов для tid - ровно та защитная конструкция, необходимость которой подтверждают результаты бенчмарка structured-output-benchmark, когда генератор документа вам не подконтролен.