当类型为整数而不是字符串时的多态 kotlinx 序列化

Bob*_*sky 5 kotlin kotlinx.serialization kotlinx

我正在尝试使用和发出包含多态项目列表的 JSON。问题是:这些项目包含type带有整数值(而不是字符串)的键。 API 端点生成并期望与此类似的 JSON:

{
  "startTime": "2022-07-27T13:32:57.379Z",
  "items": [
    {
      "type": 0,
      "results": "string",
      "restBetweenRounds": "string"
    },
    {
      "type": 1,
      "results": "string",
      "setCount": 0
    },
    {
      "type": 2,
      "items": [
        {
          "type": 0,
          "results": "string",
          "restBetweenRounds": "string"
        },
        {
          "type": 1,
          "results": "string",
          "setCount": 0
        }
      ],
      "results": "string"
    }
  ],
  "status": 0,
  "clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
Run Code Online (Sandbox Code Playgroud)

正如有关多态性的文章中所述,我创建了类的层次结构。我还尝试type在反序列化之前转换值。

object MyTransformingDeserializer : JsonTransformingSerializer<BaseItem>(PolymorphicSerializer(BaseItem::class)) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        val type = element.jsonObject["type"]!!
        val newType = JsonPrimitive(value = type.toString())
        return JsonObject(element.jsonObject.toMutableMap().also { it["type"] = newType })
    }
}


@Serializable(with = MyTransformingDeserializer::class)
sealed class BaseItem {
    abstract val type: String
}

@Serializable
@SerialName("0")
class ItemType0(
    override val type: String,
    // ...
) : BaseItem()


@Serializable
@SerialName("1")
class ItemType1(
    override val type: String,
    // ...
) : BaseItem()

@Serializable
@SerialName("2")
class ItemType2(
    override val type: String,
    // ...
) : BaseItem()
Run Code Online (Sandbox Code Playgroud)

但我得到的只是这个错误:

kotlinx.serialization.json.internal.JsonDecodingException:未找到类鉴别器“0”的多态序列化器

鉴于我无法更改 JSON 的格式,如何才能成功序列化/反序列化它?

aSe*_*emy 11

在 Kotlinx 序列化中处理多态性很困难,尤其是当您无法控制源格式时。但 KxS 确实提供了很多低级工具来手动处理几乎所有事情。

你已经很接近选择了JsonTransformingSerializer!看起来它在 KxS 选择序列化器之前不会转换 JSON。由于鉴别器只能是字符串,因此反序列化失败。

JsonContentPolymorphicSerializer

相反JsonTransformingSerializer,您可以使用JsonContentPolymorphicSerializer.

Kotlinx 序列化首先将 JSON 反序列化为JsonObject. 然后,它将将该对象提供给 的序列化器BaseItem,您可以解析并选择正确的子类。

import kotlinx.serialization.*
import kotlinx.serialization.json.*

object BaseItemSerializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
  override fun selectDeserializer(
    element: JsonElement
  ): DeserializationStrategy<out BaseItem> {

    return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
      0    -> ItemType0.serializer()
      1    -> ItemType1.serializer()
      2    -> ItemType2.serializer()
      else -> error("unknown Item type $type")
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

包括type

由于这是手动执行多态判别,因此无需包含type在您的类中。

import kotlinx.serialization.Serializable

@Serializable(with = BaseItemSerializer::class)
sealed class BaseItem

@Serializable
data class ItemType0(
  // ...
) : BaseItem()

@Serializable
class ItemType1(
  // ...
) : BaseItem()

@Serializable
class ItemType2(
  // ...
) : BaseItem()
Run Code Online (Sandbox Code Playgroud)

但是,为了完整性,您可能希望包含它,因此在序列化时将其包含在内。为此,您必须使用@EncodeDefault

import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable

@Serializable(with = BaseItemSerializer::class)
sealed class BaseItem {
  abstract val type: Int
}

@Serializable
class ItemType0(
  // ...
) : BaseItem() {
  @EncodeDefault
  override val type: Int = 0
}

// ...
Run Code Online (Sandbox Code Playgroud)

完整示例

将所有内容放在一起,这是一个完整的示例。

import kotlinx.serialization.*
import kotlinx.serialization.json.*

val mapper = Json {
  prettyPrint = true
  prettyPrintIndent = "  "
}

fun main() {

  val json = """
{
  "startTime": "2022-07-27T13:32:57.379Z",
  "status": 0,
  "clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "items": [
    {
      "type": 0,
      "results": "string",
      "restBetweenRounds": "string"
    },
    {
      "type": 1,
      "results": "string",
      "setCount": 0
    },
    {
      "type": 2,
      "items": [
        {
          "type": 0,
          "results": "string",
          "restBetweenRounds": "string"
        },
        {
          "type": 1,
          "results": "string",
          "setCount": 0
        }
      ],
      "results": "string"
    }
  ]
}
  """.trimIndent()

  val itemHolder: ItemHolder = mapper.decodeFromString(json)

  println(itemHolder)

  println(mapper.encodeToString(itemHolder))
}

@Serializable
data class ItemHolder(
  val startTime: String,
  val clientId: String,
  val status: Int,
  val items: List<BaseItem>,
)

@Serializable(with = BaseItem.Serializer::class)
sealed class BaseItem {
  abstract val type: Int

  object Serializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
    override fun selectDeserializer(
      element: JsonElement
    ): DeserializationStrategy<out BaseItem> {

      return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
        0    -> ItemType0.serializer()
        1    -> ItemType1.serializer()
        2    -> ItemType2.serializer()
        else -> error("unknown Item type $type")
      }
    }
  }  
}

@Serializable
data class ItemType0(
  val results: String,
  val restBetweenRounds: String,
) : BaseItem() {
  @EncodeDefault
  override val type: Int = 0
}

@Serializable
data class ItemType1(
  val results: String,
  val setCount: Int,
) : BaseItem() {
  @EncodeDefault
  override val type: Int = 1
}

@Serializable
data class ItemType2(
  val results: String,
  val items: List<BaseItem>,
) : BaseItem() {
  @EncodeDefault
  override val type: Int = 2
}
Run Code Online (Sandbox Code Playgroud)

这打印

ItemHolder(
  startTime=2022-07-27T13:32:57.379Z, 
  clientId=3fa85f64-5717-4562-b3fc-2c963f66afa6, 
  status=0, 
  items=[
    ItemType0(results=string, restBetweenRounds=string), 
    ItemType1(results=string, setCount=0), 
    ItemType2(
      results=string, 
      items=[
        ItemType0(results=string, restBetweenRounds=string),
        ItemType1(results=string, setCount=0)
      ]
    )
  ]
)
Run Code Online (Sandbox Code Playgroud)

{
  "startTime": "2022-07-27T13:32:57.379Z",
  "clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": 0,
  "items": [
    {
      "results": "string",
      "restBetweenRounds": "string",
      "type": 0
    },
    {
      "results": "string",
      "setCount": 0,
      "type": 1
    },
    {
      "results": "string",
      "items": [
        {
          "results": "string",
          "restBetweenRounds": "string",
          "type": 0
        },
        {
          "results": "string",
          "setCount": 0,
          "type": 1
        }
      ],
      "type": 2
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

与输入匹配!

版本

  • 科特林 1.7.10
  • Kotlinx 序列化 1.3.4