17

I am using the new Java API (JSR 353) for JSON in a SpringMVC project.

The idea is to generate some piece of Json data and have it returned to the client. The controller I have look somewhat like this:

@RequestMapping("/test")
@ResponseBody
public JsonObject test() {
        JsonObject result = Json.createObjectBuilder()
                .add("name", "Dade")
                .add("age", 23)
                .add("married", false)
                .build();
        return result;
    }

And when I access this, instead of getting the expected representation of the JSON, I get these instead:

{"name":{"chars":"Dade","string":"Dade","valueType":"STRING"},"age":{"valueType":"NUMBER","integral":true},"married":{"valueType":"FALSE"}}

Why is this? What is going on? And how do I make it returned the expected JSON properly?

dade
  • 3,340
  • 4
  • 32
  • 53
  • @SotiriosDelimanolis by API, are you referring to the JsonObject? it is the JSR 353: Java API for JSON Processing. Question updated too – dade Oct 06 '13 at 00:27

2 Answers2

24

The answer is pretty simple when you realize there is no special HandlerMethodReturnValueHandler for the new JSR 353 API. Instead, in this case, the RequestResponseBodyMethodProcessor (for @ResponseBody) uses a MappingJackson2HttpMessageConverter to serialize the return value of your handler method.

Internally, the MappingJackson2HttpMessageConverter uses an ObjectMapper. By default, the ObjectMapper uses the getters of a class to serialize an object to JSON.

Assuming you are using Glassfish's provider implementation of the JSR 353, those classes are org.glassfish.json.JsonObjectBuilderImpl$JsonObjectImpl, org.glassfish.json.JsonStringImpl, and org.glassfish.json.JsonNumberImpl, and javax.json.JsonValue$3 (an anonymous class for the value FALSE).

Because JsonObjectImpl (your result, ie. root, object) is a Map (special type), ObjectMapper serializes the map's entries as JSON key-value pair elements, where the Map key is the JSON key, and the Map value is the JSON value. For the key, it works fine, serializing as name, age, and married. For the value, it uses the classes I mentioned above and their respective getters. For example, org.glassfish.json.JsonStringImpl is implemented as

final class JsonStringImpl implements JsonString {

    private final String value;

    public JsonStringImpl(String value) {
        this.value = value;
    }

    @Override
    public String getString() {
        return value;
    }

    @Override
    public CharSequence getChars() {
        return value;
    }

    @Override
    public ValueType getValueType() {
        return ValueType.STRING;
    }
    ...
}

ObjectMapper therefore uses the Java Bean getters to serialize the JsonStringImpl object (that is the Map Entry's value), as

{"chars":"Dade","string":"Dade","valueType":"STRING"}

The same applies for the other fields.

If you want to correctly write the JSON, simply return a String.

@RequestMapping("/test", produces="application/json")
@ResponseBody
public String test() {
        JsonObject result = Json.createObjectBuilder()
                .add("name", "Dade")
                .add("age", 23)
                .add("married", false)
                .build();
        return result.toString();
}

Or make your own HandlerMethodReturnValueHandler, a little more complicated, but more rewarding.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 3
    But resulting JSON gets escaped, like `"{\"foo\": \"bar\"}"`, how to make it return just JSON, like `{"foo": "bar"}`? – Dzmitry Lazerka Feb 01 '14 at 04:02
  • @DzmitryLazerka Escaped where? The `test()` method above doesn't escape anything. – Sotirios Delimanolis Feb 01 '14 at 04:05
  • In Spring message converter, after test returned. It sees that result is a String, and for mime-type application/json must be converted to JSON. See MappingJackson2HttpMessageConverter (it's added by default if Jackson is present in WebMvcConfigurationSupport). So it converts string to JSON, by escaping it, and adding double quotes around. – Dzmitry Lazerka Feb 13 '14 at 07:50
  • @DzmitryLazerka Spring will not use a `MappingJackson2HttpMessageConverter` for the `test()` method. Because the object returned is of type `String`, it will use a `StringHttpMessageConverter`. – Sotirios Delimanolis Feb 13 '14 at 07:54
  • 1
    You're right, but you forgot to add `produces="application/json"` to @RequestMapping. I believe you don't want to send JSON as text/plain to client-side. If you add application/json to the test() method (which I believe you wanted to), then MappingJackson2HttpMessageConverter gets used. – Dzmitry Lazerka Feb 13 '14 at 19:07
  • 1
    @DzmitryLazerka True, I was missing `application/json`. There are different ways to add it. But adding it still won't use `MappingJackson2HttpMessageConverter`. The `HttpMessageConverter`s are added and checked in a specific order. `StringHttpMessageConverter` comes before `MappingJackson2HttpMessageConverter`. – Sotirios Delimanolis Feb 13 '14 at 19:09
  • What ViewResolver config are you using above to get the desired result? – Gary Sharpe Aug 23 '14 at 20:12
  • 1
    @GarySharpe No `ViewResolver` is involved here. Because of `@ResponseBody`, the returned object is serialized directly to the response. No view has to be resolved. – Sotirios Delimanolis Aug 25 '14 at 02:01
  • There is always a view resolver backing the responses. In Spring case, it uses ContentNegotiatingViewResolver for resolving content types (based on Content Type & Accept headers) to views. The underlying view that this guy renders in jackson case is MappingJackson2JsonView – Ashok Koyi Aug 11 '15 at 22:51
  • @Kalinga Careful, a `ViewResolver` will be involved only if the request isn't marked as handled within the `HandlerMethodReturnValueHandler` that is used to process a handler method's return value. **In this case**, see the first paragraph, I've assumed that a `RequestResponseBodyMethodProcessor` handled the value returned by the `@ResponseBody` annotated handler method. This component completes handling of the request by writing the response content (JSON in this case). Spring never needs to resolve a view and render it. – Sotirios Delimanolis Aug 12 '15 at 03:09
  • @DzmitryLazerka I ran into the same issue with escaped JSON response. In my case it was because I had been using a custom HttpMessageConverter List. See my answer for a Spring Configuration that worked for me. – Gary Dec 09 '16 at 18:50
1

The answer from Sotirios Delimanolis does indeed work, but in my case I had to ensure the proper HttpMessageConverter order was in place. This is because I needed to also convert JodaTime values to ISO 8601 format. This custom WebMvcConfigurerAdapter Configuration worked for me:

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

@SuppressWarnings("UnusedDeclaration")
private static final Logger log = LoggerFactory.getLogger(WebConfiguration.class);

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("Configuring jackson ObjectMapper");
    final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    final ObjectMapper objectMapper = new ObjectMapper();

    //configure Joda serialization
    objectMapper.registerModule(new JodaModule());
    objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.
            WRITE_DATES_AS_TIMESTAMPS, false);

    // Other options such as how to deal with nulls or identing...
    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    converter.setObjectMapper(objectMapper);

    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    /*
    StringHttpMessageConverter must appear first in the list so that Spring has a chance to use
     it for Spring RestController methods that return simple String. Otherwise, it will use
      MappingJackson2HttpMessageConverter and clutter the response with escaped quotes and such
     */
    converters.add(stringHttpMessageConverter);
    converters.add(converter);
    super.configureMessageConverters(converters);
}
}
Gary
  • 6,357
  • 5
  • 30
  • 36