diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/ApiResponse.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/ApiResponse.java index 9a9ed97ba4..5a5af16036 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/ApiResponse.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/responses/ApiResponse.java @@ -96,4 +96,11 @@ **/ String ref() default ""; + /** + * Set to true to resolve the response schema from method return type + * + * @since 2.2.0 + **/ + boolean useReturnTypeSchema() default false; + } diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java index e48ff1cb12..95098f0427 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java @@ -1076,10 +1076,11 @@ protected Operation parseMethod( } final Class subResource = getSubResourceWithJaxRsSubresourceLocatorSpecs(method); + Schema returnTypeSchema = null; if (!shouldIgnoreClass(returnType.getTypeName()) && !method.getGenericReturnType().equals(subResource)) { ResolvedSchema resolvedSchema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(returnType).resolveAsRef(true).jsonViewAnnotation(jsonViewAnnotation)); if (resolvedSchema.schema != null) { - Schema returnTypeSchema = resolvedSchema.schema; + returnTypeSchema = resolvedSchema.schema; Content content = new Content(); MediaType mediaType = new MediaType().schema(returnTypeSchema); AnnotationsUtils.applyTypes(classProduces == null ? new String[0] : classProduces.value(), @@ -1112,18 +1113,62 @@ protected Operation parseMethod( } } if (operation.getResponses() == null || operation.getResponses().isEmpty()) { - Content content = new Content(); - MediaType mediaType = new MediaType(); - AnnotationsUtils.applyTypes(classProduces == null ? new String[0] : classProduces.value(), - methodProduces == null ? new String[0] : methodProduces.value(), content, mediaType); + Content content = resolveEmptyContent(classProduces, methodProduces); ApiResponse apiResponseObject = new ApiResponse().description(DEFAULT_DESCRIPTION).content(content); operation.setResponses(new ApiResponses()._default(apiResponseObject)); } + if (returnTypeSchema != null) { + resolveResponseSchemaFromReturnType(operation, classResponses, returnTypeSchema, classProduces, methodProduces); + if (apiResponses != null) { + resolveResponseSchemaFromReturnType( + operation, + apiResponses.stream().toArray(io.swagger.v3.oas.annotations.responses.ApiResponse[]::new), + returnTypeSchema, + classProduces, + methodProduces); + } + } + return operation; } + protected Content resolveEmptyContent(Produces classProduces, Produces methodProduces) { + Content content = new Content(); + MediaType mediaType = new MediaType(); + AnnotationsUtils.applyTypes(classProduces == null ? new String[0] : classProduces.value(), + methodProduces == null ? new String[0] : methodProduces.value(), content, mediaType); + return content; + } + + protected void resolveResponseSchemaFromReturnType( + Operation operation, + io.swagger.v3.oas.annotations.responses.ApiResponse[] responses, + Schema schema, + Produces classProduces, Produces methodProduces) { + if (responses != null) { + for (io.swagger.v3.oas.annotations.responses.ApiResponse response: responses) { + if (response.useReturnTypeSchema()) { + ApiResponse opResponse = operation.getResponses().get(response.responseCode()); + if (opResponse != null) { + if (opResponse.getContent() != null) { + for (MediaType mediaType : opResponse.getContent().values()) { + mediaType.schema(schema); + } + } else { + Content content = resolveEmptyContent(classProduces, methodProduces); + for (MediaType mediaType : content.values()) { + mediaType.schema(schema); + } + opResponse.content(content); + } + } + } + } + } + } + private boolean shouldIgnoreClass(String className) { if (StringUtils.isBlank(className)) { return true; diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java index 6098ee5746..742141b118 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java @@ -13,6 +13,7 @@ import io.swagger.v3.core.model.ApiDescription; import io.swagger.v3.core.util.PrimitiveType; import io.swagger.v3.jaxrs2.matchers.SerializationMatchers; +import io.swagger.v3.jaxrs2.resources.ResponseReturnTypeResource; import io.swagger.v3.jaxrs2.resources.SchemaPropertiesResource; import io.swagger.v3.jaxrs2.resources.SingleExampleResource; import io.swagger.v3.jaxrs2.resources.BasicFieldsResource; @@ -2962,4 +2963,71 @@ public void testSchemaProperties() { " format: int32\n"; SerializationMatchers.assertEqualsToYaml(openAPI, yaml); } + + @Test(description = "Responses schema resolved from return type") + public void testResponseReturnType() { + Reader reader = new Reader(new OpenAPI()); + + OpenAPI openAPI = reader.read(ResponseReturnTypeResource.class); + String yaml = "openapi: 3.0.1\n" + + "paths:\n" + + " /sample/{id}:\n" + + " get:\n" + + " summary: Find by id\n" + + " description: Find by id operation\n" + + " operationId: find\n" + + " parameters:\n" + + " - name: id\n" + + " in: path\n" + + " description: ID\n" + + " required: true\n" + + " schema:\n" + + " type: integer\n" + + " format: int32\n" + + " responses:\n" + + " \"200\":\n" + + " description: Ok\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " $ref: '#/components/schemas/TestDTO'\n" + + " \"201\":\n" + + " description: \"201\"\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " $ref: '#/components/schemas/TestDTO'\n" + + " \"204\":\n" + + " description: No Content\n" + + " content:\n" + + " application/json: {}\n" + + " /sample/{id}/default:\n" + + " get:\n" + + " summary: Find by id (default)\n" + + " description: Find by id operation (default)\n" + + " operationId: findDefault\n" + + " parameters:\n" + + " - name: id\n" + + " in: path\n" + + " description: ID\n" + + " required: true\n" + + " schema:\n" + + " type: integer\n" + + " format: int32\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " $ref: '#/components/schemas/TestDTO'\n" + + "components:\n" + + " schemas:\n" + + " TestDTO:\n" + + " type: object\n" + + " properties:\n" + + " foo:\n" + + " type: string"; + SerializationMatchers.assertEqualsToYaml(openAPI, yaml); + } } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/ResponseReturnTypeResource.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/ResponseReturnTypeResource.java new file mode 100644 index 0000000000..1bb16211fc --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/ResponseReturnTypeResource.java @@ -0,0 +1,42 @@ +package io.swagger.v3.jaxrs2.resources; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@Path("/sample") +public interface ResponseReturnTypeResource { + + @GET + @Path("/{id}") + @Operation(summary = "Find by id", description = "Find by id operation") + @ApiResponse(responseCode = "200", description = "Ok", useReturnTypeSchema = true) + @ApiResponse(responseCode = "201", + description = "201", + useReturnTypeSchema = true, + content = @Content(mediaType = "application/json")) + @ApiResponse(responseCode = "204", + description = "No Content", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = Void.class))) + TestDTO find(@Parameter(description = "ID") @PathParam("id") Integer id); + + @GET + @Path("/{id}/default") + @Operation(summary = "Find by id (default)", description = "Find by id operation (default)") + TestDTO findDefault(@Parameter(description = "ID") @PathParam("id") Integer id); + + class TestDTO { + public String foo; + } +}