【问题标题】:kotlinx.serialization: Deserialize JSON array as sealed classkotlinx.serialization:将 JSON 数组反序列化为密封类
【发布时间】:2021-04-21 12:25:51
【问题描述】:

我的数据是富文本格式,存储为嵌套的 JSON 数组。文本标记存储字符串的纯文本和描述格式的注释。我想在解码时将这些嵌套 JSON 数组的特定结构映射到丰富的 Kotlin 类层次结构。

这是描述此文本编码的打字稿类型:

// Text string is an array of tokens
type Text = Array<TextToken>
// Each token is a Array[2] tuple. The first element is the plaintext.
// The second element is an array of annotations that format the text.
type TextToken = [string, Array<Annotation>]
// My question is about how to serialize/deserialize the Annotation type
// to a sealed class hierarchy.
//
// Annotations are an array where the first element is always a type discriminator string
// Each annotation type may have more elements, depending on the annotation type.
type Annotation =
 | ["b"] // Text with this annotation is bold
 | ["i"] // Text with this annotation is italic
 | ["@", number] // User mention
 | ["r", { timestamp: string, reminder: string }] // Reminder

我已经定义了一些 Kotlin 类来使用 sealed class 来表示相同的东西。这是反序列化 JSON 后我想要的输出格式:

// As JSON example: [["hello ", []], ["Jake", [["b"], ["@", 1245]]]]
data class TextValue(val tokens: List<TextToken>)

// As JSON example: ["hello ", []]
// As JSON example: ["Jake", [["b"], ["@", 1245]]]
data class TextToken(val plaintext: String, val annotations: List<Annotation>)

sealed class Annotation {
  // As JSON example: ["b"]
  @SerialName("b")
  object Bold : Annotation()

  // As JSON example: ["i"]
  @SerialName("i")
  object Italic : Annotation()

  // As JSON example: ["@", 452534]
  @SerialName("@")
  data class Mention(val userId: Int)

  // As JSON example: ["r", { "timestamp": "12:45pm", "reminder": "Walk dog" }]
  @SerialName("r")
  data class Reminder(val value: ReminderValue)
}

如何定义我的序列化程序?我尝试使用JsonTransformingSerializer 定义序列化程序,但是当我尝试为我的一个类包装默认序列化程序时出现空指针异常:

@Serializable(with = TextValueSerializer::class)
data class TextValue(val tokens: List<TextToken>)

object TextValueSerializer : JsonTransformingSerializer<TextValue>(TextValue.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        return JsonObject(mapOf("tokens" to element))
    }

    override fun transformSerialize(element: JsonElement): JsonElement {
        return (element as JsonObject)["tokens"]!!
    }
}
Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlinx.serialization.json.JsonTransformingSerializer.<init>, parameter tSerializer
    at kotlinx.serialization.json.JsonTransformingSerializer.<init>(JsonTransformingSerializer.kt)
    at example.TextValueSerializer.<init>(TextValue.kt:17)

