Custom JSON views with Spring MVC and Jackson

The Problem

Spring MVC provides amazing out-of-the-box support for returning your domain model in JSON, using Jackson under the covers.

However, often you may find that you want to return different views of the data, depending on the method that is invoked.

For example, consider the following two methods:

List<Book> findBooks() {}
Book getBook(Long bookId); {}

It’s reasonable to say that from the findBooks method, you only want to return a summarized view of the books, and then allow users to fetch the full object graph for a specific book.

Unfortunately, SpringMVC doesn’t support this natively, as the type serialization is annotated directly on the entity class (Book).

Jackson has support for the concept using it’s ResponseView annotation, but there’s no way to hook that into Spring methods.

You would be required instead to generate different VO’s for serializing, which is ultimately just annoying boilerplate code.

Introducing @ResponseView

@ResponseView is a new tag that solves this issue, allowing you to register custom views on a per-method basis.

First up, let’s annotate our domain model with Jackson’s @JsonView annotation:

@Data
class Book extends BaseEntity
{
    @JsonView(SummaryView.class)
    private String title;
    @JsonView(SummaryView.class)
    private String author;
    private String review;

    public static interface SummaryView extends BaseView {}
}

@Data
public class BaseEntity
{
    @JsonView(BaseView.class)
    private Long id;    
}

public interface BaseView {}

Note that here we’ve defined public static interface as a marker, and annotated the properties we want returned from our summary method.  (Note, I’m using Lombok’s @Data annotation to keep this blogpost short.  It’s not a requirement of this approach)

Now, let’s define the annotation, and mark up our methods:

@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseView {
    public Class<? extends BaseView> value();
}

@Controller
public class BookService
{
    @RequestMapping("/books")
    @ResponseView(SummaryView.class)
    public @ResponseBody List<Book> getBookSummaries() {}

    @RequestMapping("/books/{bookId}")
    public @ResponseBody Book getBook(@PathVariable("bookId") Long BookId) {}
}

This indicates that we want the response from getBookSummaries serialized using our SummaryView annotation.

Here’s the wiring to make it work:

/**
 * Decorator that detects a declared {@link ResponseView}, and 
 * injects support if required
 * @author martypitt
 *
 */
public class ViewInjectingReturnValueHandler implements
        HandlerMethodReturnValueHandler {

    private final HandlerMethodReturnValueHandler delegate;

    public ViewInjectingReturnValueHandler(HandlerMethodReturnValueHandler delegate)
    {
        this.delegate = delegate;
    }
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return delegate.supportsReturnType(returnType);
    }

    @Override
    public void handleReturnValue(Object returnValue,
            MethodParameter returnType, ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest) throws Exception {

        Class<? extends BaseView> viewClass = getDeclaredViewClass(returnType);
        if (viewClass != null)
        {
            returnValue = wrapResult(returnValue,viewClass);    
        }

        delegate.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }
    /**
     * Returns the view class declared on the method, if it exists.
     * Otherwise, returns null.
     * @param returnType
     * @return
     */
    private Class<? extends BaseView> getDeclaredViewClass(MethodParameter returnType) {
        ResponseView annotation = returnType.getMethodAnnotation(ResponseView.class);
        if (annotation != null)
        {
            return annotation.value();
        } else {
            return null;
        }
    }
    private Object wrapResult(Object result, Class<? extends BaseView> viewClass) {
        PojoView response = new PojoView(result, viewClass);
        return response;
    }
}
@Data
public class PojoView {
	private final Object pojo;
	private final Class<? extends BaseView> view;
	@Override
	public boolean hasView() {
		return true;
	}
}
/**
 * Adds support for Jackson's JsonView on methods
 * annotated with a {@link ResponseView} annotation
 * @author martypitt
 *
 */
public class ViewAwareJsonMessageConverter extends
        MappingJacksonHttpMessageConverter {

    public ViewAwareJsonMessageConverter()
    {
        super();
        setObjectMapper(JacksonConfiguration.newObjectMapper());
    }

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        if (object instanceof DataView && ((DataView) object).hasView())
        {
            writeView((DataView) object, outputMessage);
        } else {
            super.writeInternal(object, outputMessage);
        }
    }
    protected void writeView(DataView view, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
        ObjectMapper mapper = getMapperForView(view.getView());
        JsonGenerator jsonGenerator =
                mapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
        try {
            mapper.writeValue(jsonGenerator, view);
        }
        catch (IOException ex) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
        }
    }

    private ObjectMapper getMapperForView(Class<?> view) {
        ObjectMapper mapper = JacksonConfiguration.newObjectMapper();
        mapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
        mapper.setSerializationConfig(mapper.getSerializationConfig().withView(view));
        return mapper;
    }

}

