From 857e8b1f53559fd36e326953718d4eedf90766c8 Mon Sep 17 00:00:00 2001 From: Jachym Metlicka Date: Tue, 23 Jun 2026 00:45:35 +0200 Subject: [PATCH 1/5] fix(kotlin-spring): fix Flow and Flux array return types for reactive generators --- .../languages/KotlinSpringServerCodegen.java | 13 ++ .../main/resources/kotlin-spring/api.mustache | 2 +- .../kotlin-spring/apiDelegate.mustache | 2 +- .../kotlin-spring/apiInterface.mustache | 2 +- .../apiInterface.mustache | 1 - .../httpInterfaceReturnTypes.mustache | 2 +- .../kotlin-spring/returnTypes.mustache | 2 +- .../resources/kotlin-spring/service.mustache | 2 +- .../kotlin-spring/serviceImpl.mustache | 2 +- .../spring/KotlinSpringServerCodegenTest.java | 119 ++++++++++++++---- 10 files changed, 118 insertions(+), 29 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index b94c7c42d2bc..52e3afb48810 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -1614,6 +1614,19 @@ public void setReturnContainer(final String returnContainer) { } }); + // Flow is broken — StringDecoder intercepts String and returns the entire + // JSON array as a single blob instead of using Jackson. Fix by switching + // array-of-string operations to List (with suspend). + // See https://github.com/spring-projects/spring-framework/issues/22662 + // Note: check operation.returnType (set by doDataTypeAssignment) which holds the + // unwrapped inner type, e.g. "kotlin.String" for List arrays. + // The declarative-http-interface library forces useFlowForArrayReturnType=false, + // so this condition only fires for the spring-boot coroutines path. + if (reactive && useFlowForArrayReturnType + && operation.isArray && "kotlin.String".equals(operation.returnType)) { + operation.vendorExtensions.put("x-reactive-array-string-return", true); + } + // Generate sealed response interface metadata if enabled if (useSealedResponseInterfaces && responses != null && !responses.isEmpty()) { // Generate sealed interface name from operation ID diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache index dcc950cdeb75..b5b8e877dda6 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache @@ -106,7 +106,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} ) - {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}} + {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{#vendorExtensions.x-reactive-array-string-return}}suspend {{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}} {{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/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}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache index 9dbeecc9a1a9..e6efd9948039 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -32,7 +32,7 @@ interface {{classname}}Delegate { /** * @see {{classname}}#{{operationId}} */ - {{#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}}, + {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{#vendorExtensions.x-reactive-array-string-return}}suspend {{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/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}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache index 851b07427a6f..0f42a7e473d9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache @@ -121,7 +121,7 @@ interface {{classname}} { produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} ) - {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}} + {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{#vendorExtensions.x-reactive-array-string-return}}suspend {{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}} {{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/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}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/apiInterface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/apiInterface.mustache index 53ce2730aa8b..2e7a59289cb4 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/apiInterface.mustache @@ -43,7 +43,6 @@ import {{javaxPackage}}.validation.constraints.* {{/useBeanValidation}} {{#reactiveModeReactor}} -import reactor.core.publisher.Flux import reactor.core.publisher.Mono {{/reactiveModeReactor}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/httpInterfaceReturnTypes.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/httpInterfaceReturnTypes.mustache index 4f36af09bfba..f4bb3da4fa13 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/httpInterfaceReturnTypes.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/httpInterfaceReturnTypes.mustache @@ -6,7 +6,7 @@ {{#isArray}} {{! array handle reactive - reactor with/without ResponseEntity wrapper}} {{#reactiveModeReactor}} -{{#useResponseEntity}}Mono{{#useResponseEntity}}>>{{/useResponseEntity}} +Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{{returnContainer}}}<{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>{{/useResponseEntity}}> {{/reactiveModeReactor}} {{! array handle reactive - coroutines with/without ResponseEntity wrapper}} {{#reactiveModeCoroutines}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/returnTypes.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/returnTypes.mustache index 612aa9ec0599..a3a7a14b00ff 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/returnTypes.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/returnTypes.mustache @@ -1 +1 @@ -{{#isMap}}Map{{/isMap}}{{#isArray}}{{#reactive}}{{#useFlowForArrayReturnType}}Flow{{/useFlowForArrayReturnType}}{{^useFlowForArrayReturnType}}{{{returnContainer}}}{{/useFlowForArrayReturnType}}{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} \ No newline at end of file +{{#isMap}}Map{{/isMap}}{{#isArray}}{{#reactive}}{{#vendorExtensions.x-reactive-array-string-return}}{{{returnContainer}}}{{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{#useFlowForArrayReturnType}}Flow{{/useFlowForArrayReturnType}}{{^useFlowForArrayReturnType}}{{{returnContainer}}}{{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache index c6372d1bf827..e8972a7afd51 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache @@ -33,7 +33,7 @@ interface {{classname}}Service { {{#isDeprecated}} @Deprecated(message="Operation is deprecated") {{/isDeprecated}} - {{#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}} + {{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{#vendorExtensions.x-reactive-array-string-return}}suspend {{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/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}} {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache index 181bfa52991c..18d61ae1bb23 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service class {{classname}}ServiceImpl : {{classname}}Service { {{#operation}} - override {{#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}} { + override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{#vendorExtensions.x-reactive-array-string-return}}suspend {{/vendorExtensions.x-reactive-array-string-return}}{{^vendorExtensions.x-reactive-array-string-return}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/vendorExtensions.x-reactive-array-string-return}}{{/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}} { TODO("Implement me") } {{/operation}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 7fbc5ceea289..72dccb02eb76 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -373,13 +373,13 @@ public void delegateReactiveWithTags() throws Exception { "ApiUtil"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV2Api.kt"), - "import kotlinx.coroutines.flow.Flow", "ResponseEntity>"); + "import kotlinx.coroutines.flow.Flow", "ResponseEntity>"); assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV2Api.kt"), "exchange"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV2ApiDelegate.kt"), - "import kotlinx.coroutines.flow.Flow", "ResponseEntity>"); + "import kotlinx.coroutines.flow.Flow", "suspend fun", "ResponseEntity>"); assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV2ApiDelegate.kt"), - "suspend fun", "ApiUtil"); + "ApiUtil"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV3Api.kt"), "import kotlinx.coroutines.flow.Flow", "requestBody: Flow"); @@ -1279,8 +1279,7 @@ public void generateHttpInterfaceReactiveWithReactorResponseEntity() throws Exce Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/StoreApi.kt"); assertFileContains( path, - "import reactor.core.publisher.Flux\n" - + "import reactor.core.publisher.Mono", + "import reactor.core.publisher.Mono", " @HttpExchange(\n" + " // \"/store/inventory\"\n" + " url = PATH_GET_INVENTORY,\n" @@ -1393,8 +1392,7 @@ public void generateHttpInterfaceReactiveWithReactor() throws Exception { Path path = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/StoreApi.kt"); assertFileContains( path, - "import reactor.core.publisher.Flux\n" - + "import reactor.core.publisher.Mono", + "import reactor.core.publisher.Mono", " fun getInventory(\n" + " ): Mono>", " fun deleteOrder(\n" @@ -3129,21 +3127,21 @@ public void reactiveWithFlow() throws Exception { ); assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiController.kt"), - "List"); + "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), "Flow"); } @@ -3175,21 +3173,21 @@ public void reactiveWithDefaultValueFlow() throws Exception { ); assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiController.kt"), - "List"); + "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1Api.kt"), "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiDelegate.kt"), "Flow"); - assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), - "List"); assertFileContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), + "List"); + assertFileNotContains(Paths.get(output + "/src/main/kotlin/org/openapitools/api/TestV1ApiService.kt"), "Flow"); } @@ -4152,6 +4150,85 @@ public void springPaginatedDelegateCallPassesPageable() throws Exception { } } + @Test(description = "reactive spring-boot: array-of-string returns List with suspend, not Flow (issue #22662)") + public void reactiveArrayOfStringReturnsListNotFlow() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.INTERFACE_ONLY, true); + + List files = new DefaultGenerator() + .opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/bugs/issue_7118.yaml")) + .config(codegen)) + .generate(); + + Path apiPath = files.stream() + .filter(f -> f.getName().equals("UsersApi.kt")) + .findFirst() + .orElseThrow() + .toPath(); + + assertFileContains(apiPath, "suspend fun", "List"); + assertFileNotContains(apiPath, "Flow"); + } + + @Test(description = "declarative http interface reactor: array-of-string returns Mono>, not Flux (issue #22662)") + public void declarativeReactorArrayOfStringReturnsMono() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CodegenConstants.LIBRARY, SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY); + codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.DECLARATIVE_INTERFACE_REACTIVE_MODE, "reactor"); + codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, false); + codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); + + List files = new DefaultGenerator() + .opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/bugs/issue_7118.yaml")) + .config(codegen)) + .generate(); + + Path apiPath = files.stream() + .filter(f -> f.getName().equals("UsersApi.kt")) + .findFirst() + .orElseThrow() + .toPath(); + + assertFileContains(apiPath, "Mono>"); + assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux"); + } + + @Test(description = "declarative http interface reactor + ResponseEntity: array-of-string returns Mono>> (issue #22662)") + public void declarativeReactorArrayOfStringReturnsMonoResponseEntity() throws Exception { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CodegenConstants.LIBRARY, SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY); + codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.DECLARATIVE_INTERFACE_REACTIVE_MODE, "reactor"); + codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); + + List files = new DefaultGenerator() + .opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/bugs/issue_7118.yaml")) + .config(codegen)) + .generate(); + + Path apiPath = files.stream() + .filter(f -> f.getName().equals("UsersApi.kt")) + .findFirst() + .orElseThrow() + .toPath(); + + assertFileContains(apiPath, "Mono>>"); + assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux"); + } + private Map generateFromContract(String url) throws IOException { return generateFromContract(url, new HashMap<>(), new HashMap<>()); } From c9b4dc007d6508eae838f9304af0c55c2d1be0c4 Mon Sep 17 00:00:00 2001 From: Jachym Metlicka Date: Mon, 13 Jul 2026 14:48:57 +0200 Subject: [PATCH 2/5] fix(kotlin): unwrap Set/MutableSet return types to fix reactive Set arrays doDataTypeAssignment only unwrapped List/MutableList, leaving Set (uniqueItems) return types as the full 'kotlin.collections.Set<...>' with a raw 'set' container. This broke reactive array-of-string handling for Sets: the declarative reactor path emitted the non-compiling 'Mono>>', and the coroutines path emitted a nonsensical non-suspend 'Flow>'. Unwrap Set/MutableSet like List so returnType becomes the inner type and returnContainer is 'Set'. This makes the x-reactive-array-string-return fix apply to Set too, and also fixes the same latent bug in kotlin-server (ktor/javalin). Tightened the reactive tests to exercise the uniqueItems Set operation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../codegen/languages/AbstractKotlinCodegen.java | 12 ++++++++++++ .../spring/KotlinSpringServerCodegenTest.java | 15 +++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java index c47f30e6a367..9f98815c7b5e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java @@ -1350,6 +1350,18 @@ protected void doDataTypeAssignment(final String returnType, DataTypeAssigner da dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableList<".length(), end).trim()); dataTypeAssigner.setReturnContainer("List"); } + } else if (returnType.startsWith("kotlin.collections.MutableSet")) { + int end = returnType.lastIndexOf(">"); + if (end > 0) { + dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.MutableSet<".length(), end).trim()); + dataTypeAssigner.setReturnContainer("Set"); + } + } else if (returnType.startsWith("kotlin.collections.Set")) { + int end = returnType.lastIndexOf(">"); + if (end > 0) { + dataTypeAssigner.setReturnType(returnType.substring("kotlin.collections.Set<".length(), end).trim()); + dataTypeAssigner.setReturnContainer("Set"); + } } else if (returnType.startsWith("kotlin.collections.Map")) { int end = returnType.lastIndexOf(">"); if (end > 0) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 29e895d5fa23..018659c1cdf4 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -4186,8 +4186,9 @@ public void reactiveArrayOfStringReturnsListNotFlow() throws Exception { .orElseThrow() .toPath(); - assertFileContains(apiPath, "suspend fun", "List"); - assertFileNotContains(apiPath, "Flow"); + assertFileContains(apiPath, "suspend fun", "List", "Set"); + // neither the list nor the uniqueItems (Set) operation must leak Flow<...> or a raw/nested container + assertFileNotContains(apiPath, "Flow", "Flow>"); - assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux"); + assertFileContains(apiPath, "Mono>", "Mono>"); + assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux", + "kotlin.collections.Set<", "Mono>>"); - assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux"); + assertFileContains(apiPath, "Mono>>", "Mono>>"); + assertFileNotContains(apiPath, "Flux", "import reactor.core.publisher.Flux", + "kotlin.collections.Set<", "Mono generateFromContract(String url) throws IOException { From 6e851b417373f1bc9fec59f74282af0510728152 Mon Sep 17 00:00:00 2001 From: Jachym Metlicka Date: Mon, 13 Jul 2026 15:07:12 +0200 Subject: [PATCH 3/5] updated samples --- .../src/main/kotlin/org/openapitools/api/PetApi.kt | 5 ++--- .../src/main/kotlin/org/openapitools/api/StoreApi.kt | 1 - .../src/main/kotlin/org/openapitools/api/UserApi.kt | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt index 155e5e314ca7..e7b8463c72ad 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -21,7 +21,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List @@ -58,7 +57,7 @@ interface PetApi { ) fun findPetsByStatus( @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( @@ -68,7 +67,7 @@ interface PetApi { ) fun findPetsByTags( @NotNull @Parameter(description = "Tags to filter by", required = true) @Valid @RequestParam(value = "tags", required = true) tags: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt index d16ddcb5cbe3..fcbeebbe7764 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt @@ -20,7 +20,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt index db999b112dbe..5c6814419e1e 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt @@ -20,7 +20,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List From 7733960880f923dd49289c10bdcf2ef768ab511f Mon Sep 17 00:00:00 2001 From: Jachym Metlicka Date: Tue, 14 Jul 2026 08:58:29 +0200 Subject: [PATCH 4/5] Revert "updated samples" This reverts commit 6e851b417373f1bc9fec59f74282af0510728152. --- .../src/main/kotlin/org/openapitools/api/PetApi.kt | 5 +++-- .../src/main/kotlin/org/openapitools/api/StoreApi.kt | 1 + .../src/main/kotlin/org/openapitools/api/UserApi.kt | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt index e7b8463c72ad..155e5e314ca7 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -21,6 +21,7 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* +import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List @@ -57,7 +58,7 @@ interface PetApi { ) fun findPetsByStatus( @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( @@ -67,7 +68,7 @@ interface PetApi { ) fun findPetsByTags( @NotNull @Parameter(description = "Tags to filter by", required = true) @Valid @RequestParam(value = "tags", required = true) tags: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt index fcbeebbe7764..d16ddcb5cbe3 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt @@ -20,6 +20,7 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* +import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt index 5c6814419e1e..db999b112dbe 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt @@ -20,6 +20,7 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* +import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List From a10b41809e0e9c5b2cb2a410b6d20d39a25e6d9a Mon Sep 17 00:00:00 2001 From: Jachym Metlicka Date: Tue, 14 Jul 2026 08:58:33 +0200 Subject: [PATCH 5/5] Reapply "updated samples" This reverts commit 7733960880f923dd49289c10bdcf2ef768ab511f. --- .../src/main/kotlin/org/openapitools/api/PetApi.kt | 5 ++--- .../src/main/kotlin/org/openapitools/api/StoreApi.kt | 1 - .../src/main/kotlin/org/openapitools/api/UserApi.kt | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt index 155e5e314ca7..e7b8463c72ad 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -21,7 +21,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List @@ -58,7 +57,7 @@ interface PetApi { ) fun findPetsByStatus( @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( @@ -68,7 +67,7 @@ interface PetApi { ) fun findPetsByTags( @NotNull @Parameter(description = "Tags to filter by", required = true) @Valid @RequestParam(value = "tags", required = true) tags: kotlin.collections.List - ): Mono>> + ): Mono>> @HttpExchange( diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt index d16ddcb5cbe3..fcbeebbe7764 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/StoreApi.kt @@ -20,7 +20,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List diff --git a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt index db999b112dbe..5c6814419e1e 100644 --- a/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt +++ b/samples/server/petstore/kotlin-spring-declarative-interface-reactive-reactor-wrapped/src/main/kotlin/org/openapitools/api/UserApi.kt @@ -20,7 +20,6 @@ import org.springframework.validation.annotation.Validated import jakarta.validation.Valid import jakarta.validation.constraints.* -import reactor.core.publisher.Flux import reactor.core.publisher.Mono import kotlin.collections.List