0

I am documenting an existing Java based API using SpringDoc. There is a common response object for all of our APIs (shown below) which has two fields, a status and an object containing the actual result.

class MyResponse {
    private String status;
    private Object result; 
.........
}

Is there a way to allow me to document the actual runtime type of the result object depending on the API that is called? e.g. if my call is to the getCustomer() API, I wish to generate documentation for a Customer result object, and if my call is to the getProduct() API, I wish to generate documentation for a Product result object.

Kevin
  • 1
  • 1
  • 2

2 Answers2

1

You should use the @ApiResponse to document the type of data that will be returned. Following are the annotations of your interest -

  • @ApiResponses - Represents the array of responses a method might return, including error responses. The common set of responses can be specified at the class level while the specific once can be provided at the method level.

    • value - An array of @ApiResponse.
  • @ApiResponse - Used to describe a specific response that a method might return. Can not be used at the class level.

    • responseCode - A String value representing the response code, such as "200", "401" etc.
    • description - Description of the response. Usually the HTTP Status Message, such as "OK", "Unauthorized" etc.
    • content - Describes the content that will be returned by the method. Refer the @Content annotation for details.

    Important - Responses such as 401 - Unauthorized may not return anything. In such cases, content should be initialed as an empty @Schema as shown below.

    @ApiResponse(responseCode = "401", description = "Unauthorized", content = {@Content(schema = @Schema())})
    
  • @Content - Describes the content that will be returned as a response by the method.

    • mediaType - Specifies the type of object that will be returned by the API. Mandatory to be specified, if a valid response is returned. Can be defined at the class level. Examples application/json or text/plain etc.
    • schema - The @Schema object that will be returned as a response from the method. Refer @Schema for more details.
  • @Schema - Describes the schema object (POJO or can even be a primitive datatype, set the mediaType accordingly) that will be returned as a response. Not specifying any attributes means nothing will be returned from the object (often used with error responses).

    • implementation - Name of the class object that will be returned as a response from the method. Defaults to Void.class
  • @Parameter - Used at the method parameters with other annotations such as @RequestParam, @PathVariable etc.

    • description - Describes the parameter that is expected.
    • required - Boolean value specifying if the parameter is optional or mandatory. Defaults to the one specified by the parameter such as @RequestParam, @PathVariable etc.
  • @io.swagger.v3.oas.annotations.parameters.RequestBody - Describes the request body expected by the method handling the request.

    • description - Provides a description for the Request Body.
    • required - Boolean value specifying if the body is optional or mandatory. Defaults to true.

    Remember

    • Although @Parameter can also be used instead of this, in that case, all the class obejcts are resolved by reference, thus only the one described last is retained.
    • This @RequestBody is different from the one provided by the Spring, and thus must be used along with the @org.springframework.web.bind.annotation.RequestBody

Below is an example of a controller with the required annotations for documentation.

@RestController

// the "produces" attribute in @RequestMapping can be used to specify the default mediaType.
@RequestMapping(path = "/api/v1/user/", produces = { MediaType.APPLICATION_JSON_VALUE })

// Defines common tag for all the operartions handled by this class
@Tag(name = "User Operations", description = "APIs for operation on User")

// API responses that might be returned by all the methods in this class
@ApiResponses(value = {
        @ApiResponse(responseCode = "400", description = "Bad Request", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = UserErrorResponse.class))}),
        @ApiResponse(responseCode = "500", description = "Internal Server Error", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = UserErrorResponse.class))})
})
public class UserController {

    // POST Method

    // Swagger Annotations
    @Operation(summary = "Create User", description = "User-ID is generated and maintained by the service.", tags = {"User Operations"})
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "Created", content = {@Content(mediaType = "application/json", schema = @Schema(implementation = UserCreateResponse.class))}),
            @ApiResponse(responseCode = "409", description = "User Collision Detected", content = {@Content(schema = @Schema())})
    })

    // Spring Annotations
    @ResponseStatus(code = HttpStatus.CREATED)
    @PostMapping(value = "/patients", consumes = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<MyResponse> createUser(
        // Note the two @RequestBody from Swagger and Spring
        @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A request to create user object") @Valid @RequestBody final User user)
    {
        ...
    }


    // GET Method

    // Swagger Annotations
    @ApiResponses(value = {
            @ApiResponse(
                responseCode = "200", description = "OK", 
                // Note the way content is defined for valid objects
                content = {@Content(mediaType = "application/json", schema = @Schema(implementation = User.class))})),

            @ApiResponse(
                responseCode = "404", description = "Not Found",
                // Note the way, when no object is returned
                content = {@Content(schema = @Schema())})),
    })

    // Spring Annotations
    @ResponseStatus(HttpStatus.OK)
    @GetMapping(value = "")
    public MyResponse getUser(
            @Parameter(required = false, description = "Search by firstName") @RequestParam(required = false) String firstName) 
    {
        ...
    }
}
Debargha Roy
  • 2,320
  • 1
  • 15
  • 34
0

it sounds like you should use a generic type parameter instead of the Object type. Thing is, Springdoc only knows what you tell it, and you can inform it like Debargha suggests. However, that can get pretty messy and not very maintainable. Fortunately, it has an auto-detection feature, which can be isolated to different base API paths (i.e. "..api/v1/products../.."), which does take into account parent classes, and correctly generates a schema for them.

I had a similar issue yesterday and found a pretty compact solution for declaring Open Api schemas for complex, generic and/or nested java types. The full description is here: Annotation of Generic Java Types and Nested Arrays for Open Api Documentation

The TL:DR; is to register a custom openApiGroup in an @OpenAPIDefinition annotated class and then make a standard Spring Boot @Configuration class. In this second class you put empty classes, annotated with @Schema (desc. optional) extending the combinations of MyResponse, MyResponse<List> etc. Any endpoint extending from the path of the custom group, can now use those combinations as return types - not the name of the empty class, but the actual thing (e.g. List) and springdoc will pick it up.