Spring 3 использует сопоставитель объектов для преобразования значения (десериализации) из List<Map<String,Object> в некоторый DTO выдает ошибку для поля Instant

У меня есть код, который отлично работал с Spring boot 2, но когда я перенес его на Spring 3, я получаю сообщение об ошибке:

java.lang.IllegalArgumentException: Cannot deserialize value of type `java.time.Instant` from Object value (token `JsonToken.START_OBJECT`) at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: java.util.ArrayList[0]->com.oxane.argon.kpi.KpiFieldDTO["lastModifiedDate"])

У меня есть довольно сложный запрос, в котором используется множество объединений и CTE, после чего я получаю ограниченные данные из таблицы POJO. Поэтому я использую NamedParameterJdbcTemplate для получения списка результатов, после чего я конвертирую List<Map<String,Object>> в свой DTO с помощью сопоставителя объектов, который работал нормально до весны 2, теперь я получаю вышеуказанную ошибку. Данные для поля Instant хранятся как Datetimeoffset в БД, поскольку я не добавил ни одной строки для обратной совместимости.

Мой DTO выглядит так

public class DTO {

private String loanId;

private String obligor;

private LocalDate fsDateLatest;

private LocalDate lastUpdate;

private Instant createdDate;

private Instant lastModifiedDate;

private Integer investmentType;

private LocalDate maturityDate;

private String reportingCurrency;

private BigDecimal debtWeightedSpread1L;

private Integer lastModifiedBy;

private String lastModifiedByEmail;
//getters and setters and constructors
}

Код для преобразования данных

List<Map<String, Object>> kpiByObligorAndFs;
final List<DTO> findAllByObligorAndFsDateLessThanEqual = objectMapper.convertValue(kpiByObligorAndFs, new TypeReference<List<DTO>>() {
    });

Одна из карт из списка

{loanId=ID000470, obligor=XYZ, fsDateLatest=2023-12-31, lastUpdate=2024-03-10, lastModifiedDate=2024-05-09 15:46:30.199154 +00:00, createdDate=2024-03-27 04:43:23.046667 +00:00, investmentType=160, maturityDate=2028-05-26, reportingCurrency=USD, debtWeightedSpread1L=0.0598180100, lastModifiedBy=1303}

как выглядит твой json?

Matheus 18.06.2024 12:30

@Matheus добавил рассматриваемый json

aakash singh 18.06.2024 12:57

Это не похоже на действительную строку JSON: для начала ключи и значения должны быть заключены в кавычки, а ключи должны быть отделены от значений знаком :, а не =.

k314159 18.06.2024 13:18
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
0
3
57
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Попробуйте добавить следующую аннотацию к свойствам типа Instant (я бы предложил сделать то же самое и для типа LocalDate):

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")

Убедитесь, что шаблон подходит для вашего случая. Подробнее смотрите ответ на этот вопрос: Spring Data JPA — формат ZonedDateTime для сериализации json.

Кроме того, вполне нормально использовать ObjectMapper для десериализации JSON. Но если вам интересно, я могу предложить использовать JsonUtils класс, который может делать то же самое без создания экземпляра и настройки ObjectMapper. Если вы используете JsonUtils, ваш код может выглядеть так:

DTO dto = JsonUtils.readObjectFromJsonString("<Json String>", DTO.class);

Обратите внимание, что метод readObjectFromJsonString выбрасывает java.io.IOException. Класс JsonUtils входит в состав библиотеки Java с открытым исходным кодом MgntUtils, написанной и поддерживаемой мной. Вот Javadoc для JsonUtils . Библиотеку MgntUtils можно получить в виде артефакта Maven из Maven Central или в виде файлов Jar из Github (включая исходный код и Javadoc).

Не работает, странно то, что если я пишу тот же запрос внутри аннотации @query, он работает безупречно. Возможно, jpa использует другую конфигурацию для объектного преобразователя.

aakash singh 18.06.2024 17:12
Ответ принят как подходящий

Итак, после многочасовой отладки решение было найдено. ObjectMapper не может десериализовать значение datetimeoffset в Instant в Spring Boot 3, вероятно, это связано с тем, что формат строки datetimeoffset не напрямую совместим с десериализацией Джексона по умолчанию для Instant.

MS SQL использует свой собственный класс microsoft.sql.DateTimeOffset для Instant без надлежащей десериализации из DateTimeOffset, по крайней мере, при использовании специального преобразователя объектов. JPA сам по себе делает это прекрасно, но не тогда, когда вы используете средство отображения объектов.

Итак, когда сопоставитель объектов пытается преобразовать DateTimeOffset в Instant, он сначала вызывает Serailizer DateTimeOffset. Что по умолчанию дает сериализацию для этого строкового значения

{"minutesOffset":0,"timestamp":"2020-03-12T14:16:55.300+00:00","offsetDateTime":"2020-03-12T14:16:55.3Z"}

следовательно, вы получаете ошибку начального объекта. Если вы выполните jsonParser.getTextValue(), вы получите «{», а если вы используете jsonParser.readValueAsTree(), вы получите значение выше Json.

Итак, поскольку он сначала вызывает сериализатор DateTimeOffset, мне нужно его переопределить.

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import microsoft.sql.DateTimeOffset;

public class DateTimeOffsetSerializer extends JsonSerializer<DateTimeOffset> {

   @Override
   public void serialize(DateTimeOffset value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
       gen.writeString(value.getTimestamp().toInstant().toString());
   }
}

Но теперь проблема в том, что в вашем pojo есть поле Instant, поэтому вы не можете использовать аннотацию @JsonSerialize. Вам необходимо зарегистрировать сопоставитель объектов и добавить этот сериализатор по умолчанию для всего приложения.

Код для компонента ObjectMapper...

@Bean(name = "objectMapper")
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    final JavaTimeModule javaTimeModule = new JavaTimeModule();
    SimpleModule module = new SimpleModule();
    module.addSerializer(DateTimeOffset.class, new DateTimeOffsetSerializer());
    return builder.createXmlMapper(false).build().setSerializationInclusion(Include.NON_NULL)
            .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false).registerModule(javaTimeModule)
            .registerModule(module);
}

В настоящее время я проверяю, создаст ли этот объектный преобразователь какие-либо проблемы в старых случаях. Опубликую в случае каких-либо ошибок.

Другие вопросы по теме