Repository: springdoc/springdoc-openapi
Pull request #668 (2020-05-13 14:46:20)

Process @Parameters method parameters as Spring MergedAnnotations
I noticed you used merged annotations while reading method annotations but not method parameters.
This allows the creation of Spring Merged annotations to avoid repeating ourselves:

```java
@Target({PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Parameter(name = "accountId")
public @interface AccountId {
    @AliasFor(annotation = Parameter.class, value = "example")
    String example() default "123456";
}

...

@GetMapping(value = "/example", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(description = "example")
public Mono<Void> example(@PathVariable @AccountId String accountId) ...
```

I'll add tests if the maintainers think it's a good idea. Thanks!


Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java
index ad78f6d84..0f7c7c960 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/AbstractRequestBuilder.java
@@ -174,7 +174,10 @@ public abstract class AbstractRequestBuilder {
 		for (MethodParameter methodParameter : parameters) {
 			// check if query param
 			Parameter parameter = null;
-			io.swagger.v3.oas.annotations.Parameter parameterDoc = methodParameter.getParameterAnnotation(io.swagger.v3.oas.annotations.Parameter.class);
+			io.swagger.v3.oas.annotations.Parameter parameterDoc = AnnotatedElementUtils.findMergedAnnotation(
+					AnnotatedElementUtils.forAnnotations(methodParameter.getParameterAnnotations()),
+					io.swagger.v3.oas.annotations.Parameter.class);
+
 			final String pName = methodParameter.getParameterName();
 			ParameterInfo parameterInfo = new ParameterInfo(pName, methodParameter);
 


Test patch:
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/AccountId.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/AccountId.java
new file mode 100644
index 000000000..b4a8d4d0f
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/AccountId.java
@@ -0,0 +1,49 @@
+/*
+ *
+ *  * Copyright 2019-2020 the original author or authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+package test.org.springdoc.api.app120;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.core.annotation.AliasFor;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+@Target({PARAMETER, METHOD, ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Parameter(description = "non alias description")
+public @interface AccountId {
+
+    @AliasFor(annotation = Parameter.class, value = "name")
+    String name() default "";
+
+    @AliasFor(annotation = Parameter.class, value = "example")
+    String example() default "123456";
+
+    @AliasFor(annotation = Parameter.class, value = "in")
+    ParameterIn in() default ParameterIn.DEFAULT;
+
+    @AliasFor(annotation = Parameter.class, value = "schema")
+    Schema schema() default @Schema();
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/MetaAnnotationController.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/MetaAnnotationController.java
new file mode 100644
index 000000000..0002ef726
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/MetaAnnotationController.java
@@ -0,0 +1,163 @@
+/*
+ *
+ *  * Copyright 2019-2020 the original author or authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+package test.org.springdoc.api.app120;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.Explode;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.enums.ParameterStyle;
+import io.swagger.v3.oas.annotations.extensions.Extension;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.springframework.core.annotation.AliasFor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+
+@RestController
+public class MetaAnnotationController {
+
+	@GetMapping(value = "/simpleTest/{accountId}")
+	String simpleTest(@PathVariable @AccountId String accountId) {
+		return accountId;
+	}
+
+	/**
+	 * When there is a top level {@code @Parameter} annotation it has precedence over the meta-annotation
+	 * So the id parameter should have all the defaults, with a name of "id"
+	 */
+	@GetMapping(value = "/testTopLevelParamAnnotationOverrides/{accountId}")
+	String testTopLevelParamAnnotationOverrides(@PathVariable @AccountId @Parameter(name = "id") String accountId) {
+		return accountId;
+	}
+
+	@GetMapping(value = "/testQueryParam")
+	String testQueryParam(@RequestParam @AccountId String accountId) {
+		return accountId;
+	}
+
+	/**
+	 * {@code @AliasFor} in the {@code @AccountId} annotation allows us to override the default it provides.
+	 */
+	@GetMapping(value = "/testAliasFor")
+	String testAliasFor(@RequestParam @AccountId(example = "OVERRIDDEN EXAMPLE") String accountId) {
+		return accountId;
+	}
+
+	/**
+	 * This should inherent all the attributes of {@code @AccountId}, but give it a different name
+	 */
+	@Target({PARAMETER, METHOD, ANNOTATION_TYPE})
+	@Retention(RetentionPolicy.RUNTIME)
+	@AccountId(name = "queryAccountId")
+	@interface QueryAccountId {
+	}
+
+	@GetMapping(value = "/testMetaMetaAnnotation/{accountId}")
+	String testMetaMetaAnnotation(
+			@RequestParam @QueryAccountId String queryAccountId,
+			@PathVariable @AccountId String accountId) {
+		return accountId;
+	}
+
+	@Target({PARAMETER, METHOD, ANNOTATION_TYPE})
+	@Retention(RetentionPolicy.RUNTIME)
+	@Parameter
+	@interface TestAllAttributesAsAlias {
+
+		@AliasFor(annotation = Parameter.class, attribute="name")
+		String name() default "name";
+
+		@AliasFor(annotation = Parameter.class, attribute="in")
+		ParameterIn in() default ParameterIn.QUERY;
+
+		@AliasFor(annotation = Parameter.class, attribute="description")
+		String description() default "desc";
+
+		@AliasFor(annotation = Parameter.class, attribute="required")
+		boolean required() default true;
+
+		@AliasFor(annotation = Parameter.class, attribute="deprecated")
+		boolean deprecated() default true;
+
+		@AliasFor(annotation = Parameter.class, attribute="allowEmptyValue")
+		boolean allowEmptyValue() default true;
+
+		@AliasFor(annotation = Parameter.class, attribute="style")
+		ParameterStyle style() default ParameterStyle.DEEPOBJECT;
+
+		@AliasFor(annotation = Parameter.class, attribute="explode")
+ 		Explode explode() default Explode.TRUE;
+
+		@AliasFor(annotation = Parameter.class, attribute="allowReserved")
+		boolean allowReserved() default true;
+
+		@AliasFor(annotation = Parameter.class, attribute="schema")
+		Schema schema() default @Schema(name = "special schema", implementation = Boolean.class);
+
+		@AliasFor(annotation = Parameter.class, attribute="array")
+		ArraySchema array() default @ArraySchema();
+
+		@AliasFor(annotation = Parameter.class, attribute="content")
+		Content[] content() default {};
+
+		@AliasFor(annotation = Parameter.class, attribute="hidden")
+		boolean hidden() default false;
+
+		@AliasFor(annotation = Parameter.class, attribute="examples")
+		ExampleObject[] examples() default {};
+
+		@AliasFor(annotation = Parameter.class, attribute="example")
+		String example() default "1234";
+
+		@AliasFor(annotation = Parameter.class, attribute="extensions")
+		Extension[] extensions() default {};
+
+		@AliasFor(annotation = Parameter.class, attribute="ref")
+		String ref() default "";
+	}
+
+	@GetMapping(value = "/testAllAttributesAsAlias/")
+	String testAllAttributesAsAlias(
+			@RequestParam @TestAllAttributesAsAlias String name) {
+		return name;
+	}
+
+	@Target({PARAMETER, METHOD, ANNOTATION_TYPE})
+	@Retention(RetentionPolicy.RUNTIME)
+	@Parameter(name = "name", description = "desc", schema = @Schema(implementation = Boolean.class))
+	@interface TestNoAliasFors {
+	}
+
+	@GetMapping(value = "/testNoAliasFors/")
+	String testNoAliasFors(
+			@RequestParam @TestAllAttributesAsAlias String name) {
+		return name;
+	}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/SpringDocApp120Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/SpringDocApp120Test.java
new file mode 100644
index 000000000..28bc22f97
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app120/SpringDocApp120Test.java
@@ -0,0 +1,31 @@
+/*
+ *
+ *  * Copyright 2019-2020 the original author or authors.
+ *  *
+ *  * Licensed under the Apache License, Version 2.0 (the "License");
+ *  * you may not use this file except in compliance with the License.
+ *  * You may obtain a copy of the License at
+ *  *
+ *  *      https://www.apache.org/licenses/LICENSE-2.0
+ *  *
+ *  * Unless required by applicable law or agreed to in writing, software
+ *  * distributed under the License is distributed on an "AS IS" BASIS,
+ *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  * See the License for the specific language governing permissions and
+ *  * limitations under the License.
+ *
+ */
+package test.org.springdoc.api.app120;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import test.org.springdoc.api.AbstractSpringDocTest;
+
+
+/**
+ * Tests Spring meta-annotations as method parameters
+ */
+public class SpringDocApp120Test extends AbstractSpringDocTest {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {}
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app120.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app120.json
new file mode 100644
index 000000000..565fe81d0
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app120.json
@@ -0,0 +1,258 @@
+{
+    "openapi": "3.0.1",
+    "info": {
+        "title": "OpenAPI definition",
+        "version": "v0"
+    },
+    "servers": [
+        {
+            "url": "http://localhost",
+            "description": "Generated server url"
+        }
+    ],
+    "paths": {
+        "/simpleTest/{accountId}": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "simpleTest",
+                "parameters": [
+                    {
+                        "name": "accountId",
+                        "in": "path",
+                        "description": "non alias description",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": 123456
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testTopLevelParamAnnotationOverrides/{accountId}": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testTopLevelParamAnnotationOverrides",
+                "parameters": [
+                    {
+                        "name": "id",
+                        "in": "path",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testQueryParam": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testQueryParam",
+                "parameters": [
+                    {
+                        "name": "accountId",
+                        "in": "query",
+                        "description": "non alias description",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": 123456
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testAliasFor": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testAliasFor",
+                "parameters": [
+                    {
+                        "name": "accountId",
+                        "in": "query",
+                        "description": "non alias description",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": "OVERRIDDEN EXAMPLE"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testMetaMetaAnnotation/{accountId}": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testMetaMetaAnnotation",
+                "parameters": [
+                    {
+                        "name": "queryAccountId",
+                        "in": "query",
+                        "description": "non alias description",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": 123456
+                    },
+                    {
+                        "name": "accountId",
+                        "in": "path",
+                        "description": "non alias description",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        },
+                        "example": 123456
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testAllAttributesAsAlias/": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testAllAttributesAsAlias",
+                "parameters": [
+                    {
+                        "name": "name",
+                        "in": "query",
+                        "description": "desc",
+                        "required": true,
+                        "deprecated": true,
+                        "allowEmptyValue": true,
+                        "style": "deepObject",
+                        "explode": true,
+                        "allowReserved": true,
+                        "schema": {
+                            "type": "boolean"
+                        },
+                        "example": 1234
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/testNoAliasFors/": {
+            "get": {
+                "tags": [
+                    "meta-annotation-controller"
+                ],
+                "operationId": "testNoAliasFors",
+                "parameters": [
+                    {
+                        "name": "name",
+                        "in": "query",
+                        "description": "desc",
+                        "required": true,
+                        "deprecated": true,
+                        "allowEmptyValue": true,
+                        "style": "deepObject",
+                        "explode": true,
+                        "allowReserved": true,
+                        "schema": {
+                            "type": "boolean"
+                        },
+                        "example": 1234
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "content": {
+                            "*/*": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "components": {}
+}