You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Kotlinx序列化器触发JsonDecodingException:字符串反序列化失败

问题:自定义Kotlin序列化器反序列化字符串抛出异常

异常信息

kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 10: Expected quotation mark '"', but had 't' instead at path: $.plan
JSON input: {"plan": "test"}

相关代码

待序列化对象

@Serializable
data class Test(
    val plan: PlanAsString?
)

自定义序列化器

typealias PlanAsString = @Serializable(with = PlanSerializer::class) String

class PlanSerializer : KSerializer<String?> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StringSerializer", PrimitiveKind.STRING)

    @OptIn(ExperimentalSerializationApi::class)
    override fun deserialize(decoder: Decoder): String? {
        kotlin.runCatching {
            val plan = decoder.decodeSerializableValue(Test.serializer())
            return plan.plan
        }.onFailure {
            return decoder.decodeString()
        }
        return decoder.decodeNull()
    }

    @OptIn(ExperimentalSerializationApi::class)
    override fun serialize(encoder: Encoder, value: String?) {
        return value?.let {
            encoder.encodeString(value)
        } ?: encoder.encodeNull()
    }
}

测试代码

Json.decodeFromString<Test>("{\"plan\": \"test\"}")  // 抛出上述异常
Json.decodeFromString<Test>("{\"plan\": {\"name\": \"basic\"}}") // 可正常反序列化

问题原因

  1. 解码器位置偏移:当处理字符串值时,你先尝试用Test.serializer()解码对象结构,此时解码器指针会移动到字符串内部(比如"test"的t位置),解码失败后再调用decodeString(),解码器位置已不正确,导致找不到引号的异常。
  2. 循环引用:Test类包含PlanAsString字段,而PlanAsString又依赖当前序列化器,调用Test.serializer()会触发无限递归解码。

解决方案

通过判断JSON元素类型,分别处理字符串、对象、空值三种情况,避免解码器位置错误和循环引用:

typealias PlanAsString = @Serializable(with = PlanSerializer::class) String

class PlanSerializer : KSerializer<String?> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("PlanSerializer", PrimitiveKind.STRING)

    @OptIn(ExperimentalSerializationApi::class)
    override fun deserialize(decoder: Decoder): String? {
        val jsonDecoder = decoder as JsonDecoder
        val element = jsonDecoder.decodeJsonElement()
        
        return when (element) {
            is JsonNull -> null
            is JsonPrimitive -> {
                if (element.isString) element.content else null
            }
            is JsonObject -> {
                // 从对象中提取需要的字段(示例为"name")作为字符串返回
                element["name"]?.jsonPrimitive?.content
            }
            else -> null
        }
    }

    override fun serialize(encoder: Encoder, value: String?) {
        value?.let(encoder::encodeString) ?: encoder.encodeNull()
    }
}

代码说明

  • 将通用Decoder转为JsonDecoder,直接获取当前JSON元素,避免解码器位置偏移
  • 分类型处理:
    • JsonNull:直接返回null
    • JsonPrimitive:判断为字符串则返回内容
    • JsonObject:提取目标字段(如name)的字符串值返回

验证

修改后两个测试用例均可正常运行:

Json.decodeFromString<Test>("{\"plan\": \"test\"}")
Json.decodeFromString<Test>("{\"plan\": {\"name\": \"basic\"}}")

内容的提问来源于stack exchange,提问作者Sk1X1

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 20:54:56