I'm using Kotlin/JVM 1.8.0 and Kotlinx Serialization 1.4.1.
I need to encode a java.math.BigDecimal
and java.math.BigInteger
to JSON.
I'm using BigDecimal
and BigInteger
because the values I want to encode can be larger than a Double
can hold, and also I want to avoid errors with floating-point precision. I don't want to encode the numbers as strings because JSON is read by other programs, so it needs to be correct.
The JSON spec places no restriction on the length of numbers, so it should be possible.
When I try and use BigDecimal
and BigInteger
directly, I get an error
import java.math.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class FooNumbers(
val decimal: BigDecimal,
val integer: BigInteger,
)
Serializer has not been found for type 'BigDecimal'. To use context serializer as fallback, explicitly annotate type or property with @Contextual
Serializer has not been found for type 'BigInteger'. To use context serializer as fallback, explicitly annotate type or property with @Contextual
I tried creating custom serializers for BigDecimal
and BigInteger
(and typealias
es for convenience), but because these use toDouble()
and toLong()
they lose precision!
typealias BigDecimalJson = @Serializable(with = BigDecimalSerializer::class) BigDecimal
private object BigDecimalSerializer : KSerializer<BigDecimal> {
override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE)
override fun deserialize(decoder: Decoder): BigDecimal =
decoder.decodeDouble().toBigDecimal()
override fun serialize(encoder: Encoder, value: BigDecimal) =
encoder.encodeDouble(value.toDouble())
}
typealias BigIntegerJson = @Serializable(with = BigIntegerSerializer::class) BigInteger
private object BigIntegerSerializer : KSerializer<BigInteger> {
override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): BigInteger =
decoder.decodeLong().toBigInteger()
override fun serialize(encoder: Encoder, value: BigInteger) =
encoder.encodeLong(value.toLong())
}
When I encode and decode an example instance, a different result is returned.
import java.math.*
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
@Serializable
data class FooNumbers(
val decimal: BigDecimalJson,
val integer: BigIntegerJson,
)
fun main() {
val fooDecimal = BigDecimal("0.1234567890123456789012345678901234567890")
val fooInteger = BigInteger("9876543210987654321098765432109876543210")
val fooNumbers = FooNumbers(fooDecimal, fooInteger)
println("$fooNumbers")
val encodedNumbers = Json.encodeToString(fooNumbers)
println(encodedNumbers)
val decodedFooNumbers = Json.decodeFromString<FooNumbers>(encodedNumbers)
println("$decodedFooNumbers")
require(decodedFooNumbers == fooNumbers)
}
The require(...)
fails:
FooNumbers(decimal=0.1234567890123456789012345678901234567890, integer=9876543210987654321098765432109876543210)
{"decimal":0.12345678901234568,"integer":1086983617567424234}
FooNumbers(decimal=0.12345678901234568, integer=1086983617567424234)
Exception in thread "main" java.lang.IllegalArgumentException: Failed requirement.
at MainKt.main(asd.kt:32)
at MainKt.main(asd.kt)