Skip to content

feat(kotlin-client): add Jackson 3 support with useJackson3 option#23161

Open
yonatankarp wants to merge 8 commits intoOpenAPITools:masterfrom
yonatankarp:feat/kotlin-client-jackson3
Open

feat(kotlin-client): add Jackson 3 support with useJackson3 option#23161
yonatankarp wants to merge 8 commits intoOpenAPITools:masterfrom
yonatankarp:feat/kotlin-client-jackson3

Conversation

@yonatankarp
Copy link
Contributor

@yonatankarp yonatankarp commented Mar 6, 2026

Summary

  • Wire the existing AbstractKotlinCodegen Jackson 3 infrastructure into the kotlin client generator
  • When useJackson3=true (requires serializationLibrary=jackson), all templates use the tools.jackson package instead of com.fasterxml.jackson, and build.gradle pulls jackson-module-kotlin 3.0.1 without the separate JSR-310 module
  • Register useJackson3 CLI option with validation (requires jackson serialization, incompatible with openApiNullable)
  • Replace hardcoded com.fasterxml.jackson with {{jacksonPackage}} in all model, serializer, and library-specific templates
  • Make JavaTimeModule conditional (not needed in Jackson 3) for jvm-ktor
  • Add tests for validation and generated output
  • Add kotlin-jackson3 sample config, generated sample, CI workflow entry, and regenerated docs

Test plan

  • Unit tests pass: validation rejects useJackson3 with non-jackson serialization and with openApiNullable
  • Generated model files contain import tools.jackson.annotation.JsonProperty (not com.fasterxml.jackson)
  • Generated build.gradle contains tools.jackson.module:jackson-module-kotlin and no jackson-datatype-jsr310
  • Existing Jackson 2 samples regenerate identically (no diffs)
  • CI passes on samples-kotlin-client workflow

Summary by cubic

Adds Jackson 3 support to the Kotlin client via useJackson3 and adds Spring Boot 4 support for the jvm-spring-restclient library via useSpringBoot4, switching generated code to tools.jackson packages (annotations stay com.fasterxml.jackson.annotation) and using Spring’s JacksonJsonHttpMessageConverter with RestClient.

  • New Features

    • Added useJackson3 CLI option with validation (requires serializationLibrary=jackson; incompatible with openApiNullable); removed the old guard.
    • Added useSpringBoot4 option for jvm-spring-restclient that auto-enables Jackson 3, generates RestClient code, and uses JacksonJsonHttpMessageConverter; Gradle sets spring_boot_version to 4.0.1.
    • Switched databind/core/module imports to {{jacksonPackage}} across models and JVM clients (okhttp, retrofit2, ktor, vertx, jvm-spring-restclient); annotations remain com.fasterxml.jackson.annotation.
    • Jackson 3 serializer uses the immutable jsonMapper { addModule(kotlinModule()) ... }, sets NON_ABSENT inclusion, disables timestamps via DateTimeFeature, and optionally enables EnumFeature; SerializationFeature is used only for Jackson 2 and findAndRegisterModules is not used.
    • Gradle adds tools.jackson.module:jackson-module-kotlin:3.0.1 when enabled and drops jackson-datatype-jsr310; jvm-ktor registers JavaTimeModule only for Jackson 2.
    • Added kotlin-jackson3 and kotlin-jvm-spring-4-restclient-jackson3 samples, CI matrix entry for the former, updated docs, and tests for option validation and generated imports/dependencies.
    • Regenerated sample FILES manifests for CI stability.
  • Migration

    • Enable Jackson 3 with: -p serializationLibrary=jackson -p useJackson3=true (do not combine with openApiNullable).
    • For Spring Boot 4 clients: use -p library=jvm-spring-restclient -p useSpringBoot4=true (auto-enables Jackson 3). Generated code uses tools.jackson.* for databind/core/module and com.fasterxml.jackson.annotation for annotations; jvm-ktor does not register JavaTimeModule under Jackson 3.

Written for commit 00b3413. Summary will update on new commits.

