Tag Archives: spring-mvc

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.

Tagged ,