Skip to content

class level support for @JsonView in JsonViewResponseBodyAdvice #35350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@
/**
* A {@link ResponseBodyAdvice} implementation that adds support for Jackson's
* {@code @JsonView} annotation declared on a Spring MVC {@code @RequestMapping}
* or {@code @ExceptionHandler} method.
* or {@code @ExceptionHandler} method, or at the controller class level.
*
* <p>The serialization view specified in the annotation will be passed in to the
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
* which will then use it to serialize the response body.
*
* <p>When both method-level and class-level {@code @JsonView} annotations are present,
* the method-level annotation takes precedence.
*
* <p>Note that despite {@code @JsonView} allowing for more than one class to
* be specified, the use for a response body advice is only supported with
* exactly one class argument. Consider the use of a composite interface.
Expand All @@ -53,7 +56,9 @@ public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBo

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return super.supports(returnType, converterType) && returnType.hasMethodAnnotation(JsonView.class);
return super.supports(returnType, converterType) &&
(returnType.hasMethodAnnotation(JsonView.class) ||
returnType.getDeclaringClass().isAnnotationPresent(JsonView.class));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should @JsonView on super type be considered?

}

@Override
Expand All @@ -70,6 +75,12 @@ protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaT

private static Class<?> getJsonView(MethodParameter returnType) {
JsonView ann = returnType.getMethodAnnotation(JsonView.class);

// If no method-level annotation, check for class-level annotation
if (ann == null) {
ann = returnType.getDeclaringClass().getAnnotation(JsonView.class);
}

Assert.state(ann != null, "No JsonView annotation");

Class<?>[] classes = ann.value();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.servlet.mvc.method.annotation;

import java.lang.reflect.Method;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonView;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for class-level {@code @JsonView} support in {@link JsonViewResponseBodyAdvice}.
*
* @author Asif Ebrahim
* @since 7.0
*/
class JsonViewResponseBodyAdviceClassLevelTests {

private JsonViewResponseBodyAdvice advice;

private ServerHttpRequest request;

private ServerHttpResponse response;


@BeforeEach
void setup() {
this.advice = new JsonViewResponseBodyAdvice();
this.request = new ServletServerHttpRequest(new MockHttpServletRequest());
this.response = new ServletServerHttpResponse(new MockHttpServletResponse());
}


@Test
void supportsWithClassLevelJsonView() throws Exception {
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
MethodParameter returnType = new MethodParameter(method, -1);

assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isTrue();
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isTrue();
}

@Test
void supportsWithMethodLevelJsonView() throws Exception {
Method method = RegularController.class.getDeclaredMethod("methodWithJsonView");
MethodParameter returnType = new MethodParameter(method, -1);

assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isTrue();
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isTrue();
}

@Test
void doesNotSupportWithoutJsonView() throws Exception {
Method method = RegularController.class.getDeclaredMethod("methodWithoutAnnotation");
MethodParameter returnType = new MethodParameter(method, -1);

assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isFalse();
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isFalse();
}

@Test
void beforeBodyWriteWithClassLevelJsonView() throws Exception {
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
MethodParameter returnType = new MethodParameter(method, -1);

MappingJacksonValue container = new MappingJacksonValue(new Object());
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);

assertThat(container.getSerializationView()).isEqualTo(MyJsonView.class);
}

@Test
void beforeBodyWriteWithMethodLevelJsonView() throws Exception {
Method method = RegularController.class.getDeclaredMethod("methodWithJsonView");
MethodParameter returnType = new MethodParameter(method, -1);

MappingJacksonValue container = new MappingJacksonValue(new Object());
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);

assertThat(container.getSerializationView()).isEqualTo(MyJsonView.class);
}

@Test
void methodLevelAnnotationTakesPrecedenceOverClassLevel() throws Exception {
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithDifferentJsonView");
MethodParameter returnType = new MethodParameter(method, -1);

MappingJacksonValue container = new MappingJacksonValue(new Object());
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);

// Method-level annotation should take precedence
assertThat(container.getSerializationView()).isEqualTo(AnotherJsonView.class);
}

@Test
void determineWriteHintsWithClassLevelJsonView() throws Exception {
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
MethodParameter returnType = new MethodParameter(method, -1);

var hints = this.advice.determineWriteHints(new Object(), returnType, MediaType.APPLICATION_JSON, MappingJackson2HttpMessageConverter.class);

assertThat(hints).containsEntry(JsonView.class.getName(), MyJsonView.class);
}


// Test interfaces for JsonView
private interface MyJsonView {}

private interface AnotherJsonView {}

// Test controller with class-level @JsonView
@JsonView(MyJsonView.class)
private static class ClassLevelJsonViewController {

@RequestMapping
@ResponseBody
public String methodWithoutAnnotation() {
return "test";
}

@RequestMapping
@ResponseBody
@JsonView(AnotherJsonView.class)
public String methodWithDifferentJsonView() {
return "test";
}
}

// Test controller without class-level @JsonView
private static class RegularController {

@RequestMapping
@ResponseBody
@JsonView(MyJsonView.class)
public String methodWithJsonView() {
return "test";
}

@RequestMapping
@ResponseBody
public String methodWithoutAnnotation() {
return "test";
}
}

}