JEP 540: Simple JSON API (Incubator)
Простой JSON API, Incubator (инкубационный модуль)
| Authors | Naoto Sato, Paul Sandoz, Justin Lu, Stuart Marks |
| Ответственный | Naoto Sato |
| Тип | Feature |
| Область | JDK |
| Статус | Integrated |
| Выпуск | 28 |
| Компонент | core-libs |
| Обсуждение | core dash libs dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | M |
| Рецензенты | Alex Buckley |
| Одобрен | Paul Sandoz |
| Создан | 2024/11/13 23:54 |
| Обновлён | 2026/09/09 17:03 |
| Задача | 8344154 |
Аннотация
Определить простой стандартный API для разбора и генерации JSON-документов, чтобы для этого не требовалась внешняя библиотека. Дать возможность решать многие задачи обработки JSON при небольшом объёме кода. Это API в статусе Incubator.
История
Этот JEP заменяет JEP 198, Light-Weight JSON API, написанный в 2014 году. За прошедшие годы обстоятельства изменились, поэтому здесь мы выбираем другой подход.
Цели
- Предоставить в платформе Java стандартное средство для обработки JSON-документов, соответствующих RFC 8259, без лишних формальностей.
-
Сохранить API небольшим, простым и лёгким в освоении. Предоставить только те типы данных и операции, которые нужны для строгого соответствия RFC 8259, чтобы упростить межмашинное взаимодействие. Избегать таких возможностей, как несколько конфигураций разбора, расширения синтаксиса, привязка данных и потоковая обработка.
-
Добиться того, чтобы код, который перемещается по JSON-документам с известной структурой и извлекает из них данные, был простым и читаемым. Поскольку у JSON-документов нет схем, такой код служит схемой de facto и должен читаться как схема.
-
Обеспечить простое и быстрое исследование незнакомых JSON-документов. Мы часто работаем с JSON-документами исследовательским способом: пишем код не по спецификации, а пробуя его на примерах документов. API должен предоставлять методы, которые быстро завершаются ошибкой с понятными сообщениями, чтобы исследование шло быстро.
-
Обеспечить устойчивую обработку отсутствующих или неожиданных значений, поскольку структура JSON-документов может со временем меняться.
-
Дать самому JDK возможность разбирать и генерировать JSON-документы.
Что не является целью
- Целью не является создание API, который вытеснит устоявшиеся внешние библиотеки JSON.
Мотивация
JSON повсеместно используется в современных вычислениях. В экосистеме Java есть множество устоявшихся библиотек JSON: Jackson, Gson, Jakarta JSON Processing и Binding, Fastjson 2 и другие. Эти библиотеки не только позволяют разбирать и генерировать JSON-документы, но и поддерживают расширенные варианты синтаксиса JSON, такие как JSON5, а также включают возможности более высокого уровня, например привязку данных, то есть преобразование Java-объектов в JSON и обратно с широкими возможностями настройки, и потоковую обработку на основе событий.
Однако часто нам нужно лишь выполнить простую задачу, например извлечь какие-то данные из JSON-документа. Код на Python или Go для таких задач прост; код на Java должен быть столь же простым.
Например, рассмотрим задачу вычисления среднего значения набора прогнозируемых температур в ответе U.S. National Weather Service REST API. Ответ представляет собой JSON-документ следующего вида:
{
...
"properties": {
...
"periods": [
{
"number": 1,
"name": "Today",
"startTime": "2026-04-22T06:00:00-04:00",
"endTime": "2026-04-22T18:00:00-04:00",
"isDaytime": true,
"temperature": 54,
"temperatureUnit": "F",
...
},
{
"number": 2,
"name": "Tonight",
"startTime": "2026-04-22T18:00:00-04:00",
"endTime": "2026-04-23T06:00:00-04:00",
"isDaytime": false,
"temperature": 48,
"temperatureUnit": "F",
...
},
{
"number": 3,
"name": "Thursday",
"startTime": "2026-04-23T06:00:00-04:00",
"endTime": "2026-04-23T18:00:00-04:00",
"isDaytime": true,
"temperature": 68,
"temperatureUnit": "F",
...
},
...
]
}
}
Чтобы вычислить среднюю прогнозируемую температуру, нужно разобрать документ, перейти к тому месту структуры, где находятся прогнозы, и пройти по массиву прогнозов, извлекая данные о температуре. У нас должна быть возможность решать такие простые задачи простым кодом на Java, не устанавливая внешнюю библиотеку и не подозревая, что на другом языке мы работали бы продуктивнее.
Одна из ключевых целей, определяющих развитие платформы Java в последнее время, — дать возможность решать простые задачи проще и с меньшими формальностями. Этой цели служат, в частности, удобные фабричные методы для коллекций, объявления var, запуск программ из исходных файлов, а также Compact Source Files (компактные исходные файлы) и Instance Main Methods (экземплярные методы main). Простой JSON API для разбора и генерации JSON-документов тоже служил бы этой важной цели.
Использование JSON в JDK
Стандартный JSON API в платформе Java также открыл бы путь к более широкому использованию JSON в платформе и в самом JDK, поскольку у JDK не может быть внешних зависимостей. Один из возможных сценариев — конфигурационные файлы. JDK использует формат файлов свойств для различных конфигурационных файлов, например файлов свойств безопасности. Слабость этого формата в том, что он не может выражать структурированные данные. Чтобы представить массив в файле свойств, приходится прибегать к неуклюжим обходным путям, например к последовательно пронумерованным свойствам:
security.provider.1=SUN
security.provider.2=SunRsaSign
security.provider.3=SunEC
...
Если JSON будет встроен в JDK, конфигурационные файлы смогут естественным образом представлять массивы с помощью массивов JSON:
{
"providers": [ "SUN", "SunRsaSign", "SunEC" ],
...
}
Описание
API jdk.incubator.json построен вокруг интерфейса JsonValue, который представляет значение JSON.
В синтаксисе JSON есть четыре вида примитивов:
-
строки JSON, ограниченные двойными кавычками:
"Hello" "My name is 'Bob'" "\u006a\u0061\u0076\u0061" -
числа JSON, записанные в системе счисления с основанием 10 с помощью десятичных цифр:
6 6.0 31.84 2.9E+5 -
логические литералы JSON:
trueиfalse -
литерал null в JSON:
null
и два вида структур:
-
объекты JSON, ограниченные символами
{}и состоящие из членов, разделённых запятыми. Член имеет имя, которое также называют ключом, и значение, разделённые двоеточием:{ "address" : "123 Smith Street", "value" : 31.84, "coordinates" : [ [ 37, 23, 41 ], [ -121, 57, 10 ] ] } -
массивы JSON, ограниченные символами
[]и состоящие из значений JSON, разделённых запятыми:[ 1, 2, 3, { "value": "4" }, [ 5, 6 ] ]
Соответственно, у интерфейса JsonValue есть шесть подынтерфейсов: JsonString, JsonNumber, JsonBoolean, JsonNull, JsonObject и JsonArray. Каждый интерфейс объявляет операции, подходящие для соответствующего ему синтаксического элемента JSON: экземпляры JsonNumber и JsonBoolean предоставляют преобразования в примитивные типы Java, экземпляры JsonString — преобразование в String Java, экземпляры JsonObject открывают доступ к членам, а экземпляры JsonArray — к элементам массива.
Интерфейс JsonValue объявлен как sealed, что гарантирует: любой экземпляр JsonValue всегда относится к одному из этого фиксированного набора подтипов, поэтому исчерпывающим выражениям и операторам switch не требуется ветка default.
JSON API упрощает разбор JSON-документов, соответствующих RFC 8259. Метод parse класса Json возвращает дерево экземпляров JsonValue, которые предоставляют имена, типы и значения разобранных данных JSON. Возвращаясь к примеру с National Weather Service, мы можем вычислить среднюю прогнозируемую температуру всего в несколько строк:
String body = ... REST response body, which is a JSON document ... ;
JsonValue json = Json.parse(body);
json.get("properties").get("periods").asList().stream()
.mapToInt(j -> j.get("temperature").asInt())
.average()
.ifPresent(IO::println);
(Полный пример приведён в Приложении.)
API также упрощает генерацию JSON-документов. Например, этот код:
IO.println(JsonObject.of(Map.of("providers",
JsonArray.of(List.of(JsonString.of("SUN"),
JsonString.of("SunRsaSign"),
JsonString.of("SunEC"))))));
выводит:
{"providers":["SUN","SunRsaSign","SunEC"]}
Разбор JSON-документов и перемещение по ним
Класс Json может разобрать JSON-документ, содержащийся либо в String, либо в массиве char. JSON-документ может быть телом ответа REST API, прочитанным из сети, конфигурационным файлом, прочитанным с диска, или другими текстовыми данными, сформированными приложением.
Для разбора JSON-документа достаточно одного вызова одного из методов Json.parse:
JsonValue root = Json.parse(doc);
Разбор строгий: документ должен соответствовать RFC 8259. Расширения синтаксиса, такие как завершающие запятые и комментарии, не поддерживаются. Кроме того, в документах не должно быть объектов с повторяющимися именами членов. Эта политика, допускаемая RFC, обеспечивает максимальную совместимость и предсказуемость и снижает опасения, связанные с обработкой некорректных или неоднозначных JSON-документов. (Подробное обсуждение см. ниже.)
При успешном разборе возвращается экземпляр JsonValue. При неудачном разборе выбрасывается непроверяемое исключение JsonParseException. Исключение содержит подробное сообщение с конкретными сведениями об ошибке, её путём от корня документа и её положением в документе. Например, если документ содержит в объекте повторяющиеся имена членов, выбрасываемое исключение имеет вид:
jdk.incubator.json.JsonParseException: The duplicate member name: "providers" was
already parsed. Path: "{". Location: line 2, position 4.
В корне большинства JSON-документов находится объект JSON или массив JSON. Например, дамп потоков в формате JSON, созданный инструментом jcmd, содержит корневой объект:
{
"threadDump": {
"formatVersion": 2,
"processId": 45178,
"time": "2026-04-16T23:13:02.709630Z",
"runtimeVersion": "27-internal",
"threadContainers": [
{
"container": "<root>",
"parent": null,
"owner": null,
"threads": [
{
"tid": 3,
"time": "2026-04-16T23:13:02.906891Z",
"name": "main",
"state": "WAITING",
...
Корневой объект содержит единственный член — вложенный объект threadDump, а threadDump, в свою очередь, содержит как примитивные, так и структурные значения JSON.
Получив корневой JsonValue через Json.parse(...), вы можете извлекать значения из объектов и массивов с помощью их методов доступа, которые возвращают запрошенное значение члена или элемент массива как JsonValue. Для доступа к значению члена не нужно приводить JsonValue к JsonObject, а для доступа к элементу массива — к JsonArray.
-
get(String)получает значение члена объекта. Получение объекта дампа потоков:JsonValue threadDump = root.get("threadDump"); -
get(int)получает элемент массива. Получение корневого контейнера потоков:JsonValue firstContainer = threadDump.get("threadContainers").get(0);
Если экземпляр JsonValue имеет неподходящий тип или запрошенный член или элемент не существует, методы доступа выбрасывают JsonValueException.
Преобразование значений JSON в значения Java
Значение JSON можно преобразовать в значение Java, вызвав один из методов преобразования интерфейса JsonValue. Чтобы преобразование прошло успешно, JsonValue должен быть экземпляром соответствующего подтипа JsonValue:
| Подтип | Метод | Итоговый тип Java |
|---|---|---|
JsonString |
asString() |
java.lang.String |
JsonNumber |
asInt() |
int |
JsonNumber |
asLong() |
long |
JsonNumber |
asDouble() |
double |
JsonBoolean |
asBoolean() |
boolean |
JsonObject |
asMap() |
java.util.Map |
JsonArray |
asList() |
java.util.List |
Например, можно получить значение Java String, связанное с членом «time» дампа потоков:
JsonValue threadDumpTime = threadDump.get("time");
String time = threadDumpTime.asString();
Можно преобразовать массив контейнеров потоков в List экземпляров JsonValue и обработать каждый экземпляр:
threadDump.get("threadContainers").asList().forEach(jv -> ...);
Можно обратиться к объекту дампа потоков как к Map, чтобы получить количество членов:
int count = threadDump.asMap().size();
Можно углубляться в JSON-документ, выстраивая цепочку вызовов методов доступа и преобразуя результат в значение Java только в конце. Получение значения идентификатора первого потока в корневом контейнере потоков:
long tid = threadDump.get("threadContainers").get(0)
.get("threads").get(0).get("tid").asLong();
Устройство методов преобразования избавляет от большинства проверок instanceof и нисходящих приведений типов в случаях, когда в документе ожидается конкретный тип данных JSON:
-
asString()преобразует экземплярJsonStringвStringJava, заменяя escape-последовательности JSON из RFC 8259 соответствующими символами. -
asInt()преобразует экземплярJsonNumberвintJava, если его числовое значение может быть представлено точно. -
asLong()преобразует экземплярJsonNumberвlongJava, если его числовое значение может быть представлено точно. -
asDouble()преобразует экземплярJsonNumberвdoubleJava, если его числовое значение может быть представлено с достаточной точностью. -
asBoolean()преобразует экземплярJsonBooleanв значение Javaboolean, равноеtrueилиfalse. -
asMap()преобразует экземплярJsonObjectв неизменяемыйMapJava. Если объект JSON не содержит членов, возвращается пустойMap. -
asList()преобразует экземплярJsonArrayв неизменяемыйListJava. Если массив JSON не содержит элементов, возвращается пустойList.
Для значения null в JSON метода преобразования нет. Экземпляры JsonNull можно обрабатывать с помощью проверки instanceof JsonNull или метода tryValue.
Если JsonValue не является экземпляром подтипа, подходящего для метода преобразования, метод выбрасывает JsonValueException. Например, вызов asInt() для JsonValue, который является экземпляром JsonString, всегда будет выбрасывать это исключение. Попытки разобрать строковое значение как число не предпринимается.
Числовые преобразования могут завершиться неудачей, например, если числовое значение невозможно представить в целевом числовом типе Java; в этом случае тоже выбрасывается JsonValueException. Более подробное обсуждение обработки чисел и преобразований см. ниже.
Обработка изменений JSON-документов
JSON-документы из определённого источника со временем могут меняться так, что это нарушит ваши прежние ожидания относительно их структуры и содержимого:
-
Вы можете вызвать методы доступа, рассчитывая на имена членов или индексы массивов, которых нет в объектах JSON и массивах JSON документа.
-
Вы можете вызвать методы преобразования, применимые к одному типу JSON, для значений другого типа.
Если вызвать методы доступа или преобразования для неподходящего типа, они выбрасывают JsonValueException. Это исключение непроверяемое, чтобы скрипты и небольшие программы было проще читать и писать.
Продолжим пример с дампом потоков: напомним, что корневое значение JSON — это объект JSON с единственным членом threadDump. Этот код:
JsonValue name = root.get("threadName");
выбрасывает JsonValueException с сообщением о том, что объект JSON не содержит члена с именем "threadName", а этот код:
List<JsonValue> threadDumpList = threadDump.asList();
выбрасывает JsonValueException с сообщением о том, что член threadDump является объектом JSON, а не массивом JSON.
Сообщение исключения описывает путь от корня документа JSON к неожиданному значению JSON, а также позицию в документе JSON. Это полезно, когда цепочка методов доступа уходит глубоко в документ. Например, если бы в приведённом ранее фрагменте кода, извлекающем идентификатор потока, он по ошибке преобразовывался в boolean, а не в long:
boolean tid = threadDump.get("threadContainers").get(0)
.get("threads").get(0).get("tid").asBoolean();
то исключение, выброшенное asBoolean(), имело бы вид:
jdk.incubator.json.JsonValueException: JsonNumber is not a JsonBoolean. Path:
"{threadDump{threadContainers[0{threads[0{tid". Location: line 13, position 19.
Обработка необязательных членов
Если вы не знаете, есть ли в объекте JSON член с заданным именем, можно использовать метод доступа tryGet. Он возвращает экземпляр Optional, содержащий значение члена, или пустой Optional, если такого члена нет. (Метод get, напротив, проверяет, что член существует, и выбрасывает исключение, если его нет.) Метод tryGet выбрасывает JsonValueException, если он вызван не для JsonObject.
Рассмотрим следующий объект потока:
{
"tid": 11,
"time": "2026-04-16T23:13:02.918321Z",
"name": "Finalizer",
"state": "WAITING",
"waitingOn": "java.lang.Object@c10f5b9",
"stack": [
...
Объект потока содержит несколько необязательных членов. Один из них — член waitingOn, который содержит строковое представление JSON объекта, которого ожидает поток. Однако если поток ничего не ожидает, объект потока может выглядеть так:
{
"tid": 10,
"time": "2026-04-16T23:13:02.918177Z",
"name": "Reference Handler",
"state": "RUNNABLE",
"stack": [
...
Поэтому при обработке объектов потоков из дампа потоков нужно быть готовым к тому, что член waitingOn отсутствует. Это можно обработать с помощью tryGet:
JsonValue thread = ...
thread.tryGet("waitingOn")
.ifPresent(result -> ...);
Лямбда-выражение, переданное в ifPresent, вызывается, только если член waitingOn присутствует.
Обработка значений null
Если вы не знаете, является ли значение JSON значением null JSON, можно использовать метод доступа tryValue. Этот метод возвращает пустой Optional, если значение JSON, для которого он вызван, является JsonNull; в противном случае он возвращает это значение.
Например, объект контейнера потоков обычно выглядит так:
{
"container": "java.util.concurrent.ThreadPoolExecutor@1936a586",
"parent": "<root>",
...
Здесь значение члена parent — строка JSON, имя родительского контейнера. Однако контейнер с именем "<root>" является корнем всех контейнеров и выглядит так:
{
"container": "<root>",
"parent": null,
...
У корневого контейнера нет родителя, поэтому значение члена parent — null JSON. Поэтому при обработке объектов контейнеров из дампа потоков нужно быть готовым к тому, что член parent окажется либо строкой JSON, либо null JSON. Это можно обработать с помощью tryValue:
JsonValue container = ...
container.get("parent").tryValue()
.ifPresent(result -> ...);
Лямбда-выражение, переданное в ifPresent, вызывается, только если значение члена "parent" не является null JSON.
Обработка изменчивой структуры и содержимого
Структура и содержимое документов JSON в определённом контексте часто единообразны, но иногда они меняются. Они могут различаться у разных источников, со временем меняться у одного источника, который сам развивается, или даже различаться внутри одного документа.
Например, в дампах потоков в JDK 26 и более ранних выпусках идентификаторы потоков представлены строками JSON; в JDK 27 и более поздних выпусках идентификаторы потоков представлены числами JSON.
Код, который ожидает, что tid будет числом JSON, например:
long tid = thread.get("tid").asLong();
завершится с JsonValueException, если встретит дамп потоков, созданный версией JDK, которая выводит значения tid в виде строк JSON.
В любом из представлений числовое значение по спецификации помещается в Java-тип long. Можно использовать instanceof, чтобы проверить, что перед вами — JsonNumber или JsonString, но понятнее использовать шаблоны типов в операторе switch:
long tid = switch (thread.get("tid")) {
case JsonNumber jn -> jn.asLong();
case JsonString js -> Long.parseLong(js.asString());
default -> throw new JsonValueException("Unexpected type for \"tid\"");
};
Генерация документов JSON
Чтобы получить документ JSON в строковом виде из JsonValue, достаточно вызвать его метод toString. Этот метод возвращает компактное строковое представление, в котором все члены, элементы и значения выводятся в одной строке без пробельных символов между ними.
Например, этот код:
JsonValue json = Json.parse("""
{
"service" : "web_server",
"id" : 3
}
""");
IO.println(json.toString());
выводит:
{"service":"web_server","id":3}
(Метод toString отличается от метода asString, который выбрасывает исключение, если JsonValue, для которого он вызван, не является JsonString.)
Статический метод Json.toDisplayString выводит документ JSON в отформатированном виде: члены и элементы разделяются переводами строк, а вложенные структуры получают отступ заданной величины String. Например, этот код:
IO.println(Json.toDisplayString(json, " "));
выводит приведённую выше структуру с отступом в два пробела:
{
"service": "web_server",
"id": 3
}
Вывод обоих методов, toString и Json.toDisplayString, может быть разобран методом Json.parse, который создаст JsonValue, представляющий то же значение JSON, что и в исходном документе.
Числа JSON
Синтаксис чисел JSON, определённый в RFC 8259, позволяет представлять десятичные значения произвольной точности и диапазона. JSON API позволяет обрабатывать числа JSON без потерь; однако в большинстве приложений достаточно обычных числовых типов.
RFC 8259 указывает, что хорошей совместимости между библиотеками JSON можно добиться, используя 64-битные двоичные значения с плавающей точкой IEEE 754, которые соответствуют Java-типу double. Поэтому метод asDouble() преобразует числовое значение JSON в Java-тип double. Значение JSON должно лежать в диапазоне, который может представить double; если значение выходит за пределы диапазона, выбрасывается JsonValueException. Значения бесконечности и «не число» ("NaN") в JSON непредставимы, поэтому никогда не возвращаются. Отрицательный ноль, однако, в JSON представим, поэтому может быть возвращён.
Если точность значения JSON выше, чем может представить double, значение округляется до ближайшего значения double. Например:
double d1 = Json.parse("3.141592653589793238462643383279").asDouble();
// d1 is 3.141592653589793, the nearest double value
double d2 = Json.parse("1.8E309").asDouble();
// Throws JsonValueException, out of range.
// Double.parseDouble("1.8E309") would yield positive Infinity.
Целочисленные значения используются часто, поэтому метод asInt() преобразует числовое значение JSON в Java-значение int. Значение JSON должно точно представляться как int, иначе выбрасывается исключение. Числа, у которых синтаксически есть дробная часть, но которые представляют целые значения, преобразуются; например:
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
Метод преобразования asLong() похож на asInt(), но возвращает Java-значение long и поддерживает любое числовое значение JSON, которое может быть точно представлено как long.
Если нужен более узкий примитивный тип, чем int или double, для безопасного преобразования можно использовать Primitive Types in Patterns (примитивные типы в шаблонах) — сейчас это возможность в статусе Preview (предварительная версия). Например, если вы ожидаете, что число JSON представимо как short:
JsonValue json = Json.parse("""
{
"id": 12345,
"price": 10.99
}
""");
if (json.get("id").asInt() instanceof short s) {
// use s
} else {
// report out-of-range error
}
Как уже говорилось, числа JSON могут иметь произвольную точность и диапазон. Методы asDouble(), asInt() и asLong() по определению обрабатывают только подмножество числовых значений JSON: они отвергают значения вне диапазона и округляют значения с избыточной точностью. Чтобы обрабатывать числовые данные JSON без потери информации, можно преобразовать практически любое число JSON в экземпляр java.math.BigDecimal:
BigDecimal bd = new BigDecimal(jn.toString());
Альтернативы
-
Предоставить полный набор возможностей вместо ограниченного.
Многочисленные существующие внешние библиотеки JSON в совокупности предоставляют широкий набор возможностей. Мы никак не можем включить все эти возможности в платформу Java; вместо этого мы должны выбрать подмножество, которое даёт наибольшую пользу относительно своей стоимости.
Мы исключили часто встречающуюся возможность привязки данных (data binding). Эта возможность бесспорно полезна и удобна для многих приложений. Однако она существенно увеличила бы объём API и резко повысила бы стоимость реализации и сопровождения. Во многих сценариях привязка данных не нужна, поэтому мы считаем эту возможность не строго необходимой. То, что библиотеки JSON Jackson и Jakarta выносят свои возможности привязки данных в отдельные модули, неявно признаёт, что существуют сценарии, которым привязка данных не нужна.
Потоковый API явно необходим для некоторых узких специализированных сценариев, но он вносит в приложение немалую сложность даже для простых задач извлечения данных. Поэтому мы исключили эту возможность.
Без привязки данных и потоковой обработки остаётся подход в духе DOM, при котором документы JSON разбираются в деревья специфичных для JSON объектов, из которых легко извлекать данные. API небольшой, и стоимость его реализации и сопровождения соответственно невелика. Это удовлетворяет потребности значительной части приложений, работающих с JSON, — от самых простых до умеренно сложных.
Приложение может начать с JSON API платформы Java, но со временем дорасти до потребности в таких возможностях, как привязка данных или потоковая обработка, и тогда потребуется переход на более богатый API из внешней библиотеки. Мы не считаем такой сценарий неудачей, и он не является достаточным основанием для включения в платформу Java дорогостоящих возможностей, таких как привязка данных JSON и потоковая обработка.
-
Интегрировать внешнюю библиотеку JSON.
Мы могли бы интегрировать внешнюю библиотеку в платформу Java и JDK как нижестоящий форк. Это породило бы сложные вопросы лицензирования и управления. Постоянно возникало бы напряжение из-за изменений, идущих в обоих направлениях, вызванное разными критериями в отношении качества спецификации, совместимости, графиков выпусков и так далее. (Мы уже сталкивались с таким напряжением в прошлом с различными XML API.) Вероятно, эти издержки вместе с дополнительной нагрузкой на сопровождение JDK перевесили бы пользу от интеграции внешней библиотеки.
-
Ничего не делать, поскольку внешние библиотеки уже хорошо справляются с JSON.
Бездействие не послужило бы более широкой цели — позволить решать простые задачи легче и с меньшими церемониями, особенно в простых программах и для новичков в платформе Java.
Добавление любой внешней зависимости в приложение влечёт издержки и добавляет риски. Вероятно, есть приложения, которым было бы полезно использовать JSON, но они этого не делают, потому что их разработчики стремятся свести к минимуму издержки и риски. Таким приложениям был бы полезен стандартный JSON API в платформе Java.
-
Разрешить повторяющиеся имена членов в объектах JSON.
Это давняя проблема JSON. Ранние спецификации не определяли однозначно обработку повторяющихся имён членов в одном объекте JSON. Библиотеки JSON вели себя непоследовательно или предоставляли настраиваемые приложением параметры, задающие политику обработки повторяющихся имён.
К сожалению, объект с повторяющимися именами принципиально неоднозначен. Когда вопрос о повторяющихся именах обсуждался в рассылке ECMAScript Discussion List в 2013 году, опасение в отношении запрета повторяющихся имён состояло в том, что это сделает недействительными существующие документы. Поэтому формулировку «should be unique» (а не «must») сохранили, и она перешла в текущие спецификации. В частности, RFC 8259 гласит:
Имена внутри объекта СЛЕДУЕТ делать уникальными.
...
Объект, все имена в котором уникальны, совместим в том смысле, что все программные реализации, получающие этот объект, одинаково понимают соответствия имён и значений. Если имена внутри объекта не уникальны, поведение программ, получающих такой объект, непредсказуемо.
Непредсказуемость возникает, когда объект обрабатывается системой, состоящей из нескольких независимо разработанных библиотек JSON. Это может приводить к трудно диагностируемым ошибкам, уязвимостям безопасности, снижению совместимости и общему недостатку надёжности. Это явление рассматривается в RFC 9413, «Maintaining Robust Protocols».
По этим причинам мы выбрали строгий подход, при котором повторяющиеся имена безусловно считаются ошибкой. Строгий подход даёт высокую уверенность в корректности разобранных документов. Мы надеемся, что ошибочные документы, упомянутые в обсуждении ECMAScript 2013 года, за прошедшие годы были исправлены, а программы, создававшие эти документы, — починены.
-
Поддерживать завершающие запятые, комментарии или другие расширения синтаксиса.
Существует несколько вариантов JSON, например JSON5, которые поддерживают комментарии или завершающие запятые в массивах и объектах. Эти расширения нужны для того, чтобы документы JSON было удобнее редактировать вручную.
Мы делаем упор на простоту и на обмен данными между машинами, поэтому такие расширения не поддерживаем. Их поддержка расширила бы матрицу тестирования, повысила бы вероятность ошибок совместимости и увеличила бы общую нагрузку на разработку и сопровождение.
Распространённый обходной путь — предварительно обрабатывать входящие документы в расширенном JSON перед разбором. Например, однострочные комментарии в строках, начинающихся с символов
'#', легко удалить до разбора:String jsonc = Files.readString(Path.of("file-with-comments.json")); String json = jsonc.replaceAll("(?m)^\\s*#.*$", ""); JsonValue jv = Json.parse(json); -
Предоставить дополнительные методы преобразования.
Для большего удобства мы могли бы добавить дополнительные методы преобразования, например
asBigDecimal(). Реализовать такой метод было бы тривиально. Однако то, что метод легко добавить, само по себе не оправдывает его включение в API. Наша цель — предоставить минимальный API, поверх которого приложения могут строить свои решения. Написатьnew BigDecimal(JsonNumber.toString())так же просто.Каждый дополнительный метод, возможно, добавляет ещё немного удобства, но это и ещё одно субъективное проектное решение. Кроме того, поскольку методы преобразования единообразно определены в
JsonValue, мы стремимся сохранить этот набор методов небольшим и целенаправленным, в соответствии с нашим намерением сделать API простым в использовании. Предоставляя минимальный набор необходимых операций, мы оставляем пользователям API свободу самим определять нужные им дополнительные уровни, адаптированные под их конкретные задачи.
Тестирование
Мы тщательно протестируем JSON API, чтобы убедиться, что разбирать и генерировать можно только канонические формы JSON по RFC 8259. Это поможет гарантировать, что использование API не приведёт к несогласованности при взаимодействии с другими библиотеками JSON. Для этого мы не только добавим в JDK всесторонние модульные тесты, но и воспользуемся устоявшимся набором тестов JSON Parsing Test Suite, который содержит множество входных данных для граничных случаев.
Риски и допущения
-
Мы предполагаем, что входные документы JSON помещаются в память — в виде
Stringили массиваchar, — и что результат разбора тоже помещается в память. При нашей модели на основе дерева, если бы мы разрешили такие источники JSON, как файлы или сетевые соединения, при больших документах могли бы возникать проблемы, например нехватка памяти. Это решение соответствует нашей минималистичной философии проектирования. -
Риск этого предложения в том, что новый API может начать использоваться в приложениях, которые уже используют внешние библиотеки JSON, что приведёт к беспорядку и путанице. Мы считаем, что преимущества перевешивают этот риск.
-
Пока API находится в статусе Incubator, мы соберём больше информации о сценариях использования, связанных с генерацией и преобразованием документов JSON, чтобы развивать эти части API. Кроме того, мы продолжим учитывать готовящиеся возможности языка для Pattern Matching (сопоставление с образцом), которые могут повлиять на дизайн API.
Приложение: пример с прогнозом погоды
Следующая программа отправляет запрос к U.S. National Weather Service REST API за прогнозом погоды на семь дней для Санта-Клары (Калифорния). В теле ответа она получает документ JSON. Затем программа разбирает документ, переходит вглубь его структуры и получает массив прогнозов. После этого она извлекает температуру из каждого прогноза, вычисляет среднее значение и выводит результат.
import java.net.*;
import java.net.http.*;
import jdk.incubator.json.Json;
import jdk.incubator.json.JsonValue;
void main() throws Exception {
var query = "https://api.weather.gov/gridpoints/MTR/97,83/forecast";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(query)).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
JsonValue json = Json.parse(response.body());
json.get("properties").get("periods").asList().stream()
.mapToInt(j -> j.get("temperature").asInt())
.average()
.ifPresent(IO::println);
}
Включение API в статусе Incubator
JSON API в настоящее время является Incubator-модулем и по умолчанию отключён. Чтобы его использовать, нужно включить его параметром командной строки --add-modules jdk.incubator.json, который добавляет Incubator-модуль в набор модулей, доступных для разрешения. Чтобы запустить приведённую выше программу-пример, этот параметр нужно указать и при компиляции, и при запуске.
Чтобы запустить программу для среднего прогноза как программу из одного файла исходного кода, выполните:
$ java --add-modules jdk.incubator.json Weather.java
Вывод будет примерно таким:
WARNING: Using incubator modules: jdk.incubator.json
53.357142857142854
Чтобы скомпилировать программу с помощью javac и запустить её с помощью java, выполните:
$ javac --add-modules jdk.incubator.json Weather.java
$ java --add-modules jdk.incubator.json Weather
Чтобы интерактивно поэкспериментировать с API, можно использовать jshell. Как и раньше, Incubator-модуль нужно включить в командной строке:
$ jshell --add-modules jdk.incubator.json
jshell> import jdk.incubator.json.*
jshell> Json.parse("""
...> { "name": "Today", "temperature": 54 }
...> """)
$2 ==> {"name":"Today","temperature":54}
jshell> $2.get("temperature").asInt()
$3 ==> 54
jshell>