Dynamic Selection Of JsonView in Spring MVC Controller
Asked Answered
A

2

36

I am aware that it is possible to annotate controller methods with @JsonView(...) to statically define a single view class in Spring MVC. Unfortunately this means that I need a different endpoint for every type of view I might possibly have.

I see other people have asked this before. While this approach may work, Spring often has many ways of doing the same thing. Sometimes the solution can be much more simple than it first appears if you just have a bit of knowledge about some of the internals.

I'd like to have a single controller endpoint that can dynamically select the appropriate view based on the current principal. Is it possible for me to return a Model with an attribute that contains the appropriate view class or perhaps a MappingJacksonValue instance directly?

I see in org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#writeInternal there is a snippet of code that determines what view to use:

if (value instanceof MappingJacksonValue) {
            MappingJacksonValue container = (MappingJacksonValue) object;
            value = container.getValue();
            serializationView = container.getSerializationView();
        }

Which appears to come from org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice#beforeBodyWriteInternal but I'm having trouble working out if there is a way I could bypass this just simply by returning a particular value that contains the necessary information for the Jackson2HttpMessageConverter to pick the right view.

Any help much appreciated.

Assurance answered 5/3, 2015 at 13:1 Comment(1)
You can also configure a ContentNegotiatingViewResolverAtal
A
48

On the off chance someone else wants to achieve the same thing, it actually is very simple.

You can directly return aorg.springframework.http.converter.json.MappingJacksonValue instance from your controller that contains both the object that you want to serialise and the view class.

This will be picked up by the org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#writeInternal method and the appropriate view will be used.

It works something like this:

@RequestMapping(value = "/accounts/{id}", method = GET, produces = APPLICATION_JSON_VALUE)
public MappingJacksonValue getAccount(@PathVariable("id") String accountId, @AuthenticationPrincipal User user) {
    final Account account = accountService.get(accountId);
    final MappingJacksonValue result = new MappingJacksonValue(account);
    final Class<? extends View> view = accountPermissionsService.getViewForUser(user);
    result.setSerializationView(view);
    return result;
}
Assurance answered 17/3, 2015 at 15:41 Comment(1)
As a side note, I have since decided that in some cases doing this can be a bad idea or indication or some poor design. Consider that the endpoint in some cases may produce cache-control headers or if you want to document the API response and some JSON attributes are conditional on the view being used it can make life more complex and perhaps even expose details that should not have been (in the case of poor handling of cache control for example). My opinion now is that these might have been better as either separate URI 's or different content-types where the caller use the Accept header.Assurance
K
19

Here is a variation of the above answer which helped me. I found issues returning MappingJacksonValue directly while using Spring HATEOAS payloads. If I return it directly from the controller's handler, for some reason the Resources and ResourceSupport mixins don't get applied correctly and JSON HAL _links is rendered as links. Also Spring ResponseEntity is not rendered as it should showing body and status objects in the payload.

Using ControllerAdvice to achieve the same helped with that and now my payloads are rendered correctly and the views are applied as needed

@ControllerAdvice(assignableTypes = MyController.class)
public class MyControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice {

  @Override
  protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType,
                                         ServerHttpRequest req, ServerHttpResponse res) {
    ServletServerHttpRequest request = (ServletServerHttpRequest)req;
    String view = request.getServletRequest().getParameter("view");
    if ("hello".equals(view)) {
      bodyContainer.setSerializationView(HelloView.class);
    }
  }
}
Kone answered 31/10, 2016 at 17:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.