Skip to content

Commit cdbd7f6

Browse files
committed
The operationId is unnecessarily deduplicated for a requestBody with multiple content types. Fixes #2646.
1 parent 68be64e commit cdbd7f6

File tree

66 files changed

+576
-884
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+576
-884
lines changed

Diff for: springdoc-openapi-starter-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -693,7 +693,7 @@ protected void calculatePath(RouterOperation routerOperation, Locale locale, Ope
693693
if (apiOperation != null)
694694
openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
695695

696-
String operationId = operationParser.getOperationId(operation.getOperationId(), openAPI);
696+
String operationId = operation.getOperationId();
697697
operation.setOperationId(operationId);
698698

699699
fillParametersList(operation, queryParams, methodAttributes);

Diff for: springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocConfiguration.java

+13
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
6464
import org.springdoc.core.customizers.OpenApiCustomizer;
6565
import org.springdoc.core.customizers.OperationCustomizer;
66+
import org.springdoc.core.customizers.OperationIdCustomizer;
6667
import org.springdoc.core.customizers.ParameterObjectNamingStrategyCustomizer;
6768
import org.springdoc.core.customizers.PropertyCustomizer;
6869
import org.springdoc.core.customizers.QuerydslPredicateOperationCustomizer;
@@ -662,4 +663,16 @@ QuerydslPredicateOperationCustomizer queryDslQuerydslPredicateOperationCustomize
662663
ParameterObjectNamingStrategyCustomizer parameterObjectNamingStrategyCustomizer() {
663664
return new ParameterObjectNamingStrategyCustomizer();
664665
}
666+
667+
/**
668+
* Global open api customizer global open api customizer.
669+
*
670+
* @return the global open api customizer
671+
*/
672+
@Bean
673+
@ConditionalOnMissingBean
674+
@Lazy(false)
675+
GlobalOpenApiCustomizer globalOpenApiCustomizer() {
676+
return new OperationIdCustomizer();
677+
}
665678
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2022 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
package org.springdoc.core.customizers;
25+
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
29+
import io.swagger.v3.oas.models.OpenAPI;
30+
import io.swagger.v3.oas.models.Operation;
31+
import io.swagger.v3.oas.models.PathItem;
32+
33+
/**
34+
* @author bnasslahsen
35+
*/
36+
public class OperationIdCustomizer implements GlobalOpenApiCustomizer {
37+
38+
@Override
39+
public void customise(OpenAPI openApi) {
40+
// Map to store operationId counts
41+
Map<String, Integer> operationIdCount = new HashMap<>();
42+
43+
// Iterate through all the paths
44+
for (Map.Entry<String, PathItem> pathEntry : openApi.getPaths().entrySet()) {
45+
PathItem pathItem = pathEntry.getValue();
46+
47+
// Process all HTTP methods for the path (GET, POST, PUT, DELETE, etc.)
48+
processOperation(pathItem.getGet(), operationIdCount);
49+
processOperation(pathItem.getPost(), operationIdCount);
50+
processOperation(pathItem.getPut(), operationIdCount);
51+
processOperation(pathItem.getDelete(), operationIdCount);
52+
processOperation(pathItem.getPatch(), operationIdCount);
53+
processOperation(pathItem.getHead(), operationIdCount);
54+
processOperation(pathItem.getOptions(), operationIdCount);
55+
processOperation(pathItem.getTrace(), operationIdCount);
56+
}
57+
}
58+
59+
// Helper method to process each operation and handle duplicate operationId
60+
private void processOperation(Operation operation, Map<String, Integer> operationIdCount) {
61+
if (operation != null) {
62+
String originalOperationId = operation.getOperationId();
63+
64+
// Check if operationId already exists
65+
if (operationIdCount.containsKey(originalOperationId)) {
66+
// Get the count for the current operationId and increment
67+
int count = operationIdCount.get(originalOperationId);
68+
count++;
69+
operationIdCount.put(originalOperationId, count);
70+
71+
// Create new unique operationId by appending _x
72+
String newOperationId = originalOperationId + "_" + count;
73+
operation.setOperationId(newOperationId);
74+
}
75+
else {
76+
// First time this operationId is seen, initialize the count
77+
operationIdCount.put(originalOperationId, 0);
78+
}
79+
}
80+
}
81+
82+
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,7 @@ public static Collection<Parameter> getHeaders(MethodAttributes methodAttributes
278278
public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
279279
Operation operation, MethodAttributes methodAttributes, OpenAPI openAPI) {
280280
// Documentation
281-
String operationId = operationService.getOperationId(handlerMethod.getMethod().getName(),
282-
operation.getOperationId(), openAPI);
281+
String operationId = operation.getOperationId()!=null ? operation.getOperationId() : handlerMethod.getMethod().getName();
283282
operation.setOperationId(operationId);
284283
// requests
285284
String[] pNames = this.localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());

Diff for: springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OperationService.java

+1-92
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public OpenAPI parse(io.swagger.v3.oas.annotations.Operation apiOperation,
132132
operation.setDescription(propertyResolverUtils.resolve(apiOperation.description(), locale));
133133

134134
if (StringUtils.isNotBlank(apiOperation.operationId()))
135-
operation.setOperationId(getOperationId(apiOperation.operationId(), openAPI));
135+
operation.setOperationId(apiOperation.operationId());
136136

137137
if (apiOperation.deprecated())
138138
operation.setDeprecated(apiOperation.deprecated());
@@ -306,82 +306,6 @@ private void buildTags(io.swagger.v3.oas.annotations.Operation apiOperation, Ope
306306
}
307307
}
308308

309-
/**
310-
* Gets operation id.
311-
*
312-
* @param operationId the operation id
313-
* @param openAPI the open api
314-
* @return the operation id
315-
*/
316-
public String getOperationId(String operationId, OpenAPI openAPI) {
317-
boolean operationIdUsed = existOperationId(operationId, openAPI);
318-
String operationIdToFind = null;
319-
int counter = 0;
320-
while (operationIdUsed) {
321-
operationIdToFind = String.format("%s_%d", operationId, ++counter);
322-
operationIdUsed = existOperationId(operationIdToFind, openAPI);
323-
}
324-
if (operationIdToFind != null) {
325-
operationId = operationIdToFind;
326-
}
327-
return operationId;
328-
}
329-
330-
/**
331-
* Exist operation id boolean.
332-
*
333-
* @param operationId the operation id
334-
* @param openAPI the open api
335-
* @return the boolean
336-
*/
337-
private boolean existOperationId(String operationId, OpenAPI openAPI) {
338-
if (openAPI == null) {
339-
return false;
340-
}
341-
if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
342-
return false;
343-
}
344-
for (PathItem path : openAPI.getPaths().values()) {
345-
Set<String> pathOperationIds = extractOperationIdFromPathItem(path);
346-
if (pathOperationIds.contains(operationId)) {
347-
return true;
348-
}
349-
}
350-
return false;
351-
}
352-
353-
/**
354-
* Extract operation id from path item set.
355-
*
356-
* @param path the path
357-
* @return the set
358-
*/
359-
private Set<String> extractOperationIdFromPathItem(PathItem path) {
360-
Set<String> ids = new HashSet<>();
361-
if (path.getGet() != null && StringUtils.isNotBlank(path.getGet().getOperationId())) {
362-
ids.add(path.getGet().getOperationId());
363-
}
364-
if (path.getPost() != null && StringUtils.isNotBlank(path.getPost().getOperationId())) {
365-
ids.add(path.getPost().getOperationId());
366-
}
367-
if (path.getPut() != null && StringUtils.isNotBlank(path.getPut().getOperationId())) {
368-
ids.add(path.getPut().getOperationId());
369-
}
370-
if (path.getDelete() != null && StringUtils.isNotBlank(path.getDelete().getOperationId())) {
371-
ids.add(path.getDelete().getOperationId());
372-
}
373-
if (path.getOptions() != null && StringUtils.isNotBlank(path.getOptions().getOperationId())) {
374-
ids.add(path.getOptions().getOperationId());
375-
}
376-
if (path.getHead() != null && StringUtils.isNotBlank(path.getHead().getOperationId())) {
377-
ids.add(path.getHead().getOperationId());
378-
}
379-
if (path.getPatch() != null && StringUtils.isNotBlank(path.getPatch().getOperationId())) {
380-
ids.add(path.getPatch().getOperationId());
381-
}
382-
return ids;
383-
}
384-
385309
/**
386310
* Gets api responses.
387311
*
@@ -597,21 +521,6 @@ private Optional<List<String>> getStringListFromStringArray(String[] array) {
597521
return Optional.of(list);
598522
}
599523

600-
/**
601-
* Gets operation id.
602-
*
603-
* @param operationId the operation id
604-
* @param oldOperationId the old operation id
605-
* @param openAPI the open api
606-
* @return the operation id
607-
*/
608-
public String getOperationId(String operationId, String oldOperationId, OpenAPI openAPI) {
609-
if (StringUtils.isNotBlank(oldOperationId))
610-
return this.getOperationId(oldOperationId, openAPI);
611-
else
612-
return this.getOperationId(operationId, openAPI);
613-
}
614-
615524
/**
616525
* Merge operation.
617526
*

Diff for: springdoc-openapi-starter-common/src/test/java/org/springdoc/core/configuration/SpringDocHateoasConfigurationTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ void linksSchemaCustomizerShouldBeRegistered() {
3232
))
3333
.run(context -> {
3434
assertThat(context).getBeanNames(GlobalOpenApiCustomizer.class)
35-
.hasSize(1)
36-
.containsExactly(LINKS_SCHEMA_CUSTOMISER);
35+
.hasSize(2)
36+
.contains(LINKS_SCHEMA_CUSTOMISER);
3737
assertThat(context.getBean(LINKS_SCHEMA_CUSTOMISER)).isExactlyInstanceOf(OpenApiHateoasLinksCustomizer.class);
3838
});
3939
}

Diff for: springdoc-openapi-starter-webflux-api/src/test/java/test/org/springdoc/api/app65/SpringDocApp65Test.java

+2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import test.org.springdoc.api.AbstractSpringDocTest;
2222

2323
import org.springframework.boot.autoconfigure.SpringBootApplication;
24+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
2425
import org.springframework.context.annotation.ComponentScan;
2526

27+
@AutoConfigureWebTestClient(timeout = "3600000")
2628
public class SpringDocApp65Test extends AbstractSpringDocTest {
2729

2830
@SpringBootApplication

Diff for: springdoc-openapi-starter-webflux-api/src/test/resources/results/app73.json

+12-12
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,9 @@
1111
}
1212
],
1313
"paths": {
14-
"/hello": {
15-
"get": {
16-
"operationId": "hello",
17-
"responses": {
18-
"200": {
19-
"description": "OK"
20-
}
21-
}
22-
}
23-
},
2414
"/echo": {
2515
"post": {
26-
"operationId": "echo_1_1",
16+
"operationId": "echo",
2717
"requestBody": {
2818
"content": {
2919
"text/plain": {
@@ -57,9 +47,19 @@
5747
}
5848
}
5949
},
50+
"/hello": {
51+
"get": {
52+
"operationId": "hello",
53+
"responses": {
54+
"200": {
55+
"description": "OK"
56+
}
57+
}
58+
}
59+
},
6060
"/quotes": {
6161
"get": {
62-
"operationId": "fetchQuotes_1_1",
62+
"operationId": "fetchQuotes",
6363
"parameters": [
6464
{
6565
"name": "size",

Diff for: springdoc-openapi-starter-webflux-api/src/test/resources/results/app74.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"tags": [
1717
"book-repository"
1818
],
19-
"operationId": "findAll_1",
19+
"operationId": "findAll",
2020
"responses": {
2121
"200": {
2222
"description": "OK",

Diff for: springdoc-openapi-starter-webflux-api/src/test/resources/results/app80.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"tags": [
1717
"book-repository"
1818
],
19-
"operationId": "findAll_1",
19+
"operationId": "findAll",
2020
"responses": {
2121
"200": {
2222
"description": "OK",
@@ -88,7 +88,7 @@
8888
"tags": [
8989
"book-repository"
9090
],
91-
"operationId": "findAll",
91+
"operationId": "findAll_1",
9292
"responses": {
9393
"200": {
9494
"description": "OK",
@@ -134,15 +134,15 @@
134134
"200": {
135135
"description": "OK",
136136
"content": {
137-
"application/xml": {
137+
"application/json": {
138138
"schema": {
139139
"type": "array",
140140
"items": {
141141
"$ref": "#/components/schemas/Book"
142142
}
143143
}
144144
},
145-
"application/json": {
145+
"application/xml": {
146146
"schema": {
147147
"type": "array",
148148
"items": {
@@ -160,7 +160,7 @@
160160
"tags": [
161161
"book-repository"
162162
],
163-
"operationId": "findAll_2_1",
163+
"operationId": "findAll_2",
164164
"responses": {
165165
"200": {
166166
"description": "OK",
@@ -232,7 +232,7 @@
232232
"tags": [
233233
"book-repository"
234234
],
235-
"operationId": "findAll_2_2",
235+
"operationId": "findAll_3",
236236
"responses": {
237237
"200": {
238238
"description": "OK",
@@ -304,7 +304,7 @@
304304
"tags": [
305305
"book-repository"
306306
],
307-
"operationId": "findAll_2_3",
307+
"operationId": "findAll_4",
308308
"responses": {
309309
"200": {
310310
"description": "OK",

0 commit comments

Comments
 (0)