diff --git a/docs/generators/spring.md b/docs/generators/spring.md
index 1a444e6ef6a6..abb8ef3be6e6 100644
--- a/docs/generators/spring.md
+++ b/docs/generators/spring.md
@@ -97,6 +97,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src/main/java|
+|splitResponseTypes|Whether to generate one operation method per response content type when a response declares multiple content types.| |false|
|springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null|
|substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false|
|testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi|
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
index 2104047f125c..1f93a112b4b1 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
@@ -47,6 +47,8 @@
import org.slf4j.LoggerFactory;
import java.io.File;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
@@ -58,6 +60,7 @@
import static org.openapitools.codegen.CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
+import static org.openapitools.codegen.utils.StringUtils.underscore;
/**
*
Mustache templates are located in
@@ -78,6 +81,7 @@ public class SpringCodegen extends AbstractJavaCodegen
public static final String USE_FEIGN_CLIENT_CONTEXT_ID = "useFeignClientContextId";
public static final String DELEGATE_PATTERN = "delegatePattern";
public static final String SINGLE_CONTENT_TYPES = "singleContentTypes";
+ public static final String SPLIT_RESPONSE_TYPES = "splitResponseTypes";
public static final String VIRTUAL_SERVICE = "virtualService";
public static final String SKIP_DEFAULT_INTERFACE = "skipDefaultInterface";
public static final String GENERATE_CONSTRUCTOR_WITH_REQUIRED_ARGS = "generatedConstructorWithRequiredArgs";
@@ -151,6 +155,7 @@ public enum RequestMappingMode {
@Setter protected boolean delegatePattern = false;
protected boolean delegateMethod = false;
@Setter protected boolean singleContentTypes = false;
+ @Setter protected boolean splitResponseTypes = false;
@Setter protected boolean async = false;
@Setter protected boolean reactive = false;
@Setter protected boolean sse = false;
@@ -264,6 +269,9 @@ public SpringCodegen() {
"Whether to generate the server files using the delegate pattern", delegatePattern));
cliOptions.add(CliOption.newBoolean(SINGLE_CONTENT_TYPES,
"Whether to select only one produces/consumes content-type by operation.", singleContentTypes));
+ cliOptions.add(CliOption.newBoolean(SPLIT_RESPONSE_TYPES,
+ "Whether to generate one operation method per response content type when a response declares multiple content types.",
+ splitResponseTypes));
cliOptions.add(CliOption.newBoolean(SKIP_DEFAULT_INTERFACE,
"Whether to skip generation of default implementations for java8 interfaces", skipDefaultInterface));
cliOptions.add(CliOption.newBoolean(ASYNC, "use async Callable controllers", async));
@@ -520,6 +528,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(USE_FEIGN_CLIENT_CONTEXT_ID, this::setUseFeignClientContextId);
convertPropertyToBooleanAndWriteBack(DELEGATE_PATTERN, this::setDelegatePattern);
convertPropertyToBooleanAndWriteBack(SINGLE_CONTENT_TYPES, this::setSingleContentTypes);
+ convertPropertyToBooleanAndWriteBack(SPLIT_RESPONSE_TYPES, this::setSplitResponseTypes);
convertPropertyToBooleanAndWriteBack(SKIP_DEFAULT_INTERFACE, this::setSkipDefaultInterface);
convertPropertyToBooleanAndWriteBack(ASYNC, this::setAsync);
if (additionalProperties.containsKey(REACTIVE)) {
@@ -979,8 +988,8 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) {
final OperationMap operations = objs.getOperations();
if (operations != null) {
- final List ops = operations.getOperation();
- for (final CodegenOperation operation : ops) {
+ final List originalOps = operations.getOperation();
+ for (final CodegenOperation operation : originalOps) {
final List responses = operation.responses;
if (responses != null) {
for (final CodegenResponse resp : responses) {
@@ -1024,9 +1033,13 @@ public void setIsVoid(boolean isVoid) {
}
});
- prepareVersioningParameters(ops);
handleImplicitHeaders(operation);
}
+ prepareVersioningParameters(originalOps);
+
+ final List ops = expandOperationsByResponseType(originalOps);
+ operations.setOperation(ops);
+
// The tag for the controller is the first tag of the first operation
final CodegenOperation firstOperation = ops.get(0);
final Tag firstTag = firstOperation.tags.get(0);
@@ -1046,6 +1059,236 @@ public void setIsVoid(boolean isVoid) {
return objs;
}
+ private List expandOperationsByResponseType(List operations) {
+ if (!splitResponseTypes) {
+ return operations;
+ }
+
+ List expandedOperations = new ArrayList<>();
+ for (CodegenOperation operation : operations) {
+ expandedOperations.addAll(splitOperationByResponseType(operation));
+ }
+ return expandedOperations;
+ }
+
+ private List splitOperationByResponseType(CodegenOperation operation) {
+ CodegenResponse methodResponse = findMethodResponse(operation);
+ if (methodResponse == null || methodResponse.getContent() == null || methodResponse.getContent().size() < 2) {
+ return Collections.singletonList(operation);
+ }
+
+ List splitOperations = new ArrayList<>();
+ Set usedOperationIds = new HashSet<>();
+ for (Map.Entry contentEntry : methodResponse.getContent().entrySet()) {
+ String mediaType = contentEntry.getKey();
+ CodegenOperation splitOperation = copyOperation(operation);
+ splitOperation.operationId = toSplitOperationId(operation.operationId, mediaType, usedOperationIds);
+ splitOperation.nickname = splitOperation.operationId;
+ splitOperation.operationIdLowerCase = splitOperation.operationId.toLowerCase(Locale.ROOT);
+ splitOperation.operationIdCamelCase = camelize(splitOperation.operationId);
+ splitOperation.operationIdSnakeCase = underscore(splitOperation.operationId);
+ splitOperation.produces = copyProduces(operation.produces, mediaType);
+ splitOperation.hasProduces = splitOperation.produces != null && !splitOperation.produces.isEmpty();
+ splitOperation.vendorExtensions = new HashMap<>(operation.vendorExtensions);
+ splitOperation.vendorExtensions.put("x-accepts", new String[]{mediaType});
+ applyResponseMediaType(splitOperation, contentEntry.getValue().getSchema());
+ splitOperation.responses = copyResponsesForMediaType(operation.responses, methodResponse, mediaType, contentEntry.getValue());
+ splitOperations.add(splitOperation);
+ }
+
+ return splitOperations;
+ }
+
+ private CodegenResponse findMethodResponse(CodegenOperation operation) {
+ return operation.responses.stream()
+ .filter(response -> response.is2xx)
+ .min(Comparator.comparing(response -> response.code))
+ .orElseGet(() -> operation.responses.stream()
+ .filter(response -> response.isDefault)
+ .findFirst()
+ .orElse(null));
+ }
+
+ private String toSplitOperationId(String operationId, String mediaType, Set usedOperationIds) {
+ String contentType = StringUtils.substringBefore(mediaType, ";");
+ String candidate = operationId + camelize(sanitizeName(contentType));
+ String uniqueCandidate = candidate;
+ int counter = 2;
+ while (!usedOperationIds.add(uniqueCandidate)) {
+ uniqueCandidate = candidate + counter++;
+ }
+ return uniqueCandidate;
+ }
+
+ private List