Search code examples
spring-bootjacksonspring-kafkaspring-cloud-stream

How to override Jackson ObjectMapper configuration for Spring Cloud Stream?


In a Spring Boot/Spring Cloud Stream application, we're using the Kafka binder. I've noticed that when deserializing Kafka messages that include a ZonedDateTime property, Jackson is automatically changing the time zone (to UTC) even when the serialized form includes time zone (eg, 20210101084239+0200). After some research, I discovered this is a default deserialization feature that can be disabled, so I added the following in one of my @Configuration classes:

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer objectMapperCustomizer() {
        return builder ->  {
            builder.featuresToDisable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
            //... other custom config...
        };

But that doesn't work, presumably because the ObjectMapper that Spring Cloud Stream (or the Kafka binder) uses is not constructed from the Jackson2ObjectMapperBuilder that's configured in the application context :-(.

After some more research, I found someone claiming that it was necessary to override Spring Messaging's MappingJackson2MessageConverter since it creates its own ObjectMapper. So I added this:

    @Bean
    public MappingJackson2MessageConverter springMessagingJacksonConverter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.getObjectMapper().configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);

        return converter;
    }

Unfortunately, that still didn't work. I still am getting ZonedDateTime objects deserialized ignoring the time zone info in the JSON values.

So what bean creates the ObjectMapper used by Spring Cloud Stream Kafka binder, and how can I customize its configuration?


Solution

  • As documented in discussion comments below and here, there is a bug in Spring Cloud Stream that it's not using the ObjectMapper from the Spring application context in places that it should.

    Having said that, a workaround is to define your own Serde<> @Bean which calls the JsonSerde<> constructor that takes an ObjectMapper, which is injected from the app context. For example:

    @Configuration
    public class MyConfig {
        @Bean
        public Serde<SomeClass> someClassSerde(ObjectMapper jsonMapper) {
            return new JsonSerde<SomeClass>(SomeClass.class, jsonMapper);
        }
    }
    

    The downside to this workaround is that you have to do it for every target type that you want to serialize/deserialize in messages.