Defining projections with Jackson views, REST and Spring Boot

Sometimes it makes sense to reuse DTOs when building REST interfaces. It is convenient as you do not need to re-implement them for every interface and it makes your API consistent as changes in the DTO will be reflected everywhere.

However, you might need to send different subsets of data at different interfaces. For example, you might build an API which will send out the detailed user profile information to a desktop browser, but you might want to send only a small amount of user information to a mobile device.

Here is where projections come in handy. Projections allow us to select different subsets of the same data set, but without needing to rebuild the data-structure from scratch.

In this tutorial, we will discuss how to use Json Views using the @JsonView annotation to configure projections for REST APIs in a Spring boot application.

How to configure a Jackson view

Jackson views allows you to configure a “view” (AKA projection) of your DTO, much like how it is done in database views. To configure Jackson views, you will need to add the @JsonView annotation to the DTO fields to be projected.

Creating different views

When configuring the @JsonView annotation, you will need to define the view in which the field should be projected onto. For this, we will have to create some interfaces.

package com.nullbeans.demo.dto.jsonviews;

/**
* This interface is used to indicate that the field should be
* included when requesting a light weight view of the data structure.
*/
public interface LightView {
}

The interface above will be used to select fields which should be included in a “light weight” projection of our DTO. Let us create another interface for “heavy weight” detailed projections.

package com.nullbeans.demo.dto.jsonviews;

/**
* This interface is used to indicate that the field should be
* included when asking for a detailed view of the data structure.
*/
public interface DetailedView {
}

We will be using those interfaces as markers in the next few sections.

Configuring the @JsonView annotation in the DTO

The next step is to configure the @JsonView annotation in the DTO to indicate which fields should be included in which projections. We will start with a very simple data structure.

public class MyDataDto {

    private Long id;

    private String title;

    private String description;

    private MyChildDataDto childData;

The MyChildData DTO looks as follows.


private Long id;

private String title;

Now, let us say that we would like to included only the id and the title of the parent DTO in the light view. In this case, we should configure the @JsonView annotation with the LightView interface that we created in the previous section.

@JsonView(LightView.class)
private Long id;

@JsonView(LightView.class)
private String title;

Note that you can configure multiple views per field. In the detailed view, we would like to include all fields. In this case, we will add the DetailedView interface to all the fields.

@JsonView({LightView.class, DetailedView.class})
private Long id;

@JsonView({LightView.class, DetailedView.class})
private String title;

@JsonView({DetailedView.class})
private String description;

@JsonView({DetailedView.class})
private MyChildDataDto childData;

Notice how we configured multiple views for the id and the title field.

An important thing to keep in mind is that the inner fields of the child DTOs will not be automatically serialized unless those fields are also annotated with the @JsonView annotation.

For example, in the detailed view, we would like to display the id and title fields of the childData object. Therefore, we will add the @JsonView annotation to those fields as well.

public class MyChildDataDto {

@JsonView({DetailedView.class})
private Long id;

@JsonView({DetailedView.class})
private String title;

Configuring the @JsonView annotation in the controller

In order to complete our views’ configuration, we will also need to configure the JsonViews in the controllers. This is done by adding the @JsonView annotation to the controller methods, and picking the required view.

@RestController
@RequestMapping("/api/jsonviewsexample")
public class JsonViewsExampleController {

    //No JsonViews are configured here. Therefore, everything will be serialized
    @GetMapping("/defaultview")
    public MyDataDto getDefaultView(){
        return generateDto();
    }

    //Only the fields that are configured with the LightView interface are serialized in this REST endpoint
    @JsonView(LightView.class)
    @GetMapping("/lightview")
    public MyDataDto getLightView(){
        return generateDto();
    }
    
    //Only the fields that are configured with the LightView interface are serialized in this REST endpoint
    @JsonView(DetailedView.class)
    @GetMapping("/detailedview")
    public MyDataDto getDetailedView(){
        return generateDto();
    }

In this controller, we configure the Jackson serialization in three different ways:

