Skip to content

Commit 0b0ebab

Browse files
authored
Merge pull request #3259 from mcclellanmj/bug-parameterized-types
Bug: Fixes an issue with Annotated Generic properties getting applied to sibling properties
2 parents 6b9b69e + ee7ab4d commit 0b0ebab

File tree

9 files changed

+391
-15
lines changed

9 files changed

+391
-15
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/GenericParameterService.java

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
import java.io.IOException;
3030
import java.lang.annotation.Annotation;
31+
import java.lang.reflect.AnnotatedParameterizedType;
32+
import java.lang.reflect.AnnotatedType;
3133
import java.lang.reflect.Field;
3234
import java.lang.reflect.ParameterizedType;
3335
import java.lang.reflect.Type;
@@ -385,22 +387,10 @@ Schema calculateSchema(Components components, ParameterInfo parameterInfo, Reque
385387
MethodParameter methodParameter = parameterInfo.getMethodParameter();
386388

387389
if (parameterInfo.getParameterModel() == null || parameterInfo.getParameterModel().getSchema() == null) {
388-
Type type = GenericTypeResolver.resolveType(methodParameter.getGenericParameterType(), methodParameter.getContainingClass());
389390
Annotation[] paramAnnotations = getParameterAnnotations(methodParameter);
390-
Annotation[] typeAnnotations = new Annotation[0];
391-
if (KotlinDetector.isKotlinPresent()
392-
&& KotlinDetector.isKotlinReflectPresent()
393-
&& KotlinDetector.isKotlinType(methodParameter.getContainingClass())
394-
&& type == String.class) {
395-
Class<?> restored = KotlinInlineParameterResolver
396-
.resolveInlineType(methodParameter, type);
397-
if (restored != null) {
398-
type = restored;
399-
typeAnnotations = ((Class<?>) type).getAnnotations();
400-
}
401-
} else {
402-
typeAnnotations = methodParameter.getParameterType().getAnnotations();
403-
}
391+
TypeAndTypeAnnotations resolved = resolveTypeAndTypeAnnotationsForParameter(methodParameter);
392+
Type type = resolved.type();
393+
Annotation[] typeAnnotations = resolved.typeAnnotations();
404394
Annotation[] mergedAnnotations =
405395
Stream.concat(
406396
Arrays.stream(paramAnnotations),
@@ -437,6 +427,56 @@ Schema calculateSchema(Components components, ParameterInfo parameterInfo, Reque
437427
return schemaN;
438428
}
439429

430+
/**
431+
* Resolves type and type annotations for schema extraction (flattened {@code @ParameterObject} field,
432+
* Kotlin inline {@code String}, or default).
433+
*
434+
* @param methodParameter the method parameter
435+
* @return the resolved type and type annotations
436+
*/
437+
private TypeAndTypeAnnotations resolveTypeAndTypeAnnotationsForParameter(MethodParameter methodParameter) {
438+
if (methodParameter instanceof DelegatingMethodParameter delegatingMethodParameter
439+
&& delegatingMethodParameter.getField() != null) {
440+
AnnotatedType annotated = delegatingMethodParameter.getField().getAnnotatedType();
441+
Type type = GenericTypeResolver.resolveType(annotated.getType(), methodParameter.getContainingClass());
442+
return new TypeAndTypeAnnotations(type, annotationsFromAnnotatedTypeArguments(annotated));
443+
}
444+
445+
Type type = GenericTypeResolver.resolveType(methodParameter.getGenericParameterType(), methodParameter.getContainingClass());
446+
if (KotlinDetector.isKotlinPresent()
447+
&& KotlinDetector.isKotlinReflectPresent()
448+
&& KotlinDetector.isKotlinType(methodParameter.getContainingClass())
449+
&& type == String.class) {
450+
Class<?> restored = KotlinInlineParameterResolver.resolveInlineType(methodParameter, type);
451+
return restored != null
452+
? new TypeAndTypeAnnotations(restored, restored.getAnnotations())
453+
: new TypeAndTypeAnnotations(type, new Annotation[0]);
454+
}
455+
456+
return new TypeAndTypeAnnotations(type, methodParameter.getParameterType().getAnnotations());
457+
}
458+
459+
/**
460+
* Pair of resolved Java type and type annotations merged with parameter annotations for {@code extractSchema}.
461+
*/
462+
private record TypeAndTypeAnnotations(Type type, Annotation[] typeAnnotations) {
463+
}
464+
465+
/**
466+
* Collects annotations declared on each type argument of an {@link AnnotatedParameterizedType}.
467+
*
468+
* @param annotatedType the annotated type
469+
* @return a new array, possibly empty
470+
*/
471+
private static Annotation[] annotationsFromAnnotatedTypeArguments(AnnotatedType annotatedType) {
472+
if (!(annotatedType instanceof AnnotatedParameterizedType apt)) {
473+
return new Annotation[0];
474+
}
475+
return Arrays.stream(apt.getAnnotatedActualTypeArguments())
476+
.flatMap(typeArg -> Arrays.stream(typeArg.getAnnotations()))
477+
.toArray(Annotation[]::new);
478+
}
479+
440480
/**
441481
* Calculate request body schema schema.
442482
*
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v30.app249;
18+
19+
import org.springdoc.core.annotations.ParameterObject;
20+
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
@RestController
25+
public class HelloController {
26+
27+
@GetMapping("/items")
28+
public String list(@ParameterObject PersonQueryFilter criteria) {
29+
return "ok";
30+
}
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v30.app249;
18+
19+
import java.util.List;
20+
21+
import jakarta.validation.constraints.Pattern;
22+
23+
/**
24+
* Sample person search criteria: several {@code List} query parameters; only one uses a type-use
25+
* {@link Pattern} on the element type.
26+
*/
27+
public record PersonQueryFilter(
28+
List<String> firstNames,
29+
List<String> middleNames,
30+
List<@Pattern(regexp = "^\\d+$") String> phoneNumbers) {
31+
32+
public PersonQueryFilter {
33+
firstNames = firstNames != null ? firstNames : List.of();
34+
middleNames = middleNames != null ? middleNames : List.of();
35+
phoneNumbers = phoneNumbers != null ? phoneNumbers : List.of();
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v30.app249;
18+
19+
import test.org.springdoc.api.v30.AbstractSpringDocV30Test;
20+
21+
import org.springframework.boot.autoconfigure.SpringBootApplication;
22+
23+
/**
24+
* Regression: {@code @Pattern} on one {@code List} field in a {@code @ParameterObject} must not
25+
* be applied to sibling {@code List} query parameters' item schemas.
26+
*/
27+
public class SpringDocApp249Test extends AbstractSpringDocV30Test {
28+
29+
@SpringBootApplication
30+
static class SpringDocTestApp {
31+
}
32+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v31.app249;
18+
19+
import org.springdoc.core.annotations.ParameterObject;
20+
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
@RestController
25+
public class HelloController {
26+
27+
@GetMapping("/items")
28+
public String list(@ParameterObject PersonQueryFilter criteria) {
29+
return "ok";
30+
}
31+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v31.app249;
18+
19+
import java.util.List;
20+
21+
import jakarta.validation.constraints.Pattern;
22+
23+
/**
24+
* Sample person search criteria: several {@code List} query parameters; only one uses a type-use
25+
* {@link Pattern} on the element type.
26+
*/
27+
public record PersonQueryFilter(
28+
List<String> firstNames,
29+
List<String> middleNames,
30+
List<@Pattern(regexp = "^\\d+$") String> phoneNumbers) {
31+
32+
public PersonQueryFilter {
33+
firstNames = firstNames != null ? firstNames : List.of();
34+
middleNames = middleNames != null ? middleNames : List.of();
35+
phoneNumbers = phoneNumbers != null ? phoneNumbers : List.of();
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2019-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package test.org.springdoc.api.v31.app249;
18+
19+
import test.org.springdoc.api.v31.AbstractSpringDocTest;
20+
21+
import org.springframework.boot.autoconfigure.SpringBootApplication;
22+
23+
/**
24+
* Regression: {@code @Pattern} on one {@code List} field in a {@code @ParameterObject} must not
25+
* be applied to sibling {@code List} query parameters' item schemas.
26+
*/
27+
public class SpringDocApp249Test extends AbstractSpringDocTest {
28+
29+
@SpringBootApplication
30+
static class SpringDocTestApp {
31+
}
32+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/items": {
15+
"get": {
16+
"tags": [
17+
"hello-controller"
18+
],
19+
"operationId": "list",
20+
"parameters": [
21+
{
22+
"name": "firstNames",
23+
"in": "query",
24+
"required": false,
25+
"schema": {
26+
"type": "array",
27+
"items": {
28+
"type": "string"
29+
}
30+
}
31+
},
32+
{
33+
"name": "middleNames",
34+
"in": "query",
35+
"required": false,
36+
"schema": {
37+
"type": "array",
38+
"items": {
39+
"type": "string"
40+
}
41+
}
42+
},
43+
{
44+
"name": "phoneNumbers",
45+
"in": "query",
46+
"required": false,
47+
"schema": {
48+
"type": "array",
49+
"items": {
50+
"pattern": "^\\d+$",
51+
"type": "string"
52+
}
53+
}
54+
}
55+
],
56+
"responses": {
57+
"200": {
58+
"description": "OK",
59+
"content": {
60+
"*/*": {
61+
"schema": {
62+
"type": "string"
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}
70+
},
71+
"components": {}
72+
}

0 commit comments

Comments
 (0)