1

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.

I have a controller method with implicit JSON conversions for both incoming and outgoing data:

@RestController
public class LoginController {

    @RequestMapping(value = "/login", method = POST, produces = "application/json")
    ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
        // ...
    }
}

This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:

{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}

Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?

I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.

Edit

It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:

By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.

I'm still missing how exactly do I customise that, for example in a custom Jackson2ObjectMapperBuilder...

Community
  • 1
  • 1
Jonik
  • 80,077
  • 70
  • 264
  • 372
  • Did you try changing the parameter type from LoginRequest to String? How is Spring supposed to convert from a text/plain request to an object? – zmitrok Feb 13 '15 at 16:41
  • Implicity converting between JSON strings and (model/DTO) objects is quite standard Spring MVC functionality. I don't know exactly how, but it just does it. Related: [MappingJackson2HttpMessageConverter](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.html) (whose javadoc actually gave me a lead on who to do this). – Jonik Feb 13 '15 at 16:54
  • The converter that you mention kicks in when the payload is application/json. When you specify your payload as text/plain, you should expect to get your payload as a String. – zmitrok Feb 13 '15 at 16:56
  • Note that my question was: "Is this possible *while continuing to use the implicit JSON conversions*?" Making the param a String was always a backup plan. But it looks like I got it working by following the javadoc advice: http://stackoverflow.com/a/28505688/56285 – Jonik Feb 13 '15 at 17:56
  • Sorry, you're right, I missed that part. By the way, can you explain the reasons to accept and decode JSON payload from non-JSON content types? Are you dealing with legacy clients? – zmitrok Feb 13 '15 at 18:16
  • Just wanted to make my API more lenient and as simple as possible for clients (mobile apps on various platforms): as long as clients POST correct payload, fulfil the request. And yes, this is very minor thing. I was just curious how to stop Spring implicitly responding with 415. – Jonik Feb 16 '15 at 11:38

3 Answers3

0

I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.

If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.

public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private              ObjectMapper mapper          = new ObjectMapper();
private static final Charset      DEFAULT_CHARSET = Charset.forName("UTF-8");

public CustomerJsonHttpMessageConverter() {
    super(new MediaType("application", "json", DEFAULT_CHARSET));
}

@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
                                                                                    HttpMessageNotReadableException {
    return mapper.readValue(inputMessage.getBody(), clazz);
}

@Override
protected boolean supports(Class<?> clazz) {
    return true;
}

@Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
                                                                                   HttpMessageNotWritableException {
    String json = mapper.writeValueAsString(value);
    outputMessage.getBody().write(json.getBytes());
}

}

To have custom media type,

MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(
            Arrays.asList(
                    new MediaType("text", "plain"),
                    new MediaType("text", "html")
            ));
vtor
  • 8,989
  • 7
  • 51
  • 67
  • I'm guessing this should be doable without going as far as overriding `readInternal()` and `writeInternal()` (which I really don't want to customise). By customising `supportedMediaTypes` somehow... (see Edit on the question). – Jonik Feb 13 '15 at 17:07
  • If you really need to ignore the content-type, I think this is the only way - to write custom converter. If you just need to accept other content-types, it should be enough to use ```consumes```. It is strange that didn't work, can you please verify your spring version? ```Consumes``` comes from ```Spring 3.1.*```. In addition you could try relatively old - ```headers = "Content-type: "``` syntax. For custom media type, I edited my answer – vtor Feb 13 '15 at 17:21
  • Hmm, might not be worth the hassle then (as I can of course manually deserialise the JSON string). As stated, I use Spring Boot 1.2.1, and [everything else comes from there](https://github.com/spring-projects/spring-boot/blob/v1.2.1.RELEASE/spring-boot-dependencies/pom.xml). – Jonik Feb 13 '15 at 17:27
  • How exactly do I make Spring use the custom MappingJackson2HttpMessageConverter? I tried defining a `@Bean` factory method in my main Application, but that curiously fails with "Could not instantiate XmlMapper - not found on classpath" – Jonik Feb 13 '15 at 17:29
  • http://java.dzone.com/articles/customizing check this link. It configures ObjectMapper, so you can do for media types. – vtor Feb 13 '15 at 17:31
  • Actually disregard that last comment, had a silly MappingJackson2HttpMessageConverter vs MappingJackson2XmlHttpMessageConverter mistake. – Jonik Feb 13 '15 at 17:34
0

For anyone else who is curious about this;

It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.

However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.

If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).

typingduck
  • 825
  • 10
  • 15
-1

Update: watch out if you use this

(This was probably a stupid idea anyway.)

This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)

After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).


Original answer:

MappingJackson2HttpMessageConverter javadocs state that:

By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.

...which pointed me towards a pretty simple solution that seems to work. In main Application class:

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter converter = 
        new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
    converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
    return converter;
}

(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)

This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the @RequestBody parameter into String, and deserialise it manually.

Community
  • 1
  • 1
Jonik
  • 80,077
  • 70
  • 264
  • 372