【问题讨论】:

    标签: json typescript kotlin kotlinx.serialization


    【解决方案1】:

    您遇到的错误似乎是因为您在 TextValue 序列化程序中引用了 TextValue 序列化程序

    由于数据结构与序列化程序期望的键值对不完全匹配,因此更难让它自动执行此类操作。

    对于您当前的实施,这是您需要的,从下往上开始:

    1. 注释

      创建一个自定义序列化程序,将JsonArray 表示形式转换为其Annotation 表示形式。这是通过简单地将JsonArray 的索引映射到其对应的密封类表示来完成的。由于第一个索引始终是标识符,我们可以使用它来告知我们要映射到的类型。

      如果可能,我们可以使用自动生成的序列化器。

      []          -> Annotation.None
      ["b"]       -> Annotation.Bold
      ["@", 1245] -> Annotation.Mention
      ...
      

      为此,您可以创建一个新的序列化程序并将其附加到 Annotation 类 (@Serializable(with = AnnotationSerializer::class))。

      object AnnotationSerializer : KSerializer<Annotation> {
          override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Annotation") {}
      
          override fun serialize(encoder: Encoder, value: Annotation) {
              val jsonEncoder = encoder as JsonEncoder
      
              // Encode the Annotation as a json element by first converting the annotation
              // to a JsonElement
              jsonEncoder.encodeJsonElement(buildJsonArray {
                  when (value) {
                      is TextAnnotation.None -> {}
                      is TextAnnotation.Bold -> { add("b") }
                      is TextAnnotation.Italic -> { add("i") }
                      is TextAnnotation.Mention -> {
                          add("@")
                          add(value.userId)
                      }
                      is TextAnnotation.Reminder -> {
                          add("r")
                          add(jsonEncoder.json.encodeToJsonElement(ReminderValue.serializer(), value.value))
                      }
                  }
              })
      
          }
      
          override fun deserialize(decoder: Decoder): Annotation {
              val jsonDecoder = (decoder as JsonDecoder)
              val list = jsonDecoder.decodeJsonElement().jsonArray
      
              if (list.isEmpty()) {
                  return Annotation.None
              }
      
              return when (list[0].jsonPrimitive.content) {
                  "b" -> Annotation.Bold
                  "i" -> Annotation.Italic
                  "@" -> Annotation.Mention(list[1].jsonPrimitive.int)
                  "r" -> Annotation.Reminder(jsonDecoder.json.decodeFromJsonElement(ReminderValue.serializer(), list[1].jsonObject))
                  else -> throw error("Invalid annotation discriminator")
              }
          }
      }
      
      @Serializable(with = AnnotationValueSerializer::class)
      sealed class TextAnnotation {
      
    2. 文本令牌

      TextToken 遵循相同的策略。我们首先在第一个索引处提取标记,然后使用第二个索引构建注释。如上所述,我们需要注释TextToken 类以使用以下序列化器:

      object TextTokenSerializer : KSerializer<TextToken> {
          override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TextToken") {}
      
          override fun serialize(encoder: Encoder, value: TextToken) {
              val jsonDecoder = encoder as JsonEncoder
              jsonDecoder.encodeJsonElement(buildJsonArray {
                  add(value.plaintext)
                  add(buildJsonArray {
                      value.annotations.map {
                          add(jsonDecoder.json.encodeToJsonElement(it))
                      }
                  })
              })
          }
      
          override fun deserialize(decoder: Decoder): TextToken {
              val jsonDecoder = decoder as JsonDecoder
              val element = jsonDecoder.decodeJsonElement().jsonArray
      
              // Token
              val plaintext = element[0].jsonPrimitive.content
      
              // Iterate over the annotations
              val annotations = element[1].jsonArray.map {
                  jsonDecoder.json.decodeFromJsonElement<TextAnnotation>(it.jsonArray)
              }
      
              return TextToken(plaintext, annotations)
          }
      }
      

      返回以下 JSON 可能会更好:

      { plaintext: "Jake", annotations: [["b"], ["@", 1245]] } 将更好地映射到TextToken POJO,并且不再需要序列化程序。

    3. 文本值

      拼图的最后一部分是 TextValue 对象,它有效地包装了 TextToken 列表。最好为此使用类型别名:

      typealias TextValue = List<TextToken>
      

      在当前模型中,您可以使用序列化程序将JsonArray 解析为List&lt;TextToken&gt;,然后将该列表包装在TextValue 对象中。

      object TextValueSerializer : KSerializer<TextValue> {
          override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TextValue") {}
      
          override fun serialize(encoder: Encoder, value: TextValue) {
              val jsonEncoder = (encoder as JsonEncoder)
              jsonEncoder.encodeSerializableValue(ListSerializer(TextToken.serializer()), value.tokens)
          }
      
          override fun deserialize(decoder: Decoder): TextValue {
              val jsonDecoder = decoder as JsonDecoder
              val list = jsonDecoder.decodeJsonElement().jsonArray
      
              return TextValue(list.map { jsonDecoder.json.decodeFromJsonElement(it.jsonArray) })
          }
      }
      

    【讨论】:

    • 谢谢,非常详细!我想我被val descriptor 屏蔽了——我不知道如何制作一个完美的。在这种情况下,它只是用于类型唯一性吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-08
    • 2021-07-22
    • 1970-01-01
    • 1970-01-01
    • 2021-01-12
    • 1970-01-01
    相关资源
    最近更新 更多