I\'m currently writing a REST api using Jackson (2.4.0-rc3) and spring mvc (4.0.3), and I\'m trying to make it secure.
In this way, I try to use JsonView to select
This works great :
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public void getZone(@PathVariable long id, @RequestParam(name = "tree", required = false) boolean withChildren, HttpServletResponse response) throws IOException {
LOGGER.debug("Get a specific zone with id {}", id);
Zone zone = zoneService.findById(id);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if (withChildren) {
response.getWriter().append(mapper.writeValueAsString(zone));
} else {
response.getWriter().append(mapper.writerWithView(View.ZoneWithoutChildren.class).writeValueAsString(zone));
}
}
I've solved my problem extending ResponseEntity like this :
public class ResponseViewEntity<T> extends ResponseEntity<ContainerViewEntity<T>> {
private Class<? extends BaseView> view;
public ResponseViewEntity(HttpStatus statusCode) {
super(statusCode);
}
public ResponseViewEntity(T body, HttpStatus statusCode) {
super(new ContainerViewEntity<T>(body, BaseView.class), statusCode);
}
public ResponseViewEntity(T body, Class<? extends BaseView> view, HttpStatus statusCode) {
super(new ContainerViewEntity<T>(body, view), statusCode);
}
}
and ContainerViewEntity encapsulate the object and the selected view
public class ContainerViewEntity<T> {
private final T object;
private final Class<? extends BaseView> view;
public ContainerViewEntity(T object, Class<? extends BaseView> view) {
this.object = object;
this.view = view;
}
public T getObject() {
return object;
}
public Class<? extends BaseView> getView() {
return view;
}
public boolean hasView() {
return this.getView() != null;
}
}
After that, we have convert only the object with the good view.
public class JsonViewMessageConverter extends MappingJackson2HttpMessageConverter {
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (object instanceof ContainerViewEntity && ((ContainerViewEntity) object).hasView()) {
writeView((ContainerViewEntity) object, outputMessage);
} else {
super.writeInternal(object, outputMessage);
}
}
protected void writeView(ContainerViewEntity view, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = this.getJsonEncoding(outputMessage.getHeaders().getContentType());
ObjectWriter writer = this.getWriterForView(view.getView());
JsonGenerator jsonGenerator = writer.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writer.writeValue(jsonGenerator, view.getObject());
} catch (IOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
private ObjectWriter getWriterForView(Class<?> view) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
return mapper.writer().withView(view);
}
}
And to finish, I enable the converter
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="wc.handler.view.JsonViewMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
And that's it, I can select the View in the controller
@Override
@RequestMapping(value = "/get_by_id/{accountId}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseViewEntity<Account> getById(@PathVariable(value = "accountId") Long accountId) throws ServiceException {
return new ResponseViewEntity<Account>(this.getAccountSrv().getById(accountId), PublicView.class, HttpStatus.OK);
}
FYI, Spring 4.1 already supported using @JsonView directly on @ResponseBody and ResponseEntity:
Jackson’s @JsonView is supported directly on @ResponseBody and ResponseEntity controller methods for serializing different amounts of detail for the same POJO (e.g. summary vs. detail page). This is also supported with View-based rendering by adding the serialization view type as a model attribute under a special key.
And in http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-jsonview you can find the much simpler solution:
@RestController
public class UserController {
@RequestMapping(value = "/user", method = RequestMethod.GET)
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
I really like the solution presented here to dynamically select a json view inside your controller method.
Basically, you return a MappingJacksonValue
which you construct with the value you want to return. After that you call setSerializationView(viewClass)
with the proper view class. In my use case, I returned a different view depending on the current user, something like this:
@RequestMapping("/foos")
public MappingJacksonValue getFoo(@AuthenticationPrincipal UserDetails userDetails ) {
MappingJacksonValue value = new MappingJacksonValue( fooService.getAll() );
if( userDetails.isAdminUser() ) {
value.setSerializationView( Views.AdminView.class );
} else {
value.setSerializationView( Views.UserView.class );
}
return value;
}
BTW: If you are using Spring Boot, you can control if properties that have no view associated are serialized or not by setting this in your application.properties
:
spring.jackson.mapper.default_view_inclusion=true