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 the parts of the objects that can be serialized.
I've found the solution (which is not for me) to annotate my Controller method with the view I want. But I'd like to select on the fly the view inside the controller.
Is it possible to extend the ResponseEntity class in order to specify which JsonView I want ?
A little piece of code :
Here is the account class
public class Account {
@JsonProperty(value = "account_id")
private Long accountId;
@JsonProperty(value = "mail_address")
private String mailAddress;
@JsonProperty(value = "password")
private String password;
@JsonProperty(value = "insert_event")
private Date insertEvent;
@JsonProperty(value = "update_event")
private Date updateEvent;
@JsonProperty(value = "delete_event")
private Date deleteEvent;
@JsonView(value = PublicView.class)
public Long getAccountId() {
return accountId;
}
@JsonView(value = PublicView.class)
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
@JsonView(value = OwnerView.class)
public String getMailAddress() {
return mailAddress;
}
@JsonView(value = OwnerView.class)
public void setMailAddress(String mailAddress) {
this.mailAddress = mailAddress;
}
@JsonIgnore
public String getPassword() {
return password;
}
@JsonView(value = OwnerView.class)
public void setPassword(String password) {
this.password = password;
}
@JsonView(value = AdminView.class)
public Date getInsertEvent() {
return insertEvent;
}
@JsonView(value = AdminView.class)
public void setInsertEvent(Date insertEvent) {
this.insertEvent = insertEvent;
}
@JsonView(value = AdminView.class)
public Date getUpdateEvent() {
return updateEvent;
}
@JsonView(value = AdminView.class)
public void setUpdateEvent(Date updateEvent) {
this.updateEvent = updateEvent;
}
@JsonView(value = AdminView.class)
public Date getDeleteEvent() {
return deleteEvent;
}
@JsonView(value = OwnerView.class)
public void setDeleteEvent(Date deleteEvent) {
this.deleteEvent = deleteEvent;
}
@JsonProperty(value = "name")
public abstract String getName();
}
Here is the account controller
@RestController
@RequestMapping("/account")
public class AccountCtrlImpl implements AccountCtrl {
@Autowired
private AccountSrv accountSrv;
public AccountSrv getAccountSrv() {
return accountSrv;
}
public void setAccountSrv(AccountSrv accountSrv) {
this.accountSrv = accountSrv;
}
@Override
@RequestMapping(value = "/get_by_id/{accountId}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> getById(@PathVariable(value = "accountId") Long accountId) {
try {
return new ResponseEntity<Account>(this.getAccountSrv().getById(accountId), HttpStatus.OK);
} catch (ServiceException e) {
return new ResponseEntity<Account>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
@RequestMapping(value = "/get_by_mail_address/{mail_address}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> getByMailAddress(@PathVariable(value = "mail_address") String mailAddress) {
try {
return new ResponseEntity<Account>(this.getAccountSrv().getByMailAddress(mailAddress), HttpStatus.OK);
} catch (ServiceException e) {
return new ResponseEntity<Account>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
@RequestMapping(value = "/authenticate/{mail_address}/{password}", method = RequestMethod.GET, headers = "Accept=application/json")
public ResponseEntity<Account> authenticate(@PathVariable(value = "mail_address") String mailAddress, @PathVariable(value = "password") String password) {
return new ResponseEntity<Account>(HttpStatus.NOT_IMPLEMENTED);
}
}
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
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;
}
}
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));
}
}