From 27fcb29c69766c37624b0780480943c1dec5221e Mon Sep 17 00:00:00 2001 From: Kevin Robayna Date: Tue, 22 Sep 2026 14:47:37 +0100 Subject: [PATCH] Fix KotlinObjectMapperFactory on newer Jackson KotlinObjectMapperFactory.new() constructed KotlinModule with all default arguments. Kotlin compiles that into a call to the synthetic all-defaults overload of KotlinModule's deprecated constructor, so the emitted descriptor pins whatever parameter list that constructor had in the Jackson version the SDK was built against. That parameter list has changed repeatedly - it gained arguments in 2.11, 2.12 and 2.16, and SingletonSupport later became a boolean - so a call compiled against 2.15.4 links only against 2.12 through 2.15 and throws NoSuchMethodError on anything else. Built against 2.15.4 and run against 2.21.4: java.lang.NoSuchMethodError: 'void com.fasterxml.jackson.module. kotlin.KotlinModule.(int, boolean, boolean, boolean, com. fasterxml.jackson.module.kotlin.SingletonSupport, boolean, int, kotlin.jvm.internal.DefaultConstructorMarker)' The deprecated constructor was chosen to keep compatibility with old Jackson versions, but it never achieved that: the same call also fails on 2.9.0 through 2.11, the low end of the supported [2.9.0,) range. registerKotlinModule() leaves the constructor choice to jackson-module-kotlin, and its own signature has been unchanged since 2.9.0. Compiling against 2.15.4 and running against 2.9.0, 2.10, 2.11, 2.12, 2.13, 2.14, 2.15.4, 2.16, 2.17, 2.18, 2.19 and 2.21.4, the current code passes only on 2.12 through 2.15 while this change passes on every one of them. No public API change: new() is still a static method returning ObjectMapper. --- .../converter/KotlinObjectMapperFactory.kt | 16 ++++++------ .../KotlinObjectMapperFactoryTest.kt | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 temporal-kotlin/src/test/kotlin/io/temporal/common/converter/KotlinObjectMapperFactoryTest.kt diff --git a/temporal-kotlin/src/main/kotlin/io/temporal/common/converter/KotlinObjectMapperFactory.kt b/temporal-kotlin/src/main/kotlin/io/temporal/common/converter/KotlinObjectMapperFactory.kt index 621d02fa4d..b4b5f925e1 100644 --- a/temporal-kotlin/src/main/kotlin/io/temporal/common/converter/KotlinObjectMapperFactory.kt +++ b/temporal-kotlin/src/main/kotlin/io/temporal/common/converter/KotlinObjectMapperFactory.kt @@ -2,19 +2,19 @@ package io.temporal.common.converter import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule class KotlinObjectMapperFactory { companion object { @JvmStatic fun new(): ObjectMapper { - val mapper = JacksonJsonPayloadConverter.newDefaultObjectMapper() - - // use deprecated constructor instead of builder to maintain compatibility with old jackson versions - @Suppress("deprecation") - val km = KotlinModule() - mapper.registerModule(km) - return mapper + // Let jackson-module-kotlin construct the module rather than calling a constructor here. + // `KotlinModule()` compiles to the synthetic all-defaults overload of its deprecated + // constructor, and that parameter list changed in 2.11, 2.12 and 2.16, so the call only + // linked against the versions sharing the shape we happened to build against and threw + // NoSuchMethodError on every other version. `registerKotlinModule` has kept a single + // signature since 2.9.0, which is the whole Jackson range this SDK supports. + return JacksonJsonPayloadConverter.newDefaultObjectMapper().registerKotlinModule() } } } diff --git a/temporal-kotlin/src/test/kotlin/io/temporal/common/converter/KotlinObjectMapperFactoryTest.kt b/temporal-kotlin/src/test/kotlin/io/temporal/common/converter/KotlinObjectMapperFactoryTest.kt new file mode 100644 index 0000000000..2d26355272 --- /dev/null +++ b/temporal-kotlin/src/test/kotlin/io/temporal/common/converter/KotlinObjectMapperFactoryTest.kt @@ -0,0 +1,25 @@ +package io.temporal.common.converter + +import org.junit.Assert.assertEquals +import org.junit.Test + +class KotlinObjectMapperFactoryTest { + + data class TestPayload(val name: String, val count: Int) + + /** + * A data class has no no-arg constructor, so Jackson can only deserialize it when the Kotlin + * module is registered. This also guards against [KotlinObjectMapperFactory.new] failing to link + * against the jackson-module-kotlin version present at runtime, which is not necessarily the one + * the SDK was compiled against. + */ + @Test + fun `new should return a mapper that round-trips a Kotlin data class`() { + val mapper = KotlinObjectMapperFactory.new() + + val value = TestPayload("payload", 42) + val roundTripped = mapper.readValue(mapper.writeValueAsString(value), TestPayload::class.java) + + assertEquals(value, roundTripped) + } +}