  • getDefaultView: This endpoint will serialize all fields regardless of their Json view configuration because the controller method does not have any @JsonView annotation configured.
  • getLightView: This endpoint is annotated with the @JsonView annotation and is configured with the LightView interface. Therefore, only the fields which are marked with the LightView interface will be serialized and returned by this endpoint.
  • getDetailedView: This endpoint will serialize all fields which are configured with the DetailedView view.

We can check how the behavior of each of those different endpoints affect the output of the controller by viewing the JSON results of each of them.

LightView:

{"id":123,"title":"example title"}

DetailedView:

{"id":123,"title":"example title","description":"this is an example of how to use JsonViews on nullbeans.com","childData":{"id":4,"title":"child title"}}

DefaultView:

{"id":123,"title":"example title","description":"this is an example of how to use JsonViews on nullbeans.com","childData":{"id":4,"title":"child title"}}

Can you use multiple target views in Spring Controllers?

The short answer is No. You might be tempted to configure multiple views in your Spring controller in order to include union of the fields that are annotated with those views. For example, let us assume that the LightView and the DetailedView do not overlap. Then, we might be tempted to use both of them in the Controller on the same method so we can have all the fields included in the response.

//error
@JsonView({LightView.class, DetailedView.class})
@GetMapping("/detailedview")
public MyDataDto getDetailedView(){
return generateDto();
}

Unfortunately this will not work. You will probably get an error similar to this one during runtime.

java.lang.IllegalArgumentException: @JsonView only supported for response body advice with exactly 1 class argument: method 'getDetailedView' parameter -1
at org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice.beforeBodyWriteInternal(JsonViewResponseBodyAdvice.java:63) ~[spring-webmvc-5.3.3.jar:5.3.3]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice.beforeBodyWrite(AbstractMappingJacksonResponseBodyAdvice.java:54) ~[spring-webmvc-5.3.3.jar:5.3.3]

Sadly, only one argument (one view) is allowed in the @JsonView annotations which are used inside the Spring controllers.

How to use Jackson views with the Jackson ObjectMapper

So far, we have configured the @JsonView annotation on our controller methods to control the result of the serialization. However, we can also manually set the required view in an ObjectMapper.

This can come in handy when you need to send data over other interfaces or between your services.

@GetMapping("/detailedviewwithmapper")
public String getDetailedViewWithMapper() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();

//this line turns off the serialization of fields that are not annotated by @JsonView
mapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
String serializedResult = mapper
.writerWithView(DetailedView.class)
.writeValueAsString(generateDto());
return serializedResult;
}

In this example, we created another controller method but we serialized the response into a JSON string first before returning from the method. To set the view, simply call the writerWithView mapper method.

If you use this approach, then I really recommend that you also disable the DEFAULT_VIEW_INCLUSION mapper feature. If this feature is left enabled, then every field which does not have a JsonView configuration will be included in the serialized result.

While this might be useful in some cases, usually it is not. Most of the time developers use views and projections to narrow down the dataset. It would be inconvenient if you need to add the JsonView annotation on every field (using a different view name) just to make sure that they are excluded from your target view results.

Jackson views and inheritance

At the beginning of the tutorial, we created two interfaces that were used for configuring Jackson views. We needed to use both the “LightView” and the “DetailedView” interfaces for fields that were projected in both, and only the “DetailedView” for fields that are required in the detailed projections.

We can simplify this configuration by using inheritance. When serializing DTOs, the hierarchy of the interface used inside the @JsonView annotation is considered. If the interface matches the target view, or one of the target view’s parent classes, then the field is serialized.

For example, we could recreate the DetailedView interface as a child interface of LightView.

public interface DetailedView extends LightView{
}

In this case, if we pick the DetailedView as our target view, any field configured with the DetailedView or LightView will be serialized. Using inheritance, we can simplify our DTO configuration as follows:

public class MyDataDto {

@JsonView(LightView.class)
private Long id;

@JsonView(LightView.class)
private String title;

@JsonView({DetailedView.class})
private String description;

@JsonView({DetailedView.class})
private MyChildDataDto childData;

Summary

In this tutorial, we discussed how to use Jackson views, how to configure them using the @JsonView annotation in the DTOs and controllers to configure different projections for our DTOs in REST interfaces.

Please keep in mind that Jackson views are just one of many tools that are provided by that great library. There are also other options to consider such as JsonIgnore. There is no tool which is better than the other, only one which fits better to your specific use-case.