From 4b507868f21919a2f7ffffa105a5b59d8d59c3e6 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Sat, 15 Nov 2025 22:00:16 +0100 Subject: [PATCH] Change so that useReturnTypeSchema is considered on an http code level rather than on a method level --- .../core/models/MethodAttributes.java | 15 ++-- .../core/service/GenericResponseService.java | 55 +++++++++---- .../api/v30/app226/HelloController.java | 43 +++++++--- .../api/v31/app226/HelloController.java | 43 +++++++--- .../test/resources/results/3.0.1/app226.json | 23 +++++- .../test/resources/results/3.1.0/app226.json | 81 +++++++++++-------- 6 files changed, 177 insertions(+), 83 deletions(-) diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java index 8baac19d9..0f4b99e1f 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/MethodAttributes.java @@ -134,7 +134,7 @@ public class MethodAttributes { /** * The Use return type schema. */ - private boolean useReturnTypeSchema; + private final Map useReturnTypeSchema = new LinkedHashMap<>(); /** * Instantiates a new Method attributes. @@ -529,20 +529,21 @@ public Locale getLocale() { } /** - * Is use return type schema boolean. + * Gets use return type schema. * - * @return the boolean + * @return the use return type schema */ - public boolean isUseReturnTypeSchema() { + public Map getUseReturnTypeSchema() { return useReturnTypeSchema; } /** - * Sets use return type schema. + * Put use return type schema. * + * @param responseCode the response code * @param useReturnTypeSchema the use return type schema */ - public void setUseReturnTypeSchema(boolean useReturnTypeSchema) { - this.useReturnTypeSchema = useReturnTypeSchema; + public void putUseReturnTypeSchema(String responseCode, boolean useReturnTypeSchema) { + this.useReturnTypeSchema.put(responseCode, useReturnTypeSchema); } } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java index 2a0542872..c5e6c3197 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericResponseService.java @@ -180,22 +180,22 @@ public GenericResponseService(OperationService operationService, * @param components the components * @param apiResponsesOp the api responses op * @param methodAttributes the method attributes - * @param apiResponseAnnotations the api response annotations + * @param apiResponseAnnotation the api response annotation * @param apiResponse the api response * @param openapi31 the openapi 31 */ public static void buildContentFromDoc(Components components, ApiResponses apiResponsesOp, MethodAttributes methodAttributes, - io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations, + io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotation, ApiResponse apiResponse, boolean openapi31) { - methodAttributes.setUseReturnTypeSchema(apiResponseAnnotations.useReturnTypeSchema()); - io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotations.content(); + methodAttributes.putUseReturnTypeSchema(apiResponseAnnotation.responseCode(), apiResponseAnnotation.useReturnTypeSchema()); + io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotation.content(); Optional optionalContent = getContent(contentdoc, new String[0], methodAttributes.getMethodProduces(), null, components, methodAttributes.getJsonViewAnnotation(), openapi31); - if (apiResponsesOp.containsKey(apiResponseAnnotations.responseCode())) { + if (apiResponsesOp.containsKey(apiResponseAnnotation.responseCode())) { // Merge with the existing content - Content existingContent = apiResponsesOp.get(apiResponseAnnotations.responseCode()).getContent(); + Content existingContent = apiResponsesOp.get(apiResponseAnnotation.responseCode()).getContent(); if (optionalContent.isPresent()) { Content newContent = optionalContent.get(); if (methodAttributes.isMethodOverloaded() && existingContent != null) { @@ -387,17 +387,17 @@ private Map computeResponseFromDoc(Components components, M Set responsesArray = getApiResponses(Objects.requireNonNull(methodParameter.getMethod())); if (!responsesArray.isEmpty()) { methodAttributes.setWithApiResponseDoc(true); - for (io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations : responsesArray) { - String httpCode = apiResponseAnnotations.responseCode(); + for (io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotation : responsesArray) { + String httpCode = apiResponseAnnotation.responseCode(); ApiResponse apiResponse = new ApiResponse(); - if (StringUtils.isNotBlank(apiResponseAnnotations.ref())) { - apiResponse.$ref(apiResponseAnnotations.ref()); - apiResponsesOp.addApiResponse(apiResponseAnnotations.responseCode(), apiResponse); + if (StringUtils.isNotBlank(apiResponseAnnotation.ref())) { + apiResponse.$ref(apiResponseAnnotation.ref()); + apiResponsesOp.addApiResponse(apiResponseAnnotation.responseCode(), apiResponse); continue; } - apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description(), methodAttributes.getLocale())); - buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotations, apiResponse, openapi31); - Map extensions = AnnotationsUtils.getExtensions(propertyResolverUtils.isOpenapi31(), apiResponseAnnotations.extensions()); + apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotation.description(), methodAttributes.getLocale())); + buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotation, apiResponse, openapi31); + Map extensions = AnnotationsUtils.getExtensions(propertyResolverUtils.isOpenapi31(), apiResponseAnnotation.extensions()); if (!CollectionUtils.isEmpty(extensions)) { if (propertyResolverUtils.isResolveExtensionsProperties()) { Map extensionsResolved = propertyResolverUtils.resolveExtensions(locale, extensions); @@ -407,7 +407,7 @@ private Map computeResponseFromDoc(Components components, M apiResponse.extensions(extensions); } } - SpringDocAnnotationsUtils.getHeaders(apiResponseAnnotations.headers(), components, methodAttributes.getJsonViewAnnotation(), openapi31) + SpringDocAnnotationsUtils.getHeaders(apiResponseAnnotation.headers(), components, methodAttributes.getJsonViewAnnotation(), openapi31) .ifPresent(apiResponse::headers); apiResponsesOp.addApiResponse(httpCode, apiResponse); } @@ -622,8 +622,7 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent())) setDescription(httpCode, apiResponse); } } - if (apiResponse.getContent() != null && (methodAttributes.isUseReturnTypeSchema() || - ((isGeneric || methodAttributes.isMethodOverloaded()) && methodAttributes.isNoApiResponseDoc()))) { + if (apiResponse.getContent() != null && shouldCalculateContent(methodAttributes, isGeneric, httpCode)) { // Merge with existing schema Content existingContent = apiResponse.getContent(); Type type = GenericTypeResolver.resolveType(methodParameter.getGenericParameterType(), methodParameter.getContainingClass()); @@ -642,6 +641,28 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent())) apiResponsesOp.addApiResponse(httpCode, apiResponse); } + /** + * Whether to consider calculating additional content. + * + * @param methodAttributes the method attributes + * @param isGeneric the is generic + * @param httpCode the http code + */ + private boolean shouldCalculateContent(MethodAttributes methodAttributes, boolean isGeneric, String httpCode) { + return useReturnTypeSchema(methodAttributes, httpCode) || + ((isGeneric || methodAttributes.isMethodOverloaded()) && methodAttributes.isNoApiResponseDoc()); + } + + /** + * Whether to use return type schema. + * + * @param methodAttributes the method attributes + * @param httpCode the http code + */ + private boolean useReturnTypeSchema(MethodAttributes methodAttributes, String httpCode) { + return methodAttributes.getUseReturnTypeSchema().getOrDefault(httpCode, false); + } + /** * Evaluate response status string. * diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app226/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app226/HelloController.java index 58d6817fe..f226156a6 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app226/HelloController.java +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app226/HelloController.java @@ -4,8 +4,10 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,20 +19,35 @@ @RequestMapping public class HelloController { + public record Error(String message) { + + } + @PostMapping("/testBoolean") - @ApiResponse( - useReturnTypeSchema = true, - responseCode = "200", - description = "OK", - content = { - @Content( - mediaType = "*/*", - examples = - @ExampleObject( - name = "success", - value = "...")) - } - ) + @ApiResponses(value = { + @ApiResponse( + useReturnTypeSchema = true, + responseCode = "200", + description = "OK", + content = { + @Content( + mediaType = "*/*", + examples = + @ExampleObject( + name = "success", + value = "...")) + } + ), + @ApiResponse( + responseCode = "400", + description = "OK", + content = { + @Content( + mediaType = "*/*", + schema = @Schema(implementation = Error.class)) + } + ) + }) public Map HelloController() { return null; } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app226/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app226/HelloController.java index 47b6a8e79..4b47f8b0c 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app226/HelloController.java +++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app226/HelloController.java @@ -4,8 +4,10 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -17,20 +19,35 @@ @RequestMapping public class HelloController { + public record Error(String message) { + + } + @PostMapping("/testBoolean") - @ApiResponse( - useReturnTypeSchema = true, - responseCode = "200", - description = "OK", - content = { - @Content( - mediaType = "*/*", - examples = - @ExampleObject( - name = "success", - value = "...")) - } - ) + @ApiResponses(value = { + @ApiResponse( + useReturnTypeSchema = true, + responseCode = "200", + description = "OK", + content = { + @Content( + mediaType = "*/*", + examples = + @ExampleObject( + name = "success", + value = "...")) + } + ), + @ApiResponse( + responseCode = "400", + description = "OK", + content = { + @Content( + mediaType = "*/*", + schema = @Schema(implementation = Error.class)) + } + ) + }) public Map HelloController() { return null; } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app226.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app226.json index 55698a84d..5b5bd9ad0 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app226.json +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app226.json @@ -36,10 +36,31 @@ } } } + }, + "400" : { + "description" : "OK", + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Error" + } + } + } } } } } }, - "components": {} + "components": { + "schemas" : { + "Error" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + } + } + } + } + } } diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app226.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app226.json index f0053119b..a52e5de30 100644 --- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app226.json +++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app226.json @@ -1,45 +1,62 @@ { - "openapi": "3.1.0", - "info": { - "title": "OpenAPI definition", - "version": "v0" + "openapi" : "3.1.0", + "info" : { + "title" : "OpenAPI definition", + "version" : "v0" }, - "servers": [ - { - "url": "http://localhost", - "description": "Generated server url" - } - ], - "paths": { - "/testBoolean": { - "post": { - "tags": [ - "hello-controller" - ], - "operationId": "HelloController", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" + "servers" : [ { + "url" : "http://localhost", + "description" : "Generated server url" + } ], + "paths" : { + "/testBoolean" : { + "post" : { + "tags" : [ "hello-controller" ], + "operationId" : "HelloController", + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "*/*" : { + "schema" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" } }, - "examples": { - "success": { - "description": "success", - "value": "..." + "examples" : { + "success" : { + "description" : "success", + "value" : "..." } } } } + }, + "400" : { + "description" : "OK", + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/Error" + } + } + } } } } } }, - "components": {} -} + "components" : { + "schemas" : { + "Error" : { + "type" : "object", + "properties" : { + "message" : { + "type" : "string" + } + } + } + } + } +} \ No newline at end of file