Search code examples
javaspringspring-bootspring-mvcjackson

Spring Web: disable Jackson converter for specific classes


I'm working on a project that heavily relies on a custom implementation of a Json serializer, deserializer and representation of a json object as a class.
Let's call it MyJsonClass as an example.

The problem is, I really need to use a custom HttpMessageConverter to convert to/from this Type, because Jackson can't handle it properly.
Adding my own converter before or after Jackson's own ones in the list of converters doesn't seem to prevent the Jackson ones from being tried first. Which will log this error:

Failed to evaluate Jackson deserialization for type [... MyJsonClass ...]
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot find a deserializer for non-concrete Map type [...]

(MyJsonClass extends Map)

Being able to actually prevent Jackson from being tried to be used for application/json http calls would be great. We don't want to have these useless errors in the logs if things ultimately work (because our HttpMessageConverter is used, in the end). We do need Jackson to handle anything that isn't a Map, on the other hand.

By what I've seen about custom Jackson deserializers/serializers, they don't leave you full control on the full raw input/output (like HttpMessageConverters do), which is what we need in order to use our Json class.

This is how I register our custom converter in the configuration class:

@SpringBootApplication
public class MyApplication implements WebMvcConfigurer {

    ...

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    
        // changing the index doesn't matter
        converters.add(new MyJsonClassHttpMessageConverter());
    
        WebMvcConfigurer.super.extendMessageConverters(converters);
    }
    
    @Bean
    RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) {
        List<HttpMessageConverter<?>> converters = 
                new LinkedList<>(new RestTemplate().getMessageConverters());
    
        // changing the index doesn't matter
        converters.add(new MyJsonClassHttpMessageConverter());
    
        return restTemplateBuilderConfigurer.configure(new RestTemplateBuilder())
                .messageConverters(converters);
    }

    ...

}

Solution

  • I found the solution.

    I made a class that overrides MappingJackson2HttpMessageConverter, added it to the list of converters and removed Jackson's own converters.
    In the canRead and canWrite methods of this class I check if the Type is MyJsonClass and if so, I return false. This skips using Jackson to try the conversion so my own HttpMessageConverter is used instead.

    public class MyJsonClassIgnoringMappingJackson2HttpMessageConverter 
            extends MappingJackson2HttpMessageConverter {
    
        public MyJsonClassIgnoringMappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
            super(objectMapper);
        }
    
        @Override
        public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
            if (contextClass != null && MyJsonClass.class.isAssignableFrom(contextClass)
                    || type == MyJsonClass.class)
                return false;
            return super.canRead(type, contextClass, mediaType);
        }
    
        @Override
        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
            if (MyJsonClass.class.isAssignableFrom(clazz))
                return false;
            return super.canWrite(clazz, mediaType);
        }
    }
    

    (I'm sure the above checks could be improved, but they work like this, for me)

    In the configuration class:

    @SpringBootApplication
    public class MyApplication implements WebMvcConfigurer {
    
        @Autowired
        private ObjectMapper objectMapper; // let's use Jackson's properly-configured one
    
        ...
    
        @Override
        public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        
            replaceJacksonConverters(converters);
        
            WebMvcConfigurer.super.extendMessageConverters(converters);
        }
        
        @Bean
        RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) {
            List<HttpMessageConverter<?>> converters = 
                    new LinkedList<>(new RestTemplate().getMessageConverters());
        
            replaceJacksonConverters(converters);
        
            return restTemplateBuilderConfigurer.configure(new RestTemplateBuilder())
                    .messageConverters(converters);
        }
    
        ...
    
        private void replaceJacksonConverters(List<HttpMessageConverter<?>> converters) {
    
            for (int i = converters.size() - 1; i >= 0; --i) {
                if (converters.get(i) instanceof AbstractJackson2HttpMessageConverter)
                    converters.remove(i);
            }
            // the custom HttpMessageConverter that can properly serialize and deserialize MyJsonClass
            converters.add(new MyJsonClassHttpMessageConverter());
            // the custom MappingJackson2HttpMessageConverter defined above
            converters.add(new MyJsonClassIgnoringMappingJackson2HttpMessageConverter(objectMapper));
        }
    
    }
    

    The reason why I didn't define the MyJsonClassIgnoringMappingJackson2HttpMessageConverter as a @Bean is that not all Spring classes that add the original one to the list do it by taking it as a bean. Most of them just do "new MappingJackson2HttpMessageConverter()"... so since I'd need to replace it anyway like I did above with that replaceJacksonConverters method, I figured I didn't really need it as a bean.