In my project, I used to have a data class
similar to this:
data class SampleClassThatContainsALength(val length: Double)
Using jackson-databind
, SampleClassThatContainsALength(0.025)
would serialize to {"length":0.025}
.
Throughout the project, lengths were implicitly assumed to be in meters, but to reduce ambiguities, I implemented a Length
class, which takes care of units:
data class Length(val m: Double) : Comparable<Length> {
@get:JsonIgnore
val km: Double get() = m / 1e3
@get:JsonIgnore
val cm: Double get() = m / 1e-2
@get:JsonIgnore
val mm: Double get() = m / 1e-3
// More conversions to other units...
operator fun plus(other: Length) = (this.m + other.m).m
operator fun minus(other: Length) = (this.m - other.m).m
operator fun unaryMinus() = (-this.m).m
// More operator implementations...
}
val Number.km get() = Length(this.toDouble() * 1e3)
val Number.m get() = Length(this.toDouble())
val Number.cm get() = Length(this.toDouble() * 1e-2)
val Number.mm get() = Length(this.toDouble() * 1e-3)
// More extension functions for other units...
The data class thus looks like this now:
data class SampleClassThatContainsALength(val length: Length)
And the example object SampleClassThatContainsALength(0.025.m)
serializes to {"length":{"m":0.025}}
.
I now have the issue that I want to use the new serialization, as it clearly states the unit, but I still have some old JSON-documents (similar to the serialized JSON at the beginning) which I would like to convert implicitly to the new implementation.
So, in summary, I want both, {"length":0.025}
and {"length":{"m":0.025}}
to deserialize to SampleClassThatContainsALength(0.025.m)
.
Right now, the deserialization of {"length":{"m":0.025}}
works as expected, but the deserialization of {"length":0.025}
fails with
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `de.uni_freiburg.inatech.streem.image_converter.common.math.units.Length` (although at least one Creator exists): no double/Double-argument constructor/factory method to deserialize from Number value (0.025)
at [Source: (String)"{"length":0.025}"; line: 1, column: 11] (through reference chain: de.uni_freiburg.inatech.streem.image_converter.data.json.SampleClassThatContainsALength["length"])
Here's what I have attempted so far:
companion object
in the Length
class and added the following factory method:companion object {
@JsonCreator
@JvmStatic
fun createFromNumber(number: Number): Length = Length(number.toDouble())
}
This causes the deserialization of {"length":0.025}
to work, but now the deserialization of the new format no-longer works:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.Number` from Object value (token `JsonToken.FIELD_NAME`)
at [Source: (byte[])[19 bytes]; byte offset: #7]
Length
with @JsonCreator
like so:data class Length @JsonCreator constructor(val m: Double) : Comparable<Length> {
but the results were exactly the same as with attempt 1.
Number
(instead of a Double
):data class Length(val m: Double) : Comparable<Length> {
constructor(m: Number) : this(m.toDouble())
While the new format was now working, the old format was still broken.
Wow, that went faster than I thought, and I'm a little ashamed I haven't found this before posting. Nevertheless, here's the solution for anyone with the same problem:
// No annotation and no secondary constructor necessary
data class Length(val m: Double) : Comparable<Length> {
// ...
companion object {
// Handles the case {"length":{"m":0.025}}
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
@JvmStatic
fun createFromObject(m: Double): Length = Length(m)
// Handles the case {"length":0.025}
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
@JvmStatic
fun createFromNumber(m: String): Length = Length(m.toDouble())
}
Note that createFromNumber
now takes a String
and not a Number
or Double
, which seems to be important.
Here's what inspired me to do that: