To have different deserialization configurations you must have different ObjectMapper
instances but out of the box Spring uses MappingJackson2HttpMessageConverter
which is designed to use only one instance.
I see at least two options here:
Move away from MessageConverter to an ArgumentResolver
Create a @CustomRequestBody
annotation, and an argument resolver:
public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
private final ObjectMapperResolver objectMapperResolver;
public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
this.objectMapperResolver = objectMapperResolver;
}
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null;
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
ObjectMapper objectMapper = objectMapperResolver.getObjectMapper();
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType());
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}
@CustomRequestBody
annotation:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRequestBody {
boolean required() default true;
}
ObjectMapperResolver
is an interface we will be using to resolve actual ObjectMapper
instance to use, I will discuss it below. Of course if you have only one use case where you need custom mapping you can simply initialize your mapper here.
You can add custom argument resolver with this configuration:
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Bean
public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
return new CustomRequestBodyArgumentResolver(objectMapperResolver)
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver()));
}
}
Note: Do not combine @CustomRequestBody
with @RequestBody
, it will be ignored.
Wrap ObjectMapper
in a proxy that hides multiple instances
MappingJackson2HttpMessageConverter
is designed to work with only one instance of ObjectMapper
. We can make that instance a proxy delegate. This will make working with multiple mappers transparent.
First of all we need an interceptor that will translate all method invocations to an underlying object.
public abstract class ObjectMapperInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments());
}
protected abstract ObjectMapper getObject();
}
Now our ObjectMapper
proxy bean will look like this:
@Bean
public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) {
ProxyFactory factory = new ProxyFactory();
factory.setTargetClass(ObjectMapper.class);
factory.addAdvice(new ObjectMapperInterceptor() {
@Override
protected ObjectMapper getObject() {
return objectMapperResolver.getObjectMapper();
}
});
return (ObjectMapper) factory.getProxy();
}
Note: I had class loading issues with this proxy on Wildfly, due to its modular class loading, so I had to extend ObjectMapper
(without changing anything) just so I can use class from my module.
It all tied up together using this configuration:
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver()));
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2HttpMessageConverter());
}
}
ObjectMapperResolver
implementations
Final piece is the logic that determines which mapper should be used, it will be contained in ObjectMapperResolver
interface. It contains only one look up method:
public interface ObjectMapperResolver {
ObjectMapper getObjectMapper();
}
If you do not have a lot of use cases with custom mappers you can simply make a map of preconfigured instances with ReqeustMatcher
s as keys. Something like this:
public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver {
private final ObjectMapper defaultMapper;
private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>();
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) {
this.defaultMapper = defaultMapper;
this.mapping.putAll(mapping);
}
public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) {
this.defaultMapper = defaultMapper;
}
@Override
public ObjectMapper getObjectMapper() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sra.getRequest();
for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return defaultMapper;
}
}
You can also use a request scoped ObjectMapper
and then configure it on a per-request basis. Use this configuration:
@Bean
public ObjectMapperResolver objectMapperResolver() {
return new ObjectMapperResolver() {
@Override
public ObjectMapper getObjectMapper() {
return requestScopedObjectMapper();
}
};
}
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public ObjectMapper requestScopedObjectMapper() {
return new ObjectMapper();
}
This is best suited for custom response serialization, since you can configure it right in the controller method. For custom deserialization you must also use Filter
/HandlerInterceptor
/ControllerAdvice
to configure active mapper for current request before the controller method is triggered.
You can create interface, similar to ObjectMapperResolver
:
public interface ObjectMapperConfigurer {
void configureObjectMapper(ObjectMapper objectMapper);
}
Then make a map of this instances with RequstMatcher
s as keys and put it in a Filter
/HandlerInterceptor
/ControllerAdvice
similar to RequestMatcherObjectMapperResolver
.
P.S. If you want to explore dynamic ObjectMapper
configuration a bit further I can suggest my old answer here. It describes how you can make dynamic @JsonFilter
s at run time. It also contains my older approach with extended MappingJackson2HttpMessageConverter
that I suggested in comments.