diff --git a/.github/workflows/samples-kotlin-server-jdk17.yaml b/.github/workflows/samples-kotlin-server-jdk17.yaml
index b461b2cf8a69..b5bde97258c5 100644
--- a/.github/workflows/samples-kotlin-server-jdk17.yaml
+++ b/.github/workflows/samples-kotlin-server-jdk17.yaml
@@ -27,7 +27,7 @@ on:
# - samples/server/petstore/kotlin-spring-default/**
env:
- GRADLE_VERSION: '8.11'
+ GRADLE_VERSION: '8.14'
jobs:
build:
@@ -48,6 +48,7 @@ jobs:
- samples/server/petstore/kotlin-springboot-additionalproperties
- samples/server/petstore/kotlin-springboot-delegate-nodefaults
- samples/server/petstore/kotlin-springboot-request-cookie
+ - samples/server/petstore/kotlin-springboot-4
- samples/server/petstore/kotlin-server/jaxrs-spec
- samples/server/petstore/kotlin-server/jaxrs-spec-mutiny
- samples/server/petstore/kotlin-server/javalin
diff --git a/.github/workflows/samples-kotlin-server-jdk21.yaml b/.github/workflows/samples-kotlin-server-jdk21.yaml
index 43e96e60f9af..1421dd4714d2 100644
--- a/.github/workflows/samples-kotlin-server-jdk21.yaml
+++ b/.github/workflows/samples-kotlin-server-jdk21.yaml
@@ -13,7 +13,7 @@ on:
- 'samples/server/petstore/kotlin-server-required-and-nullable-properties/**'
env:
- GRADLE_VERSION: '8.11'
+ GRADLE_VERSION: '8.14'
jobs:
build:
diff --git a/bin/configs/kotlin-spring-boot-4.yaml b/bin/configs/kotlin-spring-boot-4.yaml
new file mode 100644
index 000000000000..60639347c763
--- /dev/null
+++ b/bin/configs/kotlin-spring-boot-4.yaml
@@ -0,0 +1,16 @@
+generatorName: kotlin-spring
+outputDir: samples/server/petstore/kotlin-springboot-4
+library: spring-boot
+inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
+templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
+additionalProperties:
+ documentationProvider: none
+ annotationLibrary: none
+ useSwaggerUI: "false"
+ serviceImplementation: "true"
+ serializableModel: "true"
+ beanValidations: "true"
+ useSpringBoot4: "true"
+ useJackson3: "true"
+ requestMappingMode: api_interface
+ gradleBuildFile: "true"
\ No newline at end of file
diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md
index ab39d3a2942a..df75fc19b7ee 100644
--- a/docs/generators/kotlin-spring.md
+++ b/docs/generators/kotlin-spring.md
@@ -59,9 +59,11 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
|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. Incompatible with `openApiNullable`.| |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|
+|useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. Enabling this option will also enable `useJakartaEe`.| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|useTags|Whether to use tags for creating interface and controller class names| |false|
|xKotlinImplementsFieldsSkip|A list of fields per schema name that should NOT be created with `override` keyword despite their presence in vendor extension `x-kotlin-implements-fields` for the schema. Example: yaml `xKotlinImplementsFieldsSkip: Pet: [photoUrls]` skips `override` for `photoUrls` in schema `Pet`| |empty map|
diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md
index 22b931b9229e..8d0552018d80 100644
--- a/docs/generators/kotlin.md
+++ b/docs/generators/kotlin.md
@@ -50,6 +50,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sourceFolder|source folder for generated code| |src/main/kotlin|
|supportAndroidApiLevel25AndBelow|[WARNING] This flag will generate code that has a known security vulnerability. It uses `kotlin.io.createTempFile` instead of `java.nio.file.Files.createTempFile` in order to support Android API level 25 and below. For more info, please check the following links https://github.com/OpenAPITools/openapi-generator/security/advisories/GHSA-23x4-m842-fmwf, https://github.com/OpenAPITools/openapi-generator/pull/9284| |false|
|useCoroutines|Whether to use the Coroutines adapter with the retrofit2 library.| |false|
+|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Not yet supported for kotlin-client; reserved for future use.| |false|
|useNonAsciiHeaders|Allow to use non-ascii headers with the okhttp library| |false|
|useResponseAsReturnType|When using retrofit2 and coroutines, use `Response`<`T`> as return type instead of `T`.| |true|
|useRxJava3|Whether to use the RxJava3 adapter with the retrofit2 library.| |false|
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 a695d550cdec..7fce10e78f53 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
@@ -56,6 +56,10 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co
public static final String JAVAX_PACKAGE = "javaxPackage";
public static final String USE_JAKARTA_EE = "useJakartaEe";
+ public static final String USE_JACKSON_3 = "useJackson3";
+ public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson";
+ public static final String JACKSON3_PACKAGE = "tools.jackson";
+ public static final String JACKSON_PACKAGE = "jacksonPackage";
public static final String SCHEMA_IMPLEMENTS = "schemaImplements";
public static final String SCHEMA_IMPLEMENTS_FIELDS = "schemaImplementsFields";
public static final String X_KOTLIN_IMPLEMENTS_SKIP = "xKotlinImplementsSkip";
@@ -80,6 +84,7 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co
protected boolean serializableModel = false;
@Setter protected boolean useJakartaEe = false;
+ @Getter @Setter protected boolean useJackson3 = false;
@Setter protected boolean nonPublicApi = false;
@@ -569,6 +574,17 @@ public void processOpts() {
} else {
applyJavaxPackage();
}
+
+ if (additionalProperties.containsKey(USE_JACKSON_3)) {
+ setUseJackson3(Boolean.parseBoolean(additionalProperties.get(USE_JACKSON_3).toString()));
+ }
+ additionalProperties.put(USE_JACKSON_3, useJackson3);
+
+ if (useJackson3) {
+ applyJackson3Package();
+ } else {
+ applyJackson2Package();
+ }
}
protected boolean isModelMutable() {
@@ -844,6 +860,14 @@ protected void applyJakartaPackage() {
writePropertyBack(JAVAX_PACKAGE, "jakarta");
}
+ protected void applyJackson2Package() {
+ writePropertyBack(JACKSON_PACKAGE, JACKSON2_PACKAGE);
+ }
+
+ protected void applyJackson3Package() {
+ writePropertyBack(JACKSON_PACKAGE, JACKSON3_PACKAGE);
+ }
+
@Override
protected boolean isReservedWord(String word) {
// We want case-sensitive escaping, to avoid unnecessary backtick-escaping.
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
index 57b5f20dbcd9..7bd9cacf59f2 100755
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
@@ -298,6 +298,9 @@ public KotlinClientCodegen() {
cliOptions.add(CliOption.newBoolean(USE_NON_ASCII_HEADERS, "Allow to use non-ascii headers with the okhttp library"));
cliOptions.add(CliOption.newBoolean(USE_RESPONSE_AS_RETURN_TYPE, "When using retrofit2 and coroutines, use `Response`<`T`> as return type instead of `T`.", true));
+
+ cliOptions.add(CliOption.newBoolean(USE_JACKSON_3,
+ "Use Jackson 3 dependencies (tools.jackson package). Not yet supported for kotlin-client; reserved for future use."));
}
@Override
@@ -466,6 +469,11 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(USE_SPRING_BOOT3);
}
+ if (isUseJackson3()) {
+ throw new IllegalArgumentException(
+ "useJackson3 is not yet supported for kotlin-client. Jackson 3 support for kotlin-client will be added in a future release.");
+ }
+
if (additionalProperties.containsKey(CodegenConstants.SERIALIZATION_LIBRARY)) {
setSerializationLibrary((String) additionalProperties.get(CodegenConstants.SERIALIZATION_LIBRARY));
additionalProperties.put(this.serializationLibrary.name(), true);
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 1706b802e1f0..f3037ad16c85 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
@@ -91,6 +91,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String DECLARATIVE_INTERFACE_REACTIVE_MODE = "declarativeInterfaceReactiveMode";
public static final String USE_SPRING_BOOT3 = "useSpringBoot3";
+ public static final String USE_SPRING_BOOT4 = "useSpringBoot4";
public static final String INCLUDE_HTTP_REQUEST_CONTEXT = "includeHttpRequestContext";
public static final String USE_FLOW_FOR_ARRAY_RETURN_TYPE = "useFlowForArrayReturnType";
public static final String REQUEST_MAPPING_OPTION = "requestMappingMode";
@@ -168,6 +169,8 @@ public String getDescription() {
@Getter @Setter
protected boolean useSpringBoot3 = false;
+ @Getter @Setter
+ protected boolean useSpringBoot4 = false;
protected RequestMappingMode requestMappingMode = RequestMappingMode.controller;
private DocumentationProvider documentationProvider;
private AnnotationLibrary annotationLibrary;
@@ -254,6 +257,8 @@ public KotlinSpringServerCodegen() {
"@RestController annotations. May be used to prevent bean names clash if multiple generated libraries" +
" (contexts) added to single project.", beanQualifiers);
addSwitch(USE_SPRING_BOOT3, "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`.", useSpringBoot3);
+ addSwitch(USE_SPRING_BOOT4, "Generate code and provide dependencies for use with Spring Boot 4.x. Enabling this option will also enable `useJakartaEe`.", useSpringBoot4);
+ addSwitch(USE_JACKSON_3, "Use Jackson 3 dependencies (tools.jackson package). Only available with `useSpringBoot4`. Defaults to true when `useSpringBoot4` is enabled. Incompatible with `openApiNullable`.", useJackson3);
addSwitch(USE_FLOW_FOR_ARRAY_RETURN_TYPE, "Whether to use Flow for array/collection return types when reactive is enabled. If false, will use List instead.", useFlowForArrayReturnType);
addSwitch(INCLUDE_HTTP_REQUEST_CONTEXT, "Whether to include HttpServletRequest (blocking) or ServerWebExchange (reactive) as additional parameter in generated methods.", includeHttpRequestContext);
addSwitch(USE_RESPONSE_ENTITY,
@@ -419,6 +424,13 @@ public String getHelp() {
@Override
public void processOpts() {
+ boolean springBoot4Enabled = useSpringBoot4
+ || (additionalProperties.containsKey(USE_SPRING_BOOT4)
+ && convertPropertyToBoolean(USE_SPRING_BOOT4));
+ if (springBoot4Enabled) {
+ additionalProperties.put(USE_JACKSON_3, "true");
+ }
+
super.processOpts();
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
@@ -467,6 +479,9 @@ public void processOpts() {
if (additionalProperties.containsKey(USE_SPRING_BOOT3)) {
this.setUseSpringBoot3(convertPropertyToBoolean(USE_SPRING_BOOT3));
}
+ if (additionalProperties.containsKey(USE_SPRING_BOOT4)) {
+ this.setUseSpringBoot4(convertPropertyToBoolean(USE_SPRING_BOOT4));
+ }
if (additionalProperties.containsKey(INCLUDE_HTTP_REQUEST_CONTEXT)) {
this.setIncludeHttpRequestContext(convertPropertyToBoolean(INCLUDE_HTTP_REQUEST_CONTEXT));
}
@@ -493,6 +508,11 @@ public void processOpts() {
// used later in recursive import in postProcessingModels
importMapping.put("com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonCreator");
+ if (isUseJackson3()) {
+ // Override databind imports for Jackson 3
+ importMapping.put("JsonDeserialize", "tools.jackson.databind.annotation.JsonDeserialize");
+ }
+
// Spring-specific import mappings for x-spring-paginated support
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
@@ -634,7 +654,9 @@ public void processOpts() {
}
}
if (SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY.equals(library)) {
- this.setUseSpringBoot3(true);
+ if (!isUseSpringBoot4()) {
+ this.setUseSpringBoot3(true);
+ }
this.setInterfaceOnly(true);
this.setUseFeignClient(false);
this.setSkipDefaultInterface(true);
@@ -689,7 +711,20 @@ public void processOpts() {
this.setAutoXSpringPaginated(convertPropertyToBoolean(AUTO_X_SPRING_PAGINATED));
}
writePropertyBack(AUTO_X_SPRING_PAGINATED, autoXSpringPaginated);
- if (isUseSpringBoot3()) {
+ if (isUseSpringBoot3() && isUseSpringBoot4()) {
+ throw new IllegalArgumentException("Choose between Spring Boot 3 and Spring Boot 4");
+ }
+
+ if (isUseJackson3() && !isUseSpringBoot4()) {
+ throw new IllegalArgumentException("useJackson3 is only available with Spring Boot >= 4");
+ }
+
+ if (isUseJackson3() && additionalProperties.containsKey("openApiNullable")
+ && Boolean.parseBoolean(additionalProperties.get("openApiNullable").toString())) {
+ throw new IllegalArgumentException("openApiNullable cannot be set with useJackson3");
+ }
+
+ if (isUseSpringBoot3() || isUseSpringBoot4()) {
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
throw new IllegalArgumentException(DocumentationProvider.SPRINGFOX.getPropertyName() + " is not supported with Spring Boot > 3.x");
}
@@ -701,6 +736,7 @@ public void processOpts() {
applyJakartaPackage();
}
writePropertyBack(USE_SPRING_BOOT3, isUseSpringBoot3());
+ writePropertyBack(USE_SPRING_BOOT4, isUseSpringBoot4());
modelTemplateFiles.put("model.mustache", ".kt");
@@ -742,14 +778,18 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("apiUtil.mustache",
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator), "ApiUtil.kt"));
- if (isUseSpringBoot3()) {
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("pom-sb4.mustache", "", "pom.xml"));
+ } else if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("pom-sb3.mustache", "", "pom.xml"));
} else {
supportingFiles.add(new SupportingFile("pom.mustache", "", "pom.xml"));
}
if (this.gradleBuildFile) {
- if (isUseSpringBoot3()) {
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("buildGradle-sb4-Kts.mustache", "", "build.gradle.kts"));
+ } else if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("buildGradle-sb3-Kts.mustache", "", "build.gradle.kts"));
} else {
supportingFiles.add(new SupportingFile("buildGradleKts.mustache", "", "build.gradle.kts"));
@@ -788,14 +828,18 @@ public void processOpts() {
if (library.equals(SPRING_CLOUD_LIBRARY)) {
LOGGER.info("Setup code generator for Kotlin Spring Cloud Client");
- if (isUseSpringBoot3()) {
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("pom-sb4.mustache", "pom.xml"));
+ } else if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("pom-sb3.mustache", "pom.xml"));
} else {
supportingFiles.add(new SupportingFile("pom.mustache", "pom.xml"));
}
if (this.gradleBuildFile) {
- if (isUseSpringBoot3()) {
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("buildGradle-sb4-Kts.mustache", "build.gradle.kts"));
+ } else if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("buildGradle-sb3-Kts.mustache", "build.gradle.kts"));
} else {
supportingFiles.add(new SupportingFile("buildGradleKts.mustache", "build.gradle.kts"));
@@ -827,10 +871,18 @@ public void processOpts() {
if (library.equals(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY)) {
LOGGER.info("Setup code generator for Kotlin Spring Declarative Http interface");
- supportingFiles.add(new SupportingFile("pom-sb3.mustache", "pom.xml"));
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("pom-sb4.mustache", "pom.xml"));
+ } else {
+ supportingFiles.add(new SupportingFile("pom-sb3.mustache", "pom.xml"));
+ }
if (this.gradleBuildFile) {
- supportingFiles.add(new SupportingFile("buildGradle-sb3-Kts.mustache", "build.gradle.kts"));
+ if (isUseSpringBoot4()) {
+ supportingFiles.add(new SupportingFile("buildGradle-sb4-Kts.mustache", "build.gradle.kts"));
+ } else {
+ supportingFiles.add(new SupportingFile("buildGradle-sb3-Kts.mustache", "build.gradle.kts"));
+ }
supportingFiles.add(new SupportingFile("settingsGradle.mustache", "settings.gradle"));
String gradleWrapperPackage = "gradle.wrapper";
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache
new file mode 100644
index 000000000000..c9c5996d0e09
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/buildGradle-sb4-Kts.mustache
@@ -0,0 +1,71 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+group = "{{groupId}}"
+version = "{{artifactVersion}}"
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+}
+
+{{#interfaceOnly}}
+tasks.bootJar {
+ enabled = false
+}
+
+{{/interfaceOnly}}
+plugins {
+ val kotlinVersion = "2.2.0"
+ id("org.jetbrains.kotlin.jvm") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "4.0.1"
+ id("io.spring.dependency-management") version "1.1.7"
+}
+
+dependencies {
+{{#reactive}} val kotlinxCoroutinesVersion = "1.10.1"
+{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib")
+ implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webmvc"){{/reactive}}{{#reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webflux")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.8.6"){{/useSwaggerUI}}{{^useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-api:2.8.6"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ implementation("org.webjars:swagger-ui:5.17.14")
+ implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ implementation("io.swagger.core.v3:swagger-annotations:2.2.28"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+{{#useJackson3}}
+ implementation("tools.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("tools.jackson.dataformat:jackson-dataformat-xml")
+ implementation("tools.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+{{^useJackson3}}
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+ implementation("org.springframework.data:spring-data-commons")
+{{#useBeanValidation}}
+ implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
+ implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
+
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
+ testImplementation("org.springframework.boot:spring-boot-starter-test") {
+ exclude(module = "junit")
+ }
+{{#reactive}}
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion")
+{{/reactive}}
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/gradle-wrapper.properties.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/gradle-wrapper.properties.mustache
index 80187ac30432..b5bd21925a03 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/gradle-wrapper.properties.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/gradle-wrapper.properties.mustache
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+{{#useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip{{/useSpringBoot4}}{{^useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip{{/useSpringBoot4}}
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache
new file mode 100644
index 000000000000..9d249a18caac
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-boot/pom-sb4.mustache
@@ -0,0 +1,196 @@
+
+ 4.0.0
+ {{groupId}}
+ {{artifactId}}
+ jar
+ {{artifactId}}
+ {{artifactVersion}}
+ {{#reactive}}
+ 1.10.1{{/reactive}}{{#springDocDocumentationProvider}}
+ 2.8.6{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ 5.17.14{{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ 1.6.6{{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ 2.2.28{{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+ 3.0.2
+ 3.0.0
+ 2.2.0
+
+ 2.2.0
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.1
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+ {{^interfaceOnly}}
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+ {{/interfaceOnly}}
+ {{#interfaceOnly}}
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+
+
+
+ {{/interfaceOnly}}
+
+ kotlin-maven-plugin
+ org.jetbrains.kotlin
+ ${kotlin.version}
+
+
+ spring
+
+ 17
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+ {{^reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+ {{/reactive}}{{#reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ ${kotlinx-coroutines.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+ ${kotlinx-coroutines.version}
+ {{/reactive}}
+
+ org.springframework.data
+ spring-data-commons
+
+
+{{#springDocDocumentationProvider}}
+ {{#useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{^useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ org.webjars
+ webjars-locator-core
+ {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+
+ io.swagger
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${findbugs-jsr305.version}
+
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-yaml
+
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-xml
+ {{^useJackson3}}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ {{/useJackson3}}
+
+ {{jacksonPackage}}.module
+ jackson-module-kotlin
+
+{{#useBeanValidation}}
+
+
+ jakarta.validation
+ jakarta.validation-api
+ {{/useBeanValidation}}
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin-test-junit5.version}
+ test
+
+
+
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/buildGradle-sb4-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/buildGradle-sb4-Kts.mustache
new file mode 100644
index 000000000000..80d88024a89b
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/buildGradle-sb4-Kts.mustache
@@ -0,0 +1,75 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+group = "{{groupId}}"
+version = "{{artifactVersion}}"
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+}
+
+plugins {
+ val kotlinVersion = "2.2.0"
+ id("org.jetbrains.kotlin.jvm") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "4.0.1"
+ id("io.spring.dependency-management") version "1.1.7"
+}
+
+tasks.getByName("bootJar") {
+ enabled = false
+}
+
+tasks.getByName("jar") {
+ enabled = true
+}
+
+dependencyManagement {
+ imports {
+ mavenBom("org.springframework.cloud:spring-cloud-dependencies:2025.1.0")
+ }
+}
+
+dependencies {
+{{#reactive}} val kotlinxCoroutinesVersion = "1.10.1"
+{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib")
+ implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webmvc"){{/reactive}}{{#reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webflux")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.8.6"){{/useSwaggerUI}}{{^useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-api:2.8.6"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ implementation("org.webjars:swagger-ui:5.17.14")
+ implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ implementation("io.swagger.core.v3:swagger-annotations:2.2.28"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+{{#useJackson3}}
+ implementation("tools.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("tools.jackson.dataformat:jackson-dataformat-xml")
+ implementation("tools.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+{{^useJackson3}}
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+
+ implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}}
+ implementation("org.springframework.boot:spring-boot-starter-oauth2-client"){{/hasAuthMethods}}
+
+{{#useBeanValidation}}
+ implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
+ implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
+
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/clientConfiguration.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/clientConfiguration.mustache
index 3ea2f6248be4..3bc7c2bfcd64 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/clientConfiguration.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/clientConfiguration.mustache
@@ -8,17 +8,20 @@ import feign.auth.BasicAuthRequestInterceptor
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
{{/-first}}
+{{^useSpringBoot4}}
{{#isOAuth}}
import org.springframework.boot.context.properties.ConfigurationProperties
{{/isOAuth}}
+{{/useSpringBoot4}}
{{/authMethods}}
+{{^useSpringBoot4}}
import org.springframework.boot.context.properties.EnableConfigurationProperties
-{{#authMethods}}
-{{#-first}}
+{{/useSpringBoot4}}
+{{#hasAuthMethods}}
import org.springframework.context.annotation.Bean
-{{/-first}}
-{{/authMethods}}
+{{/hasAuthMethods}}
import org.springframework.context.annotation.Configuration
+{{^useSpringBoot4}}
{{#authMethods}}
{{#isOAuth}}
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor
@@ -38,9 +41,29 @@ import org.springframework.security.oauth2.client.token.grant.password.ResourceO
{{/isPassword}}
{{/isOAuth}}
{{/authMethods}}
+{{/useSpringBoot4}}
+{{#useSpringBoot4}}
+{{#hasOAuthMethods}}
+import org.springframework.security.authentication.AnonymousAuthenticationToken
+import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager
+import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException
+import org.springframework.security.oauth2.core.OAuth2AccessToken
+import org.springframework.security.core.authority.AuthorityUtils
+import org.springframework.http.HttpHeaders
+
+import feign.RequestInterceptor
+import feign.RequestTemplate
+{{/hasOAuthMethods}}
+{{/useSpringBoot4}}
@Configuration
+{{^useSpringBoot4}}
@EnableConfigurationProperties
+{{/useSpringBoot4}}
class ClientConfiguration {
{{#authMethods}}
@@ -70,6 +93,7 @@ class ClientConfiguration {
{{/isApiKey}}
{{#isOAuth}}
+{{^useSpringBoot4}}
@Bean
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(oAuth2ClientContext: OAuth2ClientContext): OAuth2FeignRequestInterceptor {
@@ -127,6 +151,61 @@ class ClientConfiguration {
}
{{/isImplicit}}
+{{/useSpringBoot4}}
+{{#useSpringBoot4}}
+ @Bean
+ @ConditionalOnProperty(prefix = "spring.security.oauth2.client.registration.{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}", name = ["enabled"], havingValue = "true")
+ fun {{{flow}}}OAuth2RequestInterceptor({{{flow}}}AuthorizedClientManager: OAuth2AuthorizedClientManager): OAuth2RequestInterceptor {
+ return OAuth2RequestInterceptor(
+ OAuth2AuthorizeRequest.withClientRegistrationId("{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}")
+ .principal(AnonymousAuthenticationToken(CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}}, CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}}, AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
+ .build(),
+ {{{flow}}}AuthorizedClientManager
+ )
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = "spring.security.oauth2.client.registration.{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}", name = ["enabled"], havingValue = "true")
+ fun {{{flow}}}AuthorizedClientManager(
+ clientRegistrationRepository: ClientRegistrationRepository,
+ authorizedClientService: OAuth2AuthorizedClientService
+ ): OAuth2AuthorizedClientManager {
+ return AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService)
+ }
+
+{{/useSpringBoot4}}
{{/isOAuth}}
{{/authMethods}}
+{{#useSpringBoot4}}
+ {{#hasOAuthMethods}}
+ class OAuth2RequestInterceptor(
+ private val oAuth2AuthorizeRequest: OAuth2AuthorizeRequest,
+ private val oAuth2AuthorizedClientManager: OAuth2AuthorizedClientManager
+ ) : RequestInterceptor {
+
+ override fun apply(template: RequestTemplate) {
+ template.header(HttpHeaders.AUTHORIZATION, getBearerToken())
+ }
+
+ fun getAccessToken(): OAuth2AccessToken {
+ val authorizedClient = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest)
+ ?: throw OAuth2AuthenticationException("Client failed to authenticate")
+ return authorizedClient.accessToken
+ }
+
+ fun getBearerToken(): String {
+ val accessToken = getAccessToken()
+ return String.format(java.util.Locale.ROOT, "%s %s", accessToken.tokenType?.value, accessToken.tokenValue)
+ }
+ }
+
+ companion object {
+ {{#authMethods}}
+ {{#isOAuth}}
+ private const val CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}} = "oauth2FeignClient"
+ {{/isOAuth}}
+ {{/authMethods}}
+ }
+ {{/hasOAuthMethods}}
+{{/useSpringBoot4}}
}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/gradle-wrapper.properties.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/gradle-wrapper.properties.mustache
index 80187ac30432..b5bd21925a03 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/gradle-wrapper.properties.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/gradle-wrapper.properties.mustache
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+{{#useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip{{/useSpringBoot4}}{{^useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip{{/useSpringBoot4}}
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/pom-sb4.mustache
new file mode 100644
index 000000000000..b2701ebf9635
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/pom-sb4.mustache
@@ -0,0 +1,221 @@
+
+ 4.0.0
+ {{groupId}}
+ {{artifactId}}
+ jar
+ {{artifactId}}
+ {{artifactVersion}}
+ {{#reactive}}
+ 1.10.1
+ {{/reactive}}{{#springDocDocumentationProvider}}
+ 2.8.6
+ {{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ 5.17.14
+ {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ 1.6.6
+ {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ 2.2.28
+ {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+ 3.0.2
+ 3.0.0
+ 2.2.0
+
+ 2.2.0
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.1
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-parent
+ 2025.1.0
+ pom
+ import
+
+
+
+
+ ${project.basedir}/src/main/kotlin
+ {{^interfaceOnly}}
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+ {{/interfaceOnly}}
+ {{#interfaceOnly}}
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+
+
+
+ {{/interfaceOnly}}
+
+ kotlin-maven-plugin
+ org.jetbrains.kotlin
+ ${kotlin.version}
+
+
+ spring
+
+ 17
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+ {{^reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+ {{/reactive}}{{#reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ ${kotlinx-coroutines.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+ ${kotlinx-coroutines.version}
+ {{/reactive}}
+
+ {{#springDocDocumentationProvider}}
+ {{#useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-starter-{{#reactive}}
+ webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui
+
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{^useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
+
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ org.webjars
+ webjars-locator-core
+ {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+
+ io.swagger
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${findbugs-jsr305.version}
+
+
+ org.springframework.cloud
+ spring-cloud-starter-openfeign
+
+ {{#hasAuthMethods}}
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+ {{/hasAuthMethods}}
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-yaml
+
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-xml
+ {{^useJackson3}}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ {{/useJackson3}}
+
+ {{jacksonPackage}}.module
+ jackson-module-kotlin
+
+{{#useBeanValidation}}
+
+
+ jakarta.validation
+ jakarta.validation-api
+ {{/useBeanValidation}}
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin-test-junit5.version}
+ test
+
+
+
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/buildGradle-sb4-Kts.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/buildGradle-sb4-Kts.mustache
new file mode 100644
index 000000000000..bc05ca9c9cb3
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/buildGradle-sb4-Kts.mustache
@@ -0,0 +1,75 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+group = "{{groupId}}"
+version = "{{artifactVersion}}"
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+}
+
+plugins {
+ val kotlinVersion = "2.2.0"
+ id("org.jetbrains.kotlin.jvm") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "4.0.1"
+ id("io.spring.dependency-management") version "1.1.7"
+}
+
+tasks.getByName("bootJar") {
+ enabled = false
+}
+
+tasks.getByName("jar") {
+ enabled = true
+}
+
+dependencyManagement {
+ imports {
+ mavenBom("org.springframework.cloud:spring-cloud-dependencies:2025.1.0")
+ }
+}
+
+dependencies {
+{{#reactive}} val kotlinxCoroutinesVersion = "1.10.1"
+{{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib")
+ implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webmvc"){{/reactive}}{{#reactive}}
+ implementation("org.springframework.boot:spring-boot-starter-webflux")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.8.6"){{/useSwaggerUI}}{{^useSwaggerUI}}
+ implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-api:2.8.6"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ implementation("org.webjars:swagger-ui:5.17.14")
+ implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ implementation("io.swagger.core.v3:swagger-annotations:2.2.28"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+{{#useJackson3}}
+ implementation("tools.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("tools.jackson.dataformat:jackson-dataformat-xml")
+ implementation("tools.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+{{^useJackson3}}
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+{{/useJackson3}}
+
+{{#hasAuthMethods}}
+ implementation("org.springframework.boot:spring-boot-starter-oauth2-client"){{/hasAuthMethods}}
+
+{{#useBeanValidation}}
+ implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}}
+ implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
+
+}
\ No newline at end of file
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/gradle-wrapper.properties.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/gradle-wrapper.properties.mustache
index 80187ac30432..b5bd21925a03 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/gradle-wrapper.properties.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/gradle-wrapper.properties.mustache
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+{{#useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip{{/useSpringBoot4}}{{^useSpringBoot4}}distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip{{/useSpringBoot4}}
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/pom-sb4.mustache
new file mode 100644
index 000000000000..d46488af57e1
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-declarative-http-interface/pom-sb4.mustache
@@ -0,0 +1,217 @@
+
+ 4.0.0
+ {{groupId}}
+ {{artifactId}}
+ jar
+ {{artifactId}}
+ {{artifactVersion}}
+ {{#reactive}}
+ 1.10.1
+ {{/reactive}}{{#springDocDocumentationProvider}}
+ 2.8.6
+ {{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+ 5.17.14
+ {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+ 1.6.6
+ {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+ 2.2.28
+ {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+ 3.0.2
+ 3.0.0
+ 2.2.0
+
+ 2.2.0
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.1
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-starter-parent
+ 2025.1.0
+ pom
+ import
+
+
+
+
+ ${project.basedir}/src/main/kotlin
+ {{^interfaceOnly}}
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+ {{/interfaceOnly}}
+ {{#interfaceOnly}}
+ org.apache.maven.plugins
+ maven-source-plugin
+
+
+ attach-sources
+
+ jar
+
+
+
+ {{/interfaceOnly}}
+
+ kotlin-maven-plugin
+ org.jetbrains.kotlin
+ ${kotlin.version}
+
+
+ spring
+
+ 17
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+ {{^reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+ {{/reactive}}{{#reactive}}
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ ${kotlinx-coroutines.version}
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-reactor
+ ${kotlinx-coroutines.version}
+ {{/reactive}}
+
+ {{#springDocDocumentationProvider}}
+ {{#useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-starter-{{#reactive}}
+ webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui
+
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{^useSwaggerUI}}
+
+ org.springdoc
+ springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core
+
+ ${springdoc-openapi.version}
+ {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}}
+
+ org.webjars
+ swagger-ui
+ ${swagger-ui.version}
+
+
+ org.webjars
+ webjars-locator-core
+ {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}}
+
+ io.swagger
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger-annotations.version}
+ {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${findbugs-jsr305.version}
+
+ {{#hasAuthMethods}}
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+ {{/hasAuthMethods}}
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-yaml
+
+
+ {{jacksonPackage}}.dataformat
+ jackson-dataformat-xml
+ {{^useJackson3}}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ {{/useJackson3}}
+
+ {{jacksonPackage}}.module
+ jackson-module-kotlin
+
+{{#useBeanValidation}}
+
+
+ jakarta.validation
+ jakarta.validation-api
+ {{/useBeanValidation}}
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin-test-junit5.version}
+ test
+
+
+
\ No newline at end of file
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 0bdf7f3f128e..ed3369a97aba 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
@@ -16,11 +16,13 @@
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.kotlin.KotlinTestUtils;
import org.openapitools.codegen.kotlin.assertions.KotlinFileAssert;
+import org.openapitools.codegen.languages.AbstractKotlinCodegen;
import org.openapitools.codegen.languages.KotlinSpringServerCodegen;
import org.openapitools.codegen.languages.features.CXFServerFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.AnnotationLibrary;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DocumentationProvider;
+import org.openapitools.codegen.languages.features.SwaggerUIFeatures;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -4735,6 +4737,279 @@ public void testCompanionObjectGeneratesCompanionInModel() throws IOException {
"companion object { }"
);
}
+
+ @Test
+ public void shouldRefuseJackson3WithoutSpringBoot4() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "false");
+ codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(input);
+
+ Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(generator::generate);
+ }
+
+ @Test
+ public void shouldRefuseSpringBoot3AndSpringBoot4Together() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT3, "true");
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(input);
+
+ Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(generator::generate);
+ }
+
+ @Test
+ public void shouldRefuseOpenApiNullableWithJackson3() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
+ codegen.additionalProperties().put("openApiNullable", "true");
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.opts(input);
+
+ Assertions.assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(generator::generate);
+ }
+
+ @Test
+ public void shouldUseJakartaImportsWithSpringBoot4() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
+ codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path modelPath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/model/Pet.kt");
+ assertFileContains(modelPath, "jakarta.validation");
+ assertFileNotContains(modelPath, "javax.validation");
+ }
+
+ @Test
+ public void shouldGenerateSpringBoot4PomWithJackson3Deps() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
+ codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path pomPath = Paths.get(outputPath + "/pom.xml");
+ assertFileContains(pomPath, "spring-boot-starter-parent");
+ assertFileContains(pomPath, "4.0.1");
+ assertFileContains(pomPath, "tools.jackson.dataformat");
+ assertFileContains(pomPath, "tools.jackson.module");
+ assertFileNotContains(pomPath, "jackson-datatype-jsr310");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.dataformat");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.module");
+ }
+
+ @Test
+ public void shouldDefaultToJackson3WhenSpringBoot4Enabled() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ // useJackson3 is NOT set — should default to true
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
+ codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path pomPath = Paths.get(outputPath + "/pom.xml");
+ assertFileContains(pomPath, "tools.jackson.dataformat");
+ assertFileContains(pomPath, "tools.jackson.module");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.dataformat");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.module");
+ assertFileNotContains(pomPath, "jackson-datatype-jsr310");
+ }
+
+ @Test
+ public void shouldDeclareSpringdocVersionWhenSwaggerUIDisabled() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.SPRINGDOC.toCliOptValue());
+ codegen.additionalProperties().put(SwaggerUIFeatures.USE_SWAGGER_UI, false);
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path pomPath = Paths.get(outputPath + "/pom.xml");
+ String pomContent = new String(Files.readAllBytes(pomPath), StandardCharsets.UTF_8);
+ String propertiesBlock = pomContent.substring(
+ pomContent.indexOf(""),
+ pomContent.indexOf(""));
+ assertThat(propertiesBlock).contains("");
+ }
+
+ @Test
+ public void shouldNotUseLegacyOAuth2WithSpringBoot4CloudLibrary() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec(
+ "src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+ codegen.setLibrary("spring-cloud");
+
+ codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
+ codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path clientConfigPath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/configuration/ClientConfiguration.kt");
+ // Legacy OAuth2 classes must NOT be present
+ assertFileNotContains(clientConfigPath, "DefaultOAuth2ClientContext");
+ assertFileNotContains(clientConfigPath, "OAuth2FeignRequestInterceptor");
+ assertFileNotContains(clientConfigPath, "ClientCredentialsResourceDetails");
+ assertFileNotContains(clientConfigPath, "AuthorizationCodeResourceDetails");
+ assertFileNotContains(clientConfigPath, "ImplicitResourceDetails");
+ assertFileNotContains(clientConfigPath, "ResourceOwnerPasswordResourceDetails");
+
+ // Modern OAuth2 client classes MUST be present
+ assertFileContains(clientConfigPath, "OAuth2AuthorizedClientManager");
+ assertFileContains(clientConfigPath, "AuthorizedClientServiceOAuth2AuthorizedClientManager");
+ assertFileContains(clientConfigPath, "OAuth2AuthorizeRequest");
+ assertFileContains(clientConfigPath, "OAuth2AuthorizedClientService");
+ assertFileContains(clientConfigPath, "ClientRegistrationRepository");
+ assertFileContains(clientConfigPath, "OAuth2RequestInterceptor");
+ }
+
+ @Test
+ public void shouldDefaultToJackson3WhenSpringBoot4EnabledViaSetter() throws IOException {
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+ String outputPath = output.getAbsolutePath().replace('\\', '/');
+
+ final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/petstore.yaml");
+ final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
+ codegen.setOpenAPI(openAPI);
+ codegen.setOutputDir(output.getAbsolutePath());
+
+ // Set via setter, NOT additionalProperties
+ codegen.setUseSpringBoot4(true);
+ codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
+ codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
+
+ ClientOptInput input = new ClientOptInput();
+ input.openAPI(openAPI);
+ input.config(codegen);
+
+ DefaultGenerator generator = new DefaultGenerator();
+ generator.setGenerateMetadata(false);
+ generator.opts(input).generate();
+
+ Path pomPath = Paths.get(outputPath + "/pom.xml");
+ assertFileContains(pomPath, "tools.jackson.dataformat");
+ assertFileContains(pomPath, "tools.jackson.module");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.dataformat");
+ assertFileNotContains(pomPath, "com.fasterxml.jackson.module");
+ assertFileNotContains(pomPath, "jackson-datatype-jsr310");
+ }
}
diff --git a/samples/server/petstore/kotlin-springboot-4/.openapi-generator-ignore b/samples/server/petstore/kotlin-springboot-4/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/kotlin-springboot-4/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-4/.openapi-generator/FILES
new file mode 100644
index 000000000000..c7afb2eda91e
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/.openapi-generator/FILES
@@ -0,0 +1,27 @@
+README.md
+build.gradle.kts
+gradle/wrapper/gradle-wrapper.jar
+gradle/wrapper/gradle-wrapper.properties
+gradlew
+gradlew.bat
+pom.xml
+settings.gradle
+src/main/kotlin/org/openapitools/Application.kt
+src/main/kotlin/org/openapitools/api/ApiUtil.kt
+src/main/kotlin/org/openapitools/api/Exceptions.kt
+src/main/kotlin/org/openapitools/api/PetApiController.kt
+src/main/kotlin/org/openapitools/api/PetApiService.kt
+src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt
+src/main/kotlin/org/openapitools/api/StoreApiController.kt
+src/main/kotlin/org/openapitools/api/StoreApiService.kt
+src/main/kotlin/org/openapitools/api/StoreApiServiceImpl.kt
+src/main/kotlin/org/openapitools/api/UserApiController.kt
+src/main/kotlin/org/openapitools/api/UserApiService.kt
+src/main/kotlin/org/openapitools/api/UserApiServiceImpl.kt
+src/main/kotlin/org/openapitools/model/Category.kt
+src/main/kotlin/org/openapitools/model/ModelApiResponse.kt
+src/main/kotlin/org/openapitools/model/Order.kt
+src/main/kotlin/org/openapitools/model/Pet.kt
+src/main/kotlin/org/openapitools/model/Tag.kt
+src/main/kotlin/org/openapitools/model/User.kt
+src/main/resources/application.yaml
diff --git a/samples/server/petstore/kotlin-springboot-4/.openapi-generator/VERSION b/samples/server/petstore/kotlin-springboot-4/.openapi-generator/VERSION
new file mode 100644
index 000000000000..0610c66bc14f
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.21.0-SNAPSHOT
diff --git a/samples/server/petstore/kotlin-springboot-4/README.md b/samples/server/petstore/kotlin-springboot-4/README.md
new file mode 100644
index 000000000000..b6865a081135
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/README.md
@@ -0,0 +1,21 @@
+# openAPIPetstore
+
+This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator).
+
+## Getting Started
+
+This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in.
+
+By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl).
+
+To build the project using maven, run:
+```bash
+mvn package && java -jar target/openapi-spring-1.0.0.jar
+```
+
+To build the project using gradle, run:
+```bash
+gradle build && java -jar build/libs/openapi-spring-1.0.0.jar
+```
+
+If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:8080/)
diff --git a/samples/server/petstore/kotlin-springboot-4/build.gradle.kts b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts
new file mode 100644
index 000000000000..3459856672ac
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/build.gradle.kts
@@ -0,0 +1,43 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+group = "org.openapitools"
+version = "1.0.0"
+java.sourceCompatibility = JavaVersion.VERSION_17
+
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+}
+
+plugins {
+ val kotlinVersion = "2.2.0"
+ id("org.jetbrains.kotlin.jvm") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
+ id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
+ id("org.springframework.boot") version "4.0.1"
+ id("io.spring.dependency-management") version "1.1.7"
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib")
+ implementation("org.jetbrains.kotlin:kotlin-reflect")
+ implementation("org.springframework.boot:spring-boot-starter-webmvc")
+
+ implementation("com.google.code.findbugs:jsr305:3.0.2")
+ implementation("tools.jackson.dataformat:jackson-dataformat-yaml")
+ implementation("tools.jackson.dataformat:jackson-dataformat-xml")
+ implementation("tools.jackson.module:jackson-module-kotlin")
+ implementation("org.springframework.data:spring-data-commons")
+ implementation("jakarta.validation:jakarta.validation-api")
+ implementation("jakarta.annotation:jakarta.annotation-api:3.0.0")
+
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
+ testImplementation("org.springframework.boot:spring-boot-starter-test") {
+ exclude(module = "junit")
+ }
+}
\ No newline at end of file
diff --git a/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.jar b/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000000..e6441136f3d4
Binary files /dev/null and b/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.properties b/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000000..ca025c83a7cc
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/samples/server/petstore/kotlin-springboot-4/gradlew b/samples/server/petstore/kotlin-springboot-4/gradlew
new file mode 100755
index 000000000000..9d0ce634cb11
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+[ -h "$app_path" ]
+do
+ls=$( ls -ld "$app_path" )
+link=${ls#*' -> '}
+case $link in #(
+/*) app_path=$link ;; #(
+*) app_path=$APP_HOME$link ;;
+esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+echo "$*"
+} >&2
+
+die () {
+echo
+echo "$*"
+echo
+exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+CYGWIN* ) cygwin=true ;; #(
+Darwin* ) darwin=true ;; #(
+MSYS* | MINGW* ) msys=true ;; #(
+NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+# IBM's JDK on AIX uses strange locations for the executables
+JAVACMD=$JAVA_HOME/jre/sh/java
+else
+JAVACMD=$JAVA_HOME/bin/java
+fi
+if [ ! -x "$JAVACMD" ] ; then
+die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+else
+JAVACMD=java
+if ! command -v java >/dev/null 2>&1
+then
+die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+case $MAX_FD in #(
+max*)
+# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+# shellcheck disable=SC2039,SC3045
+MAX_FD=$( ulimit -H -n ) ||
+warn "Could not query maximum file descriptor limit"
+esac
+case $MAX_FD in #(
+'' | soft) :;; #(
+*)
+# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+# shellcheck disable=SC2039,SC3045
+ulimit -n "$MAX_FD" ||
+warn "Could not set maximum file descriptor limit to $MAX_FD"
+esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+# Now convert the arguments - kludge to limit ourselves to /bin/sh
+for arg do
+if
+case $arg in #(
+-*) false ;; # don't mess with options #(
+/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+[ -e "$t" ] ;; #(
+*) false ;;
+esac
+then
+arg=$( cygpath --path --ignore --mixed "$arg" )
+fi
+# Roll the args list around exactly as many times as the number of
+# args, so each arg winds up back in the position where it started, but
+# possibly modified.
+#
+# NB: a `for` loop captures its iteration list before it begins, so
+# changing the positional parameters here affects neither the number of
+# iterations, nor the values presented in `arg`.
+shift # remove old arg
+set -- "$@" "$arg" # push replacement arg
+done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+"-Dorg.gradle.appname=$APP_BASE_NAME" \
+-classpath "$CLASSPATH" \
+org.gradle.wrapper.GradleWrapperMain \
+"$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+xargs -n1 |
+sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+tr '\n' ' '
+)" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/samples/server/petstore/kotlin-springboot-4/gradlew.bat b/samples/server/petstore/kotlin-springboot-4/gradlew.bat
new file mode 100644
index 000000000000..25da30dbdeee
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/samples/server/petstore/kotlin-springboot-4/pom.xml b/samples/server/petstore/kotlin-springboot-4/pom.xml
new file mode 100644
index 000000000000..3ce44dbf761c
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/pom.xml
@@ -0,0 +1,131 @@
+
+ 4.0.0
+ org.openapitools
+ openapi-spring
+ jar
+ openapi-spring
+ 1.0.0
+
+ 3.0.2
+ 3.0.0
+ 2.2.0
+
+ 2.2.0
+ UTF-8
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.1
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ repackage
+
+
+
+
+
+
+ kotlin-maven-plugin
+ org.jetbrains.kotlin
+ ${kotlin.version}
+
+
+ spring
+
+ 17
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+ test-compile
+
+ test-compile
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${findbugs-jsr305.version}
+
+
+ tools.jackson.dataformat
+ jackson-dataformat-yaml
+
+
+ tools.jackson.dataformat
+ jackson-dataformat-xml
+
+
+ tools.jackson.module
+ jackson-module-kotlin
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta-annotation.version}
+ provided
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ ${kotlin-test-junit5.version}
+ test
+
+
+
\ No newline at end of file
diff --git a/samples/server/petstore/kotlin-springboot-4/settings.gradle b/samples/server/petstore/kotlin-springboot-4/settings.gradle
new file mode 100644
index 000000000000..14844905cd40
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/settings.gradle
@@ -0,0 +1,15 @@
+pluginManagement {
+ repositories {
+ maven { url = uri("https://repo.spring.io/snapshot") }
+ maven { url = uri("https://repo.spring.io/milestone") }
+ gradlePluginPortal()
+ }
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id == "org.springframework.boot") {
+ useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
+ }
+ }
+ }
+}
+rootProject.name = "openapi-spring"
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/Application.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/Application.kt
new file mode 100644
index 000000000000..2fe6de62479e
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/Application.kt
@@ -0,0 +1,13 @@
+package org.openapitools
+
+import org.springframework.boot.runApplication
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.context.annotation.ComponentScan
+
+@SpringBootApplication
+@ComponentScan(basePackages = ["org.openapitools", "org.openapitools.api", "org.openapitools.model"])
+class Application
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/ApiUtil.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/ApiUtil.kt
new file mode 100644
index 000000000000..03344e13b474
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/ApiUtil.kt
@@ -0,0 +1,19 @@
+package org.openapitools.api
+
+import org.springframework.web.context.request.NativeWebRequest
+
+import jakarta.servlet.http.HttpServletResponse
+import java.io.IOException
+
+object ApiUtil {
+ fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) {
+ try {
+ val res = req.getNativeResponse(HttpServletResponse::class.java)
+ res?.characterEncoding = "UTF-8"
+ res?.addHeader("Content-Type", contentType)
+ res?.writer?.print(example)
+ } catch (e: IOException) {
+ throw RuntimeException(e)
+ }
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/Exceptions.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/Exceptions.kt
new file mode 100644
index 000000000000..1bd78f54576a
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/Exceptions.kt
@@ -0,0 +1,30 @@
+package org.openapitools.api
+
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpStatus
+import org.springframework.web.bind.annotation.ControllerAdvice
+import org.springframework.web.bind.annotation.ExceptionHandler
+import jakarta.servlet.http.HttpServletResponse
+import jakarta.validation.ConstraintViolationException
+
+// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception
+sealed class ApiException(msg: String, val code: Int) : Exception(msg)
+
+class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code)
+
+@Configuration("org.openapitools.api.DefaultExceptionHandler")
+@ControllerAdvice
+class DefaultExceptionHandler {
+
+ @ExceptionHandler(value = [ApiException::class])
+ fun onApiException(ex: ApiException, response: HttpServletResponse): Unit =
+ response.sendError(ex.code, ex.message)
+
+ @ExceptionHandler(value = [NotImplementedError::class])
+ fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit =
+ response.sendError(HttpStatus.NOT_IMPLEMENTED.value())
+
+ @ExceptionHandler(value = [ConstraintViolationException::class])
+ fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit =
+ response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message })
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiController.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiController.kt
new file mode 100644
index 000000000000..a6dd78ee6eaf
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiController.kt
@@ -0,0 +1,154 @@
+package org.openapitools.api
+
+import org.openapitools.model.ModelApiResponse
+import org.openapitools.model.Pet
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+
+import org.springframework.web.bind.annotation.*
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.context.request.NativeWebRequest
+import org.springframework.beans.factory.annotation.Autowired
+
+import jakarta.validation.Valid
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@RestController
+@Validated
+class PetApiController(@Autowired(required = true) val service: PetApiService) {
+
+
+ @RequestMapping(
+ method = [RequestMethod.POST],
+ // "/pet"
+ value = [PATH_ADD_PET],
+ produces = ["application/xml", "application/json"],
+ consumes = ["application/json", "application/xml"]
+ )
+ fun addPet(
+ @Valid @RequestBody pet: Pet
+ ): ResponseEntity {
+ return ResponseEntity(service.addPet(pet), HttpStatus.valueOf(200))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.DELETE],
+ // "/pet/{petId}"
+ value = [PATH_DELETE_PET]
+ )
+ fun deletePet(
+ @PathVariable("petId") petId: kotlin.Long,
+ @RequestHeader(value = "api_key", required = false) apiKey: kotlin.String?
+ ): ResponseEntity {
+ return ResponseEntity(service.deletePet(petId, apiKey), HttpStatus.valueOf(400))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findByStatus"
+ value = [PATH_FIND_PETS_BY_STATUS],
+ produces = ["application/xml", "application/json"]
+ )
+ fun findPetsByStatus(
+ @NotNull @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List
+ ): ResponseEntity> {
+ return ResponseEntity(service.findPetsByStatus(status), HttpStatus.valueOf(200))
+ }
+
+
+ @Deprecated(message="Operation is deprecated")
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/findByTags"
+ value = [PATH_FIND_PETS_BY_TAGS],
+ produces = ["application/xml", "application/json"]
+ )
+ fun findPetsByTags(
+ @NotNull @Valid @RequestParam(value = "tags", required = true) tags: kotlin.collections.List
+ ): ResponseEntity> {
+ return ResponseEntity(service.findPetsByTags(tags), HttpStatus.valueOf(200))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/pet/{petId}"
+ value = [PATH_GET_PET_BY_ID],
+ produces = ["application/xml", "application/json"]
+ )
+ fun getPetById(
+ @PathVariable("petId") petId: kotlin.Long
+ ): ResponseEntity {
+ return ResponseEntity(service.getPetById(petId), HttpStatus.valueOf(200))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.PUT],
+ // "/pet"
+ value = [PATH_UPDATE_PET],
+ produces = ["application/xml", "application/json"],
+ consumes = ["application/json", "application/xml"]
+ )
+ fun updatePet(
+ @Valid @RequestBody pet: Pet
+ ): ResponseEntity {
+ return ResponseEntity(service.updatePet(pet), HttpStatus.valueOf(200))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.POST],
+ // "/pet/{petId}"
+ value = [PATH_UPDATE_PET_WITH_FORM],
+ consumes = ["application/x-www-form-urlencoded"]
+ )
+ fun updatePetWithForm(
+ @PathVariable("petId") petId: kotlin.Long,
+ @Valid @RequestParam(value = "name", required = false) name: kotlin.String?,
+ @Valid @RequestParam(value = "status", required = false) status: kotlin.String?
+ ): ResponseEntity {
+ return ResponseEntity(service.updatePetWithForm(petId, name, status), HttpStatus.valueOf(405))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.POST],
+ // "/pet/{petId}/uploadImage"
+ value = [PATH_UPLOAD_FILE],
+ produces = ["application/json"],
+ consumes = ["multipart/form-data"]
+ )
+ fun uploadFile(
+ @PathVariable("petId") petId: kotlin.Long,
+ @Valid @RequestParam(value = "additionalMetadata", required = false) additionalMetadata: kotlin.String?,
+ @Valid @RequestPart("file", required = false) file: org.springframework.web.multipart.MultipartFile
+ ): ResponseEntity {
+ return ResponseEntity(service.uploadFile(petId, additionalMetadata, file), HttpStatus.valueOf(200))
+ }
+
+ companion object {
+ //for your own safety never directly reuse these path definitions in tests
+ const val PATH_ADD_PET: String = "/pet"
+ const val PATH_DELETE_PET: String = "/pet/{petId}"
+ const val PATH_FIND_PETS_BY_STATUS: String = "/pet/findByStatus"
+ const val PATH_FIND_PETS_BY_TAGS: String = "/pet/findByTags"
+ const val PATH_GET_PET_BY_ID: String = "/pet/{petId}"
+ const val PATH_UPDATE_PET: String = "/pet"
+ const val PATH_UPDATE_PET_WITH_FORM: String = "/pet/{petId}"
+ const val PATH_UPLOAD_FILE: String = "/pet/{petId}/uploadImage"
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiService.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiService.kt
new file mode 100644
index 000000000000..5dc0b6978803
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiService.kt
@@ -0,0 +1,104 @@
+package org.openapitools.api
+
+import org.openapitools.model.ModelApiResponse
+import org.openapitools.model.Pet
+
+interface PetApiService {
+
+ /**
+ * POST /pet : Add a new pet to the store
+ *
+ *
+ * @param pet Pet object that needs to be added to the store (required)
+ * @return successful operation (status code 200)
+ * or Invalid input (status code 405)
+ * @see PetApi#addPet
+ */
+ fun addPet(pet: Pet): Pet
+
+ /**
+ * DELETE /pet/{petId} : Deletes a pet
+ *
+ *
+ * @param petId Pet id to delete (required)
+ * @param apiKey (optional)
+ * @return Invalid pet value (status code 400)
+ * @see PetApi#deletePet
+ */
+ fun deletePet(petId: kotlin.Long, apiKey: kotlin.String?): Unit
+
+ /**
+ * GET /pet/findByStatus : Finds Pets by status
+ * Multiple status values can be provided with comma separated strings
+ *
+ * @param status Status values that need to be considered for filter (required)
+ * @return successful operation (status code 200)
+ * or Invalid status value (status code 400)
+ * @see PetApi#findPetsByStatus
+ */
+ fun findPetsByStatus(status: kotlin.collections.List): List
+
+ /**
+ * GET /pet/findByTags : Finds Pets by tags
+ * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+ *
+ * @param tags Tags to filter by (required)
+ * @return successful operation (status code 200)
+ * or Invalid tag value (status code 400)
+ * @deprecated
+ * @see PetApi#findPetsByTags
+ */
+ @Deprecated(message="Operation is deprecated")
+ fun findPetsByTags(tags: kotlin.collections.List): List
+
+ /**
+ * GET /pet/{petId} : Find pet by ID
+ * Returns a single pet
+ *
+ * @param petId ID of pet to return (required)
+ * @return successful operation (status code 200)
+ * or Invalid ID supplied (status code 400)
+ * or Pet not found (status code 404)
+ * @see PetApi#getPetById
+ */
+ fun getPetById(petId: kotlin.Long): Pet
+
+ /**
+ * PUT /pet : Update an existing pet
+ *
+ *
+ * @param pet Pet object that needs to be added to the store (required)
+ * @return successful operation (status code 200)
+ * or Invalid ID supplied (status code 400)
+ * or Pet not found (status code 404)
+ * or Validation exception (status code 405)
+ * API documentation for the updatePet operation
+ * @see Update an existing pet Documentation
+ * @see PetApi#updatePet
+ */
+ fun updatePet(pet: Pet): Pet
+
+ /**
+ * POST /pet/{petId} : Updates a pet in the store with form data
+ *
+ *
+ * @param petId ID of pet that needs to be updated (required)
+ * @param name Updated name of the pet (optional)
+ * @param status Updated status of the pet (optional)
+ * @return Invalid input (status code 405)
+ * @see PetApi#updatePetWithForm
+ */
+ fun updatePetWithForm(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?): Unit
+
+ /**
+ * POST /pet/{petId}/uploadImage : uploads an image
+ *
+ *
+ * @param petId ID of pet to update (required)
+ * @param additionalMetadata Additional data to pass to server (optional)
+ * @param file file to upload (optional)
+ * @return successful operation (status code 200)
+ * @see PetApi#uploadFile
+ */
+ fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt
new file mode 100644
index 000000000000..6600f5535137
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt
@@ -0,0 +1,40 @@
+package org.openapitools.api
+
+import org.openapitools.model.ModelApiResponse
+import org.openapitools.model.Pet
+import org.springframework.stereotype.Service
+@Service
+class PetApiServiceImpl : PetApiService {
+
+ override fun addPet(pet: Pet): Pet {
+ TODO("Implement me")
+ }
+
+ override fun deletePet(petId: kotlin.Long, apiKey: kotlin.String?): Unit {
+ TODO("Implement me")
+ }
+
+ override fun findPetsByStatus(status: kotlin.collections.List): List {
+ TODO("Implement me")
+ }
+
+ override fun findPetsByTags(tags: kotlin.collections.List): List {
+ TODO("Implement me")
+ }
+
+ override fun getPetById(petId: kotlin.Long): Pet {
+ TODO("Implement me")
+ }
+
+ override fun updatePet(pet: Pet): Pet {
+ TODO("Implement me")
+ }
+
+ override fun updatePetWithForm(petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?): Unit {
+ TODO("Implement me")
+ }
+
+ override fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile): ModelApiResponse {
+ TODO("Implement me")
+ }
+}
diff --git a/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/StoreApiController.kt b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/StoreApiController.kt
new file mode 100644
index 000000000000..62c40eca0e3b
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-4/src/main/kotlin/org/openapitools/api/StoreApiController.kt
@@ -0,0 +1,87 @@
+package org.openapitools.api
+
+import org.openapitools.model.Order
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.http.ResponseEntity
+
+import org.springframework.web.bind.annotation.*
+import org.springframework.validation.annotation.Validated
+import org.springframework.web.context.request.NativeWebRequest
+import org.springframework.beans.factory.annotation.Autowired
+
+import jakarta.validation.Valid
+import jakarta.validation.constraints.DecimalMax
+import jakarta.validation.constraints.DecimalMin
+import jakarta.validation.constraints.Email
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotNull
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.Size
+
+import kotlin.collections.List
+import kotlin.collections.Map
+
+@RestController
+@Validated
+class StoreApiController(@Autowired(required = true) val service: StoreApiService) {
+
+
+ @RequestMapping(
+ method = [RequestMethod.DELETE],
+ // "/store/order/{orderId}"
+ value = [PATH_DELETE_ORDER]
+ )
+ fun deleteOrder(
+ @PathVariable("orderId") orderId: kotlin.String
+ ): ResponseEntity {
+ return ResponseEntity(service.deleteOrder(orderId), HttpStatus.valueOf(400))
+ }
+
+
+ @RequestMapping(
+ method = [RequestMethod.GET],
+ // "/store/inventory"
+ value = [PATH_GET_INVENTORY],
+ produces = ["application/json"]
+ )
+ fun getInventory(): ResponseEntity