Json
Модуль JSON создает эффективные реализации JsonReader и JsonWriter для классов приложения во время компиляции и без использования Reflection во время выполнения.
Генерация управляется аннотациями @Json, @JsonReader, @JsonWriter и связанными аннотациями уровня поля.
Сгенерированные кодеки — это обычные компоненты графа приложения, поэтому остальные модули Kora используют их напрямую:
преобразователи тел запросов и ответов HTTP-сервера и HTTP-клиента, преобразователи строковых параметров, а также сериализаторы и десериализаторы Kafka принимают JsonReader<T> или JsonWriter<T> и зарегистрированы под тегом @Json.
Благодаря этому один и тот же сгенерированный кодек переиспользуется во всем приложении.
Пошаговый разбор перед справочным описанием смотрите в разделе JSON.
Подключение¶
Зависимость build.gradle:
annotationProcessor "io.koraframework:annotation-processors" //(1)!
implementation "io.koraframework:json-common"
- Процессор аннотаций создает
JsonReaderиJsonWriterво время компиляции. Без него кодеки не создаются, и сборка графа падает с ошибкой об отсутствующей зависимостиJsonReader/JsonWriter.
Модуль:
Зависимость build.gradle.kts:
ksp("io.koraframework:symbol-processors:2.0.0.RC1") //(1)!
implementation("io.koraframework:json-common")
- Процессор
KSPсоздаетJsonReaderиJsonWriterво время компиляции. Без него кодеки не создаются, и сборка графа падает с ошибкой об отсутствующей зависимостиJsonReader/JsonWriter.
Модуль:
json-common подтягивает потоковое ядро Jackson, поэтому написанные вручную кодеки работают с tools.jackson.core.JsonParser и tools.jackson.core.JsonGenerator.
Сгенерированные кодеки не используют ни Jackson databind, ни Reflection.
Запись¶
Используйте @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 @Nullable Dto read(String json) { //(1)!
return this.reader.read(json);
}
public byte[] write(Dto dto) { //(2)!
return this.writer.toByteArray(dto);
}
}
JsonReaderобъявлен возвращающим значение, допускающееnull, потому что документ верхнего уровняnullчитается какnull.- Ни
throws, ниtry/catchне нужны: ошибкиJSONне являются проверяемыми исключениями.
@Component
class MyService(
private val reader: JsonReader<Dto>,
private val writer: JsonWriter<Dto>
) {
fun read(json: String): Dto = requireNotNull(reader.read(json)) //(1)!
fun write(dto: Dto): ByteArray = writer.toByteArray(dto) //(2)!
}
JsonReaderобъявлен возвращающим значение, допускающееnull, потому что документ верхнего уровняnullчитается какnull. Поэтому для non-null результата нужна явная проверка.try/catchне нужен: ошибкиJSONне являются проверяемыми исключениями.
JsonReader<T> читает значение из JsonParser, byte[] (при необходимости со смещением и длиной), String или InputStream.
Все эти методы объявлены возвращающими значение, допускающее null.
JsonWriter<T> записывает значение через JsonGenerator, а также может сформировать документ целиком:
toByteArray(value)— байты вUTF-8.toString(value)— компактная строка.toPrettyString(value)— форматированная строка.
Особенности поведения во время выполнения при прямом вызове кодеков:
read(...)возвращаетnull, когда парсер находится на токенеJSONnull, поэтому документ верхнего уровняnullдесериализуется вnull.- Все ошибки являются наследниками
tools.jackson.core.JacksonException, а этот класс наследуется отRuntimeException, поэтому ни один метод не объявляет проверяемых исключений. Смотрите раздел Ошибки. - Сгенерированный класс кодека находится в пакете аннотированного типа и называется
$Dto_JsonReader/$Dto_JsonWriter, а для вложенных типов имена внешних классов попадают в префикс ($Outer_Inner_JsonReader). Внедряйте интерфейсJsonReader<Dto>, а не этот класс.
Обязательные поля¶
По умолчанию все поля, объявленные в объекте, считаются обязательными (NotNull).
По умолчанию все поля, объявленные в объекте без синтаксиса Kotlin Nullability, считаются обязательными (NotNull).
Отсутствие обязательного поля в документе и явный null в обязательном поле — это две разные ошибки, и обе сообщаются с именем поля и путем в JSON.
Необязательные поля¶
Если поле JSON необязательное и может отсутствовать, используйте аннотацию @Nullable:
- Kora использует JSpecify
org.jspecify.annotations.Nullable. Любая аннотация с простым именемNullableтакже распознается.
@Nullable из JSpecify — аннотация уровня типа, поэтому для вложенных типов и массивов важна ее позиция:
@Json
public record Dto(java.util.@Nullable List<String> labels, //(1)!
byte @Nullable [] payload) { } //(2)!
- Аннотация относится к
List, а не кString. - Аннотация относится к массиву, а не к
byte.
Для Kotlin используйте синтаксис Kotlin Nullability и пометьте параметр как nullable:
Kotlin выражает допустимость null в самом типе, поэтому аннотация не нужна:
Именование полей¶
Если поле в JSON имеет имя, отличное от имени поля в классе, используйте @JsonField.
Она задает имя ключа в JSON:
@JsonField только переименовывает ключ.
Чтобы читать или записывать отдельное поле конкретным кодеком, используйте @Mapping — смотрите раздел Пользовательские преобразователи полей.
Стратегия именования¶
Чтобы переименовать сразу все поля класса, пометьте класс аннотацией @NamingStrategy и укажите реализацию NameConverter.
Стратегия применяется и к сгенерированному JsonReader, и к сгенерированному JsonWriter:
Доступные конвертеры в пакете io.koraframework.common.naming:
NoopNameConverter—myFieldNAME→myFieldNAME.CamelCaseNameConverter—myFieldNAME→myFieldName.PascalCaseNameConverter—myFieldNAME→MyFieldName.SnakeCaseNameConverter—myFieldNAME→my_field_name.SnakeCaseUpperNameConverter—myFieldNAME→MY_FIELD_NAME.
Поле, помеченное @JsonField, сохраняет имя из аннотации.
Пустая @JsonField без значения выводит поле из-под стратегии именования и оставляет объявленное имя поля.
Игнорирование поля¶
Если поле DTO не должно попадать в записываемый JSON, используйте @JsonSkip.
Сгенерированный JsonWriter полностью пропускает такое поле.
@JsonSkipвлияет только на запись. СгенерированныйJsonReaderпо-прежнему отображает это поле, поэтому пропущенное поле, которого нет во входных данных, должно быть необязательным.
@JsonSkipвлияет только на запись. СгенерированныйJsonReaderпо-прежнему отображает это поле, поэтому пропущенное поле, которого нет во входных данных, должно быть необязательным.
Тип, который только записывается, можно пометить @JsonWriter вместо @Json — тогда читатель вообще не генерируется и пропущенное поле ни к чему не обязывает.
Уровни сериализации¶
По умолчанию поля со значением null не записываются. (1)
IncludeType.NON_NULL— записывать поле только в том случае, если значение неnull.
Чтобы изменить это поведение, используйте @JsonInclude.
Аннотацию можно разместить не только на поле, но и на классе; в этом случае правило применяется сразу ко всем полям, а аннотация на поле имеет приоритет над аннотацией на классе.
Доступные варианты:
IncludeType.ALWAYS— всегда записывать поле.IncludeType.NON_NULL— записывать поле, если значение неnull.IncludeType.NON_EMPTY— записывать поле, если значение неnullи не является пустой коллекцией или ассоциативным массивом.
IncludeType.NON_EMPTY разрешается во время компиляции, поэтому работает только тогда, когда тип поля статически известен как Collection или Map.
Для параметра типа проверку на пустоту сгенерировать невозможно, и поле ведет себя как при IncludeType.NON_NULL.
Пример:
Конструктор сериализации¶
Если для чтения JSON должен использоваться конкретный конструктор, пометьте его аннотацией @JsonReader.
Можно также использовать @Json, но @JsonReader имеет более высокий приоритет:
JsonReader и JsonWriter могут быть сгенерированы для классов, record, data class, enum и sealed-типов.
Для чтения требуется конкретный, не абстрактный тип и однозначно выбираемый конструктор — правила применяются в таком порядке:
- Единственный публичный конструктор, если он один.
- Единственный публичный конструктор, помеченный
@JsonReader. - Единственный публичный конструктор, помеченный
@Json. - Единственный публичный конструктор с параметрами, если такой ровно один.
Если ни одно из правил не дает единственного кандидата, компиляция завершается ошибкой с указанием типа и причины.
Java Bean и обычные классы¶
@Json, @JsonReader и @JsonWriter не ограничиваются record и data class.
Обычный класс тоже подходит: чтение подчиняется правилам выбора конструктора выше, а для записи используются методы доступа к полям.
Для каждого нестатического поля писателю нужен метод без аргументов с именем field() или getField(), возвращающий тип поля; иначе компиляция падает с подсказкой добавить метод доступа или исключить поле через @JsonSkip.
@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; }
}
Типы-значения¶
Тип-обертка, который должен выглядеть в JSON как обычное скалярное значение, а не как объект, описывается так: @JsonReader ставится на статический фабричный метод, а @JsonWriter — на метод доступа, возвращающий значение внутри обертки.
Kora генерирует кодеки, которые делегируют работу кодекам этого внутреннего типа:
public record UserId(long id) {
@JsonReader //(1)!
public static UserId of(long value) {
return new UserId(value);
}
@JsonWriter //(2)!
public long id() {
return id;
}
}
static-фабрика ровно с одним параметром. Тип параметра определяет, какомуJsonReaderделегируется чтение.- Метод экземпляра без параметров. Точно так же подходит
static-метод, принимающий сам тип (public static long toJson(UserId u)).
class UserId(val id: Long) {
@JsonWriter //(1)!
fun toJson(): Long = id
companion object {
@JsonReader //(2)!
fun of(value: Long): UserId = UserId(value)
}
}
- Функция экземпляра без параметров. Точно так же подходит функция
companion object, принимающая сам тип (fun toJson(u: UserId): Long). - Функция
companion objectровно с одним параметром. Тип параметра определяет, какомуJsonReaderделегируется чтение.
Поле типа UserId тогда записывается как 42, а не как {"id":42}, а null записывается как null.
Это эквивалент @JsonValue и @JsonCreator(mode = DELEGATING) из Jackson.
Ограничения, проверяемые во время компиляции:
- Не более одного фабричного метода
@JsonReaderи не более одного метода@JsonWriterна тип. - Фабричный метод должен быть
public static, принимать ровно один параметр и возвращать сам тип. - У типа не может быть одновременно фабричного метода
@JsonReaderи конструктора с@JsonReader/@Json. - Метод
@JsonWriterдолжен бытьpublicи возвращать значение;static-вариант принимает сам тип единственным параметром, вариант экземпляра — ни одного. - Для внутреннего типа в графе должен быть
JsonReaderилиJsonWriter, что верно для любого поддерживаемого типа.
Обертка JsonNullable¶
Если при чтении JSON необходимо отличать отсутствующее поле от поля со значением null, используйте JsonNullable.
Основные состояния и фабричные методы:
JsonNullable.undefined()— поле отсутствует вJSON.JsonNullable.nullValue()— поле присутствует и содержитnull.JsonNullable.of(value)— поле присутствует и содержит значение; аргумент не должен бытьnull.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() выбрасывает NullPointerException при 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() при обоих уровнях записывается как null.
Единственный уровень, который добавляет здесь дополнительное поведение, — IncludeType.NON_EMPTY, и только если обёрнутый тип статически является Collection или Map: тогда поле nullValue() и поле с пустой коллекцией или картой также опускаются.
Sealed-классы и интерфейсы¶
Если в зависимости от значения конкретного поля нужно читать и записывать разные JSON-объекты, используйте
sealed-класс или интерфейс для представления этих объектов.
Sealed-типы поддерживаются двумя аннотациями:
@JsonDiscriminatorField— задает поле-дискриминатор вDTO, помеченном какsealed-класс или интерфейс.@JsonDiscriminatorValue— задает одно или несколько значений дискриминатора для подкласса.
@Json
@JsonDiscriminatorField("type")
public sealed interface Event {
@Json
@JsonDiscriminatorValue("created")
record Created(String id) implements Event {}
@Json
@JsonDiscriminatorValue({"deleted", "removed"}) //(1)!
record Deleted(String id, boolean permanent) implements Event {}
}
- Одному подклассу можно сопоставить несколько значений дискриминатора. При записи используется первое значение.
@Json
@JsonDiscriminatorField("type")
sealed interface Event {
@Json
@JsonDiscriminatorValue("created")
data class Created(val id: String) : Event
@Json
@JsonDiscriminatorValue("deleted", "removed") //(1)!
data class Deleted(val id: String, val permanent: Boolean) : Event
}
- Одному подклассу можно сопоставить несколько значений дискриминатора. При записи используется первое значение.
Сам sealed-класс или интерфейс получает общий JsonReader и JsonWriter — именно их приложение и внедряет, чтобы прочитать или записать любой подтип.
При этом у каждого конкретного подкласса должен быть собственный кодек, потому что сгенерированный кодек sealed-типа принимает кодеки подклассов как зависимости.
Проще всего это обеспечить, пометив каждый подкласс аннотацией @Json (либо @JsonReader/@JsonWriter).
Sealed-абстрактные классы и вложенные sealed-подынтерфейсы также поддерживаются — иерархия разворачивается до конкретных подклассов.
Правила, относящиеся к самому дискриминатору:
@JsonDiscriminatorValueнеобязательна. Без нее значением дискриминатора подкласса становится его простое имя класса.- Поле-дискриминатор может находиться в любом месте объекта; читатель буферизует токены, которые приходится пропустить.
- При записи дискриминатор добавляется автоматически, если только подкласс уже не объявляет поле с таким именем в
JSON— в этом случае значение берется из самого поля. @JsonDiscriminatorFieldпринимает еще иdefaultValue— дискриминатор, который используется, когда поле отсутствует в документе. Без него отсутствие дискриминатора считается ошибкой.
Приведенный ниже JSON-объект читается в класс Created:
Поддерживаются обобщенные (generic) типы DTO, включая обобщенные sealed-иерархии.
Кодек для каждого конкретного аргумента типа разрешается из графа, как и для любого другого типа поля:
Перечисления¶
Для enum JsonReader и JsonWriter можно сгенерировать теми же аннотациями @Json, @JsonReader и @JsonWriter.
По умолчанию значением enum в JSON является результат toString(), поэтому его можно переопределить:
Если требуется значение, отличное от строки из toString(), пометьте публичный метод без параметров аннотацией @Json.
В этом случае для возвращаемого типа должны быть доступны соответствующие JsonReader и JsonWriter:
Соответствие значений JSON константам строится один раз при создании кодека, поэтому чтение enum — это один поиск по ассоциативному массиву.
Значение JSON, не совпадающее ни с одной константой, отвергается с ошибкой, в которой перечислены допустимые значения.
RawJson¶
RawJson используется, когда в объект нужно включить уже готовый фрагмент JSON, не сериализуя его повторно.
При записи RawJson передается в выходной JSON как есть, поэтому значение должно быть корректным фрагментом JSON.
RawJson — тип только для записи: модуль предоставляет JsonWriter<RawJson>, но не предоставляет читателя, поэтому содержащий его DTO помечается @JsonWriter, а не @Json.
RawJson принимает либо String, либо byte[] и хранит значение как байты в UTF-8.
Так как содержимое уже закодировано, оно записывается без кавычек; значение, которое требует экранирования как строка JSON, нужно передавать обычным полем типа String.
Пользовательские преобразователи полей¶
Чтобы использовать конкретные преобразователи только для одного поля, пометьте поле аннотацией @Mapping.
Аннотацию можно повторить, чтобы указать одновременно JsonReader и JsonWriter:
Здесь HexReader реализует JsonReader<Integer> (JsonReader<Int> в Kotlin), а HexWriter — соответствующий JsonWriter:
public final class HexWriter implements JsonWriter<Integer> { //(1)!
@Override
public void write(JsonGenerator generator, Integer value) {
generator.writeString(Integer.toHexString(value));
}
}
public final class HexReader implements JsonReader<Integer> {
@Override
public Integer read(JsonParser parser) {
if (parser.currentToken() != JsonToken.VALUE_STRING) {
throw new StreamReadException(parser, "Expected hexadecimal string");
}
return Integer.parseInt(parser.getValueAsString(), 16);
}
}
final-преобразователь без зависимостей в конструкторе создается самим сгенерированным кодом и не должен быть@Component. Преобразователь, у которого есть зависимости в конструкторе, обязан быть компонентом графа — смотрите раздел Компоненты.
class HexWriter : JsonWriter<Int> { //(1)!
override fun write(generator: JsonGenerator, value: Int?) {
generator.writeString(value!!.toString(16))
}
}
class HexReader : JsonReader<Int> {
override fun read(parser: JsonParser): Int {
if (parser.currentToken() != JsonToken.VALUE_STRING) {
throw StreamReadException(parser, "Expected hexadecimal string")
}
return parser.valueAsString.toInt(16)
}
}
- Преобразователь без зависимостей в конструкторе (в
Kotlinклассыfinalпо умолчанию) создается самим сгенерированным кодом и не должен быть@Component. Преобразователь, у которого есть зависимости в конструкторе, обязан быть компонентом графа — смотрите раздел Компоненты.
Поддерживаемые типы¶
Модуль предоставляет встроенные типы, которые покрывают большинство распространенных задач.
Для коллекций и ассоциативных массивов 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
Что стоит знать об этом списке:
byte[]записывается и читается как строка вBase64.- У
RawJsonесть только писатель, а уSortedSet<T>— только читатель. Enumтребует аннотации@Jsonна самом типе перечисления, смотрите раздел Перечисления.ObjectчитаетJSON-объект вLinkedHashMap, массив — вArrayList, целое число — вBigInteger, а дробное — вDouble.- Типы даты и времени используют соответствующие форматы
ISO;MonthиDayOfWeekзаписываются по имени, а читаются как по имени, так и по числу. - Ключи
Mapдолжны быть строками.
Пользовательские типы¶
Если необходимо читать или записывать пользовательский тип, зарегистрируйте пользовательскую фабрику для JsonReader или JsonWriter.
Пример регистрации пользовательского JsonWriter:
Пример регистрации пользовательского JsonReader.
Читатель переключается по текущему токену парсера, возвращает null для JSON null, читает ожидаемый токен и выбрасывает StreamReadException для всего остального:
@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 StreamReadException(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 StreamReadException(parser,
"Expecting VALUE_STRING token, got ${parser.currentToken()}")
}
}
}
Пользовательский JsonReader<T> или JsonWriter<T> — это обычный компонент графа.
После регистрации сгенерированные кодеки автоматически подхватывают его везде, где встречается поле типа T, а также его можно закрепить за отдельным полем через @Mapping (см. Пользовательские преобразователи полей).
Ошибки¶
Все, что кодек выбрасывает во время выполнения, — это наследники tools.jackson.core.JacksonException, а этот класс наследуется от RuntimeException.
Ни один метод JsonReader или JsonWriter не объявляет проверяемых исключений, поэтому вызывающему коду не нужны ни throws, ни try/catch, если он не собирается обрабатывать ошибку.
Ошибки чтения сообщаются как tools.jackson.core.exc.StreamReadException.
Сгенерированные читатели формируют сообщения, в которых указаны тип, поле и путь в JSON до проблемного значения:
Failed to read json Dto: missing required field(s): field_1 (at <root>)
Failed to read json Dto.field4: required field must not be null (at <root>)
Failed to read json Dto.field2: expected an integer number, but got a string "abc" (at /field2)
У sealed-иерархий и перечислений есть свои сообщения, и оба перечисляют допустимые значения:
Failed to read json Event: missing required discriminator field "type", expected one of [created, deleted, removed] (at <root>)
Failed to read json Event: unknown discriminator value "updated" for field "type", expected one of [created, deleted, removed] (at <root>)
Failed to read json enum: expected one of [1, 2], but got "3" (at /status)
Когда кодек используется через другой модуль Kora, исключение транслируется на границе: декларативный контроллер HTTP-сервера превращает неразобранное тело или параметр в ответ 400, а продюсер Kafka заворачивает ошибку сериализации в SerializationException.
Проблемы времени компиляции процессор Kora сообщает в той же структуре — тип, проблема, подсказка и способ исправления.
Чаще всего это отсутствие метода доступа для записываемого поля, абстрактный тип или интерфейс, не являющийся поддерживаемой sealed-иерархией, и неоднозначный конструктор для читателя.
Jackson¶
Если для чтения и записи JSON вместо сгенерированных во время компиляции кодеков нужно использовать Jackson databind, применяйте JacksonModule.
Он предоставляет помеченные тегом @Json преобразователи для HTTP-клиента и HTTP-сервера, и поскольку это обычные компоненты, а преобразователи на основе кодеков — компоненты по умолчанию, приоритет получают преобразователи Jackson — смотрите раздел Фабрика по умолчанию.
JacksonModule покрывает ровно такой набор преобразователей:
HttpServerRequestMapper<T>иHttpServerResponseMapper<T>.HttpClientRequestMapper<T>,HttpClientResponseMapper<T>иHttpClientResponseMapper<HttpResponseEntity<T>>.
Все остальное — строковые параметры, Kafka и любое прямое внедрение JsonReader/JsonWriter — продолжает работать на сгенерированных кодеках.
Каждый преобразователь JacksonModule зависит от компонента ObjectMapper, поэтому в графе обязательно должна присутствовать фабрика, предоставляющая ObjectMapper. Без нее граф не соберется.
Зависимость build.gradle:
annotationProcessor "io.koraframework:annotation-processors"
implementation "io.koraframework:jackson-module"
Модуль и фабрика ObjectMapper:
@KoraApp
public interface Application extends JacksonModule {
default ObjectMapper objectMapper() { //(1)!
return new ObjectMapper();
}
}
- Требуется всем преобразователям
JacksonModule; настройте его по необходимости (модули, возможности и так далее).
Зависимость build.gradle.kts:
ksp("io.koraframework:symbol-processors:2.0.0.RC1")
implementation("io.koraframework:jackson-module")
Модуль и фабрика ObjectMapper:
@KoraApp
interface Application : JacksonModule {
fun objectMapper(): ObjectMapper = ObjectMapper() //(1)!
}
- Требуется всем преобразователям
JacksonModule; настройте его по необходимости (модули, возможности и так далее).
ObjectMapper здесь — это tools.jackson.databind.ObjectMapper; jackson-module подтягивает Jackson databind транзитивно.
Показанный выше процессор Kora позволяет @Json, @JsonReader и @JsonWriter по-прежнему генерировать кодеки, так что сгенерированная и Jackson-сериализация могут сосуществовать (например, Jackson для HTTP и сгенерированные кодеки для Kafka).