/**
 * Modified Spring 3.1's internal Return value handlers, and wires up a decorator
 * to add support for @JsonView
 * 
 * @author martypitt
 *
 */
@Slf4j
public class JsonViewSupportFactoryBean implements InitializingBean {

    @Autowired
    private RequestMappingHandlerAdapter adapter;
    @Override
    public void afterPropertiesSet() throws Exception {
        HandlerMethodReturnValueHandlerComposite returnValueHandlers = adapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> handlers = Lists.newArrayList(returnValueHandlers.getHandlers());
        decorateHandlers(handlers);
        adapter.setReturnValueHandlers(handlers);
    }
    private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        for (HandlerMethodReturnValueHandler handler : handlers) {
            if (handler instanceof RequestResponseBodyMethodProcessor)
            {
                ViewInjectingReturnValueHandler decorator = new ViewInjectingReturnValueHandler(handler);
                int index = handlers.indexOf(handler);
                handlers.set(index, decorator);
                log.info("JsonView decorator support wired up");
                break;
            }
        }        
    }

}

Those two classes are responsible for the heavy lifting.   ViewInjectingReturnValueHandler identifies methods with our @ResponseView annotation, and wrap them so they can be formatted correctly.

JsonViewSupportFactoryBean modifies Spring’s internal wiring, wrapping the existing RequestResponseBodyMethodProcessor with our decorator.

From here, it’s just a matter of updating the Spring configuration:

    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="com.mangofactory.concorde.api.ViewAwareJsonMessageConverter" />
        </mvc:message-converters>
    </mvc:annotation-driven>

That’s it.

Now, calls to any methods annotated with @ResponseView will have the appropriate json view rendered.

Update: Example available on Github

There’s a working example of this implementation now available on github, here.

Final thought:  Interfaces vs Classes

I’d recommend using Interfaces for declaring the view markers, as opposed to Classes as shown elsewhere.  Using Interfaces will allow you to build fairly fine-grained views, using multiple inheritance   As you never have to worry about the implementation details, there’s none of the normal multiple-inheritance nasties lurking.

Advertisements
Tagged ,