yonatankarp and others added 6 commits March 10, 2026 14:30
Wire the existing AbstractKotlinCodegen Jackson 3 infrastructure into
the kotlin client generator. When useJackson3=true (requires
serializationLibrary=jackson), all templates use the tools.jackson
package instead of com.fasterxml.jackson, and build.gradle pulls
jackson-module-kotlin 3.0.1 without the separate JSR-310 module.

- Register useJackson3 CLI option in KotlinClientCodegen
- Add validation: requires jackson serialization, incompatible with openApiNullable
- Replace hardcoded com.fasterxml.jackson with {{jacksonPackage}} in all
  model, serializer, and library-specific templates
- Make JavaTimeModule conditional (not needed in Jackson 3) for jvm-ktor
- Add Jackson 3 dependency block in build.gradle.mustache
- Add tests for validation and generated output
- Add kotlin-jackson3 sample config and generated sample
- Add sample to CI workflow matrix
- Regenerate generator docs
Jackson 3 only moves databind/core/module to tools.jackson package.
The annotations artifact stays at com.fasterxml.jackson.annotation.
Also add AbstractKotlinCodegen Jackson 3 infrastructure so this branch
is self-contained for CI testing.
Jackson 3 moved/renamed several APIs:
- findAndRegisterModules() not needed (modules auto-discovered)
- SerializationFeature.WRITE_DATES_AS_TIMESTAMPS -> DateTimeFeature
- DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE -> EnumFeature

Add conditionals in Serializer.kt.mustache for Jackson 2 vs 3.
Jackson 3 ObjectMapper is immutable — no more chaining .configure()
and .setSerializationInclusion(). Use jsonMapper {} builder DSL with:
- changeDefaultPropertyInclusion for NON_ABSENT inclusion
- enable/disable for DateTimeFeature, EnumFeature, DeserializationFeature
- addModule(kotlinModule()) instead of jacksonObjectMapper()
The upstream kotlin-spring PR added a guard throwing IllegalArgumentException
for useJackson3 on kotlin-client. Since this branch implements that support,
remove the guard while keeping the proper validation below it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yonatankarp yonatankarp force-pushed the feat/kotlin-client-jackson3 branch from 56fde75 to 69d1430 Compare March 10, 2026 13:34
Add useSpringBoot4 option that auto-enables Jackson 3 and generates
RestClient code using JacksonJsonHttpMessageConverter with Spring Boot 4.
@yonatankarp yonatankarp marked this pull request as ready for review March 10, 2026 16:59
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

7 issues found across 104 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/petstore/kotlin-jackson3/docs/PetApi.md">

<violation number="1" location="samples/client/petstore/kotlin-jackson3/docs/PetApi.md:328">
P1: Malformed Markdown parameter table: The table header row (`| Name | Type | Description | Notes |`) appears mid-table between parameters instead of only at the beginning. This indicates a bug in the `api_doc.mustache` template where headers are incorrectly rendered inside parameter loops rather than once for all parameters, which will break HTML rendering in published documentation.</violation>
</file>

<file name="samples/client/petstore/kotlin-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt">

<violation number="1" location="samples/client/petstore/kotlin-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt:14">
P1: Extension property `isRedirect` is shadowed by OkHttp's member property and will never be invoked. The `@Suppress("EXTENSION_SHADOWED_BY_MEMBER")` annotation acknowledges this conflict. When `response.isRedirect` is called in `ApiClient.kt:427`, Kotlin resolves to OkHttp's built-in implementation which only returns true for specific codes (300/301/302/303/307/308), not the documented 300-399 range. HTTP codes like 304, 305 will incorrectly fall through to `ServerError` instead of `Redirection`. Consider renaming to `isAnyRedirect` or `is3xx` to avoid shadowing.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java:474">
P1: BUG: Setting `useSpringBoot4=false` incorrectly enables Jackson 3 and may crash the generator. The condition only checks if the key exists, not its boolean value.</violation>
</file>

<file name="samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt">

<violation number="1" location="samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt:5">
P2: Comment contradicts implementation: The documentation states body is excluded for caching purposes, but body is included as a data class constructor property, which breaks the stated caching/dedup invariant. Kotlin data class properties participate in equals() and hashCode(), so two requests with different payloads will not share config identity as the comment suggests.</violation>
</file>

<file name="samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt">

<violation number="1" location="samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt:60">
P1: The 'Content-Type' header is overwritten. The map is first assigned "application/json", then immediately overwritten with "application/xml". Since the RestClient is configured with JacksonJsonHttpMessageConverter (which serializes the Pet object as JSON), the request will send JSON data with an incorrect XML content-type header, causing server-side parsing failures.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-retrofit2/infrastructure/ApiClient.kt.mustache:53">
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.</violation>
</file>

<file name=".github/workflows/samples-kotlin-client.yaml">

<violation number="1" location=".github/workflows/samples-kotlin-client.yaml:29">
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.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


### Parameters
| **petId** | **kotlin.Long**| ID of pet that needs to be updated | |
| **name** | **kotlin.String**| Updated name of the pet | [optional] |
Copy link
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: Malformed Markdown parameter table: The table header row (| Name | Type | Description | Notes |) appears mid-table between parameters instead of only at the beginning. This indicates a bug in the api_doc.mustache template where headers are incorrectly rendered inside parameter loops rather than once for all parameters, which will break HTML rendering in published documentation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-jackson3/docs/PetApi.md, line 328:

<comment>Malformed Markdown parameter table: The table header row (`| Name | Type | Description | Notes |`) appears mid-table between parameters instead of only at the beginning. This indicates a bug in the `api_doc.mustache` template where headers are incorrectly rendered inside parameter loops rather than once for all parameters, which will break HTML rendering in published documentation.</comment>

<file context>
@@ -0,0 +1,397 @@
+
+### Parameters
+| **petId** | **kotlin.Long**| ID of pet that needs to be updated | |
+| **name** | **kotlin.String**| Updated name of the pet | [optional] |
+| Name | Type | Description  | Notes |
+| ------------- | ------------- | ------------- | ------------- |
</file context>
Fix with Cubic

* Provides an extension to evaluation whether the response is a 3xx code
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
val Response.isRedirect : Boolean get() = this.code in 300..399
Copy link
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: Extension property isRedirect is shadowed by OkHttp's member property and will never be invoked. The @Suppress("EXTENSION_SHADOWED_BY_MEMBER") annotation acknowledges this conflict. When response.isRedirect is called in ApiClient.kt:427, Kotlin resolves to OkHttp's built-in implementation which only returns true for specific codes (300/301/302/303/307/308), not the documented 300-399 range. HTTP codes like 304, 305 will incorrectly fall through to ServerError instead of Redirection. Consider renaming to isAnyRedirect or is3xx to avoid shadowing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt, line 14:

<comment>Extension property `isRedirect` is shadowed by OkHttp's member property and will never be invoked. The `@Suppress("EXTENSION_SHADOWED_BY_MEMBER")` annotation acknowledges this conflict. When `response.isRedirect` is called in `ApiClient.kt:427`, Kotlin resolves to OkHttp's built-in implementation which only returns true for specific codes (300/301/302/303/307/308), not the documented 300-399 range. HTTP codes like 304, 305 will incorrectly fall through to `ServerError` instead of `Redirection`. Consider renaming to `isAnyRedirect` or `is3xx` to avoid shadowing.</comment>

<file context>
@@ -0,0 +1,24 @@
+ * Provides an extension to evaluation whether the response is a 3xx code
+ */
+@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
+val Response.isRedirect : Boolean get() = this.code in 300..399
+
+/**
</file context>
Fix with Cubic

Comment on lines +474 to +477
if (additionalProperties.containsKey(USE_SPRING_BOOT4)) {
convertPropertyToBooleanAndWriteBack(USE_SPRING_BOOT4);
additionalProperties.put(USE_JACKSON_3, "true");
setUseJackson3(true);
Copy link
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: BUG: Setting useSpringBoot4=false incorrectly enables Jackson 3 and may crash the generator. The condition only checks if the key exists, not its boolean value.

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/java/org/openapitools/codegen/languages/KotlinClientCodegen.java, line 474:

<comment>BUG: Setting `useSpringBoot4=false` incorrectly enables Jackson 3 and may crash the generator. The condition only checks if the key exists, not its boolean value.</comment>

<file context>
@@ -469,9 +471,10 @@ public void processOpts() {
-        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(USE_SPRING_BOOT4)) {
+            convertPropertyToBooleanAndWriteBack(USE_SPRING_BOOT4);
+            additionalProperties.put(USE_JACKSON_3, "true");
</file context>
Suggested change
if (additionalProperties.containsKey(USE_SPRING_BOOT4)) {
convertPropertyToBooleanAndWriteBack(USE_SPRING_BOOT4);
additionalProperties.put(USE_JACKSON_3, "true");
setUseJackson3(true);
if (additionalProperties.containsKey(USE_SPRING_BOOT4) && Boolean.parseBoolean(additionalProperties.get(USE_SPRING_BOOT4).toString())) {
convertPropertyToBooleanAndWriteBack(USE_SPRING_BOOT4);
additionalProperties.put(USE_JACKSON_3, "true");
setUseJackson3(true);
}
Fix with Cubic

val localVariableBody = pet
val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
localVariableHeaders["Content-Type"] = "application/json"
Copy link
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: The 'Content-Type' header is overwritten. The map is first assigned "application/json", then immediately overwritten with "application/xml". Since the RestClient is configured with JacksonJsonHttpMessageConverter (which serializes the Pet object as JSON), the request will send JSON data with an incorrect XML content-type header, causing server-side parsing failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/apis/PetApi.kt, line 60:

<comment>The 'Content-Type' header is overwritten. The map is first assigned "application/json", then immediately overwritten with "application/xml". Since the RestClient is configured with JacksonJsonHttpMessageConverter (which serializes the Pet object as JSON), the request will send JSON data with an incorrect XML content-type header, causing server-side parsing failures.</comment>

<file context>
@@ -0,0 +1,348 @@
+        val localVariableBody = pet
+        val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
+        val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
+        localVariableHeaders["Content-Type"] = "application/json"
+        localVariableHeaders["Content-Type"] = "application/xml"
+        localVariableHeaders["Accept"] = "application/xml, application/json"
</file context>
Fix with Cubic

{{/moshi}}
{{#jackson}}
import com.fasterxml.jackson.databind.ObjectMapper
import {{jacksonPackage}}.databind.ObjectMapper
Copy link
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

- samples/client/petstore/kotlin-explicit
- samples/client/petstore/kotlin-gson
- samples/client/petstore/kotlin-jackson
- samples/client/petstore/kotlin-jackson3
Copy link
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


/**
* Defines a config object for a given request.
* NOTE: This object doesn't include 'body' because it
Copy link
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.

P2: Comment contradicts implementation: The documentation states body is excluded for caching purposes, but body is included as a data class constructor property, which breaks the stated caching/dedup invariant. Kotlin data class properties participate in equals() and hashCode(), so two requests with different payloads will not share config identity as the comment suggests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt, line 5:

<comment>Comment contradicts implementation: The documentation states body is excluded for caching purposes, but body is included as a data class constructor property, which breaks the stated caching/dedup invariant. Kotlin data class properties participate in equals() and hashCode(), so two requests with different payloads will not share config identity as the comment suggests.</comment>

<file context>
@@ -0,0 +1,19 @@
+
+/**
+ * Defines a config object for a given request.
+ * NOTE: This object doesn't include 'body' because it
+ *       allows for caching of the constructed object
+ *       for many request definitions.
</file context>
Fix with Cubic

@@ -0,0 +1,10 @@
generatorName: kotlin
outputDir: samples/client/petstore/kotlin-jvm-spring-4-restclient-jackson3
Copy link
Member

@wing328 wing328 Mar 10, 2026

Choose a reason for hiding this comment

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

please add this new folder to the workflow file as well (samples-kotlin-client.yaml)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yap, I moved it from draft just to trigger the review - thanks for the comment!

{{#enumUnknownDefaultCase}}
enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
{{/enumUnknownDefaultCase}}
disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants