Skip to content

Commit 710ad02

Browse files
author
bnasslahsen
committed
Request Body cannot be configured as optional. Fixes #603
1 parent 156d93b commit 710ad02

31 files changed

+570
-186
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public abstract class AbstractRequestBuilder {
8585
private static final List<Class> PARAM_TYPES_TO_IGNORE = new ArrayList<>();
8686

8787
// using string litterals to support both validation-api v1 and v2
88-
private static final String[] ANNOTATIONS_FOR_REQUIRED = { NotNull.class.getName(), org.springframework.web.bind.annotation.RequestBody.class.getName(), "javax.validation.constraints.NotBlank", "javax.validation.constraints.NotEmpty" };
88+
private static final String[] ANNOTATIONS_FOR_REQUIRED = { NotNull.class.getName(), "javax.validation.constraints.NotBlank", "javax.validation.constraints.NotEmpty" };
8989

9090
private static final String POSITIVE_OR_ZERO = "javax.validation.constraints.PositiveOrZero";
9191

@@ -198,7 +198,7 @@ else if (!RequestMethod.GET.equals(requestMethod)) {
198198
requestBodyInfo.setRequestBody(operation.getRequestBody());
199199
requestBodyBuilder.calculateRequestBodyInfo(components, methodAttributes,
200200
parameterInfo, requestBodyInfo);
201-
applyBeanValidatorAnnotations(requestBodyInfo.getRequestBody(), parameterAnnotations);
201+
applyBeanValidatorAnnotations(requestBodyInfo.getRequestBody(), parameterAnnotations, methodParameter.isOptional());
202202
}
203203
customiseParameter(parameter, parameterInfo);
204204
}
@@ -372,12 +372,16 @@ private void applyBeanValidatorAnnotations(final Parameter parameter, final List
372372
applyValidationsToSchema(annos, schema);
373373
}
374374

