Repository: springdoc/springdoc-openapi
Pull request #1159 (2021-05-08 17:26:14)

Support for @JsonView on  @ExceptionHandler methods
Currently, springdoc-api ignores the` @JsonView` annotation on exception handler methods, and it generates a generic schema(all fields) for the ResponseBean. However, spring rest supports `@JsonView` annotation on exception handler methods and generates a different schema for different Views of ResponseBean.

This PR introduces the support for `@JsonView` annotation on Exception handler methods.


In the below example, exception handlers for INTERNAL_SERVER_ERROR and BAD_REQUEST have different JsonViews, but springdoc generates the same schema **/components/schemas/FooBean** for both error responses. 


```java
public class FooBean {
    @JsonView(Views.View2.class)
    private String message;
    @JsonView(Views.View1.class)
    private int code;
}

//Exception handler class
@ControllerAdvice(assignableTypes = HelloController.class)
public class FooErrorHandler {

	@ExceptionHandler
	@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
	@JsonView(Views.View1.class)
	public ResponseEntity<FooBean> storeAssignmentPublishingError(Exception e) {
		return new ResponseEntity<>(new FooBean("INTERNAL_SERVER_ERROR",500), HttpStatus.INTERNAL_SERVER_ERROR);
	}

	@ExceptionHandler
	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
	@JsonView(Views.View2.class)
	public ResponseEntity<FooBean> storeAssignmentPublishingError(CustomException e) {
		return new ResponseEntity<>(new FooBean("BAD Request",400), HttpStatus.BAD_REQUEST);
	}
}
```
//Existing response. Documentation is not generated as per `@JsonView` annotation 

```json
 "responses":{
               "500":{
                  "description":"Internal Server Error",
                  "content":{
                     "*/*":{
                        "schema":{
                           "$ref":"#/components/schemas/FooBean"
                        }
                     }
                  }
               },
               "400":{
                  "description":"Bad Request",
                  "content":{
                     "*/*":{
                        "schema":{
                           "$ref":"#/components/schemas/FooBean"
                        }
                     }
                  }
               },
               "200":{
                  "description":"OK",
                  "content":{
                     "*/*":{
                        "schema":{
                           "type":"string"
                        }
                     }
                  }
               }
            },
       "components":{
         "schemas":{
                 "FooBean":{
                    "type":"object",
                    "properties":{
                       "message":{
                          "type":"string"
                       },
                       "code":{
                          "type":"integer",
                          "format":"int32"
                       }
                    }
                 }
              }
          }
```
//After this PR
In the below example, springdoc generates the different schema (**#/components/schemas/FooBean_View1** ,  **#/components/schemas/FooBean_View2**) for error responses. 

```json
 "responses":{
          "500":{
            "description":"Internal Server Error",
            "content":{
              "*/*":{
                "schema":{
                  "$ref":"#/components/schemas/FooBean_View1"
                }
              }
            }
          },
          "400":{
            "description":"Bad Request",
            "content":{
              "*/*":{
                "schema":{
                  "$ref":"#/components/schemas/FooBean_View2"
                }
              }
            }
          },
          "200":{
            "description":"OK",
            "content":{
              "*/*":{
                "schema":{
                  "type":"string"
                }
              }
            }
          }
        },
       "components":{
          "schemas":{
            "FooBean_View1":{
              "type":"object",
              "properties":{
                "code":{
                  "type":"integer",
                  "format":"int32"
                }
              }
            },
            "FooBean_View2":{
              "type":"object",
              "properties":{
                "message":{
                  "type":"string"
                },
                "code":{
                  "type":"integer",
                  "format":"int32"
                }
              }
            }
          }
        }
```




Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
index d4ea9f76a..0032db85e 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
@@ -165,6 +165,8 @@ public class GenericResponseService {
 					ApiResponses apiResponsesOp = new ApiResponses();
 					MethodAttributes methodAttributes = new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(),
 							springDocConfigProperties.getDefaultProducesMediaType(), controllerAdviceInfoApiResponseMap);
+					//calculate JsonView Annotation
+					methodAttributes.setJsonViewAnnotation(AnnotatedElementUtils.findMergedAnnotation(method, JsonView.class));
 					Map<String, ApiResponse> apiResponses = computeResponseFromDoc(components, methodParameter, apiResponsesOp, methodAttributes);
 					buildGenericApiResponses(components, methodParameter, apiResponsesOp, methodAttributes);
 					apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);


