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));
}
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();
}
}
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);
}
@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.