19 thoughts on “Custom JSON views with Spring MVC and Jackson

  1. Pavel says:

    Hi,
    We are creating some REST services for a spring mvc application and we have found the same problem when trying to tell “spring” which “jackson” view we want it to render.
    Your article have been very usefull to understand how to do it, but a silly question remains to me: where can I find the ResponseView annotation ? I searched on the latest versions of spring and jackson but haven’t found anything about it.
    Thanks

    • Marty Pitt says:

      Hi Pavel

      Oops – I missed the annotation out from the post.

      I’ve added it now. Thanks for catching it!

      • Pavel says:

        Hi Marty,
        Thanks for your reply.
        I suppose that ViewAwareJsonMessageConverter checks the return value to detect your PojoView and configure Jackson to use the view defined by it. Right? In that case I don’t see what does MetadataInjectionFactoryBean declared in the spring configuration. Isn’t JsonViewSupportFactoryBean that should be declared instead?

      • Marty Pitt says:

        Hi Pavel.

        My apologies – the original post was a late night effort, and the MetadataInjectionFactoryBean is entirely unrelated (though, something else cool we’re doing that I plan to blog about).

        You’re correct, the ViewAwareJsonMessageConverter is what does the work.

        I’ve put a fully working example up on github, which is available here: https://github.com/martypitt/JsonViewExample

      • Seb says:

        Thanks for this nice post. I have the same question than Pavel about the MetadataInjectionFactoryBean / JsonViewSupportFactoryBean . And where does PojoView come from ? Regards.

      • Marty Pitt says:

        Hi Seb.

        Yep, there’s a few errors in the code samples above (late night effort). – Best to grab the working example from Github:
        https://github.com/martypitt/JsonViewExample

  2. Assaf says:

    Hi Marty and thank you for the post that is exactly what i was looking for, I have d/l your example and it worked perfectly.. but when I am trying to integrate it to my project I am having problems…

    The problem is that i can’t inject the RequestMappingHandlerAdapter adapter to the JsonViewSupportFactoryBean cause there is more then one in the context
    No unique bean of type [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter] is defined: expected single matching bean but found 2

    tried inject it manually as a property in the XML , the context loaded but it ain’t working ..

    Is there a chance for help?
    thanks

    • Marty Pitt says:

      Seems kinda weird that you’d have two. What’s the use case that requires two registered?

      If you’ve injected it manually (I assume using something like:

      then are you sure you’ve wired it to the correct adapter? (ie., if there’s two present, maybe it’s wired to the wrong one?)

      Alternatively, you’d need to create a simple example that shows the problem.

      • Assaf says:

        Hi, thank you for the response .. I guess whats happens is related to that bug.. https://jira.springsource.org/browse/SPR-9344

        I dont know why two instances of RequestMappingHandlerAdapter are generated.. I looked at the applications context one of them contains the ViewAwareJsonMessageConverter which define from the XML and one of them contain only the default message convectors and from some reason he is the dominant… I didnt got to a solution yet .. if you can help i will appreciate..
        Thanks

  3. Assaf says:

    Hi .. I have find some work around to the problem intercepting to context loading and inject the converter to each of the handler .. I have other issue though 🙂 my controllers usually return response of ResponseEntity so if i am doing analogy to your example ResponseEntity<List> it seems that the return object is not an instance of the DataView class but List will work.. how can i configure/customize your code in order also return object from type like ResponseEntity<List> ?
    Thanks

  4. TheZuck says:

    Hi,

    Great post, I have a feeling this post answers my question on stackoverflow but I’ll need to dig into it much more in order to apply it to my problem. If you feel like nudging me in the right direction, please visit stackoverflow (http://stackoverflow.com/questions/14114140/jackson-custom-serialization-under-spring-3-mvc) and comment.

    Thanks, and great job on this post 🙂
    Amir

  5. Akram says:

    Hi,
    In BookController :
    The constructor Book(String, String, String, int) is undefined

    Can you please post an updated src code.
    thanks

  6. Barry says:

    Hi, thanks for this post.
    I’m using Spring 4 and I’m getting UnsupportedOperationException at this part…
    handlers.set(index, decorator)
    The reason is that ‘handlers’ is an UnmodifiableList. My guess is that this is a change made for Spring 4. So, now I’m trying to figure out a workaround.
    If you have any ideas, would be much appreciated. Thanks!

    • Barry says:

      Well, for what it’s worth, I did work around this problem, by simply replacing the list (instead of modifying it). But still, it doesn’t seem to ever use it. The methods in my
      ViewInjectingReturnValueHandler are never called.

      private void decorateHandlers(List handlers)
      {
      List customList = new ArrayList();
      for (HandlerMethodReturnValueHandler handler : handlers)
      {
      if (handler instanceof RequestResponseBodyMethodProcessor)
      {
      handler = new ViewInjectingReturnValueHandler(handler);
      log.info(“JsonView decorator support wired up”);
      }
      customList.add(handler);
      }
      adapter.setReturnValueHandlers(customList);
      }

      • Barry says:

        Scratch that last comment about it not using the custom class. It is working fine with the code I posted.

  7. David Harris says:

    Thank you for this post it helped me a lot. I’m using Spring 4 and it has a few changes that required me to update the JsonViewSupportFactoryBean. Here is the body of that class:

    @Autowired
    private RequestMappingHandlerAdapter adapter;

    @Override
    public void afterPropertiesSet() throws Exception {
    List myHandlers = new ArrayList();
    for (HandlerMethodReturnValueHandler handler : adapter.getReturnValueHandlers()) {
    if (handler instanceof RequestResponseBodyMethodProcessor){
    ViewInjectingReturnValueHandler decorator = new ViewInjectingReturnValueHandler(handler);
    myHandlers.add(decorator);
    }else
    myHandlers.add(handler);
    }
    adapter.setReturnValueHandlers(myHandlers);
    }

  8. torcq says:

    Full example with Spring 4.x & Jacksn 4.x https://github.com/bigloupe/JsonViewExample

  9. David Harris says:

    For what its worth to anyone reading this post. Since Spring 4.1 this is now supported out of the box.
    http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-jsonview
    You can Annotate your controller methods with @JsonView to get the same results. I only found that out because switching to Spring 4.2.1 on one of my projects caused this solution to stop working, the writeInternal method of the ViewAwareJsonMessageConverter wasn’t being called anymore.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: