How Jackson Views Let One DTO Replace Many and End DTO Explosion
The article explains how Jackson Views can consolidate multiple DTO classes into a single DTO by using view interfaces and @JsonView annotations, dramatically reducing code duplication, simplifying maintenance, and providing flexible field selection for different API scenarios.
Background and Pain Points
In typical API development a single entity often needs to be presented in different ways: a list view only requires id and username, a detail view needs all public fields, and an admin view must include sensitive information. Developers usually create separate DTO classes such as UserSummaryDTO, UserDetailDTO and UserAdminDTO. This leads to a rapid increase in the number of DTO classes, high code duplication, and a maintenance burden whenever the underlying entity changes.
Traditional Approach and Its Drawbacks
Proliferation of DTO classes (summary, detail, admin, etc.)
Repeated field definitions across DTOs
Every field change requires updates in multiple DTOs
Project structure becomes bulky and harder to read
Jackson Views Solution
Jackson provides a powerful feature called JsonView that controls which fields are serialized based on a view class.
1. Define View Interfaces
public class Views {
public interface Public {}
public interface Summary extends Public {}
public interface Detail extends Summary {}
public interface Admin extends Detail {}
}2. Annotate DTO Fields with @JsonView
public class UserDTO {
@JsonView(Views.Public.class)
private Long id;
@JsonView(Views.Summary.class)
private String username;
@JsonView(Views.Detail.class)
private String email;
@JsonView(Views.Detail.class)
private String phone;
@JsonView(Views.Detail.class)
private String address;
@JsonView(Views.Detail.class)
private String avatar;
@JsonView(Views.Admin.class)
private LocalDateTime updateTime;
@JsonView(Views.Admin.class)
private String internalNote; // admin‑only field
// getters/setters omitted for brevity
}3. Apply Views in Controllers
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// List API – only basic info
@GetMapping("/users")
@JsonView(Views.Summary.class)
public List<UserDTO> getUserList() {
return userService.getAllUsers();
}
// Detail API – full public info
@GetMapping("/users/{id}")
@JsonView(Views.Detail.class)
public UserDTO getUserDetail(@PathVariable Long id) {
return userService.getUserById(id);
}
// Admin API – all fields including sensitive ones
@GetMapping("/admin/users/{id}")
@JsonView(Views.Admin.class)
public UserDTO getUserForAdmin(@PathVariable Long id) {
return userService.getUserById(id);
}
}4. Effect Demonstration
Calling GET /api/users returns a JSON array containing only id and username:
[
{"id":1,"username":"张三"},
{"id":2,"username":"李四"}
]Calling GET /api/users/1 returns the detail view with all public fields:
{
"id":1,
"username":"张三",
"email":"[email protected]",
"phone":"13800138000",
"address":"北京市朝阳区",
"avatar":"http://example.com/avatar1.jpg"
}Calling GET /api/admin/users/1 returns the admin view, which additionally includes updateTime and internalNote:
{
"id":1,
"username":"张三",
"email":"[email protected]",
"phone":"13800138000",
"address":"北京市朝阳区",
"avatar":"http://example.com/avatar1.jpg",
"updateTime":"2024-01-15T10:30:00",
"internalNote":"VIP用户,需要重点关注"
}Advanced Usage
1. Composite Views
Define additional interfaces that combine existing views, e.g. a BasicContact view that extends both Views.Basic and Views.Contact:
public interface BasicContact extends Views.Basic, Views.Contact {}2. Dynamic View Selection
Expose a request parameter to choose the view at runtime:
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id,
@RequestParam(defaultValue = "summary") String view) {
UserDTO user = userService.getUserById(id);
Class<?> viewClass = switch (view.toLowerCase()) {
case "detail" -> Views.Detail.class;
case "admin" -> Views.Admin.class;
default -> Views.Summary.class;
};
// The chosen viewClass can be passed to Jackson via MappingJacksonValue if needed
return ResponseEntity.ok().body(user);
}Best Practices
Prefer inheritance over flat interfaces : reuse fields by letting a view extend a more generic one.
Keep view granularity moderate : avoid overly fine‑grained views that explode the number of classes, but also avoid too coarse views that lose flexibility.
Name views clearly : names like Public, Summary, Detail, Admin immediately convey their purpose.
Common Pitfalls and Correct Patterns
Wrong: Deep inheritance chains such as A → B → C → D → E increase maintenance complexity.
// Bad example – view hierarchy too deep
public interface A extends B {}
public interface B extends C {}
public interface C extends D {}
public interface D extends E {}Right: Limit hierarchy depth to three levels and keep each view focused.
public interface Public {}
public interface Summary extends Public {}
public interface Detail extends Summary {}Interaction with Other Annotations
Jackson Views can be combined with other Jackson annotations such as @JsonProperty, @JsonFormat, and @JsonIgnore to customize field names, date formats, or completely hide fields in specific views.
public class UserDTO {
@JsonView(Views.Summary.class)
@JsonProperty("user_id")
private Long id;
@JsonView(Views.Detail.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonView(Views.Admin.class)
@JsonIgnore
private String sensitiveData;
}Conclusion
Jackson Views provide a concise, flexible way to serve different data shapes from a single DTO, eliminating the need for a proliferation of DTO classes. By defining a clear view hierarchy, annotating fields appropriately, and applying the views in controller methods, developers can reduce maintenance cost, improve code readability, and adapt quickly to changing business requirements.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
