springdoc / springdoc-openapi

Library for OpenAPI 3 with spring-boot

Home Page:https://springdoc.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question] How can I identify which annotation on a method was used to include that method in a GroupedOpenApi?

azizabah opened this issue · comments

Describe the question
We generate two sets of OpenApi specs - one for our internal dev portal that contains all endpoints and one for our external API doc portal that contains a subset of the first specs. We do this by building two GroupedOpenApi beans like this:

@Bean
    open fun externalProduct(): GroupedOpenApi? {
        val filter = OpenApiMethodFilter { method ->
                    method.isAnnotationPresent(ExternalProduct::class.java)
        }
        return buildApi("external", filter)
    }

@Bean
    open fun internalCluster(): GroupedOpenApi? {
        val filter = OpenApiMethodFilter { method ->
            method.isAnnotationPresent(InternalCluster::class.java) ||
                    method.isAnnotationPresent(ExternalProduct::class.java)
        }
        return buildApi("internal", filter)
    }

private fun buildApi(
        name: String,
        methodFilter: OpenApiMethodFilter,
    ): GroupedOpenApi {
        return GroupedOpenApi.builder()
            .group(name)
            .pathsToMatch("$genericBasePath/**")
            .addOpenApiMethodFilter(methodFilter)
            .build()

BuildApi does a few more things but they aren't relevant to the question. I would like to be able to add in a note at the end of the description (on each path inside the open api object and for the internalCluster groupedOpenApi) to indicate whether this was included because of the InternalCluster or the ExternalProduct annotation. As far as I can tell, I can't get any relevant information about the methods to use in a later OpenApiCustomizer. I also looked at if there was a way to compare separate GroupedOpenApi's for path overlap but I didn't see that as an option either.

The overall goal is to allow our developers to be able to see in our internal dev portal if an endpoint is also exposed externally without them having to add custom tags or description on every endpoint and instead rely on just the annotations.

@azizabah,

You can use addOpenApiMethodFilter, as follow:

	@Bean
	public GroupedOpenApi group1OpenApi() {
		return GroupedOpenApi.builder()
			.group("annotatedGroup1")
			.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Group1.class))
			.build();
	}

@bnasslahsen - Thanks for taking a look. I believe there's a disconnect. I'm aware of add open api method filter. I'm using it already as can be seen in the example.

If you look again, the question at hand is how do I determine which annotation caused a method to be included in internalCluster's GroupedOpenApi bean since there are two.

@azizabah,

DId you see: OperationCustomizer, where you can check whether the relevant annotation is present, and then add the description ?

@bnasslahsen - I have but I must be missing where I can get back to checking that. When adding an OpenApiCustomer like this:

.addOpenApiCustomizer { openApi ->
                // Do something
            }

I can't seem to figure out where I can get a reference to the underlying method again. Is it listed as an extension? Since that's a map of <String, Object>, it's hard to know what's in there if I try to get at it like this:

.addOpenApiCustomizer { openApi ->
                openApi?.paths?.keys?.forEach {
                    var operation = openApi?.paths!![it]?.get
                    operation.extensions... ?
                }
            }

You have also .addOperationCustomizer available.
See

@RestController
public class AnnotatedController {
@Group1
@GetMapping("/annotated")
public String annotatedGet() {
return "annotated";
}
@Group1
@PostMapping("/annotated")
public String annotatedPost() {
return "annotated";
}
@Group2
@PutMapping("/annotated")
public String annotatedPut() {
return "annotated";
}
@Bean
public GroupedOpenApi group1OpenApi() {
Predicate<Method> hidingCondition = method -> method.isAnnotationPresent(Group1.class);
return GroupedOpenApi.builder()
.group("annotatedGroup1")
.addOperationCustomizer(getOperationCustomizer(hidingCondition))
.build();
}
@Bean
public GroupedOpenApi group2OpenApi() {
Predicate<Method> filterCondition = method -> method.isAnnotationPresent(Group2.class);
return GroupedOpenApi.builder()
.group("annotatedGroup2")
.addOperationCustomizer(getOperationCustomizer(filterCondition))
.build();
}
@Bean
public GroupedOpenApi group3OpenApi() {
Predicate<Method> hidingCondition = method -> method.isAnnotationPresent(Group1.class) || method.isAnnotationPresent(Group2.class);
return GroupedOpenApi.builder()
.group("annotatedCombinedGroup")
.addOperationCustomizer(getOperationCustomizer(hidingCondition))
.build();
}
private OperationCustomizer getOperationCustomizer(Predicate<Method> filterCondition) {
return (operation, handlerMethod) -> filterCondition.test(handlerMethod.getMethod()) ? operation : null;
}
@Retention(RetentionPolicy.RUNTIME)
@interface Group1 {
}
@Retention(RetentionPolicy.RUNTIME)
@interface Group2 {
}
}

@bnasslahsen - That works perfectly. Thank you for the recommendation.