jackson-module-kotlin
jackson-module-kotlin copied to clipboard
Mutator inference during deserialization doesn't work for Kotlin (data) objects
Search before asking
- [X] I searched in the issues and found nothing similar.
- [X] I searched in the issues of databind and other modules used and found nothing similar.
- [X] I have confirmed that the problem only occurs when using Kotlin.
Describe the bug
If I have a data class with a property defined in the body, deserializing works fine.
If I have a data object with a property defined in the body, deserializing throws an exception when deserializing due to an unrecognized field.
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "int" (class Second), not marked as ignorable (0 known properties: ])
I think this works fine for classes due to MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS and MapperFeature.INFER_PROPERTY_MUTATORS (both default to true), but with Kotlin objects it doesn't work.
To Reproduce
data class First(
val foo: String,
) {
val int: Int = 1
}
data object Second {
val int: Int = 2
}
fun main() {
val mapper = jacksonObjectMapper()
println(mapper.writeValueAsString(First("bar"))) // {"foo":"bar","int":1}
println(mapper.writeValueAsString(Second)) // {"int":2}
println(mapper.readValue<First>("{\"foo\":\"bar\",\"int\":1}")) // First(foo=bar)
println(mapper.readValue<Second>("{\"int\":2}")) // THROWS!
}
Expected behavior
I expect no exception to be thrown in the above example.
Versions
Kotlin: 2.0.0 Jackson-module-kotlin: 2.17.2 Jackson-databind: 2.17.2
Additional context
No response
I just verified that this issue also exists when using Kotlin 1.9.25.
@hudson155 Sorry for the delay in replying.
I have checked and this appears to be normal behavior for Jackson.
When deserializing a class, if an unrequested property exists on JSON, it fails by default.
Enabling FAIL_ON_UNKNOWN_PROPERTIES will suppress this problem.
https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/latest/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES
package com.fasterxml.jackson.module.kotlin
import com.fasterxml.jackson.databind.DeserializationFeature
data object Second {
val int: Int = 2
}
fun main() {
val mapper = jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
println(mapper.readValue<Second>("{\"int\":2}")) // -> Second
}
Closed as it appears to have been resolved.
It's not resolved @k163377. The bug still exists
See the tests here: https://github.com/hudson155/kairo/blob/main/kairo-serialization/src/test/kotlin/kairo/serialization/module/PolymorphismObjectMapperTest.kt
When I have these classes
internal sealed class Vehicle {
abstract val model: String?
abstract val wheels: Int
internal data class Car(
override val model: String,
val plate: String,
val capacity: Int,
) : Vehicle() {
override val wheels: Int = 4
}
internal data class Motorcycle(
val plate: String,
) : Vehicle() {
override val model: Nothing? = null
override val wheels: Int = 2
}
internal data object Bicycle : Vehicle() {
override val model: Nothing? = null
override val wheels: Int = 2
}
}
These tests pass for the data classes
@Test
fun `deserialize, car`(): Unit = runTest {
val string = "{\"type\": \"Car\", \"capacity\": 5, \"plate\": \"ABC 1234\", \"model\": \"Ford\", \"wheels\": 2}"
val vehicle = Vehicle.Car(model = "Ford", plate = "ABC 1234", capacity = 5)
mapper.readValue<Vehicle.Car>(string).shouldBe(vehicle)
mapper.readValue<Vehicle>(string).shouldBe(vehicle)
}
@Test
fun `deserialize, motorcycle`(): Unit = runTest {
val string = "{\"type\": \"Motorcycle\", \"plate\": \"MVM 12\", \"model\": null, \"wheels\": 2}"
val vehicle = Vehicle.Motorcycle(plate = "MVM 12")
mapper.readValue<Vehicle.Motorcycle>(string).shouldBe(vehicle)
mapper.readValue<Vehicle>(string).shouldBe(vehicle)
}
but this test does NOT pass for the data object, which represents this bug.
@Test
fun `deserialize, bicycle`(): Unit = runTest {
val vehicle = Vehicle.Bicycle
val string = "{\"type\": \"Bicycle\", \"model\": null, \"wheels\": 2}"
mapper.readValue<Vehicle.Bicycle>(string).shouldBe(vehicle)
mapper.readValue<Vehicle>(string).shouldBe(vehicle)
}
@hudson155
Please submit a minimum reproduction, including mapper settings, etc.
Also, tests should be written in JUnit5 if possible.
Here's a minimum reproduction
data class DataClass(val foo: String) {
val extra: Int = 42
}
data object DataObject {
val extra: Int = 42
}
fun main() {
val mapper = jacksonObjectMapper()
mapper.readValue<DataClass>("{ \"foo\": \"bar\", \"extra\": 42 } ") // works!
mapper.readValue<DataObject>("{ \"extra\": 42 } ") // should work, but throws
}
@hudson155
As I replied to you first, try FAIL_ON_UNKNOWN_PROPERTIES.
val mapper = jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
Right, with that workaround it won't fail deserialization ✅
It's correct that this bug only exists when FAIL_ON_UNKNOWN_PROPERTIES is turned on (default state), and that the bug is covered up by disabling the flag.
However, for use cases like mine where failing on unknown properties is in fact the desired behaviour, this workaround isn't a permanent alternative to a bug fix.
With POJOs and Kotlin data classes, disabling FAIL_ON_UNKNOWN_PROPERTIES is not required in order to successfully deserialize the same string that was serialized, which includes properties defined on the class but not in the constructor.
The data class in my example serializes to { \"foo\": \"bar\", \"extra\": 42 }, which can be deserialized just fine regardless of the user's choice on whether to ignore unknown properties, since although "extra" is not part of the constructor, Jackson recognizes that it exists as a property on the class and therefore isn't considered "unknown".
data class DataClass(val foo: String) {
val extra: Int = 42
}
Trying to deserialize that same string to the data class if it were defined without "extra" would fail, which is expected since the property is now unknown.
data class DataClass(val foo: String)
While this behaviour works for data classes and POJOs, it's broken for Kotlin data objects. Trying to deserialize the string { "asdf": 42 } to the data object from my example should only work with the FAIL_ON_UNKNOWN_PROPERTIES flag disabled, because "asdf" is unknown. But trying to deserialize the string { "extra": 42 } should not fail either way. It's not an unknown property, since it's defined on the class.