Test patch:
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CustomException.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CustomException.java
new file mode 100644
index 000000000..de9cec8be
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CustomException.java
@@ -0,0 +1,7 @@
+package test.org.springdoc.api.app157;
+
+public class CustomException extends RuntimeException{
+    public CustomException(String message) {
+        super(message);
+    }
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooBean.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooBean.java
new file mode 100644
index 000000000..48231d72d
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooBean.java
@@ -0,0 +1,31 @@
+package test.org.springdoc.api.app157;
+
+import com.fasterxml.jackson.annotation.JsonView;
+
+public class FooBean {
+    @JsonView(Views.View2.class)
+    private String message;
+    @JsonView(Views.View1.class)
+    private int code;
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public FooBean(String message, int code) {
+        this.message = message;
+        this.code = code;
+    }
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooErrorHandler.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooErrorHandler.java
new file mode 100644
index 000000000..c94813fce
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/FooErrorHandler.java
@@ -0,0 +1,26 @@
+package test.org.springdoc.api.app157;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ControllerAdvice(assignableTypes = HelloController.class)
+public class FooErrorHandler {
+
+	@ExceptionHandler
+	@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
+	@JsonView(Views.View1.class)
+	public ResponseEntity<FooBean> storeAssignmentPublishingError(Exception e) {
+		return new ResponseEntity<>(new FooBean("INTERNAL_SERVER_ERROR",500), HttpStatus.INTERNAL_SERVER_ERROR);
+	}
+
+	@ExceptionHandler
+	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
+	@JsonView(Views.View2.class)
+	public ResponseEntity<FooBean> storeAssignmentPublishingError(CustomException e) {
+		return new ResponseEntity<>(new FooBean("BAD Request",400), HttpStatus.BAD_REQUEST);
+	}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java
new file mode 100644
index 000000000..20f84e557
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java
@@ -0,0 +1,22 @@
+package test.org.springdoc.api.app157;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api")
+@OpenAPIDefinition(info = @Info(title = "API Examples", version = "1.0"), tags = @Tag(name = "Operations"))
+public class HelloController {
+
+	@PostMapping("/foo")
+	public String create(@RequestBody String foo) {
+		return "foo";
+	}
+}
+
+
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java
new file mode 100644
index 000000000..e97b838fc
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java
@@ -0,0 +1,12 @@
+package test.org.springdoc.api.app157;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import test.org.springdoc.api.AbstractSpringDocTest;
+
+
+public class SpringDocApp157Test extends AbstractSpringDocTest {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {}
+
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/Views.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/Views.java
new file mode 100644
index 000000000..bdf6c61f5
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/Views.java
@@ -0,0 +1,8 @@
+package test.org.springdoc.api.app157;
+
+public class Views {
+    public static class View1 {
+    }
+    public static class View2 extends View1 {
+    }
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json
new file mode 100644
index 000000000..b8efc40ca
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json
@@ -0,0 +1,95 @@
+{
+  "openapi":"3.0.1",
+  "info":{
+    "title":"API Examples",
+    "version":"1.0"
+  },
+  "servers":[
+    {
+      "url":"http://localhost",
+      "description":"Generated server url"
+    }
+  ],
+  "tags":[
+    {
+      "name":"Operations"
+    }
+  ],
+  "paths":{
+    "/api/foo":{
+      "post":{
+        "tags":[
+          "hello-controller"
+        ],
+        "operationId":"create",
+        "requestBody":{
+          "content":{
+            "application/json":{
+              "schema":{
+                "type":"string"
+              }
+            }
+          },
+          "required":true
+        },
+        "responses":{
+          "500":{
+            "description":"Internal Server Error",
+            "content":{
+              "*/*":{
+                "schema":{
+                  "$ref":"#/components/schemas/FooBean_View1"
+                }
+              }
+            }
+          },
+          "400":{
+            "description":"Bad Request",
+            "content":{
+              "*/*":{
+                "schema":{
+                  "$ref":"#/components/schemas/FooBean_View2"
+                }
+              }
+            }
+          },
+          "200":{
+            "description":"OK",
+            "content":{
+              "*/*":{
+                "schema":{
+                  "type":"string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components":{
+    "schemas":{
+      "FooBean_View1":{
+        "type":"object",
+        "properties":{
+          "code":{
+            "type":"integer",
+            "format":"int32"
+          }
+        }
+      },
+      "FooBean_View2":{
+        "type":"object",
+        "properties":{
+          "message":{
+            "type":"string"
+          },
+          "code":{
+            "type":"integer",
+            "format":"int32"
+          }
+        }
+      }
+    }
+  }
+}