Json
Модуль JSON создает эффективные реализации JsonReader и JsonWriter для классов приложения во время компиляции и без использования Reflection во время выполнения.
Генерация управляется аннотациями @Json, @JsonReader, @JsonWriter и связанными аннотациями уровня поля.
JsonModule также предоставляет готовые преобразователи для HTTP-клиента, HTTP-сервера, строковых параметров и Kafka.
Это позволяет использовать один и тот же сгенерированный JsonReader или JsonWriter в разных модулях Kora.
Пошаговый разбор перед справочным описанием смотрите в разделе JSON.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Запись¶
Используйте @JsonWriter, чтобы создать только JsonWriter.
Этот вариант полезен, когда тип нужно только записывать в JSON:
Чтение¶
Используйте @JsonReader, чтобы создать только JsonReader.
Этот вариант полезен, когда тип нужно только читать из JSON:
Чтение и запись¶
Используйте @Json, чтобы создать одновременно JsonReader и JsonWriter.
В большинстве случаев @Json — предпочтительная аннотация:
Интерфейсы чтения и записи¶
JsonReader<T> и JsonWriter<T> — это обычные компоненты графа приложения.
После генерации или ручной регистрации их можно внедрять по сигнатуре, как любую другую зависимость.
@Component
public final class MyService {
private final JsonReader<Dto> reader;
private final JsonWriter<Dto> writer;
public MyService(JsonReader<Dto> reader, JsonWriter<Dto> writer) {
this.reader = reader;
this.writer = writer;
}
public Dto read(String json) throws IOException {
return this.reader.read(json);
}
public byte[] write(Dto dto) throws IOException {
return this.writer.toByteArray(dto);
}
}
JsonReader читает значение из JsonParser, byte[], String или InputStream.
Методы readUnchecked(...) делают то же самое, но преобразуют IOException в UncheckedIOException.
JsonWriter записывает значение через JsonGenerator и также может вернуть byte[], строку или форматированную строку через toByteArray(...), toString(...) и toPrettyString(...).
Методы toByteArrayUnchecked(...), toStringUnchecked(...) и toPrettyStringUnchecked(...) преобразуют IOException в UncheckedIOException.
Особенности поведения во время выполнения при прямом вызове кодеков:
read(...)возвращаетnull, когда парсер находится на токенеJSONnull, поэтому документ верхнего уровняnullдесериализуется вnull.- Некорректный
JSONили неожиданный токен приводит кJsonParseExceptionизJackson, который является подтипомIOException. - Варианты
readUnchecked(...)иto...Unchecked(...)пробрасывают любойIOException(включаяJsonParseException), обернутый вUncheckedIOException.
Обязательные поля¶
По умолчанию все поля, объявленные в объекте, считаются обязательными (NotNull).
По умолчанию все поля, объявленные в объекте без синтаксиса Kotlin Nullability, считаются обязательными (NotNull).
Необязательные поля¶
Если поле JSON необязательное и может отсутствовать, используйте аннотацию @Nullable:
- Подойдет любая аннотация
@Nullable, напримерjavax.annotation.Nullable,jakarta.annotation.Nullableилиorg.jetbrains.annotations.Nullable.
Для Kotlin используйте синтаксис Kotlin Nullability и пометьте параметр как nullable:
Именование полей¶
Если поле в JSON имеет имя, отличное от имени поля в классе, используйте @JsonField.
Она задает имя ключа в JSON, а также позволяет указать отдельные реализации JsonReader и JsonWriter для поля.
Если для поля нужны отдельные преобразователи, укажите их в reader и writer:
Игнорирование поля¶
Если поле в DTO не нужно читать или записывать, используйте @JsonSkip.
Такое поле игнорируется при чтении и записи JSON.
Уровни сериализации¶
По умолчанию поля со значением null не записываются. (1)
IncludeType.NON_NULL— записывать поле только в том случае, если значение неnull.
Чтобы изменить это поведение, используйте @JsonInclude.
Аннотацию можно разместить не только на поле, но и на классе; в этом случае правило применяется сразу ко всем полям.
Доступные варианты:
IncludeType.ALWAYS— всегда записывать поле.IncludeType.NON_NULL— записывать поле, если значение неnull.IncludeType.NON_EMPTY— записывать поле, если значение неnullи не является пустой коллекцией или ассоциативным массивом.
Пример:
Конструктор сериализации¶
Если для чтения JSON должен использоваться конкретный конструктор, пометьте его аннотацией @JsonReader.
Можно также использовать @Json, но @JsonReader имеет более высокий приоритет:
JsonReader и JsonWriter могут быть сгенерированы для классов, record, enum и sealed-типов.
Для чтения класса должен быть один публичный конструктор либо конструктор, явно помеченный @JsonReader или @Json.
Java Bean и обычные классы¶
@Json, @JsonReader и @JsonWriter не ограничиваются record и data class.
Обычный класс тоже подходит: для чтения требуется единственный публичный конструктор (или конструктор, помеченный @JsonReader/@Json), а для записи используются методы доступа к полям.
@JsonField можно разместить на приватных полях, чтобы переименовать ключ в JSON:
@JsonWriter
public class DtoJavaBean {
@JsonField("string_field")
private String field1;
@JsonField("int_field")
private int field2;
public DtoJavaBean(String field1, int field2) {
this.field1 = field1;
this.field2 = field2;
}
public String getField1() { return field1; }
public int getField2() { return field2; }
}
Обертка JsonNullable¶
Если при чтении JSON необходимо отличать отсутствующее поле от поля со значением null, используйте JsonNullable.
Основные состояния и фабричные методы:
JsonNullable.undefined()— поле отсутствует вJSON.JsonNullable.nullValue()— поле присутствует и содержитnull.JsonNullable.of(value)— поле присутствует и содержит значение.JsonNullable.ofNullable(value)— создаетnullValue(), если значение равноnull, иначеof(value).
При записи JSON undefined() пропускается, nullValue() записывается как null, а of(value) записывает само значение.
@Nullable против JsonNullable¶
Обычное необязательное поле (@Nullable в Java или nullable-тип в Kotlin) сводит два разных входных значения JSON к одному и тому же результату: и отсутствующее поле, и поле с явным значением null читаются как null.
JsonNullable разделяет эти случаи, что и делает его правильным типом для тел HTTP-запросов PATCH, где клиент отправляет только те поля, которые действительно хочет изменить.
Три возможных результата чтения для поля JsonNullable<T>:
Входной JSON |
Результат чтения | isDefined() |
isNull() |
value() |
|---|---|---|---|---|
{} (поле отсутствует) |
JsonNullable.undefined() |
false |
false |
выбрасывает |
{"field": null} |
JsonNullable.nullValue() |
true |
true |
null |
{"field": value} |
JsonNullable.of(value) |
true |
false |
value |
Поскольку value() выбрасывает исключение при undefined(), всегда защищайте доступ проверкой isDefined() (или проверяйте isNull()) перед вызовом.
Частичное обновление PATCH¶
В запросе PATCH отсутствующее поле означает «оставить без изменений», а явный null означает «очистить значение».
JsonNullable позволяет обработчику различить эти два случая и применить только те поля, которые клиент действительно отправил:
@Json
public record UserPatch(JsonNullable<String> name,
JsonNullable<String> email) { }
public void apply(User user, UserPatch patch) {
if (patch.name().isDefined()) { //(1)!
user.setName(patch.name().value());
}
if (patch.email().isDefined()) {
user.setEmail(patch.email().value()); //(2)!
}
// fields left as undefined() are not touched
}
- Поле присутствовало в теле запроса, поэтому его необходимо применить (даже если значение — явный
null). value()возвращаетnull, когда клиент отправил{"email": null}, что очищает поле.
@Json
data class UserPatch(
val name: JsonNullable<String>,
val email: JsonNullable<String>
)
fun apply(user: User, patch: UserPatch) {
if (patch.name.isDefined()) { //(1)!
user.name = patch.name.value()
}
if (patch.email.isDefined()) {
user.email = patch.email.value() //(2)!
}
// fields left as undefined() are not touched
}
- Поле присутствовало в теле запроса, поэтому его необходимо применить (даже если значение — явный
null). value()возвращаетnull, когда клиент отправил{"email": null}, что очищает поле.
Взаимодействие с уровнями сериализации: IncludeType.ALWAYS и IncludeType.NON_NULL не меняют способ записи JsonNullable (применяются его собственные правила undefined/nullValue/of).
Только IncludeType.NON_EMPTY влияет на JsonNullable, рассматривая поле undefined() или nullValue() как пустое, так что оно опускается в выводе.
Sealed-классы и интерфейсы¶
Если в зависимости от значения конкретного поля нужно читать и записывать разные JSON-объекты, используйте
sealed-класс или интерфейс для представления этих объектов.
Sealed-типы поддерживаются двумя аннотациями:
@JsonDiscriminatorField— задает поле-дискриминатор вDTO, помеченном какsealed-класс или интерфейс.@JsonDiscriminatorValue— задает одно или несколько значений дискриминатора для подкласса.
@Json
@JsonDiscriminatorField("type")
public sealed interface Event {
@JsonDiscriminatorValue("firstType")
record FirstTypeEvent(String id, String type) implements Event {}
@JsonDiscriminatorValue("secondType")
record SecondTypeEvent(String id, Integer code) implements Event {}
@JsonDiscriminatorValue("thirdType")
record ThirdTypeEvent(String id, Boolean status) implements Event {}
}
@Json
@JsonDiscriminatorField("type")
sealed interface Event {
@JsonDiscriminatorValue("firstType")
data class FirstTypeEvent(val id: String, val type: String) : Event
@JsonDiscriminatorValue("secondType")
data class SecondTypeEvent(val id: String, val code: Integer) : Event
@JsonDiscriminatorValue("thirdType")
data class ThirdTypeEvent(val id: String, val status: Boolean) : Event
}
Подклассы получают JsonReader и JsonWriter по тем же правилам, как если бы они были помечены @Json.
Сам sealed-класс или интерфейс также получает общий JsonReader и JsonWriter.
Поддерживаются вложенные sealed-иерархии, а @JsonDiscriminatorValue может принимать несколько значений для одного подкласса.
Приведенный ниже JSON-объект записывается в класс FirstTypeEvent:
Поддерживаются обобщенные (generic) типы DTO, включая обобщенные sealed-иерархии.
Кодек для каждого конкретного аргумента типа разрешается из графа, как и для любого другого типа поля:
Перечисления¶
Для enum JsonReader и JsonWriter можно сгенерировать теми же аннотациями @Json, @JsonReader и @JsonWriter.
По умолчанию значением enum в JSON является результат toString(), поэтому его можно переопределить:
Если требуется значение, отличное от строки из toString(), пометьте публичный метод без параметров аннотацией @Json.
В этом случае для возвращаемого типа должны быть доступны соответствующие JsonReader и JsonWriter:
При чтении значение JSON, не совпадающее ни с одной константой enum, приводит к JsonParseException из Jackson, где перечислены допустимые значения.
RawJson¶
RawJson используется, когда в объект нужно включить уже готовый фрагмент JSON, не сериализуя его повторно.
При записи RawJson передается в выходной JSON как есть, поэтому значение должно быть корректным фрагментом JSON.
Поддерживаемые типы¶
Модуль предоставляет встроенные типы, которые покрывают большинство распространенных задач.
Для коллекций и ассоциативных массивов Kora использует JsonReader или JsonWriter типа элемента.
Список поддерживаемых типов
- Boolean
- boolean
- Short
- short
- Integer
- int
- Long
- long
- Double
- double
- Float
- float
- byte[]
- String
- UUID
- BigInteger
- BigDecimal
- RawJson
- Object
- Enum
- List
- Set
- SortedSet
- Map
- LocalDate
- LocalTime
- LocalDateTime
- Instant
- OffsetTime
- OffsetDateTime
- ZonedDateTime
- Year
- YearMonth
- MonthDay
- Month
- DayOfWeek
- ZoneId
- Duration
Пользовательские типы¶
Если необходимо читать или записывать пользовательский тип, зарегистрируйте пользовательскую фабрику для JsonReader или JsonWriter.
Пример регистрации пользовательского JsonWriter:
Пример регистрации пользовательского JsonReader.
Reader переключается по текущему токену парсера, возвращает null для JSON null, читает ожидаемый токен и выбрасывает JsonParseException для всего остального:
@KoraApp
public interface Application {
default JsonReader<ZoneOffset> zoneOffsetJsonReader() {
return parser -> switch (parser.currentToken()) {
case VALUE_NULL -> null;
case VALUE_STRING -> ZoneOffset.of(parser.getValueAsString());
default -> throw new JsonParseException(parser,
"Expecting VALUE_STRING token, got " + parser.currentToken());
};
}
}
@KoraApp
interface Application {
fun zoneOffsetJsonReader(): JsonReader<ZoneOffset> = JsonReader { parser ->
when (parser.currentToken()) {
JsonToken.VALUE_NULL -> null
JsonToken.VALUE_STRING -> ZoneOffset.of(parser.valueAsString)
else -> throw JsonParseException(parser,
"Expecting VALUE_STRING token, got ${parser.currentToken()}")
}
}
}
Пользовательский JsonReader<T> или JsonWriter<T> — это обычный компонент графа.
После регистрации сгенерированные кодеки автоматически подхватывают его везде, где встречается поле типа T, а также его можно закрепить за отдельным полем через @JsonField(reader = ..., writer = ...) (см. Именование полей).
Jackson¶
Если для чтения и записи JSON вместо сгенерированных во время компиляции кодеков нужно использовать Jackson, применяйте JacksonModule.
Он заменяет преобразователи запросов/ответов HTTP-клиента и HTTP-сервера на основанные на Jackson.
Каждый преобразователь JacksonModule зависит от компонента ObjectMapper, поэтому в графе обязательно должна присутствовать фабрика, предоставляющая ObjectMapper. Без нее граф не соберется.
Зависимость build.gradle:
annotationProcessor "ru.tinkoff.kora:json-annotation-processor"
implementation "ru.tinkoff.kora:jackson-module"
Модуль и фабрика ObjectMapper:
@KoraApp
public interface Application extends JacksonModule {
default ObjectMapper objectMapper() { //(1)!
return new ObjectMapper();
}
}
- Требуется всем преобразователям
JacksonModule; настройте его по необходимости (модули, возможности и так далее).
Зависимость build.gradle.kts:
Модуль и фабрика ObjectMapper:
@KoraApp
interface Application : JacksonModule {
fun objectMapper(): ObjectMapper = ObjectMapper() //(1)!
}
- Требуется всем преобразователям
JacksonModule; настройте его по необходимости (модули, возможности и так далее).
Показанный выше json-annotation-processor позволяет @Json, @JsonReader и @JsonWriter по-прежнему генерировать кодеки, так что сгенерированная и Jackson-сериализация могут сосуществовать (например, Jackson для HTTP и сгенерированные кодеки для Kafka).
Сами HTTP-преобразователи JacksonModule зависят только от ObjectMapper.