Cannot deserialize wrapper for simple types if their value property is public
Describe the bug
When using single-property BigDecimal / Double (and most likely any other) wrapper (data) classes to deserialize JSON values into, that property needs to be declared private because otherwise an exception is thrown.
To Reproduce
Simple Example: { "foo": 42.123 }
This:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.math.BigDecimal
data class DtoWithPublicProperty(
val foo: WrapperWithPublicValue
)
data class WrapperWithPublicValue(val value: BigDecimal)
val objectMapper = jacksonObjectMapper()
val json = """{"foo":42.123}"""
objectMapper.readValue<DtoWithPublicProperty>(json)
Throws an exception like: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of [..] WrapperWithPublicValue (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (42.123) at [Source: (String)"{"foo":42.123}"; line: 1, column: 8]
Changing it to:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import java.math.BigDecimal
data class DtoWithPrivateProperty(
val foo: WrapperWithPrivateValue
)
data class WrapperWithPrivateValue(private val value: BigDecimal) {
fun toBigDecimal() = value
}
val objectMapper = jacksonObjectMapper()
val json = """{"foo":42.123}"""
objectMapper.readValue<DtoWithPrivateProperty>(json)
Works as expected without an exception.
Expected behavior It should not matter if the property is public or private when deserializing JSON into more complex object structures.
Versions Kotlin: 1.4.31 (and 1.3.x) Jackson-module-kotlin: 2.12.2 (and 2.11.x) Jackson-databind: 2.12.2 (and 2.11.x)
Additional context
- Does not fix the problem:
data class WrapperWithPublicValue @JsonCreator constructor(val value: BigDecimal)data class WrapperWithPublicValue(@JsonIgnore val value: BigDecimal)- Changing the wrapped data type to
Doubleor adding additional constructors / static creator functions with eitherBigDecimalorDouble
- Most unexpectedly the explicit
@JsonCreatordoes not help at all. - Workaround - actively ignoring the logical getter of the property:
data class WrapperWithPublicValue(@get:JsonIgnore val value: BigDecimal)
- Using simple classes instead of data classes does not change anything.
I created a small project with tests to reproduce the issue: https://github.com/slu-it/bug-jackson-kotlin-wrapper-with-public-param/blob/main/src/test/kotlin/example/IssueTests.kt
good lord kotlin is a horrid looking language, it just looks terrible xD
Change from field to getter/setter - @JsonAutoDetect
@slu-it I suspect this is due to 1-argument constructors being ambiguous by nature: they can either be "delegating" (in which the whole incoming JSON value must directly map to type of the one argument) or "property-based" (in which the incoming JSON must be Object, with 1 property of which name and type match the one constructor argument).
With Kotlin data classes most of the time users want properties-based. So plain @JsonCreator is of no help, but you want
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
OR explicitly name the argument/property with @JsonProperty("value") (which is a hint that it must be properties-based, given explicit annotation).
And Jackson 2.12 offers one more even easier way to default to this:
https://cowtowncoder.medium.com/jackson-2-12-most-wanted-3-5-246624e2d3d0
HTH.
Thanks for the response @cowtowncoder! I experimented a bit from the link you suggested:
val objectMapper = jacksonObjectMapper()
.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
Produces the same exception, but this time for public and private properties - so this is actually worse than before:
Cannot construct instance of `example.WrapperWithPublicValue` (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (42.123)
at [Source: (String)"{"foo":42.123}"; line: 1, column: 8] (through reference chain: example.DtoWithPublicPropery["foo"])
Changing it to:
val objectMapper = jacksonObjectMapper()
.setConstructorDetector(ConstructorDetector.USE_DELEGATING)
Produces a new exception:
Cannot deserialize value of type `java.math.BigDecimal` from Object value (token `JsonToken.FIELD_NAME`)
at [Source: (String)"{"foo":42.123}"; line: 1, column: 2]
Not sure what that's about though ...
Trying with your suggestion:
data class WrapperWithPublicValue @JsonCreator(mode = Mode.PROPERTIES) constructor(val value: BigDecimal)
Also throws an exception, equal to what happens if the @JsonCreator is not there at all:
Cannot construct instance of `example.WrapperWithPublicValue` (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (42.123)
at [Source: (String)"{"foo":42.123}"; line: 1, column: 8] (through reference chain: example.DtoWithPublicPropery["foo"])
What actually works is your suggestion, but with Mode.DELEGATING:
data class WrapperWithPublicValue @JsonCreator(mode = Mode.DELEGATING) constructor(val value: BigDecimal)
Ah. Looking at your example it is rather.... complicated. The issue is that you actually want to use TWO levels of constructors, so BOTH delegating AND properties, at different levels. I should have read the example. Given this, defaulting for one mode can only reduce annotations but not eliminate (since it only gets it right for 50% of cases :) ).
At outer level (DtoWithPublicProperty) you want properties style to map a value to foo property, but that value then delegated to constructor-argument of WrapperWithPublicValue.
So you likely need to annotate one or both of value definitions to achieve that.
My advice on mode = PROPERTIES (or @JsonProperty) would be for DtoWithPublicProperty; and then mode = DELEGATING for WrapperWithPublicValue. It is possible you only need one of those, if you wanted to minimize annotations.
I think I understand now. I assumed wrongly, based on that a private field works, that Jackson's general strategy for deciding how to create an instance when deserializing is based mainly on whats in the JSON and choosing the "best" available option given the target type. But is might actually be more focused on the target type when trying to decide how to fit the JSON than I thought.
What I assumed was:
Given the example JSON { "foo": 42.123 } and the above data structure, Jackson identifies that foo is a JSON number value and the target type is a WrapperWithPublicValue that has a single argument constructor with a compatible BigDecimal data type and uses that, since there are no explicit @JsonCreator factories. I thought properties would only be considered if the JSON would have been something like { "foo": { "value": 42.123} }.
So if this is not actually a bug, I think you can close the issue. Unless you want to take this as input for potential changes :)
For future people finding this on Google:
So if someone wants to have as few annotations as possible - in cases like described by my example - it might actually be best to go with a private properties and a toXXX() function for the wrapped type. Otherwise there are plenty of options in previous comments.
(My reason for doing it this way, is that I want to define "Domain Primitive" value types that are validated against functional constraints (min/max, length, illegal characters etc.) and simply use them everywhere throughout the application - from DTOs to business objects. That's why Id like as few annotations as possible - keep "technical" aspects as far away as possible from my types.)
I ran into a similar issue with wrapper types that cannot be deserialized Cannot construct instance ... (although at least one Creator exists). I'm not sure if this is related, but since the ticket is still open it might be. I have created a minimal test class to reproduce the issue: https://gist.github.com/ciderale/4ebca49ee687b03c269e91dd9fb5501c
The linked example looks like a breaking change in 2.12, but I couldn't find anything in the release notes. The example works with jackson 2.11 but throws an exception in 2.12 onwards. Interestingly, the test does work if the kotlin module is not registered (which obviously is not an option in more complex scenarios). Might this be somehow related to https://github.com/FasterXML/jackson-databind/issues/2962 ?