Search code examples
spring-bootswaggerdocumentationspringdocoas3

Annotation of Generic Java Types and Nested Arrays for Open Api Documentation


I got a little Spring Boot web service that allows you to build and query graph models. In order to reliably integrate other services with this service, I'm trying to describe the code using the io.swagger.v3.oas.annotations to generate some oas3 documentation.

Issue

In order for the most efficient querying, some routes returns Map<T,R> where the keys just so happen to correspond to different ids, tags, etc. The types of these changes often enough, that it'd be an annoying amount of work to declare specific DTO's for every single combination. So I'm looking for a generic, general approach.

On top of this, any response is encapsulated in a generic DetaliedResponse type to facilitate including more information to the end user. That means that the final return-type declaration for the controller methods (routes) looks along these lines:

DetailedResponse<T>
Map<T,R>
T[][]

If there's nothing else to do but make non-generic DetailedResponse type declarations for all routes, then so be it, but I reckon I'm not the first trying to use springdoc on other things than "Book" or "Pet".

Additionally, I can see in the OAS3 specification, that this is possible going the other way https://swagger.io/docs/specification/data-models/dictionaries/

Anywho, if anyone can help me understand how to describe the 3 nested / generic types described above, or help me translate them into oas3 annotations (I can piece them together from there), it'd be much appreciated!

The exact dependency I'm using is:

    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.1.0</version>

Solution

  • Half-Way-There Solution

    Relying on the plugins' auto-scheme-detection feature, I created a custom OpenAPI group:

    @Configuration
    @OpenAPIDefinition
    public class DocsConfiguration {
    
        @Bean
        public GroupedOpenApi customOpenApi() {
            return GroupedOpenApi.builder()
                    .group("custom")
                    .pathsToMatch("/api/v1/**")
                    .build();
        }
    }
    

    matching all controllers (each controller path starts with /api/v1/...).

    Then, for each "abnormal" data type returned (which was every single one), I created a "Custom OpenAPI Specification Config) and declared an empty class tagged with @Schema extending what I was actually trying to make a Schema of:

    @Configuration
    public class COASConfig {
    
        ...
    
        @Schema(description = "Detailed response with Map of keys: Integer and values: List of EdgeDTO")
        public static class MapIntegerListEdgeDTO extends DetailedResponse<Map<Integer,List<EdgeDTO>>> {}
    
        ...
    
        @Schema(description = "Detailed response of the metadata for a model.")
        public static class DRModelMetadataDTO extends DetailedResponse<ModelMetaDataDTO>{}
    
       ...
    
        @Schema(description = "Detailed response of an array of travel branch options.")
        public static class ListTravelBranchOptionDTO extends DetailedResponse<List<TravelBranchOptionDTO>>{}
    
        @Schema(description = "Detailed response of a 2D array of travel branch options.")
        public static class List2DTravelBranchOptionDTO extends DetailedResponse<List<List<TravelBranchOptionDTO>>>{}
    
       }
    

    I've tested this, and it works (at least for the typescript code-generator aswell as swagger-ui) - works as in, it picks up each and every type returned correctly alongside everything else. Some values you'd otherwise specify in the @Content annotation, can be declared in the @GetMapping or at class level and springdoc picks it up too. In addition, relying on the auto-detection reduces the number of annotations per method, making it more concise, which is pretty neat:

    @Operation(summary = "...")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "..."),
            @ApiResponse(responseCode = "206", description = "Partial success"),
            @ApiResponse(responseCode = "500", description = "Internal model registry missing"),
            @ApiResponse(responseCode = "404", description = "No such model"),
            @ApiResponse(responseCode = "400", description = "No point ids provided"),
            @ApiResponse(responseCode = "400", description = "No such points in model")
    })
    @GetMapping("/{modelId}/edges")
    public @ResponseBody ResponseEntity<DetailedResponse<Map<Integer, List<EdgeDTO>>>>
    getEdgeSetForPoints(...)
    

    However, it is a compromise. It's not a catch-all solution and somehow it gives the code-gen issues with primitive arrays (T[]), so I've had to convert everything to Collection (mainly List). Furthermore, the docs generator is not able to discern that these empty classes are combinations of other schemas. This ain't that bad as everything is automated, but definitely not optimal, as the typescript code-gen does generate new unique types for most things, which might be an issue in the long run.

    It's good for now, but hardly scalable or maintainable since it doesn't preserve the convenience of generics. I don't think this is how you're supposed to handle it either, so if you got any suggestions to alternative solutions, I'm all ears :D