FasterXML / jackson-module-kotlin

Module that adds support for serialization/deserialization of Kotlin (http://kotlinlang.org) classes and data classes.
Apache License 2.0
1.11k stars 175 forks source link

Cannot use value class as map key in 2.17 #777

Open geirsagberg opened 4 months ago

geirsagberg commented 4 months ago

Search before asking

Describe the bug

Jackson 2.17 introduced support for deserializing value classes. However, using a value class as a key in a map still fails on deserialization.

To Reproduce

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

@JvmInline
value class Id(val value: Int)

data class Foo(val valuesById: Map<Id, String>)

val objectMapper = jacksonObjectMapper()

val serialized = objectMapper.writeValueAsString(Foo(mapOf(Id(1) to "one", Id(2) to "two")))

val deserialized = objectMapper.readValue(serialized, Foo::class.java)

Throws:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot find a (Map) Key deserializer for type...

Expected behavior

When deserializing a map Map<MyValueClass, Any> where MyValueClass wraps a supported primitive, the map should deserialize without throwing an error.

Versions

Kotlin: 1.9.22 Jackson-module-kotlin: 2.17.0 Jackson-databind: 2.17.0

Additional context

No response

k163377 commented 4 months ago

I am positively considering adding this feature. I have made a prototype at kogera for policy consideration. https://github.com/ProjectMapK/jackson-module-kogera/pull/224

It will be basically the same usability as deserialization of value class. The difference is that the wrapped type is also deserialized by the context-accessible KeyDeserializer.

However, I am busy and sick, so it may take some time until I can merge it into kotlin-module. I hope to complete it by May at the latest.


@cowtowncoder I have two questions about Jackson functionality.

I don't believe there is a JsonCreator like feature used to deserialize keys, is this correct?

Also, are there any caveats about searching for another KeyDeserializer from a KeyDeserializer? https://github.com/ProjectMapK/jackson-module-kogera/pull/224/files#diff-b03a6f91081da3aa22c53b161b0a2f6f1a0cf4ba318329907046b5f5335b17c8R72-R73

I am assuming that this is a very limited edge case, but I am assuming that there are nested types that require a special deserialization method. https://github.com/ProjectMapK/jackson-module-kogera/pull/224/files#diff-11c6e0b61e487e3914690655846ffd6100b92c022082ea610bb8d086f7be18dcR87-R116

junkdog commented 3 months ago

Just to clarify, previous versions of jackson don't exhibit errors when using value classes as keys in maps. As such, I think this should be labeled as a bug and not an enhancement.

k163377 commented 3 months ago

@junkdog A generic key deserializer for the value class is not currently provided by jackosn-module-kotlin. This means that Cannot find a (Map) Key deserializer should be thrown as for other classes.

It is still possible to deserialize a value class as a Map key by registering the appropriate key deserializer.

For these reasons, I interpret this as a feature request.

cowtowncoder commented 3 months ago

Maybe the difference here is wrt serialization (where "value.toString()" is used for "unknown" types) vs deserialization (where exception will be thrown)? Handling differs quite a bit b/w reading (deser) and writing (ser).

cowtowncoder commented 3 months ago

@cowtowncoder I have two questions about Jackson functionality.

I don't believe there is a JsonCreator like feature used to deserialize keys, is this correct?

No I don't think so; except for @JsonKey annotation... use of which might work via custom AnnotationIntrospector.

Also, are there any caveats about searching for another KeyDeserializer from a KeyDeserializer? https://github.com/ProjectMapK/jackson-module-kogera/pull/224/files#diff-b03a6f91081da3aa22c53b161b0a2f6f1a0cf4ba318329907046b5f5335b17c8R72-R73

Not sure; in general ideally would be resolved earlier and not during reading (in fact, it really should be done during construction/resolution for deserialization; sometimes not doable for serialization). But other than that I suspect it should be fine.

k163377 commented 2 months ago

except for @JsonKey annotation... use of which might work via custom AnnotationIntrospector.

Is this not a function for @JsonValue? I wanted to make sure there is no such thing as @JsonKeyCreator.

k163377 commented 2 months ago

I will not implement this feature in kotlin-module until the following issues are resolved, as it will override custom implementations defined by the user. https://github.com/FasterXML/jackson-databind/issues/4444

I am unable to work on a fix as my busy state has become rather worse. If anyone has a strong desire for this feature, I would appreciate it if you could fix it.

cowtowncoder commented 2 months ago

Is this not a function for @JsonValue? I wanted to make sure there is no such thing as @JsonKeyCreator

No, as name implies, it is for JSON values -- keys are specifically separate thing (from Jackson perspective), key names for java.util.Maps bound from JSON Object property names (vs property values). Part of the problem is that Property names are exposed as JsonToken.FIELD_NAME so regular value JsonDeserializer cannot be used; and thereby there is separate KeyDeserializer. At any rate, handling is quite separate and as such I don't see how @JsonValue could be used outside limited cases (like with maybe Enum type)

But there are also other relevant annotations: @JsonDeserialize(keyUsing = <KeyDeserializer class> and @JsonSerialize(keyUsing = <key serializer class> that can be used on class to mark handler(s).