Skip to content

Commit 43885d7

Browse files
thejeff77claude
andcommitted
feat(kotlin-spring): add suspendFunctions option for coroutine support
Add a new `suspendFunctions` boolean config option (default: false) to the kotlin-spring generator. When enabled, all generated API operations get the `suspend` keyword on controller interfaces, delegate interfaces, service interfaces, and standalone controllers. This enables Spring MVC + Kotlin coroutines without requiring the full reactive stack (WebFlux/Flow). Users no longer need `runBlocking` in their delegate implementations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 05dd7fb commit 43885d7

7 files changed

Lines changed: 96 additions & 4 deletions

File tree

docs/generators/kotlin-spring.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
5656
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |null|
5757
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |null|
5858
|sourceFolder|source folder for generated code| |src/main/kotlin|
59+
|suspendFunctions|Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.| |false|
5960
|title|server title name or client service name| |OpenAPI Kotlin Spring|
6061
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
6162
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
100100
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";
101101
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
102102
public static final String COMPANION_OBJECT = "companionObject";
103+
public static final String SUSPEND_FUNCTIONS = "suspendFunctions";
103104

104105
@Getter
105106
public enum DeclarativeInterfaceReactiveMode {
@@ -166,6 +167,7 @@ public String getDescription() {
166167
@Setter private boolean autoXSpringPaginated = false;
167168
@Setter private boolean useSealedResponseInterfaces = false;
168169
@Setter private boolean companionObject = false;
170+
@Setter private boolean suspendFunctions = false;
169171

170172
@Getter @Setter
171173
protected boolean useSpringBoot3 = false;
@@ -273,6 +275,7 @@ public KotlinSpringServerCodegen() {
273275
addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map");
274276
addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated);
275277
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
278+
addSwitch(SUSPEND_FUNCTIONS, "Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.", suspendFunctions);
276279
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
277280
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
278281
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
@@ -663,6 +666,11 @@ public void processOpts() {
663666
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);
664667
writePropertyBack(USE_FLOW_FOR_ARRAY_RETURN_TYPE, useFlowForArrayReturnType);
665668

669+
if (additionalProperties.containsKey(SUSPEND_FUNCTIONS)) {
670+
this.setSuspendFunctions(convertPropertyToBoolean(SUSPEND_FUNCTIONS));
671+
}
672+
writePropertyBack(SUSPEND_FUNCTIONS, suspendFunctions);
673+
666674
if (additionalProperties.containsKey(BEAN_QUALIFIERS) && library.equals(SPRING_BOOT)) {
667675
this.setBeanQualifiers(convertPropertyToBoolean(BEAN_QUALIFIERS));
668676
}

modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
106106
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
107107
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
108108
)
109-
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
109+
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}
110110
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
111111
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
112112
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},

modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface {{classname}}Delegate {
3232
/**
3333
* @see {{classname}}#{{operationId}}
3434
*/
35-
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}},
35+
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}},
3636
{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
3737
{{/hasParams}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
3838
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},

modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ interface {{classname}} {
121121
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
122122
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
123123
)
124-
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
124+
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}
125125
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
126126
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
127127
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},

modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface {{classname}}Service {
3333
{{#isDeprecated}}
3434
@Deprecated(message="Operation is deprecated")
3535
{{/isDeprecated}}
36-
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}}
36+
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}}
3737
{{/operation}}
3838
}
3939
{{/operations}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5054,4 +5054,87 @@ public void shouldAddParameterWithInHeaderWhenImplicitHeadersIsTrue() throws IOE
50545054
Assert.assertTrue(content.contains("testHeader"),
50555055
"Header name 'testHeader' should appear in the annotation");
50565056
}
5057+
5058+
@Test
5059+
public void suspendFunctionsInterfaceOnly() throws Exception {
5060+
Path root = generateApiSources(Map.of(
5061+
KotlinSpringServerCodegen.SUSPEND_FUNCTIONS, true,
5062+
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
5063+
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
5064+
KotlinSpringServerCodegen.INTERFACE_ONLY, true,
5065+
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
5066+
), Map.of(
5067+
CodegenConstants.MODELS, "false",
5068+
CodegenConstants.MODEL_TESTS, "false",
5069+
CodegenConstants.MODEL_DOCS, "false",
5070+
CodegenConstants.APIS, "true",
5071+
CodegenConstants.SUPPORTING_FILES, "false"
5072+
));
5073+
verifyGeneratedFilesContain(
5074+
Map.of(
5075+
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
5076+
"suspend fun deletePet(",
5077+
"suspend fun getPetById("),
5078+
root.resolve("src/main/kotlin/org/openapitools/api/UserApi.kt"), List.of(
5079+
"suspend fun logoutUser()"),
5080+
root.resolve("src/main/kotlin/org/openapitools/api/StoreApi.kt"), List.of(
5081+
"suspend fun getInventory()")
5082+
)
5083+
);
5084+
}
5085+
5086+
@Test
5087+
public void suspendFunctionsWithDelegatePattern() throws Exception {
5088+
Path root = generateApiSources(Map.of(
5089+
KotlinSpringServerCodegen.SUSPEND_FUNCTIONS, true,
5090+
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
5091+
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
5092+
KotlinSpringServerCodegen.DELEGATE_PATTERN, true,
5093+
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
5094+
), Map.of(
5095+
CodegenConstants.MODELS, "false",
5096+
CodegenConstants.MODEL_TESTS, "false",
5097+
CodegenConstants.MODEL_DOCS, "false",
5098+
CodegenConstants.APIS, "true",
5099+
CodegenConstants.SUPPORTING_FILES, "false"
5100+
));
5101+
verifyGeneratedFilesContain(
5102+
Map.of(
5103+
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
5104+
"suspend fun deletePet(",
5105+
"suspend fun getPetById("),
5106+
root.resolve("src/main/kotlin/org/openapitools/api/PetApiDelegate.kt"), List.of(
5107+
"suspend fun deletePet(",
5108+
"suspend fun getPetById(")
5109+
)
5110+
);
5111+
}
5112+
5113+
@Test
5114+
public void suspendFunctionsDefaultsToFalse() throws Exception {
5115+
Path root = generateApiSources(Map.of(
5116+
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
5117+
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
5118+
KotlinSpringServerCodegen.INTERFACE_ONLY, true,
5119+
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
5120+
), Map.of(
5121+
CodegenConstants.MODELS, "false",
5122+
CodegenConstants.MODEL_TESTS, "false",
5123+
CodegenConstants.MODEL_DOCS, "false",
5124+
CodegenConstants.APIS, "true",
5125+
CodegenConstants.SUPPORTING_FILES, "false"
5126+
));
5127+
verifyGeneratedFilesContain(
5128+
Map.of(
5129+
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
5130+
"fun deletePet(",
5131+
"fun getPetById(")
5132+
)
5133+
);
5134+
// Verify no suspend keyword appears
5135+
Path petApiPath = root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt");
5136+
String content = new String(java.nio.file.Files.readAllBytes(petApiPath));
5137+
Assert.assertFalse(content.contains("suspend fun"),
5138+
"suspend should not be present when suspendFunctions is not enabled");
5139+
}
50575140
}

0 commit comments

Comments
 (0)