I am using Spring data pagination in my REST Controller and returning Paged entity. I would like to control the data returned as JSON with the help of JSONViews.
I am a
Try with below piece of code,
@Configuration
public class MyInterceptorConfig extends WebMvcConfigurerAdapter{
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper() {
private static final long serialVersionUID = 1L;
@Override
protected DefaultSerializerProvider _serializerProvider(SerializationConfig config) {
// replace the configuration with my modified configuration.
// calling "withView" should keep previous config and just add my changes.
return super._serializerProvider(config.withView(TravelRequestView.MyRequests.class));
}
};
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
converter.setObjectMapper(mapper);
converters.add(converter);
}
Although I don't want to take credit for this, It was a reference from
Jackson JsonView not being applied
It would retrieve all the variables of an entity which are annotated with jsonview (TravelRequestView.MyRequests.class) along with all the variables which are not annotated with jsonview. If you don't want certain properties of an object, annotated with different view.
I actually found a simpler and better way of doing this. The problem I had was with the fact that I cannot set @JsonView annotations on the Page object I was receiving. So I created an implementation of the Page interface and added my JsonViews to it. And instead of returning Page, I am now returning MyPage
public class MyPage<T> implements Page<T> {
private Page<T> pageObj;
public MyPage(Page<T> pageObj) {
this.pageObj = pageObj;
}
@JsonView(PaginatedResult.class)
@Override
public int getNumber() {
return pageObj.getNumber();
}
@Override
public int getSize() {
return pageObj.getSize();
}
@JsonView(PaginatedResult.class)
@Override
public int getNumberOfElements() {
return pageObj.getNumberOfElements();
}
@JsonView(PaginatedResult.class)
@Override
public List<T> getContent() {
return pageObj.getContent();
}
@Override
public boolean hasContent() {
return pageObj.hasContent();
}
@Override
public Sort getSort() {
return pageObj.getSort();
}
@JsonView(PaginatedResult.class)
@Override
public boolean isFirst() {
return pageObj.isFirst();
}
@JsonView(PaginatedResult.class)
@Override
public boolean isLast() {
return pageObj.isLast();
}
@Override
public boolean hasNext() {
return pageObj.hasNext();
}
@Override
public boolean hasPrevious() {
return pageObj.hasPrevious();
}
@Override
public Pageable nextPageable() {
return pageObj.nextPageable();
}
@Override
public Pageable previousPageable() {
return pageObj.previousPageable();
}
@Override
public Iterator<T> iterator() {
return pageObj.iterator();
}
@JsonView(PaginatedResult.class)
@Override
public int getTotalPages() {
return pageObj.getTotalPages();
}
@JsonView(PaginatedResult.class)
@Override
public long getTotalElements() {
return pageObj.getTotalElements();
}
}
Setting DEFAULT_VIEW_INCLUSION has a global effect, while all we need is to be able to serialize a Page object. The following code will register a serializer for Page, and is a simple change to your code:
@Bean
public Module springDataPageModule() {
return new SimpleModule().addSerializer(Page.class, new JsonSerializer<Page>() {
@Override
public void serialize(Page value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeNumberField("totalElements",value.getTotalElements());
gen.writeNumberField("totalPages", value.getTotalPages());
gen.writeNumberField("number", value.getNumber());
gen.writeNumberField("size", value.getSize());
gen.writeBooleanField("first", value.isFirst());
gen.writeBooleanField("last", value.isLast());
gen.writeFieldName("content");
serializers.defaultSerializeValue(value.getContent(),gen);
gen.writeEndObject();
}
});
}
Another (arguably more elegant) solution is to register the following ResponseBodyAdvice. It will make sure your REST endpoint will still return a JSON array, and set a HTTP header 'X-Has-Next-Page' to indicate whether there is more data. The advantages are: 1) No extra count(*) query to your DB (single query) 2) Response is more elegant, since it returns a JSON array
/**
* ResponseBodyAdvice to support Spring data Slice object in JSON responses.
* If the value is a slice, we'll write the List as an array, and add a header to the HTTP response
*
* @author blagerweij
*/
@ControllerAdvice
public class SliceResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Slice) {
Slice slice = (Slice) body;
response.getHeaders().add("X-Has-Next-Page", String.valueOf(slice.hasNext()));
return slice.getContent();
}
return body;
}
}
you need to add annotation @JsonView(TravelRequestView.MyRequests.class) recursively. Add it to the field you want to see in Page class.
public class Page<T> {
@JsonView(TravelRequestView.MyRequests.class)
private T view;
...
}
or enable DEFAULT_VIEW_INCLUSION for ObjectMapper:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="defaultViewInclusion" value="true"/>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
or use dto objects for your responses where you can control all your views
If you are using spring-boot, then another simpler solution would be add the following to application.yml
spring:
jackson:
mapper:
DEFAULT_VIEW_INCLUSION: true
or application.properties
spring.jackson.mapper.DEFAULT_VIEW_INCLUSION=true
With this approach, we have the advantage of retaining the ObjectMapper
managed by Spring Container and not creating a new ObjectMapper. So once we use the spring managed ObjectMapper, then any Custom Serialzers we define will still continue to work e.g CustomDateSerialzer
Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html
Still cleaner is to tell Jackson to Serialize all props for a bean of type Page. To do that, you just have to declare to adapt slighly the Jackson BeanSerializer : com.fasterxml.jackson.databind.ser.BeanSerializer Create a class that extends that BeanSerializer called PageSerialier and if the bean is of type Page<> DO NOT apply the property filtering.
As show in the code below, i just removed the filtering for Page instances :
public class MyPageSerializer extends BeanSerializer {
/**
* MODIFIED By Gauthier PEEL
*/
@Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider)
throws IOException, JsonGenerationException {
final BeanPropertyWriter[] props;
// ADDED
// ADDED
// ADDED
if (bean instanceof Page) {
// for Page DO NOT filter anything so that @JsonView is passthrough at this level
props = _props;
} else {
// ADDED
// ADDED
if (_filteredProps != null && provider.getActiveView() != null) {
props = _filteredProps;
} else {
props = _props;
}
}
// rest of the method unchanged
}
// inherited constructor removed for concision
}
Then you need to declare it to Jackson with a Module :
public class MyPageModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
@Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (serializer instanceof BeanSerializerBase) {
return new MyPageSerializer ((BeanSerializerBase) serializer);
}
return serializer;
}
});
}
}
Spring conf now : in your @Configuration create a new @Bean of the MyPageModule
@Configuration
public class WebConfigPage extends WebMvcConfigurerAdapter {
/**
* To enable Jackson @JsonView to work with Page<T>
*/
@Bean
public MyPageModule myPageModule() {
return new MyPageModule();
}
}
And you are done.