Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
- samples/client/petstore/kotlin-explicit
- samples/client/petstore/kotlin-gson
- samples/client/petstore/kotlin-jackson
- samples/client/petstore/kotlin-jackson3
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing CI coverage for new sample kotlin-jvm-spring-4-restclient-jackson3. The PR adds kotlin-jackson3 to the workflow but omits the Spring Boot 4 Jackson 3 sample, creating a CI blind spot where this sample can break without detection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/samples-kotlin-client.yaml, line 29:

<comment>Missing CI coverage for new sample `kotlin-jvm-spring-4-restclient-jackson3`. The PR adds `kotlin-jackson3` to the workflow but omits the Spring Boot 4 Jackson 3 sample, creating a CI blind spot where this sample can break without detection.</comment>

<file context>
@@ -26,6 +26,7 @@ jobs:
           - samples/client/petstore/kotlin-explicit
           - samples/client/petstore/kotlin-gson
           - samples/client/petstore/kotlin-jackson
+          - samples/client/petstore/kotlin-jackson3
           - samples/client/petstore/kotlin-model-prefix-type-mappings
           # needs Android configured
</file context>
Fix with Cubic

- samples/client/petstore/kotlin-model-prefix-type-mappings
# needs Android configured
#- samples/client/petstore/kotlin-json-request-string
Expand Down
10 changes: 10 additions & 0 deletions bin/configs/kotlin-jackson3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: kotlin
outputDir: samples/client/petstore/kotlin-jackson3
inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
additionalProperties:
serializationLibrary: jackson
useJackson3: "true"
artifactId: kotlin-petstore-jackson3
enumPropertyNaming: UPPERCASE
enumUnknownDefaultCase: "true"
2 changes: 1 addition & 1 deletion docs/generators/kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +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|
|useJackson3|Use Jackson 3 dependencies (tools.jackson package). Requires serializationLibrary=jackson. Incompatible with openApiNullable.| |false|
|useNonAsciiHeaders|Allow to use non-ascii headers with the okhttp library| |false|
|useResponseAsReturnType|When using retrofit2 and coroutines, use `Response`&lt;`T`&gt; as return type instead of `T`.| |true|
|useRxJava3|Whether to use the RxJava3 adapter with the retrofit2 library.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public KotlinClientCodegen() {
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."));
"Use Jackson 3 dependencies (tools.jackson package). Requires serializationLibrary=jackson. Incompatible with openApiNullable."));
}

@Override
Expand Down Expand Up @@ -469,18 +469,23 @@ 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);
} else {
additionalProperties.put(this.serializationLibrary.name(), true);
}

if (isUseJackson3()) {
if (this.serializationLibrary != SERIALIZATION_LIBRARY_TYPE.jackson) {
throw new IllegalArgumentException("useJackson3 requires serializationLibrary=jackson");
}
if (additionalProperties.containsKey("openApiNullable")
&& Boolean.parseBoolean(additionalProperties.get("openApiNullable").toString())) {
throw new IllegalArgumentException("openApiNullable cannot be set with useJackson3");
}
}

