Kotlin 序列化 - 使用 JsonContentPolymorphicSerializer 作为拦截器对密封层次结构进行多态反序列化

问题描述 投票:0回答:1

我需要将 JSON 模型反序列化为严格的类层次结构,具有很强的前向兼容性要求。
根据文档,这是通过 封闭多态性

sealed
类来完成的。
我正在使用的模型如下所示:

@Serializable
sealed class BaseChatMessageDto {
    abstract val id: Int
    abstract val localId: Int
    abstract val fromId: Int
    abstract val replyTo: Int?
    abstract val remoteStatus: RemoteStatus?
    abstract val createdAt: Int
    abstract val type: ChatMessageType
}

ChatMessageType
是客户端知道的消息类型的枚举。自定义序列化器可确保任何未知类型被反序列化为
ChatMessageType.UNKNOWN
:

object ChatMessageTypeFallback :
    ForwardCompatibleEnumSerializer<ChatMessageType>(ChatMessageType::class)
@Serializable(ChatMessageTypeFallback::class)
enum class ChatMessageType {
    @SerialName("text")
    TEXT,
    @SerialName("file")
    FILE,
    @SerialName("photo")
    PHOTO,
    @SerialName("sticker")
    STICKER,
    @SerialName("gif")
    GIF,
    @SerialName("task")
    TASK,
    @SerialName("service")
    SERVICE,
    @ForwardsCompatible
    @SerialName("unknown")
    UNKNOWN
}

因此,

type
中的
BaseChatMessageDto
充当
kotlinx.serialization
编译器插件的隐式类型鉴别器。每个消息类型都必须反序列化为自己类的对象,例如:

@Serializable
@SerialName("sticker")
data class StickerChatMessageDto(
    override val id: Int,
    @SerialName("local_id")
    override val localId: Int,
    // Override the rest of BaseChatMessageDto

    @SerialName("sticker_id")
    val stickerId: Int?,
    // Other fields specific for "sticker" type

    // Override ChatMessageWithReactionsDto field
    override val reactions: List<Reaction>,
): BaseChatMessageDto(), ChatMessageWithReactionsDto

一些类型,如

FILE
PHOTO
被认为是丰富的消息,并且有自己的超类:

@Serializable
sealed class RichChatMessageDto: BaseChatMessageDto(), ChatMessageWithReactionsDto {
    abstract override val id: Int
    @SerialName("local_id")
    abstract override val localId: Int
    abstract override val type: ChatMessageType
    // Override the rest of BaseChatMessageDto

    abstract val text: String
    // Other abstract members of RichChatMessageDto

    // Override ChatMessageWithReactionsDto field
    abstract override val reactions: List<Reaction>?
}

@Serializable
@SerialName("file")
data class FileChatMessageDto(
    override val type: ChatMessageType = ChatMessageType.FILE,
    // Override everything else from RichChatMessageDto
): RichChatMessageDto()

UnknownChatMessageDto
通过在遇到未知
ChatMessageType
时反序列化来提供前向兼容性:

@Serializable
@SerialName("unknown")
data class UnknownChatMessageDto(
    // Override everything.
): BaseChatMessageDto()

但是这个逻辑中有一个非常特殊的情况——删除的消息。在服务器端,它们不是通过

type
来区分的,而是通过
status
来区分的。如果
status
DELETED
,则返回基本模型,无论其
type
是什么。因此,我需要创建一个反序列化拦截器,首先查看
status
,如有必要,使用
DeletedMessageDto.serializer()
,否则继续使用默认的反序列化管道。据我所知,
JsonContentPolymorphicSerializer
是专门为此设计的,所以:

@Serializable
data class DeletedChatMessageDto(
    // Override everything
): BaseChatMessageDto()
@Serializable(
    with = DeletedMessageInterceptorSerializer::class
)
sealed class BaseChatMessageDto {
    // ...
}

object DeletedMessageInterceptorSerializer: JsonContentPolymorphicSerializer<BaseChatMessageDto>(
    BaseChatMessageDto::class
) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<BaseChatMessageDto> {
        val status = try {
            val primitive = element.jsonObject["status"]?.jsonPrimitive
            // Parse status into enum
        } catch (e: Exception) {
            e.printStackTrace()
            RemoteStatus.UNKNOWN
        }
        return when (status) {
            RemoteStatus.DELETED -> DeletedChatMessageDto.serializer()
            else -> PolymorphicSerializer(BaseChatMessageDto::class)
        }
    }
}

但它并没有按预期工作。

DeletedChatMessageDto
似乎可以很好地反序列化,但对于其他类型它会失败:

kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'text'

如果我像这样向我的

SerializersModule
提供
Json

