LocalDateTime not serializing based on given serializer registered with JavaTimeModule - spring-boot

I'm facing an issue where Spring boot (v2.6.13) is not parsing LocalDateTime based on a registered serializer, the response of LocalDateTime of RestController is always an array of integers.
#Bean
public Module javaTimeModule() {
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(new CustomLocalDateTimeSerializer());
return module;
}
class CustomLocalDateTimeSerializer extends StdSerializer<LocalDateTime> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
protected CustomLocalDateTimeSerializer() {
super(LocalDateTime.class);
}
#Override
public void serialize(
LocalDateTime localDateTime,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(localDateTime.format(formatter));
}
}
Notes:
Injecting object mapper and serialize the object returns the correct format.
I've defined an object mapper annotated with #Primary, but still facing the same issue.
I want to configure everything globally - don't wanna use #JsonSerialize on each attribute-
it seems like Spring is using a different object mapper for serializing a method returned object.

I've found the issue and resolved it.
I have a WebMvcConfigurationSupport configuration which was overriding the configured object mapper.
I've solved it using following code
private final ObjectMapper objectMapper;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters)
{
var converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
converters.add(converter);
super.configureMessageConverters(converters);
}

Related

InvalidFormatException for Date - fixing without using JsonFormat or modifying original class

Introduction
We are using a custom starter hosted on a nexus repository, that contains spring-cloud-feign clients that make requests to microservices.
One of the microservices returns the dates as "dd-MM-yyyy HH:mm:ssZ" and this works in most of our applications. However, we have one application that is throwing the following error:
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
Current work-around
My Current work-around, as I don't want to pollute the starter, is to extend the class and create a local feign-client and local pojo with the proper JsonFormat:
public class DocumentMetaDataFix extends DocumentMetaData {
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
private Date creationDate;
#JsonFormat(
shape = Shape.STRING,
pattern = "yyyy-MM-dd HH:mm:ss"
)
Failed Fixes
I have tried the following in my configuration class, in order to try affecting the de-serialization from another path. However, the DocumentMetaDataSerializer is never called. The ObjectMapper bean IS called.
#Configuration
#EnableSpringDataWebSupport
#RequiredArgsConstructor
public class MyConfig extends WebMvcConfigurerAdapter {
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
Full Stacktrace
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-10-16 14:23:17": not a valid representation (error: Failed to parse Date value '2019-10-16 14:23:17': Unparseable date: "2019-10-16 14:23:1
7")
at [Source: (ByteArrayInputStream); line: 1, column: 580] (through reference chain: eu.europa.ec.nova.documentstore.DocumentMetaData["creationDate"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1548)
at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:910)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:524)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:467)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:195)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:285)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:268)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 70 common frames omitted
So, any ideas?
I have searched through the project for references to Jackson in case there is anything else in my project causing this.
I am will try to go inside the ObjectMapper and try to debug the current parameters/fields of the configuration at ObjectMapper.java:3084 from the stacktace:
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
... 67 common frames omitted
Update
I added a breakpoint in the objectmapper constructor, and am seeing that it is being initialized from more than one location. This led me to suspect that spring-boot is not using my ObjectMapper. Instead it is using an internal spring one that is called from MappingJackson2HttpMessageConverter .
<init>:480, ObjectMapper
build:606, Jackson2ObjectMapperBuilder
<init>:59, MappingJackson2HttpMessageConverter
<init>:74, AllEncompassingFormHttpMessageConverter
I will therefore try to over-ride this internal spring one, based on results I found from: How to customise the Jackson JSON mapper implicitly used by Spring Boot?
However this also failed.
References
Is it possible to configure Jackson custom deserializers at class level for different data types?
https://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper
https://www.baeldung.com/jackson-deserialization
very useful: https://mostafa-asg.github.io/post/customize-json-xml-spring-mvc-output/
How to customise Jackson in Spring Boot 1.4
Update - final list of tries
It still fails with an error.
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
builder.serializationInclusion(Include.NON_EMPTY);
builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
//converters.add(cmsaMessageConverter());
converters.add(new StringHttpMessageConverter());
converters.add(new FormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomBigDecimalDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.deserializerByType(DocumentMetaData.class, new DocumentMetaDataDeserializer());
}
};
}
#Primary
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.setDateFormat(new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"));
//mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, true);
return mapper;
}
#Bean
public Module dynamoDemoEntityDeserializer() {
SimpleModule module = new SimpleModule();
module.addDeserializer(DocumentMetaData.class, new DocumentMetaDataDeserializer());
return module;
}
public static class DocumentMetaDataDeserializer extends JsonDeserializer<DocumentMetaData> {
#Override
public DocumentMetaData deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// return DynamoDemoEntity instance;
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
public DocumentMetaData deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer t) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return null;
}
}
It still fails with an error.
Try using LocalDateTime,
this is what I'm doing and working for me
#JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime date;

Custom object mapper not taking effect in springboot

I've a springboot application. I'm trying to replace null values with a custom string ("NA"). So, I've configured an object mapper with a custom serializer provider that in turn has a custom null value serializer. You can check the code below.
#Configuration
public class JacksonConfig {
#Bean
public ObjectMapper jacksonObjectMapper() {
return new CustomObjectMapper();
}
#Bean
public SerializationConfig serializationConfig() {
return jacksonObjectMapper().getSerializationConfig();
}
}
class CustomObjectMapper extends ObjectMapper {
CustomObjectMapper() {
super();
DefaultSerializerProvider.Impl serializerProvider = new DefaultSerializerProvider.Impl();
serializerProvider.setNullValueSerializer(new JsonSerializer<Object>() {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString("NA");
}
});
this.setSerializerProvider(serializerProvider);
}
}
Now the problem is, even these beans are getting created, the custom null value serializer is still not taking effect. In the response, I'm still seeing lot of null values. Is there anything else I'm missing here?

json serializer with spring boot

I have BigDecimalSerializer
public class BigDecimalSerializer extends JsonSerializer<BigDecimal> {
#Override
public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(value.setScale(6, BigDecimal.ROUND_HALF_UP).toString());
}
}
and then
#JsonSerialize(using = BigDecimalSerializer.class)
private BigDecimal foo;
is there any way that instead of doing annotate in each member variable, I tell the spring boot at once that apply to all project ?
Try configuring the ObjectMapper by adding a custom module. In case you're using spring-data-rest this can look like this:
#Configuration
public static class ObjectMapperConfigurer extends RepositoryRestConfigurerAdapter {
#Override
public void configureJacksonObjectMapper(final ObjectMapper objectMapper) {
SimpleModule myModule = new SimpleModule();
myModule.addSerializer(BigDecimal.class, BigDecimalSerializer.class);
objectMapper.registerModule(myModule));
}
}
Otherwise simply provide your own ObjectMapper bean and configure it on creation.

What is the suggested way to modify / override / extend built-in Jackson 2 ObjectMapper in Spring Webflux?

Currently I have a minimalistic Spring / Netty, Reactor / Web Flux project with Jackson libraries
#Configuration
public class EmbeddedSpringServer extends DelegatingWebFluxConfiguration {
#Bean
MyController controller() {
return new AdminController();
}
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmbeddedSpringServer.class);
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create("0.0.0.0", 8082).newHandler(adapter).subscribe();
applicationContext.registerShutdownHook();
}
}
build.gradle:
compile 'org.springframework:spring-context:5.0.2.RELEASE'
compile 'org.springframework:spring-web:5.0.2.RELEASE'
compile 'org.springframework:spring-webflux:5.0.2.RELEASE'
compile 'io.projectreactor.ipc:reactor-netty:0.7.2.RELEASE'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.3'
Controller class works fine (it returns a Mono<> with a DTO type).
Because Jackson is present in classpath Web Flux automatically creates an Object Mapper instance via DefaultServerCodecConfigurer however it's not clear how to override object mapper instance, because most Web Flux configuration classes are package private.
What I'd like to achieve is to create my own object mapper to add custom LocalDateTime serialization implemented in jackson-modules-java8
ObjectMapper mapper = new ObjectMapper()
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule())
;
The problem is that's it's not clear how to modify Jackson2JsonEncoder created in package private org.springframework.http.codec.support.AbstractCodecConfigurer.AbstractDefaultCodecs.
You can disable that Jackson feature right from your application.properties file with:
spring.jackson.serialization.write-dates-as-timestamps=false
It turned out simpler than I thought initially as DelegatingWebFluxConfiguration already has a configureHttpMessageCodecs method overriding which is enough
#Configuration
public class EmbeddedSpringServer extends DelegatingWebFluxConfiguration {
#Bean
MyController controller() {
return new MyController();
}
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(EmbeddedSpringServer.class);
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create("0.0.0.0", 8082).newHandler(adapter).subscribe();
applicationContext.registerShutdownHook();
}
#Bean
ObjectMapper objectMapper(){
return new ObjectMapper()
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
.registerModule(new JavaTimeModule());
}
#Override
protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper()));
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper()));
}
}
as for the Spring Boot I think it can be also achieved by returning a webFluxConfigurer bean
#Bean
WebFluxConfigurer webFluxConfigurer(ObjectMapper objectMapper) {
return new WebFluxConfigurer() {
#Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper());
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper());
}
};
}
as they are picked by DelegatingWebFluxConfiguration created by #EnableWebFlux automatically.
N.B. default implementation of Jackson2ObjectMapperBuilder already registers these date modules automatically the problem with dates was not related, I ended with
#Override
protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(Jackson2ObjectMapperBuilder
.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build()));
}
to achieve simple date serialization.

