I came across a weird situation where Jackson serializes kotlin.time.Duration in a way I cannot make sense from. For example in this code:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlin.time.DurationUnit
import kotlin.time.toDuration
val x = 1.toDuration(DurationUnit.SECONDS)
val mapper = jacksonObjectMapper()
val y = mapper.writeValueAsString(x)!!
println(y)
results in:
2000000000
Which is exactly 2 seconds, in nanoseconds.
How does this happen?
Kotlin version: 1.9.21
Jackson version: 2.15.2
The issue is that Kotlin doesn’t keep Duration as “just a number of nanoseconds.” Instead, it squeezes two pieces of information into one Long:
The size of the duration (like 1_000_000_000 for one second).
The unit it’s stored in (nanoseconds or milliseconds).
It does this by using the last bit (LSB) of the number as a flag:
If the last bit is 0 -> the rest of the number means nanoseconds.
If the last bit is 1 -> the rest means milliseconds.
To make room for that flag, it shifts the actual number left by one:
binary number << 1
(which is the same as multiplying by 2).
1.second1. Convert to nanoseconds:
1_000_000_000 (decimal)
= 111011100110101100101000000000 (binary)
2. Apply the encoding rule (<< 1):
111011100110101100101000000000 << 1
= 1110111001101011001010000000000
3. Convert it to decimal:
2_000_000_000
So the hidden Long inside Duration for 1.second is actually 2_000_000_000.
What's the issue? Well, Jackson doesn’t know about Kotlin’s encoding or that it should call toString() or inWholeNanoSeconds(): It just serializes the backing field directly, and that’s why you see 2000000000.
It’s not really “2 seconds”: it’s just the encoded form of 1 second.
To make Jackson output a human-friendly Duration instead of the raw encoded Long, you could use java.time.Duration:
val javaDur: java.time.Duration = 1.seconds.toJavaDuration()
println(mapper.writeValueAsString(javaDur)) // 1PTS - 1000000000
Or use a serializer:
class DurationNanosSerializer : JsonSerializer<Duration>() {
override fun serialize(
value: Duration,
gen: JsonGenerator,
serializers: SerializerProvider
) {
gen.writeNumber(value.inWholeNanoseconds) // 1000000000
}
}
Both approaches stop Jackson from dumping the raw backing Long and instead give it instructions on what to output, that's it, 1 second.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With