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);
}
...
}
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.