From 003b3d1d45321642c95cd570b1931086b53b8b82 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 29 Aug 2026 16:09:40 +0900 Subject: [PATCH 1/5] feat(kotlin-spring): support generic ResponseEntity return types --- docs/generators/kotlin-spring.md | 1 + .../languages/KotlinSpringServerCodegen.java | 15 +++- .../main/resources/kotlin-spring/api.mustache | 2 +- .../kotlin-spring/apiDelegate.mustache | 2 +- .../kotlin-spring/apiInterface.mustache | 2 +- .../resources/kotlin-spring/api_test.mustache | 2 +- .../httpInterfaceReturnTypes.mustache | 18 ++--- .../spring/KotlinSpringServerCodegenTest.java | 72 +++++++++++++++++++ .../resources/3_0/kotlin/issue_24804.yaml | 26 +++++++ 9 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/kotlin/issue_24804.yaml diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index e4a0074ee562..049ec99eb421 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -71,6 +71,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useFeignClientUrl|Whether to generate Feign client with url parameter.| |true| |useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true| |useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled.| |false| +|generateGenericResponseEntity|Use a generic type for the ResponseEntity wrapping return values of generated API methods. If enabled, methods are generated with return type ResponseEntity<*>| |false| |useResponseEntity|Whether (when false) to return actual type (e.g. List<Fruit>) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity<List<Fruit>>). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition| |true| |useSealedResponseInterfaces|Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)| |false| |useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| 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 12047a88e12f..a4465496fa3f 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 @@ -100,6 +100,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen public static final String USE_TAGS = "useTags"; public static final String BEAN_QUALIFIERS = "beanQualifiers"; public static final String USE_RESPONSE_ENTITY = "useResponseEntity"; + public static final String GENERATE_GENERIC_RESPONSE_ENTITY = "generateGenericResponseEntity"; public static final String DECLARATIVE_INTERFACE_REACTIVE_MODE = "declarativeInterfaceReactiveMode"; public static final String USE_SPRING_BOOT3 = "useSpringBoot3"; @@ -176,6 +177,7 @@ public enum RequestMappingMode { @Setter private boolean beanQualifiers = false; @Setter private DeclarativeInterfaceReactiveMode declarativeInterfaceReactiveMode = DeclarativeInterfaceReactiveMode.coroutines; @Setter private boolean useResponseEntity = true; + @Setter private boolean generateGenericResponseEntity = false; @Setter private boolean autoXSpringPaginated = false; @Setter private boolean generateSortValidation = false; @Setter private boolean generatePageableConstraintValidation = false; @@ -304,6 +306,9 @@ public KotlinSpringServerCodegen() { addSwitch(USE_RESPONSE_ENTITY, "Whether (when false) to return actual type (e.g. List) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity>). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition", useResponseEntity); + addSwitch(GENERATE_GENERIC_RESPONSE_ENTITY, + "Use a generic type for the ResponseEntity wrapping return values of generated API methods. If enabled, methods are generated with return type ResponseEntity<*>", + generateGenericResponseEntity); addSwitch(USE_SEALED_RESPONSE_INTERFACES, "Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)", useSealedResponseInterfaces); @@ -613,11 +618,19 @@ public void processOpts() { additionalProperties.put(CodegenConstants.LIBRARY, library); } - if(additionalProperties.containsKey(USE_RESPONSE_ENTITY)) { + if (additionalProperties.containsKey(USE_RESPONSE_ENTITY)) { this.setUseResponseEntity(Boolean.parseBoolean(additionalProperties.get(USE_RESPONSE_ENTITY).toString())); } writePropertyBack(USE_RESPONSE_ENTITY, useResponseEntity); + if (additionalProperties.containsKey(GENERATE_GENERIC_RESPONSE_ENTITY)) { + this.setGenerateGenericResponseEntity(Boolean.parseBoolean(additionalProperties.get(GENERATE_GENERIC_RESPONSE_ENTITY).toString())); + } + if (!useResponseEntity) { + this.setGenerateGenericResponseEntity(false); + } + writePropertyBack(GENERATE_GENERIC_RESPONSE_ENTITY, generateGenericResponseEntity); + if(additionalProperties.containsKey(USE_SEALED_RESPONSE_INTERFACES)) { this.setUseSealedResponseInterfaces(Boolean.parseBoolean(additionalProperties.get(USE_SEALED_RESPONSE_INTERFACES).toString())); } 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 9ff4eb9308dc..4bd0bcad9729 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache @@ -120,7 +120,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v {{/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}}, {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} - {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} { + {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} { return {{>returnValue}} } 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 e6efd9948039..1bb8aa987f63 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -36,7 +36,7 @@ interface {{classname}}Delegate { {{/-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}}, - {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { + {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} 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 8059e27f83b6..25315ecd569a 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache @@ -135,7 +135,7 @@ interface {{classname}} { {{/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}}, {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} - {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>{{/useResponseEntity}}{{^skipDefaultApiInterface}} { + {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultApiInterface}} { {{^isDelegate}} return {{>returnValue}} {{/isDelegate}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache index 8950633ae979..f9c5c8043374 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache @@ -36,7 +36,7 @@ class {{classname}}Test { {{/allParams}} {{#includeHttpRequestContext}}val {{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}} = TODO(){{/includeHttpRequestContext}} {{#vendorExtensions.x-spring-paginated}}val pageable: Pageable = TODO(){{/vendorExtensions.x-spring-paginated}} - val response: {{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{>returnTypes}}{{#useResponseEntity}}>{{/useResponseEntity}} = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#reactive}}exchange{{/reactive}}{{^reactive}}request{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, {{/includeHttpRequestContext}}{{/hasParams}}pageable{{/vendorExtensions.x-spring-paginated}}) + val response: {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#reactive}}exchange{{/reactive}}{{^reactive}}request{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, {{/includeHttpRequestContext}}{{/hasParams}}pageable{{/vendorExtensions.x-spring-paginated}}) // TODO: test validations } 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 f4bb3da4fa13..a9ce4486e91e 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 @@ -1,26 +1,26 @@ {{! handle reactive map and array}} {{#reactive}} {{#isMap}} -{{#reactiveModeReactor}}Mono<{{/reactiveModeReactor}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}Map{{#useResponseEntity}}>{{/useResponseEntity}}{{#reactiveModeReactor}}>{{/reactiveModeReactor}} +{{#reactiveModeReactor}}Mono<{{/reactiveModeReactor}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}Map{{/generateGenericResponseEntity}}{{#useResponseEntity}}>{{/useResponseEntity}}{{#reactiveModeReactor}}>{{/reactiveModeReactor}} {{/isMap}} {{#isArray}} {{! array handle reactive - reactor with/without ResponseEntity wrapper}} {{#reactiveModeReactor}} -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}}> +Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{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}}>{{/generateGenericResponseEntity}}{{#useResponseEntity}}>{{/useResponseEntity}}> {{/reactiveModeReactor}} {{! array handle reactive - coroutines with/without ResponseEntity wrapper}} {{#reactiveModeCoroutines}} -{{#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}} +{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{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}}>{{/generateGenericResponseEntity}}{{#useResponseEntity}}>{{/useResponseEntity}} {{/reactiveModeCoroutines}} {{/isArray}} {{! handle reactive non-container - with/without ResponseEntity wrapper}} {{^returnContainer}} {{#reactiveModeReactor}} -Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#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}}> +Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#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}}{{/generateGenericResponseEntity}}{{#useResponseEntity}}>{{/useResponseEntity}}> {{/reactiveModeReactor}} {{#reactiveModeCoroutines}} {{#useResponseEntity}} -ResponseEntity<{{/useResponseEntity}}{{#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}}> +ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#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}}{{/generateGenericResponseEntity}}{{#useResponseEntity}}> {{/useResponseEntity}} {{/reactiveModeCoroutines}} {{/returnContainer}} @@ -29,19 +29,19 @@ ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorEx {{! handle non-reactive map and array}} {{#isMap}} {{#useResponseEntity}} -ResponseEntity<{{/useResponseEntity}}Map{{#useResponseEntity}}> +ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}Map{{/generateGenericResponseEntity}}{{#useResponseEntity}}> {{/useResponseEntity}} {{/isMap}} {{#isArray}} {{! array handle non-reactive - with/without ResponseEntity wrapper}} {{#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}}> +ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{{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}}>{{/generateGenericResponseEntity}}{{#useResponseEntity}}> {{/useResponseEntity}} {{/isArray}} {{! handle reactive non-container - with/without ResponseEntity wrapper}} {{^returnContainer}} {{#useResponseEntity}} -ResponseEntity<{{/useResponseEntity}}{{#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}}> +ResponseEntity<{{/useResponseEntity}}{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#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}}{{/generateGenericResponseEntity}}{{#useResponseEntity}}> {{/useResponseEntity}} {{/returnContainer}} -{{/reactive}} \ No newline at end of file +{{/reactive}} 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 7671d2683121..4046e5663621 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 @@ -3202,6 +3202,78 @@ public void nonReactiveWithResponseEntity() throws Exception { ); } + @Test + public void genericResponseEntityForMultipleResponseShapes() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + additionalProperties.put(KotlinSpringServerCodegen.GENERATE_GENERIC_RESPONSE_ENTITY, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/issue_24804.yaml", additionalProperties); + + assertFileContains(files.get("OperationsApiController.kt").toPath(), + "fun startOperation(): ResponseEntity<*>"); + assertFileContains(files.get("OperationsApiTest.kt").toPath(), + "val response: ResponseEntity<*> = api.startOperation()"); + } + + @Test + public void genericResponseEntityForInterfaceAndDelegate() throws Exception { + Map interfaceProperties = new HashMap<>(); + interfaceProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + interfaceProperties.put(KotlinSpringServerCodegen.GENERATE_GENERIC_RESPONSE_ENTITY, true); + interfaceProperties.put(INTERFACE_ONLY, true); + + Map interfaceFiles = generateFromContract( + "src/test/resources/3_0/kotlin/issue_24804.yaml", interfaceProperties); + assertFileContains(interfaceFiles.get("OperationsApi.kt").toPath(), + "fun startOperation(): ResponseEntity<*>"); + + Map delegateProperties = new HashMap<>(); + delegateProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + delegateProperties.put(KotlinSpringServerCodegen.GENERATE_GENERIC_RESPONSE_ENTITY, true); + delegateProperties.put(DELEGATE_PATTERN, true); + + Map delegateFiles = generateFromContract( + "src/test/resources/3_0/kotlin/issue_24804.yaml", delegateProperties); + assertFileContains(delegateFiles.get("OperationsApi.kt").toPath(), + "fun startOperation(): ResponseEntity<*>"); + assertFileContains(delegateFiles.get("OperationsApiDelegate.kt").toPath(), + "fun startOperation(): ResponseEntity<*>"); + } + + @Test + public void genericResponseEntityIsIgnoredWithoutResponseEntity() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, false); + additionalProperties.put(KotlinSpringServerCodegen.GENERATE_GENERIC_RESPONSE_ENTITY, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/issue_24804.yaml", additionalProperties); + + assertFileContains(files.get("OperationsApiController.kt").toPath(), + "fun startOperation(): Unit"); + assertFileNotContains(files.get("OperationsApiController.kt").toPath(), "ResponseEntity<*>"); + } + + @Test + public void genericResponseEntityForDeclarativeHttpInterface() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + additionalProperties.put(KotlinSpringServerCodegen.GENERATE_GENERIC_RESPONSE_ENTITY, true); + additionalProperties.put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/issue_24804.yaml", + additionalProperties, + new HashMap<>(), + configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY)); + + assertFileContains(files.get("DefaultApi.kt").toPath(), + "fun startOperation(", + "): ResponseEntity<*>"); + } + /** * Regression test for https://github.com/OpenAPITools/openapi-generator/issues/17445. * OpenAPI 'default' responses must emit responseCode = "default" in @ApiResponse (swagger2), diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/issue_24804.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue_24804.yaml new file mode 100644 index 000000000000..6815e47503d2 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue_24804.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.3 +info: + title: Generic response entity reproduction + version: 1.0.0 +paths: + /operations: + post: + operationId: startOperation + responses: + '201': + description: Operation completed successfully + '202': + description: Operation accepted for asynchronous processing + content: + application/json: + schema: + $ref: '#/components/schemas/OperationStatusResponse' +components: + schemas: + OperationStatusResponse: + type: object + required: + - status + properties: + status: + type: string From 9d46838067b402eb8e9ed6aef269e1504efeaf00 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 29 Aug 2026 16:20:36 +0900 Subject: [PATCH 2/5] docs(kotlin-spring): refresh generated documentation --- docs/generators/kotlin-spring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index 049ec99eb421..7210790a52a9 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -33,6 +33,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |documentationProvider|Select the OpenAPI documentation provider.|
**none**
Do not publish an OpenAPI specification.
**source**
Publish the original input OpenAPI specification.
**springdoc**
Generate an OpenAPI 3 specification using SpringDoc.
|springdoc| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| +|generateGenericResponseEntity|Use a generic type for the ResponseEntity wrapping return values of generated API methods. If enabled, methods are generated with return type ResponseEntity<*>| |false| |generateJsonIncludeAnnotations|Whether to generate policy @JsonInclude annotations on model properties. When true, emits spec-honest annotations (required-field protection and the optional non-nullable policy from optionalNonNullPropertyJsonInclude). When false, none are generated and the global ObjectMapper owns inclusion. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning. A per-property override set via the `x-jackson-json-include-policy` vendor extension is always honored regardless of this flag.| |false| |generateJsonSetterNullsAnnotations|Whether to generate @JsonSetter(nulls = ...) annotations on optional non-nullable model properties. When true, emits @JsonSetter (Nulls.FAIL when openApiNullable is true, otherwise Nulls.SKIP) so an explicit null in the payload is handled explicitly. When false, none are generated and deserialization null-handling defers to the global ObjectMapper. When left unset it defaults to false (7.23.0-equivalent output) and logs a warning; set it explicitly to silence the warning.| |false| |generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library is spring-boot or spring-cloud.| |false| @@ -71,7 +72,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useFeignClientUrl|Whether to generate Feign client with url parameter.| |true| |useFlowForArrayReturnType|Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.| |true| |useJackson3|Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled.| |false| -|generateGenericResponseEntity|Use a generic type for the ResponseEntity wrapping return values of generated API methods. If enabled, methods are generated with return type ResponseEntity<*>| |false| |useResponseEntity|Whether (when false) to return actual type (e.g. List<Fruit>) and handle non-happy path responses via exceptions flow or (when true) return entire ResponseEntity (e.g. ResponseEntity<List<Fruit>>). If disabled, method are annotated using a @ResponseStatus annotation, which has the status of the first response declared in the Api definition| |true| |useSealedResponseInterfaces|Generate sealed interfaces for endpoint responses that all possible response types implement. Allows controllers to return any valid response type in a type-safe manner (e.g., sealed interface CreateUserResponse implemented by User, ConflictResponse, ErrorResponse)| |false| |useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| From c26e4e41e40e4e120f8b9d69d1b9516ce1eec615 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 29 Aug 2026 17:24:32 +0900 Subject: [PATCH 3/5] fix(kotlin-spring): preserve sealed response return types --- .../main/resources/kotlin-spring/api.mustache | 2 +- .../kotlin-spring/apiDelegate.mustache | 2 +- .../kotlin-spring/apiInterface.mustache | 2 +- .../resources/kotlin-spring/api_test.mustache | 2 +- .../spring/KotlinSpringServerCodegenTest.java | 31 +++++++++++++++++++ 5 files changed, 35 insertions(+), 4 deletions(-) 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 4bd0bcad9729..177cc0337d91 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache @@ -120,7 +120,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v {{/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}}, {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} - {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} { + {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{>returnTypes}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} { return {{>returnValue}} } 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 1bb8aa987f63..a7d2fef26117 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -36,7 +36,7 @@ interface {{classname}}Delegate { {{/-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}}, - {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { + {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{>returnTypes}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} 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 25315ecd569a..132f78475c9c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache @@ -135,7 +135,7 @@ interface {{classname}} { {{/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}}, {{/includeHttpRequestContext}}{{/hasParams}}{{#vendorExtensions.x-pageable-extra-annotation}}{{{.}}} {{/vendorExtensions.x-pageable-extra-annotation}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}{{#hasParams}} - {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultApiInterface}} { + {{/hasParams}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/useResponseEntity}}{{^skipDefaultApiInterface}} { {{^isDelegate}} return {{>returnValue}} {{/isDelegate}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache index f9c5c8043374..83d92f25e755 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api_test.mustache @@ -36,7 +36,7 @@ class {{classname}}Test { {{/allParams}} {{#includeHttpRequestContext}}val {{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}} = TODO(){{/includeHttpRequestContext}} {{#vendorExtensions.x-spring-paginated}}val pageable: Pageable = TODO(){{/vendorExtensions.x-spring-paginated}} - val response: {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#reactive}}exchange{{/reactive}}{{^reactive}}request{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, {{/includeHttpRequestContext}}{{/hasParams}}pageable{{/vendorExtensions.x-spring-paginated}}) + val response: {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{>returnTypes}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}} = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}}, {{/hasParams}}{{#reactive}}exchange{{/reactive}}{{^reactive}}request{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}}, {{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}}, {{/includeHttpRequestContext}}{{/hasParams}}pageable{{/vendorExtensions.x-spring-paginated}}) // TODO: test validations } 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 4046e5663621..d00657780def 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 @@ -3274,6 +3274,37 @@ public void genericResponseEntityForDeclarativeHttpInterface() throws Exception "): ResponseEntity<*>"); } + @Test + public void sealedResponseInterfaceIsPreservedWithoutResponseEntity() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, false); + additionalProperties.put(KotlinSpringServerCodegen.USE_SEALED_RESPONSE_INTERFACES, true); + additionalProperties.put(INTERFACE_ONLY, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/sealed-response-interfaces.yaml", additionalProperties); + + assertFileContains(files.get("UsersApi.kt").toPath(), + "): CreateUserResponse", + "): GetUserResponse"); + } + + @Test + public void sealedResponseInterfaceDoesNotChangeControllerServiceReturnType() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + additionalProperties.put(KotlinSpringServerCodegen.USE_SEALED_RESPONSE_INTERFACES, true); + additionalProperties.put(SERVICE_INTERFACE, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/sealed-response-interfaces.yaml", additionalProperties); + + assertFileContains(files.get("UsersApiController.kt").toPath(), + "): ResponseEntity"); + assertFileContains(files.get("UsersApiService.kt").toPath(), + "): User"); + } + /** * Regression test for https://github.com/OpenAPITools/openapi-generator/issues/17445. * OpenAPI 'default' responses must emit responseCode = "default" in @ApiResponse (swagger2), From 4792aa712a32699b6edd024cc318bc61c11d82d4 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 29 Aug 2026 17:59:45 +0900 Subject: [PATCH 4/5] fix(kotlin-spring): preserve sealed delegate response types --- .../resources/kotlin-spring/apiDelegate.mustache | 2 +- .../spring/KotlinSpringServerCodegenTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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 a7d2fef26117..1bb8aa987f63 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -36,7 +36,7 @@ interface {{classname}}Delegate { {{/-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}}, - {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{>returnTypes}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { + {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} 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 d00657780def..7e89953f1a52 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 @@ -3305,6 +3305,21 @@ public void sealedResponseInterfaceDoesNotChangeControllerServiceReturnType() th "): User"); } + @Test + public void sealedResponseInterfaceIsPreservedInDelegate() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true); + additionalProperties.put(KotlinSpringServerCodegen.USE_SEALED_RESPONSE_INTERFACES, true); + additionalProperties.put(DELEGATE_PATTERN, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/sealed-response-interfaces.yaml", additionalProperties); + + assertFileContains(files.get("UsersApiDelegate.kt").toPath(), + "): ResponseEntity", + "): ResponseEntity"); + } + /** * Regression test for https://github.com/OpenAPITools/openapi-generator/issues/17445. * OpenAPI 'default' responses must emit responseCode = "default" in @ApiResponse (swagger2), From 650c58579ac16f2271ebd55f6e829f96e4cdaa01 Mon Sep 17 00:00:00 2001 From: c19yamahar Date: Sat, 29 Aug 2026 20:33:58 +0900 Subject: [PATCH 5/5] fix(kotlin-spring): preserve sealed delegate types without response entity --- .../resources/kotlin-spring/apiDelegate.mustache | 2 +- .../spring/KotlinSpringServerCodegenTest.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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 1bb8aa987f63..b7b931e8b3de 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -36,7 +36,7 @@ interface {{classname}}Delegate { {{/-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}}, - {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{>returnTypes}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { + {{/includeHttpRequestContext}}{{/hasParams}}pageable: Pageable{{/vendorExtensions.x-spring-paginated}}): {{#useResponseEntity}}ResponseEntity<{{#generateGenericResponseEntity}}*{{/generateGenericResponseEntity}}{{^generateGenericResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/generateGenericResponseEntity}}>{{/useResponseEntity}}{{^useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{>returnTypes}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{>returnTypes}}{{/useSealedResponseInterfaces}}{{/useResponseEntity}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} 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 7e89953f1a52..31f71ca0ff94 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 @@ -3289,6 +3289,21 @@ public void sealedResponseInterfaceIsPreservedWithoutResponseEntity() throws Exc "): GetUserResponse"); } + @Test + public void sealedResponseInterfaceIsPreservedInDelegateWithoutResponseEntity() throws Exception { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, false); + additionalProperties.put(KotlinSpringServerCodegen.USE_SEALED_RESPONSE_INTERFACES, true); + additionalProperties.put(DELEGATE_PATTERN, true); + + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/sealed-response-interfaces.yaml", additionalProperties); + + assertFileContains(files.get("UsersApiDelegate.kt").toPath(), + "): CreateUserResponse", + "): GetUserResponse"); + } + @Test public void sealedResponseInterfaceDoesNotChangeControllerServiceReturnType() throws Exception { Map additionalProperties = new HashMap<>();