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.

Java Companion
Java Companion
Java Companion
How Jackson Views Let One DTO Replace Many and End DTO Explosion

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.

JavadtoSpringjacksonjsonview
Java Companion
Written by

Java Companion

A highly professional Java public account

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.