serializersModule += SerializersModule {
    polymorphic(BaseChatMessageDto::class) {
        defaultDeserializer {
            UnknownChatMessageDto.serializer()
        }
    }
}

然后

DeletedChatMessageDto
s are 可以很好地反序列化,但所有其他类型都毫不奇怪地反序列化为
UnknownChatMessageDto

如果我显式注册子类:

serializersModule += SerializersModule {
    polymorphic(BaseChatMessageDto::class) {
        subclass(TextChatMessageDto::class)
        subclass(FileChatMessageDto::class)
        subclass(PhotoChatMessageDto::class)
    }
}

然后抛出另一个异常:

Caused by: java.lang.IllegalArgumentException: Polymorphic serializer for class <package>.FileChatMessageDto (Kotlin reflection is not available) has property 'type' that conflicts with JSON class discriminator. You can either change class discriminator in JsonConfiguration, rename property with @SerialName annotation or fall back to array polymorphism

如果我尝试使用 @simon-jacobs 的关于使用

typealias
的建议,如下所示:

typealias BaseChatMessageDto =
    @Serializable(DeletedMessageInterceptorSerializer::class)
    BaseChatMessageDtoPlain

@Serializable
sealed class BaseChatMessageDtoPlain {
    // ...
}

// I tried both BaseChatMessageDtoPlain and BaseChatMessageDto here
object DeletedMessageInterceptorSerializer: JsonContentPolymorphicSerializer<BaseChatMessageDtoPlain>(
    BaseChatMessageDtoPlain::class
) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<BaseChatMessageDtoPlain> {
        val status = // ...
        return when (status) {
            RemoteStatus.DELETED -> DeletedChatMessageDto.serializer()
            else -> BaseChatMessageDtoPlain.serializer()
        }
    }

}

然后,当 BaseChatMessageDto 是另一个可序列化类型的属性时,会调用序列化器

is
,但不会调用返回
List<BaseChatMessageDto>
的 Retrofit 调用,这会导致已删除消息出现异常:

kotlinx.serialization.MissingFieldException: Field 'text' is required for type with serial name 'text', but it was missing at path: $[1]

我不太明白为什么

DeletedMessageInterceptorSerializer
没有通过
typealias
应用到这个特定的调用。

我知道我可以使用

JsonContentPolymorphicSerializer
手动选择基于
type
的序列化器,但我想将尽可能多的逻辑卸载到编译器插件,以确保类型安全、代码清晰并减少可能出错的地方。 我在这里做错了什么?
PolymorphicSerializer(BaseChatMessageDto::class)
不是进行封闭多态反序列化管道的方法吗?

android json kotlin serialization kotlinx.serialization
1个回答
0
投票

type
鉴别器

type
用作多态序列化的默认 JSON 键。据我所知,可能可以使用多态序列化来处理枚举占用的属性名称,但这样做会让自己的生活变得困难。

我建议根本不要使用枚举类,或者至少不要使用它的

type
属性。无论如何,它在服务器端代码中似乎没有真正的功能,因为您有相应的类层次结构。

相反,请将

type
中的
BaseChatMessageDto
留空。序列化器可以在 JSON 本身中选取
type
键,并使用它来选择正确的子类,前提是这些子类具有与值
type
对应的序列名称(您看起来已经处理过)。

使用
PolymorphicSerializer
和替代品

PolymorphicSerializer(BaseChatMessageDto::class)
不是进行封闭多态反序列化管道的方法吗?

不,不是。该序列化器与定义

SerializersModule
相关联,如 文档1 所暗示的那样。要使用它,您需要注册每个子类(结合删除如上所述的
type
属性)。

尽管通过将不同的序列化器指定为

BaseChatMessageDto
的默认序列化器与用于反序列化传入 JSON 对象的序列化器,您仍然可以在没有模块的情况下使其工作。

为此,请保留

BaseChatMessageDto
及其普通
@Serializable
注释,并在
DeletedMessageInterceptorSerializer
中引用此内容:

return when (status) {
    RemoteStatus.DELETED -> DeletedChatMessageDto.serializer()
    else -> BaseChatMessageDto.serializer()
}

并在实际解码消息时使用拦截器序列化器2

Json.decodeFromString(DeletedMessageInterceptorSerializer, incomingMessage)

1

PolymorphicSerializer
对应的密封类是
SealedClassSerializer
,您可以手动实例化它。不过我认为上面建议的方法比使用这个内部 API 更典型。

2或者,如果您的消息被解码为更大的类层次结构的一部分,您可以通过注释适当的封闭类属性来指定

DeletedMessageInterceptorSerializer

© www.soinside.com 2019 - 2024. All rights reserved.