2
votes

I've recently converted from Springfox to Springdoc-openapi for generating my OpenAPI for my Spring Boot Rest API service.

Everything was working perfectly until I added a security scheme. Once I did that, my schemes no longer appear and an error appears on the SwaggerUI page:

Could not resolve reference: Could not resolve pointer: /components/schemas/Ping does not exist in document

I am setting up my configuration programmatically, and have 2 groups.

I'm using Spring Boot v2.4.0 with springdoc-openapi-ui v1.5.1

Snippet of my pom.xml:

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-ui</artifactId>
            <version>1.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-hateoas</artifactId>
            <version>1.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-security</artifactId>
            <version>1.5.1</version>
        </dependency>

Snippet from configuration:

    @Bean
public GroupedOpenApi apiV1() {
    String[] paths = {"/v1/**"};
    String[] packagesToScan = {"com.test.controller"};
    return GroupedOpenApi.builder()
            .group("v1")
            .packagesToScan(packagesToScan)
            .pathsToMatch(paths)
            .addOpenApiCustomiser(buildV1OpenAPI())
            .build();
}

@Bean
public GroupedOpenApi apiV2() {
    String[] paths = {"/v2/**"};
    String[] packagesToScan = {"com.test.controller"};
    return GroupedOpenApi.builder()
            .group("v2")
            .packagesToScan(packagesToScan)
            .pathsToMatch(paths)
            .addOpenApiCustomiser(buildV2OpenAPI())
            .build();
}

public OpenApiCustomiser buildV1OpenAPI() {
    return openApi -> openApi.info(apiInfo().version("v1"));
}

public OpenApiCustomiser buildV2OpenAPI() {
    final String securitySchemeName = "Access Token";
    return openApi -> openApi.info(apiInfo().version("v2"))
            .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
            .components(new Components().addSecuritySchemes(securitySchemeName, new SecurityScheme()
                    .type(SecurityScheme.Type.APIKEY)
                    .in(SecurityScheme.In.HEADER)
                    .name(HttpHeaders.AUTHORIZATION)));
}

// Describe the apis
private Info apiInfo() {
    return new Info()
            .title("Title")
            .description("API Description");
}

For my v1 group, everything works fine. My Schemas appear on the Swagger UI page and I see them in the components section of the generated api-doc.

    "components": {
    "schemas": {
        "ApplicationErrorResponse": {
            ...
            }
        },
        "Ping": {
            ...
        }
    }
}

For my v2 group, the Schemas are not generated.

    "components": {
    "securitySchemes": {
        "Access Token": {
            "type": "apiKey",
            "name": "Authorization",
            "in": "header"
        }
    }
}

Any idea why my Schemas are not automatically scanned and added when adding the security scheme to the OpenAPI components programmatically? Am I missing something in my config?

Here's my request mapping in my controller.

@Operation(summary = "Verify API and backend connectivity",
        description = "Confirm connectivity to the backend, as well and verify API service is running.")
@OkResponse
@GetMapping(value = API_VERSION_2 + "/ping", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Ping> getPingV2(HttpServletRequest request) {

... }

And here's my @OkResponse annotation:

    @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@ApiResponse(responseCode = HTTP_200,
        description = HTTP_200_OK,
        headers = {
                @Header(name = CONTENT_VERSION_HEADER, description = CONTENT_VERSION_HEADER_DESCRIPTION, schema = @Schema(type = "string")),
                @Header(name = DEPRECATION_MESSAGE_HEADER, description = DEPRECATION_MESSAGE_HEADER_DESCRIPTION, schema = @Schema(type = "string")),
                @Header(name = DESCRIPTION_HEADER, description = DESCRIPTION_HEADER_DESCRIPTION, schema = @Schema(type = "string"))
        })
public @interface OkResponse {
}

My v1 mappings are defined similarly.

2

2 Answers

1
votes

So, it would seem that when solely relying on OpenApiCustomiser for creating the OpenAPI, the scanned components are ignored, or at least overwritten with just the components specified in the customizer (I could have also programmatically added all of my schemas, but this would have been very cumbersome to maintain).

Changing my config to the following resolved my issue:

@Bean
public OpenAPI customOpenAPI() {
    final String securitySchemeName = "Access Token";
    return new OpenAPI()
            .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
            .components(new Components().addSecuritySchemes(securitySchemeName, new SecurityScheme()
                    .type(SecurityScheme.Type.APIKEY)
                    .in(SecurityScheme.In.HEADER)
                    .name(HttpHeaders.AUTHORIZATION)))
            .info(apiInfo());
}

@Bean
public GroupedOpenApi apiV1() {
    String[] paths = {"/v1/**"};
    String[] packagesToScan = {"com.test.controller"};
    return GroupedOpenApi.builder()
            .group("v1")
            .packagesToScan(packagesToScan)
            .pathsToMatch(paths)
            .addOpenApiCustomiser(buildV1OpenAPI())
            .build();
}

@Bean
public GroupedOpenApi apiV2() {
    String[] paths = {"/v2/**"};
    String[] packagesToScan = {"com.test.controller"};
    return GroupedOpenApi.builder()
            .group("v2")
            .packagesToScan(packagesToScan)
            .pathsToMatch(paths)
            .addOpenApiCustomiser(buildV2OpenAPI())
            .build();
}

public OpenApiCustomiser buildV1OpenAPI() {
    return openApi -> openApi.info(openApi.getInfo().version("v1"));
}

public OpenApiCustomiser buildV2OpenAPI() {
    return openApi -> openApi.info(openApi.getInfo().version("v2"));
}

// Describe the apis
private Info apiInfo() {
    return new Info()
            .title("Title")
            .description("API Description.");
}

While this technically does also add the Authorize button and security scheme to the v1 group, it can be ignored because those API endpoints are not secured anyway (internal API and they should be going away soon anyway).

Probably a better solution anyway as the Info is basically identical between the groups.

1
votes

Instead of creating new Components you should just modify them:

public OpenApiCustomiser buildV2OpenAPI() {
    final String securitySchemeName = "Access Token";
    return openApi -> {
        openApi.info(apiInfo().version("v2"))
            .addSecurityItem(new SecurityRequirement().addList(securitySchemeName));
        
        openApi.getComponents().addSecuritySchemes(securitySchemeName, new SecurityScheme()
            .type(SecurityScheme.Type.APIKEY)
            .in(SecurityScheme.In.HEADER)
            .name(HttpHeaders.AUTHORIZATION));

        return openApi;
    
    };
}