json - kotlinx.serialization:将 JSON 数组反序列化为密封类

标签 json typescript kotlin kotlinx.serialization

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

这是描述此文本编码的 typescript 类型:

// 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 类来表示相同的事物。这是我反序列化 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)

最佳答案

您收到的错误似乎是因为您在 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. 文本 token

    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<TextToken>然后将该列表包装在 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) })
        }
    }
    

关于json - kotlinx.serialization:将 JSON 数组反序列化为密封类,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65753636/

相关文章:

android - 在 Android 中使用 OpenCV 时遇到问题

hibernate - 为什么 FetchType.LAZY 在 Hibernate/JPA 中不起作用?

android - 如何在 Android Studio 3.1.3 中查看 Kotlin 中有趣的源代码?

javascript - 使用Angular提取特定的Json数据

JavaScript 可选链动态属性

javascript - Json 日期中的时区

typescript - 有没有办法在 typescript 中委托(delegate)索引属性?

typescript - 请求正文中的 bool 参数在 NestJS api 中始终为真

java - 在 Java/Android 中解析 UNIX 日期中的 JSON,无需任何库

java - JSON Volley 嵌套 JSON 数组 com.android.volley.ParseError : org. json.JSONException