如何在 Kotlinx 序列化中对 BigDecimal 和 BigInteger 进行 JSON 编码而不丢失精度?

aSe*_*emy 6 serialization json bigdecimal kotlin kotlinx.serialization

我正在使用 Kotlin/JVM 1.8.0 和 Kotlinx 序列化 1.4.1。

我需要将 ajava.math.BigDecimal和编码java.math.BigInteger为 JSON。

我正在使用BigDecimaland ,BigInteger因为我想要编码的值可能大于 aDouble可以容纳的值,而且我还想避免浮点精度的错误。我不想将数字编码为字符串,因为 JSON 是由其他程序读取的,因此它需要正确。

JSON 规范对数字的长度没有限制,所以应该是可能的。

当我尝试直接使用BigDecimaland时BigInteger,出现错误

import java.math.*
import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class FooNumbers(
  val decimal: BigDecimal,
  val integer: BigInteger,
)
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

BigDecimal我尝试为and BigIntegertypealiases 为了方便起见)创建自定义序列化器,但是因为这些使用toDouble()并且toLong()它们失去了精度!

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())
}
Run Code Online (Sandbox Code Playgroud)

当我对示例实例进行编码和解码时,会返回不同的结果。

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)
}
Run Code Online (Sandbox Code Playgroud)

失败require(...)

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)
Run Code Online (Sandbox Code Playgroud)

aSe*_*emy 9

Kotlinx Serialization 1.5.0中可以对原始 JSON 进行编码,该版本于 2023 年 2 月 24 日发布,并且处于实验阶段。在早期版本中这是不可能的。

tl:dr:跳至此答案底部的“完整示例”

解码使用JsonDecoder

请注意,只有编码需要解决方法 - 解码,BigDecimal并且BigInteger只要JsonDecoder使用即可直接工作!

private object BigDecimalSerializer : KSerializer<BigDecimal> {

  // ...

  override fun deserialize(decoder: Decoder): BigDecimal =
    when (decoder) {
      // must use decodeJsonElement() to get the value, and then convert it to a BigDecimal
      is JsonDecoder -> decoder.decodeJsonElement().jsonPrimitive.content.toBigDecimal()
      else -> decoder.decodeString().toBigDecimal()
    }
}
Run Code Online (Sandbox Code Playgroud)

编码使用JsonUnquotedLiteral

JsonUnquotedLiteral()为了进行编码,在编码 JSON 时必须使用新函数。

private object BigDecimalSerializer : KSerializer<BigDecimal> {

  // ...

  override fun serialize(encoder: Encoder, value: BigDecimal) =
    when (encoder) {
      // use JsonUnquotedLiteral() to encode the BigDecimal literally
      is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.toPlainString()))
      else -> encoder.encodeString(value.toPlainString())
    }
}
Run Code Online (Sandbox Code Playgroud)

使用类型别名的全局配置

Kotlinx Serialization 用于typealias定义全局可用的序​​列化策略。让我们做同样的事情BigDecimal

typealias BigDecimalJson = @Serializable(with = BigDecimalSerializer::class) BigDecimal
Run Code Online (Sandbox Code Playgroud)

用法示例

创建序列化器后,typealias可以使用 esFooNumber自动使用KSerializers.

@Serializable
data class FooNumbers(
  val decimal: BigDecimalJson,
  val integer: BigIntegerJson,
)
Run Code Online (Sandbox Code Playgroud)

实际的主要功能没有改变——和以前一样。

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)
}
Run Code Online (Sandbox Code Playgroud)

现在BigDecimalBigInteger可以被精确地编码和解码,不会损失精度!

FooNumbers(decimal=0.1234567890123456789012345678901234567890, integer=9876543210987654321098765432109876543210)
{"decimal":0.1234567890123456789012345678901234567890,"integer":9876543210987654321098765432109876543210}
FooNumbers(decimal=0.1234567890123456789012345678901234567890, integer=9876543210987654321098765432109876543210)
Run Code Online (Sandbox Code Playgroud)

完整示例

这是完整的代码:

import java.math.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.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)
}

typealias BigDecimalJson = @Serializable(with = BigDecimalSerializer::class) BigDecimal

@OptIn(ExperimentalSerializationApi::class)
private object BigDecimalSerializer : KSerializer<BigDecimal> {

  override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE)

  /**
   * If decoding JSON uses [JsonDecoder.decodeJsonElement] to get the raw content,
   * otherwise decodes using [Decoder.decodeString].
   */
  override fun deserialize(decoder: Decoder): BigDecimal =
    when (decoder) {
      is JsonDecoder -> decoder.decodeJsonElement().jsonPrimitive.content.toBigDecimal()
      else           -> decoder.decodeString().toBigDecimal()
    }

  /**
   * If encoding JSON uses [JsonUnquotedLiteral] to encode the exact [BigDecimal] value.
   *
   * Otherwise, [value] is encoded using encodes using [Encoder.encodeString].
   */
  override fun serialize(encoder: Encoder, value: BigDecimal) =
    when (encoder) {
      is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.toPlainString()))
      else           -> encoder.encodeString(value.toPlainString())
    }
}

typealias BigIntegerJson = @Serializable(with = BigIntegerSerializer::class) BigInteger

@OptIn(ExperimentalSerializationApi::class)
private object BigIntegerSerializer : KSerializer<BigInteger> {

  override val descriptor = PrimitiveSerialDescriptor("java.math.BigInteger", PrimitiveKind.LONG)

  /**
   * If decoding JSON uses [JsonDecoder.decodeJsonElement] to get the raw content,
   * otherwise decodes using [Decoder.decodeString].
   */
  override fun deserialize(decoder: Decoder): BigInteger =
    when (decoder) {
      is JsonDecoder -> decoder.decodeJsonElement().jsonPrimitive.content.toBigInteger()
      else           -> decoder.decodeString().toBigInteger()
    }

  /**
   * If encoding JSON uses [JsonUnquotedLiteral] to encode the exact [BigInteger] value.
   *
   * Otherwise, [value] is encoded using encodes using [Encoder.encodeString].
   */
  override fun serialize(encoder: Encoder, value: BigInteger) =
    when (encoder) {
      is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.toString()))
      else           -> encoder.encodeString(value.toString())
    }
}
Run Code Online (Sandbox Code Playgroud)