Jackson @JsonView and it’s meaningful use with Spring Boot REST

Amit Patil
4 min readJan 25, 2020

--

Most of the time we come across requirements in an application where we want to provide different views of the same model data to different clients/users.

To solve this, we may end up creating multiple models/DTOs to represent different views of the same state to different clients. For models with a limited number of fields this approach may work, but for large models, it will become difficult to manage and will result in duplication of code.

Jackson’s @JsonView comes to the rescue

We can use Jackson’s @JsonView to solve the above problem. Similar to database views, where we can build multiple virtual tables(views) with different combinations of columns in the underlying table, we can define multiple views of the same Model/DTOs with different combinations of fields.

How to use? 🤔

  1. Define view as a class or interface.
  2. Use @JsonView annotation in Model/DTOs to map fields to one or more views.

Jackson reads these @JsonView annotations on fields during serialization/ de-serialization of objects and serialize/deserialize only fields in view and will skip all other fields.

Implementation

  1. Define Views,
public class View {//Enclosing type to define User views
public static interface UserView {
//External View for User
public static interface External {
}
//Intenal View for User, will inherit all filds in External
public static interface Internal extends External {
}
}
}

2. Annotate Model/DTO fields with Views.

public class User {   @JsonView(value = { View.UserView.External.class })
private Integer id;
@JsonView(value = { View.UserView.External.class })
private String firstName;
@JsonView(value = { View.UserView.External.class })
private String lastName;
@JsonView(value = { View.UserView.Internal.class })
private String ssn;
@JsonView(value = { View.UserView.Internal.class })
private Calendar dob;
@JsonView(value = { View.UserView.Internal.class })
private String mobileNo;
}

Serializing using External View,

final User user = new User();
user.setId(2)
user.setSsn("mockedSsn");
user.setMobileNo("34234234");
user.setLastName("Bar");
user.setFirstName("Foo");
user.setDob(Calendar.getInstance());
final ObjectMapper objectMapper = new ObjectMapper()
.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
final String serializedValue = objectMapper.writerWithView(View.UserView.External.class)
.writeValueAsString(mockedUser);

Will serialize in External View of a user,{“id”:2,”firstName”:”Foo”,”lastName”:”Bar”}

Serializing using Internal View,

final String serializedValue = objectMapper
.writerWithView(View.UserView.Internal.class)
.writeValueAsString(mockedUser);

Will result in Internal View of a user,{“id”:2,”firstName”:”Foo”,”lastName”:”Bar”,”ssn”:”mockedSsn”,”dob”:1579799822543,”mobileNo”:”34234234"}

Deserialization works in exactly the same way, For External View,

final String serializedUserJson = "{\"id\":2,\"firstName\":\"Foo\",\"lastName\":\"Bar\",\"ssn\":\"mockedSsn\",\"dob\":1579799822543,\"mobileNo\":\"34234234\"}";final User user = objectMapper.readerWithView(View.UserView.External.class)
.forType(User.class)
.readValue(serializedUserJson);

Will only map External view fields, Similarly for Internal view will only consider internal fields for deserialization.

final User user = objectMapper.readerWithView(View.UserView.Internal.class)
.forType(User.class)
.readValue(serializedInternalUserJson);

Spring Support and Integration for REST APIs

Spring supports Jackson @JsonView annotation directly and can be leveraged to customize, control the serialization/deserialization of REST API response and request body data.

Below are some important REST factors I always consider while designing REST APIs structures,

  1. “Don’t expose more than you think needs exposing. If a certain property on an object is internal to your business and not useful to a consumer, then don’t return it”.
  2. “Accept only necessary information for the system to perform as intended operations. Restrict information allowed to add/update by different clients at different contexts to ensure consistency of data.”

For example, if SSN is used as the natural primary in the application, we may want to restrict PUT-/users/{userId} call to update it to ensure data consistency and integrity. We may want to restrict the other fields like primary keys, created/updated dates, isDeleted which are mostly read-only to the clients from being updated directly from API client.

As usual, Spring guys integrate Jackson @JsonView and makes solving above problems piece of cake😄.

In Spring default configuration, MapperFeature.DEFAULT_VIEW_INCLUSION is set to false. Which means, when enabling a JSON View, non annotated fields are not serialized. To change this default behaviour, you need to autowiare ObjectMapper and set it’s MapperFeature.DEFAULT_VIEW_INCLUSION true.

Controlling response body serialization

How to? 🤔

  1. Just annotate Controller method with Jackson’s @JsonView annotation with a view to be used to serialize the response body and spring does all heavy lifting for us😅
@RestController
public class UserController {
@GetMapping("/int/users")
// Serialize response with Intenal View
@JsonView(value = View.UserView.Internal.class)

public List<User> getAllInternal() {
return userService.getAllUsers();
}
@GetMapping("/ext/users")
@ResponseStatus(code = HttpStatus.OK)
// Serialize response with External View
@JsonView(value = View.UserView.External.class)

public List<User> getAllExternal() {
return userService.getAllUsers();
}
}

Controlling request body using @JsonView

How to? 🤔

  1. Just use @JsonView annotation at Controller method @RequestBody parameter with View to be used for deserialization of request body to Model/DTO.

Define separate views for POST, PUT operations on User resource.

public interface View {
public static interface UserView {
// View for User POST call request body
public static interface Post {
}
// View for User POST call request body
public static interface PUT {
}
}
}
public class User { private Integer id; @JsonView(value = {View.UserView.Post.class, View.UserView.PUT.class})
private String firstName;
@JsonView(value = {View.UserView.Post.class, View.UserView.PUT.class})
private String lastName;
@JsonView(value = {View.UserView.Post.class})
private String ssn;
@JsonView(value = {View.UserView.Post.class})
private Calendar dob;
@JsonView(value = {View.UserView.Post.class, View.UserView.PUT.class})
private String mobileNo;
private Calendar createdOn;
private Calendar updatedOn;
private boolean isDeleted;
}

So, only firstName, lastName and mobileNo are allowed to update in PUT call whereas excluding id, createdOn, updatedOn and isDeleted all other fields are allowed in POST call.

This is how we can enforce these views to the request body,

@RestController
public class UserController {
// Using View.UserView.Post view for POST call
@PostMapping(path = "/users", consumes={ "application/json" })
public ResponseEntity<User> post(@RequestBody @JsonView(value = View.UserView.Post.class) User user) {
// Save user here
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
// Using View.UserView.PUT view for PUT call
@PutMapping(path = "/users/{userId}", consumes {"application/json"})
public ResponseEntity<?> put(@RequestBody @JsonView(value = View.UserView.PUT.class) User user) {
// Update user here
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}

So, @JsonView comes very handy to control/restrict data flow in and out of REST APIs and makes developer’s lives easy to focus on the actual business logic in applications.

--

--