Migrating to Spring 4 -Jackson Edition

One of the biggest pitfalls of migrating from Springboot version 3 to version 4 is the challengin transition from Jackson 2 to 3 owing to the differences in configuration of the serializers/deserializers and the subsequent behavioural changes.
Lets us go through the building blocks of Jackson 2 and then compare it side by side with its Jackson 3 counterpart.
ObjectMapper — Jackson 2
Jackson 2 uses ObjectMapper to manage the serialization/deserialization of request/responses .
@Bean
public ObjectMapper getCustomObjectMapper(){
ObjectMapper objectMapper= new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
objectMapper.registerModules(new JavaTimeModule(),new Jdk8Module());
return objectMapper;
}
Spring container defines its own ObjectMapper internally with deafult configurations that is used to deserialize any incoming request. However customization of spring’s inbuilt ObjectMapper can be achieved by declaring and configuring its builder, Jackson2ObjectMapperBuilderCustomizer bean.
Jackson2ObjectMapperBuilderCustomizer — Jackson2
@Bean
public Jackson2ObjectMapperBuilderCustomizer getJackson2ObjectMapperBuilderCustomizer(){
return builder -> builder.serializationInclusion(JsonInclude.Include.NON_NULL)
.featuresToEnable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.modules(new JavaTimeModule(),new Jdk8Module());
}
Jackson2ObjectMapperBuilderCustomizer facilitates enabling and disabling specific Jackson serialization/deserialization deafults .
It also allows for explict registration of modules like JDK8Module, JavaTimeModule, JacksonModuleParameterNames, etc. Alternatively it can also picks up any modules added to the classpath eliminating the need for explict registration.
When a Jackson2ObjectMapperBuilderCustomizer bean is defined , spring uses this builder to create both implicitly (created by spring) and explicitly configured ObjectMapper beans. Note that these configurations are added to the ObjectMapper on top of the default spring configurations.
JsonMapper — Jackson 3
Jackson 3 uses an immutable and threadsafe JsonMapper in lieu of mutable Objectmapper. Thus effectively eliminating the risk of accidental reconfiguration unlike objectMapper.
@Bean
public JsonMapper getCustomJsonMapper(){
return JsonMapper.builder()
.enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.defaultDateFormat(DateFormat.getInstance())
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES,true)
.build();
}
JsonMapperBuilderCustomizer — Jackson 3
Similiar to Jackson2ObjectMapperBuilderCustomizer, JsonMapper also has a builder JsonMapperBuilderCustomizer, which when builds JsonMapper instances.
@Bean
public JsonMapperBuilderCustomizer jsonMapperBuilderCustomizer(){
return builder -> builder
.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.configure(SerializationFeature.INDENT_OUTPUT,false)
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES,true)
.changeDefaultPropertyInclusion(inc ->
inc.withValueInclusion(JsonInclude.Include.NON_NULL))
.build();
}
The builders configurations on top of spring auto configurations are cascaded down to the mappers beans.
It is also possible to attain complete control of the creation and configuration of ObjectMapper/JsonMapper instances and disable Spring’s auto configuration by annotating the mapper bean with @Primary.
@Bean
@Primary
public ObjectMapper getCustomObjectMapper(){
ObjectMapper objectMapper= new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
objectMapper.registerModules(new JavaTimeModule(),new Jdk8Module());
return objectMapper;
}
This behaviour exhibited by both JsonMapper and ObjectMapper instructs Spring to use only your mapper configuration bypassing all spring defaults.
@Bean
@Primary
public JsonMapper getCustomJsonMapper(){
return JsonMapper.builder()
.enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.defaultDateFormat(DateFormat.getInstance())
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES,true)
.build();
}
Alternatively, we can also override the HttpMessageConverters in Spring to configure the serializers/desirializers.
MappingJackson2HttpMessageConverter — Jackson 2
In Jackson 2 declaring a MappingJackson2HttpMessageConverter bean overrides the deafult HttpMessageConverters.
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(new Jackson2ObjectMapperBuilder()
.serializers(new CustomDateSerializer())
.serializationInclusion(JsonInclude.Include.NON_NULL).build());
}
JacksonJsonHttpMessageConverter — Jackson 3
The same can be done in Jackson 3 by defining a bean of type JacksonJsonHttpMessageConverter.
@Bean
public JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter() {
return new JacksonJsonHttpMessageConverter(JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.defaultDateFormat(DateFormat.getInstance())
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
.build());
}
However this configuration only dictates the behavior of internal spring mapper instances ( the one’s used to serialize/deserialize incoming/outgoing requests/responses) and will not cascade down to any explicitily configured objectMapper/jsonMapper instances.
Another point of contention could be when feign comes into picture.
The global jackson configuration via either the builder configuration or mapper or HttpMessageConverter configuration will be used for feign serialization/deserialization.
That being said, it is possible to override the global configuration and define custom feign configuration.
SpringEncoder/SpringDecoder -Jackson 2
We can override Jackson defaults for feign interactions in Jackson 2, by defining SpringEncoder/SpringDecoder beans.
public class CustomJacksonFeignConfig
{
@Bean
public Decoder feignDecoder() {
return new SpringDecoder(() -> new HttpMessageConverters(
new MappingJackson2HttpMessageConverter(new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT))));
}
@Bean
public Encoder feignEncoder() {
return new SpringEncoder(() -> new HttpMessageConverters(
new MappingJackson2HttpMessageConverter(new ObjectMapper()
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.enable(SerializationFeature.INDENT_OUTPUT)))));
}
}
We configure a custom objectmapper with the desired configurations and register it with Spring encoder/decoder. We then register the encoder/decoder configuration with the feign client.
@FeignClient(name = "example", configuration = CustomJAcksonFeignConfig.class)
public interface ExampleClient{
Jackson3Decoder /Jackson3Encoder — Jackson 3
Similarly, Jackson 3 uses Jackson3Decoder /Jackson3Encoder to configure feign sereializers/deserializers.
@Bean
public Decoder feignDecoder() {
return new Jackson3Decoder(JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.defaultDateFormat(DateFormat.getInstance())
.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
.build());
}
@Bean
public Encoder feignEcoder() {
return new Jackson3Encoder(JsonMapper.builder()
.disable(SerializationFeature.CLOSE_CLOSEABLE)
.defaultDateFormat(DateFormat.getInstance())
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, true)
.build());
}
JsonSerializer<T> /JsonDeserializer<T> — Jackson 2
Jackson also allows to define and our own custom serializers/deserialisers by extending and overriding JsonSerializer<T> /JsonDeserializer<T>.
@Component
public class CustomDateSerializer extends JsonSerializer<Date> {
public static final String DateFormat="MMDDYYYY";
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(new SimpleDateFormat(DateFormat).format(date));
}
}
ValueSerializer<T> /ValueDeserializer<T> — Jackson 3
In Jackson 3 JsonSerializer<T> /JsonDeserializer<T> interfaces are replaced by ValueSerializer<T> / ValueDeserializer<T> interfaces
@Component
public class CustomDateSerializer extends ValueSerializer<Date>{
public static final String DateFormat="MMDDYYYY";
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializationContext ctxt) throws JacksonException {
jsonGenerator.writeString(new SimpleDateFormat(DateFormat).format(date));
}
@Component
public class CustomDateDeserializer extends ValueDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws JacksonException {
return LocalDate.parse(jsonParser.getValueAsString()).atStartOfDay();
}
Apart from these there are a few changes in package hierarchy from com.fasterxml.jackson.* to tools.jackson.*.
However the annotations are within the same package heirarchy i.e com.fasterxml.jackson.*.
Once these are taken care of , the migration to Spring 4 within the context of jackson serialiazation/deserialization should be seamless.
Migrating to Spring 4 -Jackson Edition was originally published in Javarevisited on Medium, where people are continuing the conversation by highlighting and responding to this story.
This post first appeared on Read More

