Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson Kotlin Duration Serialization Problem

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

like image 577
Muhammad Khosravi Avatar asked Jun 20 '26 13:06

Muhammad Khosravi


1 Answers

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:

  1. The size of the duration (like 1_000_000_000 for one second).

  2. The unit it’s stored in (nanoseconds or milliseconds).

It does this by using the last bit (LSB) of the number as a flag:

  1. If the last bit is 0 -> the rest of the number means nanoseconds.

  2. 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).


f.e 1.second

1. 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.

like image 109
aran Avatar answered Jun 23 '26 04:06

aran



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!