Darshan Mehta
Darshan Mehta

Reputation: 30829

Spring Boot : Different ObjectMapper instances for Request and Response

I have following controller in my spring boot application:

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<ResponseDto<MyClass> process(@RequestBody RequestDto<MyClass> request){
    return null;
}

MyClass has a field, let's say 'myField' and I want different NamingStrategyconfiguration for request and response for this field (this is because I don't want to create a new class just for one field). I have configured ObjectMapper instance as below:

@Bean
public ObjectMapper objectMapper(){
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setPropertyNamingStrategy(namingStrategy);
    return objectMapper;
}

This will be used both for Request and Response (i.e. deserialization and serialization), is there any way in spring boot by which I can instruct the controller to use different ObjectMapper instances?

Upvotes: 10

Views: 7137

Answers (4)

Anderson
Anderson

Reputation: 2750

Szymon Stepniak tells the correct answer, while I have some comments.

  1. It also works that just define a converter bean. Note that it must not be a type of MappingJackson2HttpMessageConverter, since it will hide the default one via auto-config(See org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration#mappingJackson2HttpMessageConverter). The type of HttpMessageConverter or AbstractHttpMessageConverter works well.
    @Bean  
    public HttpMessageConverter<Object> yourConverter() {
        AbstractHttpMessageConverter<Object> result = new MappingJackson2HttpMessageConverter(yourObjectMapper()) {
            @Override
            protected boolean canRead(MediaType mediaType) {
                // canRead() accepts null mediaType as default
                return mediaType != null && super.canRead(mediaType);
            }

            @Override
            protected boolean canWrite(MediaType mediaType) {
                // canWrite() accepts null mediaType as default
                return mediaType != null && super.canWrite(mediaType);
            }
        };
        result.setSupportedMediaTypes(List.of(MediaType.valueOf("application/test+json"))));
        return result;
    }
  1. The default one includes a MIME of application/*+json, which means it can accept application/test+json too. So your converter must have a higher priority. Define a bean of HttpMessageConverters(with "s", as a converter manager) can make it explicitly. Remember all the other customized converter beans need to be passed into its constructor, or they will be disabled.
@Bean
public HttpMessageConverters customConverters() {
// Pass all customized converter beans here
    return new HttpMessageConverters(true, Collections.singletonList(yourConverter(),someOtherConverterBean()));
}
  1. Consider override canRead and canWrite if you accepted null mediaType before.

  2. Add produces(related to Accept http header) as well, so that you will not response json in a default style.

    @RequestMapping(method = RequestMethod.POST, consumes = {"application/json", "application/test+json"}, produces = {"application/json", "application/test+json"})
    public ResponseEntity<ResponseDto<MyClass> process(@RequestBody RequestDto<MyClass> request) {
        return null;
    }

Upvotes: 0

user2934313
user2934313

Reputation:

You can use a deserialization modifier in your ObjectMapper to override the set of enabled features at object deserialization time via a module. This one should do the trick:

    public class FeatureModifyingBeanDeserializerModifier extends BeanDeserializerModifier {

        private Collection<Class<?>> modifiedClasses;

        public FeatureModifyingBeanDeserializerModifier(Collection<Class<?>> modifiedClasses) {
            this.modifiedClasses = Collections.unmodifiableSet(new HashSet<Class<?>>(modifiedClasses));
        }

        @Override
        public JsonDeserializer<?> modifyDeserializer(
                DeserializationConfig config, BeanDescription beanDesc, final JsonDeserializer<?> deserializer) {
            JsonDeserializer<?> result = deserializer;
            Class<?> beanClass = beanDesc.getBeanClass();

            if (modifiedClasses.contains(beanClass)) {
                result = new FeatureModifyingStdDeserializer(deserializer, beanClass);
            }
            return result;
        }

        private static class FeatureModifyingStdDeserializer extends StdDeserializer<Object> {

            private JsonDeserializer<?> deserializer;

            private FeatureModifyingStdDeserializer(
JsonDeserializer<?> deserializer, Class<?> beanClass) {
                super(beanClass);
                this.deserializer = deserializer;
            }

            @Override
            public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
                p.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
                return deserializer.deserialize(p, ctxt);
            }
        }      
    }

You have to register it with the ObjectMapper as a module like this:

ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();

module.setDeserializerModifier(new FeatureModifyingBeanDeserializerModifer(Arrays.asList(Journey.class)));
objectMapper.registerModule(module);

For serialization, you can add an @JsonSerialize annotation to the Journey class and serialize it in whatever way you want. If you need to write an unescaped string you can use writeRaw from JsonGenerator.

Upvotes: 1

Szymon Stepniak
Szymon Stepniak

Reputation: 42224

You can solve it with content negotiation. Firstly, define your custom HttpMessageConverter. In following example I have defined a custom converter that is applied when the request Content-Type header is set to application/test+json:

@Bean
public HttpMessageConverters customConverters() {
    final AbstractJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(new ObjectMapper());
    converter.setSupportedMediaTypes(Collections.singletonList(MediaType.valueOf("application/test+json")));

    return new HttpMessageConverters(true, Collections.singletonList(converter));
}

For simplicity of this example I've used newly created ObjectMapper - in your case you will have to pass here previously configured object.

Next thing is to tell your action to accept only appliction/test+json requests (keep in mind, that from now on it requires to Content-Type:application/test+json header to present in every request to this endpoint):

@RequestMapping(method = RequestMethod.POST, consumes = "application/test+json")
public ResponseEntity<ResponseDto<MyClass> process(@RequestBody RequestDto<MyClass> request){
    return null;
}

Last thing is to make sure that when you call this endpoint, Content-Type:application/test+json header is set. Of course you can use any other name for desired content type, presented name is just an example.

Upvotes: 5

kurt
kurt

Reputation: 1550

One dirty hack: you may write custom serializer and deserializer for MyClass, there you explicitly use two separate object mapper one for serialization (for response) and second for deserialization (for request).

But it's better to find a way to explicitly customize spring object mapper.

Upvotes: -1

Related Questions