У меня есть код, который отлично работал с 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}
@Matheus добавил рассматриваемый json
Это не похоже на действительную строку JSON: для начала ключи и значения должны быть заключены в кавычки, а ключи должны быть отделены от значений знаком :, а не =.




Попробуйте добавить следующую аннотацию к свойствам типа 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 использует другую конфигурацию для объектного преобразователя.
Итак, после многочасовой отладки решение было найдено. 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);
}
В настоящее время я проверяю, создаст ли этот объектный преобразователь какие-либо проблемы в старых случаях. Опубликую в случае каких-либо ошибок.
как выглядит твой json?