Using Jackson in Jersey with multiple configured ObjectMappers

Is it possible to setup Jersey using Jackson for serialization/deserialization using multiple configured ObjectMappers?
What I would like to be able to do is register a "default" Jackson ObjectMapper and then have the ability to register another feature which provides an ObjectMapper with some specialized configuration which under certain circumstance will "override" the "default" ObjectMapper.
For example, this ContextResolver would be for the "default" mapper:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper mObjectMapper;
public JacksonMapperProvider() {
mObjectMapper = createMapper();
}
protected abstract ObjectMapper createMapper() {
ObjectMapper mapper = createMapper();
return mapper
.setSerializationInclusion(Include.ALWAYS)
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mObjectMapper;
}
}
And this ContextResolver would be to override the "default" mapper:
#Provider
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public class SpecializedMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper mObjectMapper;
public SpecializedMapperProvider() {
mObjectMapper = createMapper();
}
protected abstract ObjectMapper createMapper() {
ObjectMapper mapper = createMapper();
return mapper
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
.registerModule(new SpecializedModule1())
.registerModule(new SpecializedModule2());
}
#Override
public ObjectMapper getContext(Class<?> type) {
if(SomeType.isAssignableFrom(type)) {
return mObjectMapper;
}
return null;
}
}
I see in the JacksonJsonProvider code that Jackson supports ObjectMapper provider injection/resolution. However, in practice, what I am seeing is that the "order" of the providers seems random (I'm guessing it's not, but I can't sort out how to control the order). Sometimes the "override" comes before the "default" and everything works, but on the next server startup the order changes.
I have attempted to get this to work in a number of ways including:
Registering the ContextResolver<ObjectMapper> implementations manually (in differing orders)
Registering the ContextResolver<ObjectMapper> implementations via #Provider annotations
Specifying a priority when registering
I am using the following:
Jersey 2.8
Jackson 2.3.3
Perhaps I am taking a completely incorrect approach?
Is there a better way to achieve what I am trying to do?
Maybe I should just define two separate JAX-RS applications and have a single ObjectMapper configuration for each?
You can configure the order of providers, but it would actually be best to use one provider in this situation:
#Provider
public class JacksonMapperProvider implements ContextResolver<ObjectMapper> {
private final ObjectMapper defaultMapper;
private final ObjectMapper specializedMapper;
public JacksonMapperProvider() {
defaultMapper = createDefaultMapper();
specializedMapper = createSpecializedMapper();
}
private static ObjectMapper createDefaultMapper() {
return new ObjectMapper()
.setSerializationInclusion(Include.ALWAYS)
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
}
private static ObjectMapper createSpecializedMapper() {
return new ObjectMapper()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"))
.registerModule(new SpecializedModule1())
.registerModule(new SpecializedModule2());
}
#Override
public ObjectMapper getContext(Class<?> type) {
if (SomeType.isAssignableFrom(type)) {
return specializedMapper;
}
else {
return defaultMapper;
}
}
}
The newest way is
new ObjectMapper().configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
to get ALLOW_UNQUOTED_FIELD_NAMES in recent versions of jackson.

Resources