diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java index 3313f65fc..a2a7d9b45 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java @@ -618,16 +618,20 @@ public void applyBeanValidatorAnnotations(final Parameter parameter, final List< */ public void applyBeanValidatorAnnotations(final RequestBody requestBody, final List annotations, boolean isOptional) { Map annos = new HashMap<>(); - boolean requestBodyRequired = false; + boolean springRequestBodyRequired = false; + boolean swaggerRequestBodyRequired = false; if (!CollectionUtils.isEmpty(annotations)) { annotations.forEach(annotation -> annos.put(annotation.annotationType().getSimpleName(), annotation)); - requestBodyRequired = annotations.stream() + springRequestBodyRequired = annotations.stream() .filter(annotation -> org.springframework.web.bind.annotation.RequestBody.class.equals(annotation.annotationType())) .anyMatch(annotation -> ((org.springframework.web.bind.annotation.RequestBody) annotation).required()); + swaggerRequestBodyRequired = annotations.stream() + .filter(annotation -> io.swagger.v3.oas.annotations.parameters.RequestBody.class.equals(annotation.annotationType())) + .anyMatch(annotation -> ((io.swagger.v3.oas.annotations.parameters.RequestBody) annotation).required()); } boolean validationExists = Arrays.stream(ANNOTATIONS_FOR_REQUIRED).anyMatch(annos::containsKey); - if (validationExists || (!isOptional && requestBodyRequired)) + if (validationExists || (!isOptional && (springRequestBodyRequired || swaggerRequestBodyRequired))) requestBody.setRequired(true); Content content = requestBody.getContent(); for (MediaType mediaType : content.values()) { @@ -766,14 +770,25 @@ private boolean isRequestBodyParam(RequestMethod requestMethod, ParameterInfo pa return (isBodyAllowed && (parameterInfo.getParameterModel() == null || parameterInfo.getParameterModel().getIn() == null) && !delegatingMethodParameter.isParameterObject()) && - ((methodParameter.getParameterAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class) != null - || methodParameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestBody.class) != null - || AnnotatedElementUtils.findMergedAnnotation(Objects.requireNonNull(methodParameter.getMethod()), io.swagger.v3.oas.annotations.parameters.RequestBody.class) != null) + (checkRequestBodyAnnotation(methodParameter) || checkOperationRequestBody(methodParameter) || checkFile(methodParameter) || Arrays.asList(methodAttributes.getMethodConsumes()).contains(MULTIPART_FORM_DATA_VALUE)); } + /** + * Checks whether Swagger's or Spring's RequestBody annotation is present on a parameter or method + * + * @param methodParameter the method parameter + * @return the boolean + */ + private boolean checkRequestBodyAnnotation(MethodParameter methodParameter) { + return methodParameter.hasParameterAnnotation(org.springframework.web.bind.annotation.RequestBody.class) + || methodParameter.hasParameterAnnotation(io.swagger.v3.oas.annotations.parameters.RequestBody.class) + || AnnotatedElementUtils.isAnnotated(Objects.requireNonNull(methodParameter.getParameter()), io.swagger.v3.oas.annotations.parameters.RequestBody.class) + || AnnotatedElementUtils.isAnnotated(Objects.requireNonNull(methodParameter.getMethod()), io.swagger.v3.oas.annotations.parameters.RequestBody.class); + } + /** * Check file boolean. * diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230Controller.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230Controller.java new file mode 100644 index 000000000..de37d116f --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230Controller.java @@ -0,0 +1,22 @@ +package test.org.springdoc.api.v30.app230; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author edudar + */ +@RestController +@RequestMapping +public class App230Controller { + + @PostMapping("/") + public String swaggerTest(@App230RequestBody MyRequest myRequest) { + return null; + } + + public record MyRequest(int id) { + } + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230RequestBody.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230RequestBody.java new file mode 100644 index 000000000..a275a0a7b --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/App230RequestBody.java @@ -0,0 +1,21 @@ +package test.org.springdoc.api.v30.app230; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.swagger.v3.oas.annotations.parameters.RequestBody; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; + +/** + * @author edudar + */ +@Target({PARAMETER, METHOD, ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@RequestBody(required = true) +@interface App230RequestBody { + +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/SpringDocApp230Test.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/SpringDocApp230Test.java new file mode 100644 index 000000000..981e3e664 --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app230/SpringDocApp230Test.java @@ -0,0 +1,40 @@ +/* + * + * * + * * * + * * * * + * * * * * + * * * * * * Copyright 2019-2024 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 test.org.springdoc.api.v30.app230; + +import test.org.springdoc.api.v30.AbstractSpringDocV30Test; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author edudar + */ +public class SpringDocApp230Test extends AbstractSpringDocV30Test { + + @SpringBootApplication + static class SpringDocTestApp {} +} diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app230.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app230.json new file mode 100644 index 000000000..5d328c26d --- /dev/null +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app230.json @@ -0,0 +1,58 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://localhost", + "description": "Generated server url" + } + ], + "paths": { + "/": { + "post": { + "tags": [ + "app-230-controller" + ], + "operationId": "swaggerTest", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "MyRequest": { + "properties": { + "id": { + "format": "int32", + "type": "integer" + } + }, + "type": "object" + } + } + } +}