41

I need to add a custom Jackson deserializer for java.lang.String to my Spring 4.1.x MVC application. However all answers (such as this) refer to configuring the ObjectMapper for the complete web application and the changes will apply to all Strings across all @RequestBody in all controllers.

I only want to apply the custom deserialization to @RequestBody arguments used within particular controllers. Note that I don't have the option of using @JsonDeserialize annotations for the specific String fields.

Can you configure custom deserialization for specific controllers only?

Community
  • 1
  • 1
Mark
  • 28,783
  • 8
  • 63
  • 92
  • What about writing an object mapper ? I think you can add the deserialization logic you need inside it. – reos Jun 15 '16 at 20:21
  • The issue isn't creating an object mapper. My question is how can I configure an object mapper on a per controller basis instead of globally within the web application. – Mark Jun 15 '16 at 21:43
  • I understand your question, I suggest to write an object mapper that would be used in all controllers but it can i can deserialize the object depending on the request it receives. – reos Jun 15 '16 at 21:45
  • OK. So maybe to make things clearer it is a custom deserializer for java.lang.String. In my use case it probably doesn't matter if it is applied to all strings across all controllers but I would prefer to restrict it to particular controllers. – Mark Jun 16 '16 at 09:03
  • 1
    Hi Mark, did you find a way to do this since your question? I'm struggling finding a way to achieve this – djioul Jun 01 '17 at 22:07
  • What spring configuration method are you using? – teppic Sep 12 '17 at 03:40
  • @teppic xml though I can move to Java configuration if required. – Mark Sep 12 '17 at 10:28
  • You can make ObjectMapper request scoped by extending MappingJackson2HttpMessageConverter with trivial modifications. This will allow you to customise mapping on per request (controller method) basis. It may be an overkill for your situation though. – chimmi Sep 12 '17 at 11:38
  • You can also make a HandlerMethodArgumentResolver that uses specially configured ObjectMapper, similar to answer below, but without AOP magic. Bottom line is this - you need separate instance of ObjectMapper for this particular case, but Spring only uses one global instance out of the box. So you need to either change that (custom HttpMessageConverter) or move from message converters at all (custom ArgumentResolver/AOP) – chimmi Sep 12 '17 at 12:56
  • @chimmi please add some answers to provide good examples for others. – Mark Sep 12 '17 at 15:20
  • I will post tomorrow night if no one gets ahead of me. Don't have much time now and also need to test ArgumentResolver option. – chimmi Sep 12 '17 at 21:58
  • You can load the controllers that require a customised `ObjectMapper` in their own context. Define a second `DispatcherServlet` in your web.xml and point it at a context that defines the custom mapper and just loads the controllers you need. – teppic Sep 13 '17 at 10:04
  • Given the amount of code to make it actually work, I definitely think it's worth considering to give the controller an `ObjectMapper` instance (albeit through DI or just construct it yourself). Make methods `void` and give them a `HttpServletResponse` argument, set content type to `application/json`, character encoding to `UTF-8` and make the mapper write to `response.getWriter()`. Refactor things a bit to avoid repetitive code. Yes, you're not using the 'magic' of Spring and do things yourself, but it makes the code so much more transparant in what it does. – edgraaff Jan 30 '23 at 10:32

5 Answers5

33

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 ReqeustMatchers 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 RequstMatchers 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 @JsonFilters at run time. It also contains my older approach with extended MappingJackson2HttpMessageConverter that I suggested in comments.

Josep Pascual
  • 168
  • 1
  • 7
chimmi
  • 2,054
  • 16
  • 30
  • In my case it was necessary to use WebMvcConfigurerAdapter.extendMessageConverters instead of WebMvcConfigurerAdapter.configureMessageConverters because otherwise the default converters were lost. When using extendMessageConverters I just filtered the incoming List, removing all other MappingJackson2HttpMessageConverter-s before adding my own implementation. – Tarmo Nov 19 '18 at 14:19
  • I tried your first solution like also explained [here](https://blog.jcore.com/2021/03/a-worthy-companion-for-requestbody/). It is working fine. It's great. But ... how can I achieve to have it working with both `@CustomRequestBody` and `@Valid` ? It seems `@Valid` requires `@RequestBody` and `@CustomRequestBody` does not work with `@RequestBody` I'd also like to know why `@CustomRequestBody` does not work with `@RequestBody` – tweetysat Sep 29 '21 at 17:27
0

Probably this would help, but it ain't pretty. It would require AOP. Also I did not validate it. Create a @CustomAnnotation.

Update your controller:

void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity);

Then implemment the AOP part:

@Around("execution(* *(@CustomAnnotation (*)))")
public void advice(ProceedingJoinPoint proceedingJoinPoint) {
  // Here you would add custom ObjectMapper, I don't know another way around it
  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
  String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator()));

  SomeEntity someEntity = /* deserialize */;
  // This could be cleaner, cause the method can accept multiple parameters
  proceedingJoinPoint.proceed(new Object[] {someEntity});
}
Albert Bos
  • 2,012
  • 1
  • 15
  • 26
  • If Im not mistaken this approach will behave as follows: Spring will use existing converter to read request body and only then this advice will kick in droping the result and reading request again. Beside the fact that request is converted two times this has two side effects: you will probably get `IllegalStateException` on `getReader` because `getInputStream` was already called (fixable), and you will get `HttpMessageNotReadableException` if existing converter was not able to read request. – chimmi Sep 12 '17 at 13:17
0

You could try Message Converters. They have a context about http input request (for example, docs see here, JSON). How to customize you could see here. Idea that you could check HttpInputMessage with special URIs, which used in your controllers and convert string as you want. You could create special annotation for this, scan packages and do it automatically.

Note

Likely, you don't need implementation of ObjectMappers. You can use simple default ObjectMapper to parse String and then convert string as you wish. In that case you would create RequestBody once.

egorlitvinenko
  • 2,736
  • 2
  • 16
  • 36
0

You can create custom deserializer for your String data.

Custom Deserializer

public class CustomStringDeserializer extends JsonDeserializer<String> {

  @Override
  public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {

    String str = p.getText();

    //return processed String
  }

}

Now suppose the String is present inside a POJO use @JsonDeserialize annotation above the variable:

public class SamplePOJO{
  @JsonDeserialize(using=CustomStringDeserializer.class)
  private String str;
  //getter and setter
}

Now when you return it as a response it will be Deserialized in the way you have done it in CustomDeserializer.

Hope it helps.

SachinSarawgi
  • 2,632
  • 20
  • 28
-4

You can define a POJO for each different type of request parameter that you would like to deserialize. Then, the following code will pull in the values from the JSON into the object that you define, assuming that the names of the fields in your POJO match with the names of the field in the JSON request.

ObjectMapper mapper = new ObjectMapper(); 
YourPojo requestParams = null;

try {
    requestParams = mapper.readValue(JsonBody, YourPOJO.class);

} catch (IOException e) {
    throw new IOException(e);
}
Alex S
  • 572
  • 7
  • 15
  • 1
    The POJOs already exist. I'm looking to customize derserialization of specific fields within them but on a controller specific basis and without touching the POJOs. – Mark Jun 15 '16 at 19:58