我需要将 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)
不是进行封闭多态反序列化管道的方法吗?
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
。