Skip to content

Commit 8d85e0e

Browse files
committed
Duplicate key class Parameter when documenting two GET methods with same path and PathVariable, differing only by produces media type. Fixes #3073
1 parent 4d505d6 commit 8d85e0e

File tree

5 files changed

+228
-1
lines changed

5 files changed

+228
-1
lines changed

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
import io.swagger.v3.oas.models.parameters.Parameter;
6969
import io.swagger.v3.oas.models.parameters.RequestBody;
7070
import org.apache.commons.lang3.StringUtils;
71+
import org.slf4j.Logger;
72+
import org.slf4j.LoggerFactory;
7173
import org.springdoc.core.customizers.DelegatingMethodParameterCustomizer;
7274
import org.springdoc.core.customizers.ParameterCustomizer;
7375
import org.springdoc.core.customizers.SpringDocCustomizers;
@@ -118,6 +120,11 @@
118120
*/
119121
public abstract class AbstractRequestService {
120122

123+
/**
124+
* The constant LOGGER.
125+
*/
126+
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRequestService.class);
127+
121128
/**
122129
* The constant PARAM_TYPES_TO_IGNORE.
123130
*/
@@ -407,7 +414,12 @@ else if (!RequestMethod.GET.equals(requestMethod) || OpenApiVersion.OPENAPI_3_1.
407414
*/
408415
private LinkedHashMap<ParameterId, Parameter> getParameterLinkedHashMap(Components components, MethodAttributes methodAttributes, List<Parameter> operationParameters, Map<ParameterId, io.swagger.v3.oas.annotations.Parameter> parametersDocMap) {
409416
LinkedHashMap<ParameterId, Parameter> map = operationParameters.stream().collect(Collectors.toMap(ParameterId::new, parameter -> parameter, (u, v) -> {
410-
throw new IllegalStateException(String.format("Duplicate key %s", u));
417+
LOGGER.warn(
418+
"Duplicate OpenAPI parameter detected: name='{}', in='{}'. Keeping the first found and ignoring the rest. " +
419+
"Declare the parameter only once.",
420+
u.getName(), u.getIn()
421+
);
422+
return u;
411423
}, LinkedHashMap::new));
412424

413425
for (Entry<ParameterId, io.swagger.v3.oas.annotations.Parameter> entry : parametersDocMap.entrySet()) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package test.org.springdoc.api.v31.app192;
28+
29+
public class Feature {
30+
31+
private String name;
32+
33+
public String getName() {
34+
return name;
35+
}
36+
37+
public void setName(String name) {
38+
this.name = name;
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package test.org.springdoc.api.v31.app192;
28+
29+
import java.util.List;
30+
31+
import io.swagger.v3.oas.annotations.Operation;
32+
import io.swagger.v3.oas.annotations.Parameter;
33+
import io.swagger.v3.oas.annotations.enums.ParameterIn;
34+
import reactor.core.publisher.Flux;
35+
36+
import org.springframework.http.MediaType;
37+
import org.springframework.web.bind.annotation.GetMapping;
38+
import org.springframework.web.bind.annotation.PathVariable;
39+
import org.springframework.web.bind.annotation.RequestMapping;
40+
import org.springframework.web.bind.annotation.RestController;
41+
42+
/**
43+
* @author bnasslahsen
44+
*/
45+
@RestController
46+
@RequestMapping
47+
public class HelloController {
48+
49+
@Operation(operationId = "getFeaturesJson",
50+
parameters =
51+
@Parameter(name = "organizationId" , description = "toto", in = ParameterIn.PATH))
52+
@GetMapping(value = "/{organizationId}/features", produces = MediaType.APPLICATION_JSON_VALUE)
53+
public List<Feature> getFeaturesAsJson(
54+
@PathVariable String organizationId) {
55+
return List.of(/* ... */);
56+
}
57+
58+
@Operation(operationId = "getFeaturesNdjson",
59+
parameters = @Parameter(name = "organizationId", description = "titi", in = ParameterIn.PATH
60+
))
61+
@GetMapping(value = "/{organizationId}/features", produces = "application/x-ndjson")
62+
public List<Flux> getFeaturesAsNdjson(
63+
@PathVariable String organizationId) {
64+
return null;
65+
}
66+
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package test.org.springdoc.api.v31.app192;
28+
29+
import test.org.springdoc.api.v31.AbstractSpringDocTest;
30+
31+
import org.springframework.boot.autoconfigure.SpringBootApplication;
32+
import org.springframework.context.annotation.ComponentScan;
33+
34+
public class SpringDocApp192Test extends AbstractSpringDocTest {
35+
36+
@SpringBootApplication
37+
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app192" })
38+
static class SpringDocTestApp {}
39+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/{organizationId}/features": {
15+
"get": {
16+
"tags": [
17+
"hello-controller"
18+
],
19+
"operationId": "getFeaturesJson",
20+
"parameters": [
21+
{
22+
"name": "organizationId",
23+
"in": "path",
24+
"description": "titi",
25+
"required": true,
26+
"schema": {
27+
"type": "string"
28+
}
29+
}
30+
],
31+
"responses": {
32+
"200": {
33+
"description": "OK",
34+
"content": {
35+
"application/x-ndjson": {
36+
"schema": {
37+
"type": "array",
38+
"items": {
39+
"type": "string"
40+
}
41+
}
42+
},
43+
"application/json": {
44+
"schema": {
45+
"type": "array",
46+
"items": {
47+
"$ref": "#/components/schemas/Feature"
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
},
57+
"components": {
58+
"schemas": {
59+
"Feature": {
60+
"type": "object",
61+
"properties": {
62+
"name": {
63+
"type": "string"
64+
}
65+
}
66+
}
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)