375-
private void applyBeanValidatorAnnotations(final RequestBody requestBody, final List<Annotation> annotations) {
375+
private void applyBeanValidatorAnnotations(final RequestBody requestBody, final List<Annotation> annotations, boolean isOptional) {
376376
Map<String, Annotation> annos = new HashMap<>();
377377
if (annotations != null)
378378
annotations.forEach(annotation -> annos.put(annotation.annotationType().getName(), annotation));
379-
boolean annotationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
380-
if (annotationExists)
379+
boolean validationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey);
380+
boolean requestBodyRequired = annotations.stream()
381+
.filter(annotation -> org.springframework.web.bind.annotation.RequestBody.class.equals(annotation.annotationType()))
382+
.anyMatch(annotation -> ((org.springframework.web.bind.annotation.RequestBody) annotation).required());
383+
384+
if (validationExists || (!isOptional && requestBodyRequired) )
381385
requestBody.setRequired(true);
382386
Content content = requestBody.getContent();
383387
for (MediaType mediaType : content.values()) {

springdoc-openapi-common/src/main/java/org/springdoc/core/ControllerAdviceInfo.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@
55

66
import io.swagger.v3.oas.models.responses.ApiResponse;
77

8-
import org.springframework.web.bind.annotation.ControllerAdvice;
9-
108
public class ControllerAdviceInfo {
119

12-
private ControllerAdvice controllerAdvice;
10+
private Object controllerAdvice;
1311

1412
private Map<String, ApiResponse> apiResponseMap = new LinkedHashMap<>();
1513

16-
public ControllerAdviceInfo(ControllerAdvice controllerAdvice) {
14+
public ControllerAdviceInfo(Object controllerAdvice) {
1715
this.controllerAdvice = controllerAdvice;
1816
}
1917

20-
public ControllerAdvice getControllerAdvice() {
18+
public Object getControllerAdvice() {
2119
return controllerAdvice;
2220
}
2321

springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseBuilder.java

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.web.bind.annotation.ExceptionHandler;
5252
import org.springframework.web.bind.annotation.RequestMapping;
5353
import org.springframework.web.bind.annotation.ResponseStatus;
54+
import org.springframework.web.method.ControllerAdviceBean;
5455
import org.springframework.web.method.HandlerMethod;
5556

5657
import static org.springdoc.core.Constants.DEFAULT_DESCRIPTION;
@@ -59,7 +60,7 @@
5960
@SuppressWarnings("rawtypes")
6061
public class GenericResponseBuilder {
6162

62-
private final Map<String, ApiResponse> genericMapResponse = new LinkedHashMap<>();
63+
private List<ControllerAdviceInfo> controllerAdviceInfos = new ArrayList<>();
6364

6465
private final OperationBuilder operationBuilder;
6566

@@ -81,8 +82,7 @@ public GenericResponseBuilder(OperationBuilder operationBuilder, List<ReturnType
8182

8283
public ApiResponses build(Components components, HandlerMethod handlerMethod, Operation operation,
8384
MethodAttributes methodAttributes) {
84-
ApiResponses apiResponses = new ApiResponses();
85-
genericMapResponse.forEach(apiResponses::addApiResponse);
85+
ApiResponses apiResponses = methodAttributes.calculateGenericMapResponse(getGenericMapResponse(handlerMethod.getBeanType()));
8686
//Then use the apiResponses from documentation
8787
ApiResponses apiResponsesFromDoc = operation.getResponses();
8888
if (!CollectionUtils.isEmpty(apiResponsesFromDoc))
@@ -95,32 +95,31 @@ public ApiResponses build(Components components, HandlerMethod handlerMethod, Op
9595

9696
public void buildGenericResponse(Components components, Map<String, Object> findControllerAdvice) {
9797
// ControllerAdvice
98-
List<Method> methods = getMethods(findControllerAdvice);
99-
// for each one build ApiResponse and add it to existing responses
100-
for (Method method : methods) {
101-
if (!operationBuilder.isHidden(method)) {
102-
RequestMapping reqMappringMethod = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
103-
String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
104-
if (reqMappringMethod != null)
105-
methodProduces = reqMappringMethod.produces();
106-
Map<String, ApiResponse> apiResponses = computeResponse(components, new MethodParameter(method, -1), new ApiResponses(),
107-
new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType()), true);
108-
apiResponses.forEach(genericMapResponse::put);
109-
}
110-
}
111-
}
112-
113-
private List<Method> getMethods(Map<String, Object> findControllerAdvice) {
114-
List<Method> methods = new ArrayList<>();
11598
for (Map.Entry<String, Object> entry : findControllerAdvice.entrySet()) {
99+
List<Method> methods = new ArrayList<>();
116100
Object controllerAdvice = entry.getValue();
117101
// get all methods with annotation @ExceptionHandler
118102
Class<?> objClz = controllerAdvice.getClass();
119103
if (org.springframework.aop.support.AopUtils.isAopProxy(controllerAdvice))
120104
objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerAdvice);
105+
ControllerAdviceInfo controllerAdviceInfo = new ControllerAdviceInfo(controllerAdvice);
121106
Arrays.stream(objClz.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(ExceptionHandler.class)).forEach(methods::add);
107+
// for each one build ApiResponse and add it to existing responses
108+
for (Method method : methods) {
109+
if (!operationBuilder.isHidden(method)) {
110+
RequestMapping reqMappringMethod = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
111+
String[] methodProduces = { springDocConfigProperties.getDefaultProducesMediaType() };
112+
if (reqMappringMethod != null)
113+
methodProduces = reqMappringMethod.produces();
114+
Map<String, ApiResponse> controllerAdviceInfoApiResponseMap = controllerAdviceInfo.getApiResponseMap();
115+
Map<String, ApiResponse> apiResponses = computeResponse(components, new MethodParameter(method, -1), new ApiResponses(),
116+
new MethodAttributes(methodProduces, springDocConfigProperties.getDefaultConsumesMediaType(),
117+
springDocConfigProperties.getDefaultProducesMediaType(), controllerAdviceInfoApiResponseMap), true);
118+
apiResponses.forEach(controllerAdviceInfoApiResponseMap::put);
119+
}
120+
}
121+
controllerAdviceInfos.add(controllerAdviceInfo);
122122
}
123-
return methods;
124123
}
125124

126125
private Map<String, ApiResponse> computeResponse(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
@@ -130,7 +129,7 @@ private Map<String, ApiResponse> computeResponse(Components components, MethodPa
130129
if (!responsesArray.isEmpty()) {
131130
methodAttributes.setWithApiResponseDoc(true);
132131
if (!springDocConfigProperties.isOverrideWithGenericResponse())
133-
for (String key : genericMapResponse.keySet())
132+
for (String key : methodAttributes.getGenericMapResponse().keySet())
134133
apiResponsesOp.remove(key);
135134
for (io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations : responsesArray) {
136135
ApiResponse apiResponse = new ApiResponse();
@@ -181,7 +180,7 @@ private void buildContentFromDoc(Components components, ApiResponses apiResponse
181180

182181
private void buildApiResponses(Components components, MethodParameter methodParameter, ApiResponses apiResponsesOp,
183182
MethodAttributes methodAttributes, boolean isGeneric) {
184-
if (!CollectionUtils.isEmpty(apiResponsesOp) && (apiResponsesOp.size() != genericMapResponse.size() || isGeneric)) {
183+
if (!CollectionUtils.isEmpty(apiResponsesOp) && (apiResponsesOp.size() != methodAttributes.getGenericMapResponse().size() || isGeneric)) {
185184
// API Responses at operation and @ApiResponse annotation
186185
for (Map.Entry<String, ApiResponse> entry : apiResponsesOp.entrySet()) {
187186
String httpCode = entry.getKey();
@@ -194,7 +193,7 @@ private void buildApiResponses(Components components, MethodParameter methodPara
194193
// Use response parameters with no description filled - No documentation
195194
// available
196195
String httpCode = evaluateResponseStatus(methodParameter.getMethod(), methodParameter.getMethod().getClass(), isGeneric);
197-
ApiResponse apiResponse = genericMapResponse.containsKey(httpCode) ? genericMapResponse.get(httpCode)
196+
ApiResponse apiResponse = methodAttributes.getGenericMapResponse().containsKey(httpCode) ? methodAttributes.getGenericMapResponse().get(httpCode)
198197
: new ApiResponse();
199198
if (httpCode != null)
200199
buildApiResponses(components, methodParameter, apiResponsesOp, methodAttributes, httpCode, apiResponse,
@@ -345,4 +344,11 @@ else if (returnType instanceof ParameterizedType) {
345344
result = true;
346345
return result;
347346
}
347+
348+
private Map<String, ApiResponse> getGenericMapResponse(Class<?> beanType) {
349+
return controllerAdviceInfos.stream()
350+
.filter(controllerAdviceInfo -> new ControllerAdviceBean(controllerAdviceInfo.getControllerAdvice()).isApplicableToBeanType(beanType))
351+
.map(ControllerAdviceInfo::getApiResponseMap)
352+
.collect(LinkedHashMap::new, Map::putAll, Map::putAll);
353+
}
348354
}

springdoc-openapi-common/src/main/java/org/springdoc/core/MethodAttributes.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Map;
2424

2525
import com.fasterxml.jackson.annotation.JsonView;
26+
import io.swagger.v3.oas.models.responses.ApiResponse;
27+
import io.swagger.v3.oas.models.responses.ApiResponses;
2628
import org.apache.commons.lang3.ArrayUtils;
2729

2830
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -56,11 +58,14 @@ public class MethodAttributes {
5658

5759
private String[] methodConsumes = {};
5860

59-
public MethodAttributes(String[] methodProducesNew, String defaultConsumesMediaType, String defaultProducesMediaType) {
61+
private Map<String, ApiResponse> genericMapResponse = new LinkedHashMap<>();
62+
63+
public MethodAttributes(String[] methodProducesNew, String defaultConsumesMediaType, String defaultProducesMediaType, Map<String, ApiResponse> genericMapResponse) {
6064
this.methodProduces = methodProducesNew;
6165
this.defaultConsumesMediaType = defaultConsumesMediaType;
6266
this.defaultProducesMediaType = defaultProducesMediaType;
6367
this.headers = new LinkedHashMap<>();
68+
this.genericMapResponse = genericMapResponse;
6469
}
6570

6671
public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType) {
@@ -189,4 +194,15 @@ public void setJsonViewAnnotationForRequestBody(JsonView jsonViewAnnotationForRe
189194
public Map<String, String> getHeaders() {
190195
return headers;
191196
}
197+
198+
public ApiResponses calculateGenericMapResponse(Map<String, ApiResponse> genericMapResponse) {
199+
ApiResponses apiResponses = new ApiResponses();
200+
genericMapResponse.forEach(apiResponses::addApiResponse);
201+
this.genericMapResponse = genericMapResponse;
202+
return apiResponses;
203+
}
204+
205+
public Map<String, ApiResponse> getGenericMapResponse() {
206+
return genericMapResponse;
207+
}
192208
}

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/ErrorMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
import java.util.Arrays;
44
import java.util.Collections;

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/GlobalControllerAdvice.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
import java.util.ArrayList;
44
import java.util.List;
@@ -10,6 +10,7 @@
1010

1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
13+
1314
import org.springframework.http.HttpStatus;
1415
import org.springframework.http.MediaType;
1516
import org.springframework.http.ResponseEntity;
@@ -21,14 +22,12 @@
2122
import org.springframework.web.bind.MissingServletRequestParameterException;
2223
import org.springframework.web.bind.annotation.ControllerAdvice;
2324
import org.springframework.web.bind.annotation.ExceptionHandler;
24-
import org.springframework.web.bind.annotation.RequestMapping;
2525
import org.springframework.web.bind.annotation.ResponseStatus;
2626

2727

2828

29-
@ControllerAdvice()
30-
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
31-
public class GlobalControllerAdvice //extends ResponseEntityExceptionHandler
29+
@ControllerAdvice(assignableTypes = PersonController.class)
30+
public class GlobalControllerAdvice //extends ResponseEntityExceptionHandler
3231
{
3332
/**
3433
* Note use base class if you wish to leverage its handling.

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/Person.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
import javax.validation.constraints.Email;
44
import javax.validation.constraints.Max;
@@ -8,7 +8,6 @@
88
import javax.validation.constraints.Pattern;
99
import javax.validation.constraints.Size;
1010

11-
1211
import org.hibernate.validator.constraints.CreditCardNumber;
1312

1413

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/PersonController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
import java.util.ArrayList;
44
import java.util.List;

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/PersonController2.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
import java.util.ArrayList;
44
import java.util.List;

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app110/Problem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package sample;
1+
package test.org.springdoc.api.app110;
22

33
public class Problem {
44

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1-
package test.org.springdoc.api.app109;
1+
package test.org.springdoc.api.app110;
22

3+
import io.swagger.v3.oas.models.OpenAPI;
4+
import io.swagger.v3.oas.models.info.Info;
5+
import io.swagger.v3.oas.models.info.License;
36
import test.org.springdoc.api.AbstractSpringDocTest;
47

8+
import org.springframework.beans.factory.annotation.Value;
59
import org.springframework.boot.autoconfigure.SpringBootApplication;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.test.context.TestPropertySource;
12+
13+
@TestPropertySource(properties = {
14+
"application-description=description",
15+
"application-version=v1" })
16+
public class SpringDocApp110Test extends AbstractSpringDocTest {
617

7-
public class SpringDocApp109Test extends AbstractSpringDocTest {
818
@SpringBootApplication
9-
static class SpringDocTestApp {}
19+
static class SpringDocTestApp {
20+
21+
@Bean
22+
public OpenAPI customOpenAPI(@Value("${application-description}") String appDesciption, @Value("${application-version}") String appVersion) {
23+
24+
return new OpenAPI()
25+
.info(new Info()
26+
.title("sample application API")
27+
.version(appVersion)
28+
.description(appDesciption)
29+
.termsOfService("http://swagger.io/terms/")
30+
.license(new License().name("Apache 2.0").url("http://springdoc.org")));
31+
}
32+
}
1033
}

springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app111/ErrorMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package test.org.springdoc.api.app110;
1+
package test.org.springdoc.api.app111;
22

33
import java.util.Arrays;
44
import java.util.Collections;

0 commit comments

Comments
 (0)