if (additionalProperties.containsKey(MAP_FILE_BINARY_TO_BYTE_ARRAY)) {
setMapFileBinaryToByteArray(convertPropertyToBooleanAndWriteBack(MAP_FILE_BINARY_TO_BYTE_ARRAY));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,13 @@ dependencies {
{{/gson}}
{{#jackson}}
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
{{#useJackson3}}
implementation "tools.jackson.module:jackson-module-kotlin:3.0.1"
{{/useJackson3}}
{{^useJackson3}}
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.20.0"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.0"
{{/useJackson3}}
{{/jackson}}
{{#kotlinx_serialization}}
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ import kotlinx.datetime.LocalTime
import java.util.UUID
{{/gson}}
{{#jackson}}
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
{{^useJackson3}}
import {{jacksonPackage}}.databind.DeserializationFeature
import {{jacksonPackage}}.databind.ObjectMapper
import {{jacksonPackage}}.databind.SerializationFeature
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import {{jacksonPackage}}.module.kotlin.jacksonObjectMapper
{{/useJackson3}}
{{#useJackson3}}
import {{jacksonPackage}}.databind.DeserializationFeature
import {{jacksonPackage}}.databind.ObjectMapper
import {{jacksonPackage}}.databind.cfg.DateTimeFeature
{{#enumUnknownDefaultCase}}
import {{jacksonPackage}}.databind.cfg.EnumFeature
{{/enumUnknownDefaultCase}}
import com.fasterxml.jackson.annotation.JsonInclude
import {{jacksonPackage}}.module.kotlin.jsonMapper
import {{jacksonPackage}}.module.kotlin.kotlinModule
{{/useJackson3}}
{{/jackson}}
{{#kotlinx_serialization}}
import java.math.BigDecimal
Expand Down Expand Up @@ -120,6 +133,7 @@ import java.util.concurrent.atomic.AtomicLong
}
{{/gson}}
{{#jackson}}
{{^useJackson3}}
@JvmStatic
val jacksonObjectMapper: ObjectMapper = jacksonObjectMapper()
.findAndRegisterModules()
Expand All @@ -129,6 +143,24 @@ import java.util.concurrent.atomic.AtomicLong
{{/enumUnknownDefaultCase}}
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, {{failOnUnknownProperties}})
{{/useJackson3}}
{{#useJackson3}}
@JvmStatic
val jacksonObjectMapper: ObjectMapper = jsonMapper {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the type of jacksonObjectMapper be specified as JsonMapper instead of ObjectMapper?

Especially if it is to be used with in the generated code for the client, which passes it to a JacksonJsonHttpMessageConverter that takes in JsonMapper as parameter.

This is my generated code with the snapshot of this PR:
Image

addModule(kotlinModule())
changeDefaultPropertyInclusion { it.withValueInclusion(JsonInclude.Include.NON_ABSENT) }
{{#enumUnknownDefaultCase}}
enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
{{/enumUnknownDefaultCase}}
disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the default for this serialization feature WRITE_DATES_AS_TIMESTAMPS changed in Jackson 3 from true in Jackson 2 to false in Jackson 3. Perhaps it can be omitted from the generated code if on Jackson 3?

I believe the same applies for FAIL_ON_UNKNOWN_PROPERTIES, however, as this is controlled with configuration options, there must be a check on that option.

{{#failOnUnknownProperties}}
enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
{{/failOnUnknownProperties}}
{{^failOnUnknownProperties}}
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
{{/failOnUnknownProperties}}
}
{{/useJackson3}}
{{/jackson}}
{{#kotlinx_serialization}}
private var isAdaptersInitialized = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import com.google.gson.GsonBuilder
import java.text.DateFormat
{{/gson}}
{{#jackson}}
import com.fasterxml.jackson.databind.ObjectMapper
import {{jacksonPackage}}.databind.ObjectMapper
{{/jackson}}

{{#operations}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import java.text.DateFormat
{{/gson}}
{{#jackson}}
import io.ktor.serialization.jackson.*
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import {{jacksonPackage}}.module.kotlin.jacksonObjectMapper
import {{jacksonPackage}}.databind.ObjectMapper
import {{jacksonPackage}}.databind.SerializationFeature
{{^useJackson3}}
import {{jacksonPackage}}.datatype.jsr310.JavaTimeModule
{{/useJackson3}}
import {{jacksonPackage}}.core.util.DefaultIndenter
import {{jacksonPackage}}.core.util.DefaultPrettyPrinter
{{/jackson}}
import {{packageName}}.auth.*

Expand Down Expand Up @@ -96,7 +98,9 @@ import {{packageName}}.auth.*
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
indentObjectsWith(DefaultIndenter(" ", "\n"))
})
{{^useJackson3}}
registerModule(JavaTimeModule())
{{/useJackson3}}
}
{{/jackson}}
protected val UNSAFE_HEADERS: List<String> = listOf(HttpHeaders.ContentType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import org.threeten.bp.OffsetTime
import com.google.gson.reflect.TypeToken
{{/gson}}
{{#jackson}}
import com.fasterxml.jackson.core.type.TypeReference
import {{jacksonPackage}}.core.type.TypeReference
{{/jackson}}
{{#moshi}}
import com.squareup.moshi.adapter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import com.squareup.moshi.Moshi
import retrofit2.converter.moshi.MoshiConverterFactory
{{/moshi}}
{{#jackson}}
import com.fasterxml.jackson.databind.ObjectMapper
import {{jacksonPackage}}.databind.ObjectMapper
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Incompatible Jackson version with Retrofit2: {{jacksonPackage}}.databind.ObjectMapper (Jackson 3 when useJackson3=true) is passed to JacksonConverterFactory.create() which expects com.fasterxml.jackson.databind.ObjectMapper (Jackson 2). This type mismatch will cause compilation errors for generated jvm-retrofit2 clients when useJackson3=true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache, line 53:

<comment>Incompatible Jackson version with Retrofit2: `{{jacksonPackage}}.databind.ObjectMapper` (Jackson 3 when useJackson3=true) is passed to `JacksonConverterFactory.create()` which expects `com.fasterxml.jackson.databind.ObjectMapper` (Jackson 2). This type mismatch will cause compilation errors for generated jvm-retrofit2 clients when useJackson3=true.</comment>

<file context>
@@ -50,7 +50,7 @@ import com.squareup.moshi.Moshi
 {{/moshi}}
 {{#jackson}}
-import com.fasterxml.jackson.databind.ObjectMapper
+import {{jacksonPackage}}.databind.ObjectMapper
 import retrofit2.converter.jackson.JacksonConverterFactory
 {{/jackson}}
</file context>
Fix with Cubic

import retrofit2.converter.jackson.JacksonConverterFactory
{{/jackson}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import java.io.IOException

{{#jackson}}
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.type.TypeReference
import {{jacksonPackage}}.core.type.TypeReference
{{/jackson}}
{{#gson}}
import com.google.gson.reflect.TypeToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.vertx.core.Vertx
import io.vertx.core.buffer.Buffer
import java.nio.charset.StandardCharsets
{{#jackson}}
import com.fasterxml.jackson.core.type.TypeReference
import {{jacksonPackage}}.core.type.TypeReference
{{/jackson}}
{{#gson}}
import com.google.gson.reflect.TypeToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.assertj.core.api.Assertions;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.AbstractKotlinCodegen;
import org.openapitools.codegen.antlr4.KotlinLexer;
import org.openapitools.codegen.antlr4.KotlinParser;
import org.openapitools.codegen.config.CodegenConfigurator;
Expand Down Expand Up @@ -923,6 +925,114 @@ public void testCompanionObjectGeneratesCompanionInModel() throws IOException {
TestUtils.assertFileContains(petModel, "companion object { }");
}

@Test
public void shouldRefuseJackson3WithoutJacksonSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/petstore.yaml");
final KotlinClientCodegen codegen = new KotlinClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(output.getAbsolutePath());

codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, "moshi");

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/2_0/petstore.yaml");
final KotlinClientCodegen codegen = new KotlinClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(output.getAbsolutePath());

codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson");
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 shouldGenerateJackson3Imports() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');

final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/petstore.yaml");
final KotlinClientCodegen codegen = new KotlinClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(output.getAbsolutePath());

codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson");

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/client/models/Pet.kt");
TestUtils.assertFileContains(modelPath, "import com.fasterxml.jackson.annotation.JsonProperty");

Path serializerPath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt");
TestUtils.assertFileContains(serializerPath, "import tools.jackson.databind.ObjectMapper");
TestUtils.assertFileContains(serializerPath, "import tools.jackson.module.kotlin.jsonMapper");
TestUtils.assertFileNotContains(serializerPath, "com.fasterxml.jackson.databind");
}

@Test
public void shouldGenerateBuildGradleWithJackson3Deps() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');

final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/petstore.yaml");
final KotlinClientCodegen codegen = new KotlinClientCodegen();
codegen.setOpenAPI(openAPI);
codegen.setOutputDir(output.getAbsolutePath());

codegen.additionalProperties().put(AbstractKotlinCodegen.USE_JACKSON_3, "true");
codegen.additionalProperties().put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson");

ClientOptInput input = new ClientOptInput();
input.openAPI(openAPI);
input.config(codegen);

DefaultGenerator generator = new DefaultGenerator();
generator.setGenerateMetadata(false);
generator.opts(input).generate();

Path buildGradlePath = Paths.get(outputPath + "/build.gradle");
TestUtils.assertFileContains(buildGradlePath, "tools.jackson.module:jackson-module-kotlin");
TestUtils.assertFileNotContains(buildGradlePath, "jackson-datatype-jsr310");
TestUtils.assertFileNotContains(buildGradlePath, "com.fasterxml.jackson");
}

private static class ModelNameTest {
private final String expectedName;
private final String expectedClassName;
Expand Down
23 changes: 23 additions & 0 deletions samples/client/petstore/kotlin-jackson3/.openapi-generator-ignore
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading