Skip to content

Missing empty constructor issue: deserialization breaking change from 2.17 to 2.18 #846

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
2 of 4 tasks
baylrock opened this issue Nov 9, 2024 · 21 comments
Open
2 of 4 tasks
Labels

Comments

@baylrock
Copy link

baylrock commented Nov 9, 2024

Search before asking

  • I searched in the issues and found nothing similar.
  • I have confirmed that the same problem is not reproduced if I exclude the KotlinModule.
  • I searched in the issues of databind and other modules used and found nothing similar.
  • I have confirmed that the problem does not reproduce in Java and only occurs when using Kotlin and KotlinModule.

Describe the bug

It appears that 2.18 introduced a change to the constructor detection, causing existing use cases to fail. I observed it in a case where a class extending a Map without an empty constructor can no longer be instantiated. See the test case example.

To Reproduce

With jackson 2.18.0 onboard:

    @Test
    fun test() {
        assertThrows<MissingKotlinParameterException> {
            jacksonObjectMapper().readValue<Old>("""{ "key":"value" }""")
        }

        assertDoesNotThrow {
            jacksonObjectMapper().readValue<New>("""{ "key":"value" }""")
        }
    }

    // what was working prior to 2.18
    class Old : TreeMap<String, String> {
        constructor(map: Map<String, String>) : super(map)
    }

    // what has to be changed to work with 2.18
    class New : TreeMap<String, String> {
        constructor() : super()
        constructor(map: Map<String, String>) : super(map)
    }

Expected behavior

Changes should be backward-compatible per the versioning standard (minor version changed).

Versions

Kotlin:
Jackson-module-kotlin: 2.18.0
Jackson-databind: 2.18.0

Additional context

I'm not 100% sure if this is a Kotlin module issue or not, as I operate in Kotlin codebase only and not invested enough to test it in plane java.
Generally, the fix to the issue is simple; the questionable part is that, if this is intentional, this is technically a breaking change in a minor version change.

@baylrock baylrock added the bug label Nov 9, 2024
@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 9, 2024

Needs to be checked against 2.18.1 at least (ideally also 2.18.2-SNAPSHOT, build from 2.18 branch, but that's optional) as there are multiple relevant jackson-databind fixes.

@fischl-viesure
Copy link

I can confirm the bug with these versions of jackson-module-kotlin:

I also tested with version 2.17.2 and the behavior to assure that the MissingKotlinParameterException is not thrown with the older version, as described.

@k163377
Copy link
Contributor

k163377 commented Nov 16, 2024

The root cause of this also seemed to be the same as #841 (FasterXML/jackson-databind#4777).

I first checked how the Old constructor is handled in KotrinamesAnnotationIntrospector and both were determined to be creator with mode=DEFAULT.
To be precise, 2.17 uses findCreatorAnnotation and 2.18 uses findDefaultCreator, but since the mode is the same, there is probably no difference.

On the other hand, as a result of StdValueInstantiator.createFromObjectSettings now registering withArgsCreator, valueInstantiator.canCreateFromObjectWith now returns true.
This changes the processing path of MapDeserializer.deserialize, causing KotlinValueInstantiator.createFromObjectWith to be called unexpectedly, resulting in an error.

@cowtowncoder
Copy link
Member

@baylrock Please include actual exception message (with at least some of the stack trace).

@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 17, 2024

It looks like intent was for Constructor to use "Delegating" mode; and if so perhaps it is necessary to use annotation

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)

since I don't see any good heuristics for databind to determine this intent (constructor parameter name and underlying property. Not sure why in 2.17 that would be mode selected.

EDIT: Actually, I do have a guess. Perhaps it is due to type of single-argument being Map, tilting heuristics toward delegating.

@baylrock
Copy link
Author

@cowtowncoder issue was found when migrating from 2.12 to 2.18. So it shouldn't be about 2.17 being weird.

@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 17, 2024

@baylrock That is assuming behavior as of 2.17 was correct, which while not unreasonable assumption is not always true. In case of refactoring of property introspection for 2.18, logic was rewritten to address actual flaws for some well-known cases, but making sure all unit tests pass. Problem here is that there is an unlimited number of combinations, edge cases, and so coverage is always incomplete. While case presented here is not very complex, it seems likely it wasn't covered, and change in logic was not detected.

The question, then, is which of behaviors is more correct: that of 2.17, or that of 2.18.
Put another way: just because 2.17 behaves in certain way is not a guarantee it is the intended ("correct") way.

Having said that, I do now have one idea wrt why Creater mode heuristics may have changed with 2.18 -- I vaguely remember there being some logic to consider some types (specifically Map) as indicating preference towards choosing Mode.DELEGATING.

But with all of this said: I would suggest adding explicit

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)

(with whatever mechanism Kotlin requires)
for the intended Constructor. That will eliminate any need to use heuristics on work with any 2.x version o Jackson.

@fischl-viesure
Copy link

@baylrock Please include actual exception message (with at least some of the stack trace).

I take the liberty and step in for baylrock, 'cause I just happened to see the updates to this ticket now.

This is the exception when instantiating class Old with version 2.18.2:

com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [map type; class JacksonTest$Old, [simple type, class java.lang.String] -> [simple type, class java.lang.String]] value failed for JSON property map due to missing (therefore NULL) value for creator parameter map which is a non-nullable type
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 17] (through reference chain: JacksonTest$Old["N/A"]->JacksonTest$Old["map"])

	at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:97)
	at com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:214)
	at com.fasterxml.jackson.databind.deser.std.MapDeserializer._deserializeUsingCreator(MapDeserializer.java:711)
	at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:432)
	at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:32)
	at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4917)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3860)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3843)
	at JacksonTest.test(JacksonTest.kt:34)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)

Also, annotating the constructor, as you described, with…

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)

…made the exception go away 🥳 (anyway, my problem turned out to not being the one described here 😊).

Thank you cowtowncoder for helping us along!

@rafalh
Copy link

rafalh commented Jan 9, 2025

I've stepped onto this issue too, but in a slightly different case. I assume the root cause is the same.
This test shows the change of behavior:

data class MapWrapper1 @JsonCreator constructor(private val m: Map<String, String>) {
    val foo by m
}
data class MapWrapper2(private val m: Map<String, String>) {
    val foo by m
}
class JacksonKotlinTest {
    private val objectMapper = jacksonObjectMapper()
    private val json = """{"foo": "a", "bar": "b"}"""

    @Test
    fun test1() {
        val v = objectMapper.readValue(json, MapWrapper1::class.java)
        assertEquals("a", v.foo)
    }

    @Test
    fun test2() {
        val v = objectMapper.readValue(json, MapWrapper2::class.java)
        assertEquals("a", v.foo)
    }
}

test1 always passes. test2 passes in Jackson 2.17 and fails in Jackson 2.18.
Please note that I don't have to set the mode in @JsonCreator to DELEGATING and instead keep it as the default (DEFAULT).

The question, then, is which of behaviors is more correct: that of 2.17, or that of 2.18.

I am wondering the same. It seems the current behavior is more in line with pure Jackson without Kotlin module. I've tested normal Java classes and records and in both versions they failed to deserialize if there was no @JsonCreator annotation.

Anyway, it's a bit counter-intuitive that @JsonCreator(Mode.DEFAULT) is not implicit. I would assume this annotation is only needed if there are more than one constructor or if I want a non-default mode. Default mode is documented: "DEFAULT means that caller is to use standard heuristics for choosing mode to use.". For some reason Jackson doesn't use that standard heuristics in the case above (single constructor with Map parameter).

If the current behavior of 2.18 is considered valid, it should probably be documented as a breaking change in 2.18 to help people migrate.

@cowtowncoder
Copy link
Member

Quick note: missing @JsonCreator is very different from @JsonCreator(mode = DEFAULT) -- latter indicates annotated Constructor/Factory method is explicitly to be used. Missing @JsonCreator means it won't be, unless auto-detection kicks in (when no Creator is annotated and there's just one viable candidate).

Not sure that helps untangle the issue but thought I'll mention it.

@aishanand
Copy link

aishanand commented Jan 15, 2025

We upgraded to spring boot 3.4 that uses jackson-core 2.18.0 and the existing code broke with the error. When will this issue be fixed?? It's forcing all the applications that went through upgrade to have the empty constructors only work fine :(

https://github.com/aishanand/personal- Github code space to recreate the issue

Exception in thread "main" java.lang.IllegalArgumentException: Cannot construct instance of com.jackson.issue.deserialize.dto.FruitDTO(although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"name":null,"flavorDTO":{"color":"yellow","taste":"sweet","smell":{"smellsGood":true}}}') at [Source: UNKNOWN; byte offset: #UNKNOWN] at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4636) at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4567) at com.jackson.issue.deserialize.DeserializeApplication.main(DeserializeApplication.java:18) Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance ofcom.jackson.issue.deserialize.dto.FruitDTO (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"name":null,"flavorDTO":{"color":"yellow","taste":"sweet","smell":{"smellsGood":true}}}') at [Source: UNKNOWN; byte offset: #UNKNOWN] at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1754) at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1379) at com.fasterxml.jackson.databind.deser.std.StdDeserializer._deserializeFromString(StdDeserializer.java:311) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1592) at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:197) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187) at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4631)

Image

[Note:https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.4-Release-Notes that uses 2.18.0]

@cowtowncoder
Copy link
Member

cowtowncoder commented Jan 15, 2025 via email

@aishanand
Copy link

@cowtowncoder
2.18.2 is indeed used by spring 3.4 too !
here is the pom https://github.com/aishanand/personal/blob/main/pom.xml
dependency tree
Image

@cowtowncoder
Copy link
Member

Ah. I only saw the email notification and replied without reading the context.

As to fix, it is not clear behavior can or will be changed.

But instead of adding an empty constructor (which really shouldn't be needed) please considering adding annotation -- something I have mentioned multiple times in this thread:

@JsonCreator(mode = JsonCreator.Mode.DELEGATING)

on constructor. That will indicate that the incoming value should match the full argument value of 1-arg constructor, and not 1 property of JSON Object with name matching that argument.

I will also file an issue on jackson-databind to see if there is a way to create Java-only reproduction, and if so if the behavior could be changed.

@cowtowncoder
Copy link
Member

Ok the issue I created on databind side does not seem to capture actual issue. Reading through comments here there may even be multiple different problem cases.

But fundamentally what would be needed would be Java translation of failing test case here, so I could see what is going on and perhaps how to resolve it.

One case I did notice tho was the case with no annotations: Jackson 2.18 can only auto-detect Mode.PROPERTIES constructors implicitly.

But Kotlin module could detect things differently, by KotlinAnnotationIntrospector.kt or KotlinNamesAnnotationIntrospector.kt implementing/overriding findDefaultCreator() -- that would then be used by databind as THE constructor to use.

It looks like this already exists, but I guess it won't detect case(s) here. WDYT @k163377 ?

@k163377
Copy link
Contributor

k163377 commented Feb 8, 2025

@cowtowncoder
I have submitted an Issue regarding a question I discovered during the translation process to Java.
Please check it.
FasterXML/jackson-databind#4960

@farazahmed-10P
Copy link

farazahmed-10P commented Mar 4, 2025

I encountered a similar issue while migrating to springdoc-openapi-starter-webmvc-ui=2.8.5, which includes jackson-databind=2.18.2, and I also have jackson-module-kotlin=2.18.2 in my project.

Everything was working fine, using old version of springdoc-openapi-starter-webmvc-ui=2.7.0 with jackson-module-kotlin=2.17.3 & jackson-databind=2.17.3.

Caused by: kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Unknown origin of public abstract operator fun invoke(p1: P1, p2: P2, p3: P3): R defined in kotlin.Function3[FunctionInvokeDescriptor@6cf414f9] (class kotlin.reflect.jvm.internal.impl.builtins.functions.FunctionInvokeDescriptor) at kotlin.reflect.jvm.internal.RuntimeTypeMapper.mapSignature(RuntimeTypeMapper.kt:226) at kotlin.reflect.jvm.internal.KFunctionImpl.<init>(KFunctionImpl.kt:52) at kotlin.reflect.jvm.internal.CreateKCallableVisitor.visitFunctionDescriptor(util.kt:314) at kotlin.reflect.jvm.internal.CreateKCallableVisitor.visitFunctionDescriptor(util.kt:291) at kotlin.reflect.jvm.internal.impl.descriptors.impl.FunctionDescriptorImpl.accept(FunctionDescriptorImpl.java:826) at kotlin.reflect.jvm.internal.KDeclarationContainerImpl.getMembers(KDeclarationContainerImpl.kt:62) at kotlin.reflect.jvm.internal.KClassImpl$Data$declaredNonStaticMembers$2.invoke(KClassImpl.kt:173) at kotlin.reflect.jvm.internal.KClassImpl$Data$declaredNonStaticMembers$2.invoke(KClassImpl.kt:173) at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:70) at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) at kotlin.reflect.jvm.internal.KClassImpl$Data.getDeclaredNonStaticMembers(KClassImpl.kt:173) at kotlin.reflect.jvm.internal.KClassImpl$Data$allNonStaticMembers$2.invoke(KClassImpl.kt:182) at kotlin.reflect.jvm.internal.KClassImpl$Data$allNonStaticMembers$2.invoke(KClassImpl.kt:182) at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:70) at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) at kotlin.reflect.jvm.internal.KClassImpl$Data.getAllNonStaticMembers(KClassImpl.kt:182) at kotlin.reflect.full.KClasses.getMemberProperties(KClasses.kt:148) at com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector.findDefaultCreator(KotlinNamesAnnotationIntrospector.kt:99) at com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair.findDefaultCreator(AnnotationIntrospectorPair.java:746) at com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair.findDefaultCreator(AnnotationIntrospectorPair.java:746) at com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair.findDefaultCreator(AnnotationIntrospectorPair.java:746) at com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair.findDefaultCreator(AnnotationIntrospectorPair.java:746) at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector._addCreators(POJOPropertiesCollector.java:669) at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.collectAll(POJOPropertiesCollector.java:451) at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.getPropertyMap(POJOPropertiesCollector.java:389) at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.getProperties(POJOPropertiesCollector.java:222) at com.fasterxml.jackson.databind.introspect.BasicBeanDescription._properties(BasicBeanDescription.java:161) at com.fasterxml.jackson.databind.introspect.BasicBeanDescription.findProperties(BasicBeanDescription.java:236) at org.springdoc.core.converters.PolymorphicModelConverter.resolve(PolymorphicModelConverter.java:126) at org.springdoc.core.converters.OAS31ModelConverter.resolve(OAS31ModelConverter.java:49) at org.springdoc.core.customizers.KotlinDeprecatedPropertyCustomizer.resolve(KotlinDeprecatedPropertyCustomizer.kt:55)

@cowtowncoder
Copy link
Member

Quick note: 2.18.3 is now out.

@mattelacchiato
Copy link

mattelacchiato commented Mar 20, 2025

2.18.3 has still this issue. Here is a simple test, which works fine on 2.17.3, but not on 2.18.3:

import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class JsonParserIssueTest {

    data class MeasuredValue(
        @JsonValue val value: Float,
    )

    data class Root(
        val foo: MeasuredValue,
    )

    @Test
    fun `jackson issue demo`() {
        // simple test if https://github.com/FasterXML/jackson-module-kotlin/issues/846 is fixed
        val message = """{"foo":1.234}"""
        assertThat(jacksonObjectMapper().readValue<Root>(message).foo).isEqualTo(MeasuredValue(1.234f))
    }
}

Error message is:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of de.snh.it.emobility.mdc.mqtt.JsonParserIssueTest$Root (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION disabled); line: 1, column: 2]

@cowtowncoder
Copy link
Member

This looks like it should work as expected: Root with PROPERTIES-based constructor (with property "foo"), and MeasuredValue with DELEGATING constructor due to existence of @JsonValue annotation.

Not sure if it was possible to create Java-only reproduction to see if it is jackson-databind that fails introspection.

As a temp work-around, adding @JsonProperty for foo might work around the issue.

@zckoh
Copy link

zckoh commented Apr 1, 2025

This seems to be breaking for @JsonSubTypes classes too, specifically where there is a private variable value which is of a function type and it has @JsonIgnore added to it, it seems like @JsonIgnore is not ignoring that property for deserialization.

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type",
)
@JsonSubTypes(
    JsonSubTypes.Type(value = Banana::class, name = "banana"),
    JsonSubTypes.Type(value = Apple::class, name = "apple"),
)
sealed class Fruit(
    @JsonIgnore
    private val accessor: (Map<String, String>) -> String,
) {
    fun accessValue(input: Map<String, String>): String? = accessor.invoke(input)
}

object Banana : Fruit({ it.getOrDefault("color", "") })

object Apple : Fruit({ it.getOrDefault("color", "") })


class ExampleTest : FunSpec({
    context("test example") {
        val objectMapper = ObjectMapper().registerKotlinModule()
        val jsonString = """{ "type": "banana" }""".trimIndent()
        val deserializedKey = objectMapper.readValue(jsonString, Fruit::class.java)
        deserializedKey::class shouldBe Banana::class // <== expect to deserialize into correct subtype class
    }
})

The above test passes with jackson 2.17.2 but fails with the following error in 2.18.3:

Unknown origin of public abstract operator fun invoke(p1: P1): R defined in kotlin.Function1[FunctionInvokeDescriptor@78007060] (class kotlin.reflect.jvm.internal.impl.builtins.functions.FunctionInvokeDescriptor)
kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Unknown origin of public abstract operator fun invoke(p1: P1): R defined in kotlin.Function1[FunctionInvokeDescriptor@78007060] (class kotlin.reflect.jvm.internal.impl.builtins.functions.FunctionInvokeDescriptor)
	at kotlin.reflect.jvm.internal.RuntimeTypeMapper.mapSignature(RuntimeTypeMapper.kt:226)
	at kotlin.reflect.jvm.internal.KFunctionImpl.<init>(KFunctionImpl.kt:55)
	at kotlin.reflect.jvm.internal.CreateKCallableVisitor.visitFunctionDescriptor(util.kt:314)
	at kotlin.reflect.jvm.internal.CreateKCallableVisitor.visitFunctionDescriptor(util.kt:291)
	at kotlin.reflect.jvm.internal.impl.descriptors.impl.FunctionDescriptorImpl.accept(FunctionDescriptorImpl.java:826)
	at kotlin.reflect.jvm.internal.KDeclarationContainerImpl.getMembers(KDeclarationContainerImpl.kt:62)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.declaredNonStaticMembers_delegate$lambda$22(KClassImpl.kt:173)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.accessor$KClassImpl$Data$lambda10(KClassImpl.kt)
	at kotlin.reflect.jvm.internal.KClassImpl$Data$$Lambda$10.invoke(Unknown Source)
	at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:70)
	at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.getDeclaredNonStaticMembers(KClassImpl.kt:173)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.allNonStaticMembers_delegate$lambda$26(KClassImpl.kt:182)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.accessor$KClassImpl$Data$lambda14(KClassImpl.kt)
	at kotlin.reflect.jvm.internal.KClassImpl$Data$$Lambda$14.invoke(Unknown Source)
	at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:70)
	at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32)
	at kotlin.reflect.jvm.internal.KClassImpl$Data.getAllNonStaticMembers(KClassImpl.kt:182)
	at kotlin.reflect.full.KClasses.getMemberProperties(KClasses.kt:148)
	at com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector.findDefaultCreator(KotlinNamesAnnotationIntrospector.kt:99)

To workaround this, I had to add a custom noop deserializer and use it as the custom deserializer for this ignored property. This passes with both jackson 2.17.2 and 2.18.3:

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type",
)
@JsonSubTypes(
    JsonSubTypes.Type(value = Banana::class, name = "banana"),
    JsonSubTypes.Type(value = Apple::class, name = "apple"),
)
sealed class Fruit(
    @JsonIgnore
    @JsonDeserialize(using = AccessorDeserializer::class) // <== This is the custom no-op deserializer
    private val accessor: (Map<String, String>) -> String,
) {
    fun accessValue(input: Map<String, String>): String? = accessor.invoke(input)
}

object Banana : Fruit({ it.getOrDefault("color", "") })

object Apple : Fruit({ it.getOrDefault("color", "") })


class ExampleTest : FunSpec({
    context("test example") {
        val objectMapper = ObjectMapper().registerKotlinModule()
        val jsonString = """{ "type": "banana" }""".trimIndent()
        val deserializedKey = objectMapper.readValue(jsonString, Fruit::class.java)
        deserializedKey::class shouldBe Banana::class // <== expect to deserialize into correct subtype class
    }
})

class AccessorDeserializer : JsonDeserializer<(Map<String, String>) -> String>() {  // <== custom no-op deserializer
    override fun deserialize(
        p: JsonParser,
        ctxt: DeserializationContext,
    ): (Map<String, String>) -> String = { "" }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants