0

I am configuring a RESTful web service via Spring, with various representations, including JSON. I want the interface to be symmetrical, meaning the format of an object serialized to JSON via a GET is also the format that a POST/PUT would accept. Unfortunately I can only get GETs to work.

Here's my configuration for sending and receiving JSON, which consists of a JSON message converter and view:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="messageConverters">
        <util:list>
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
        </util:list>
    </property>
</bean>

<bean id="contentNegotiatingViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <util:map>
            <entry key="json" value="application/json"/>
        </util:map>
    </property>
    <property name="defaultViews">
        <util:list>
            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/>
        </util:list>
    </property>
</bean>

When I hit a controller with a GET to return an object, for example, a Book, it outputs something like this.

{"book":{"isbn":"1234","author":"Leo Tolstoy","title":"War and Peace"}}

If I turn around and re-submit some similar JSON via a POST or PUT, Spring cannot consume it, complaining about Unrecognized field "book" (Class com.mycompany.Book), not marked as ignorable. Additionally, if I strip off the "book" wrapper element (I'd rather not, but just to see what happens), I get a 400 BAD REQUEST. In either case, my controller code is never hit.

Here's my controller - I'd rather not have any JSON-specific code here (or annotations on my classes being marshalled/unmarshalled) as they will have multiple representations - I want to use Spring's decoupled MVC infrastructure that pushes that kind of thing (marshalling/view resolving/etc.) into configuration files:

@RequestMapping(method=PUT, value="/books/{isbn}")
@ResponseStatus(NO_CONTENT)
public void saveBook(@RequestBody Book book, @PathVariable String isbn) {
    book.setIsbn(isbn);
    bookService.saveBook(book)
}

@RequestMapping(method=GET, value="/books/{isbn}")
public ModelAndView getBook(@PathVariable String isbn) {
    return new ModelAndView("books/show", "book", bookService.getBook(isbn));
}
SingleShot
  • 18,821
  • 13
  • 71
  • 101

3 Answers3

1

Even though it is embarrassing, I am answering my own question for posterity :-)

It turns out that the equivalent controller method in my real code represented by this example method that I posted:

void saveBook(@RequestBody Book book, @PathVariable String isbn)

Actually looks more like this (note: Long vice String):

void saveBook(@RequestBody Book book, @PathVariable Long isbn)

And the value being passed can't be converted to a Long (it is alphanumeric). So... I screwed up! :-)

However, Spring wasn't very nice about it and simply spit out 400 Bad Request. I attached a debugger to discover this.

The use of ModelAndView still generates an outer wrapper element that I will have to deal with somehow (as I want to user ModelAndView to support JSP views and such). I will probably have to provide a custom view for that.


Update on the wrapper element:

It turns out that it is created by Spring marshalling a Map of objects representing the model. This map has a key named "book" (generated from the class name I suppose because its there even if I simply return a Book). Here is a hackish way around it until I can find a better way:

/**
 * When using a Spring Controller that is ignorant of media types, the resulting model
 * objects end up in a map as values. The MappingJacksonJsonView then converts this map
 * to JSON, which (possibly) incorrectly wraps the single model object in the map
 * entry's key. This class eliminates this wrapper element if there is only one model
 * object.
 */
public class SimpleJacksonJsonView extends MappingJacksonJsonView {

    @Override
    @SuppressWarnings("unchecked")
    protected Object filterModel(Map<String, Object> model) {
        Map<String, Object> filteredModel = (Map<String, Object>) super.filterModel(model);
        if(filteredModel.size() != 1) return filteredModel;
        return filteredModel.entrySet().iterator().next().getValue();
    }
}
SingleShot
  • 18,821
  • 13
  • 71
  • 101
  • The MappingJackson2JsonView has a property extractValueFromSingleKeyModel. When it is set to true, it behaves exactly like your SimpleJacksonJsonView. – Felix Oct 15 '13 at 15:43
  • At the time of this question, I believe that property did not exist. Thanks! – SingleShot Oct 17 '13 at 04:11
0

Note you are specifying to use GET on your controller method, so you can configure a specific POST method to be able to receive both GET and POST form RESTFull methods, Something like this:

@RequestMapping(method=GET, value="/books/{isbn}")
public ModelAndView getBook(@PathVariable String isbn) {
    return new ModelAndView("books/show", "book", bookService.getBook(isbn));
}

@RequestMapping(method=POST, value="/books/{isbn}")
public ModelAndView getByPostBook(@PathVariable String isbn) {
    return getBook(isbn);
}
Iogui
  • 1,526
  • 1
  • 17
  • 28
  • Thanks. I don't want to "get by post", I want to PUT and/or POST a JSON object to the server (I'm making a RESTful service). I don't believe the problem is in how I define my request mappings, but how I configure the the message converter for incoming JSON. See my example controller method tagged with "PUT" - that is the interface that fails to be called due to errors in message unmarshalling. – SingleShot Feb 23 '11 at 18:52
  • 1
    Sory @SingleShot, Think I don't understood your question. Well, if your problem is with the message converter, I think you will have to customyze your own. Actually I don't like the things that Jackson do when working jason objects. So I found a way to get hid of it by building a custon Gson message converter [see if it helps you get a new idea](http://stackoverflow.com/questions/5019162/custom-httpmessageconverter-with-responsebody-to-do-json-things) – Iogui Feb 24 '11 at 02:35
  • Thanks. Even though you didn't answer my exact question, you allowed me to learn about Gson :-) – SingleShot Feb 24 '11 at 03:38
0

@SingleShot, {"isbn":"1234","author":"Leo Tolstoy","title":"War and Peace"} without the wrapper book is the correct representation for a book instance in JSON, the book wrapper is added because of the modelName "book" that you have when you are returning the ModelAndView in your GET method, and MappingJacksonJSONView tacks on the book wrapper.

A better pattern would be to simply return the book object in your Get and annotate the method with @ResponseBody

    @RequestMapping(method=RequestMethod.GET, value="/books/{isbn}")
public @ResponseBody Book getBook(@PathVariable String isbn) {
    return new Book("isbn","title","author");
}

Regarding your PUT not resolving the correct Controller method, can you confirm that you have your request content type as application/json.

Biju Kunjummen
  • 49,138
  • 14
  • 112
  • 125
  • Thanks. I prefer your @ResponseBody suggestion, but find that use of the ModelAndView is necessary to let my controller resolve to a JSP when HTML is desired (I produce HTML and XML in addition to JSON). I do set the Content-Type to application/json. However, I will explore your suggestions. – SingleShot Feb 23 '11 at 21:20
  • I tested the style you suggested - it has the same result: a "book" wrapper element. – SingleShot Feb 24 '11 at 05:52
  • I have a hackish solution to the wrapper element. See my answer. – SingleShot Feb 24 '11 at 07:07