@JsonClassDiscriminator不会更改 JSON 类鉴别器



问题

由于项目架构,向后兼容性等,我需要更改一个抽象类和所有继承它的类的类标识符。理想情况下,我希望它是一个enum。

我尝试使用@JsonClassDiscriminator,但Kotlinx仍然使用type成员作为鉴别符,这与类中的成员有名称冲突。我更改了成员名来测试会发生什么,Kotlinx只是使用type作为鉴别符。

此外,在注释之外,我希望避免更改这些类。它是共享代码,所以任何不向后兼容的更改都将是有问题的。

我准备了一些代码,从项目中分离出来,用于测试行为。

fun main() {
val derived = Derived("type", "name") as Base
val json = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
}.encodeToString(derived)
print(json)
}
@Serializable
@JsonClassDiscriminator("source")
data class Derived(
val type: String?,
val name: String?,
) : Base() {
override val source = FooEnum.A
}
@Serializable
@JsonClassDiscriminator("source")
abstract class Base {
abstract val source: FooEnum
}
enum class FooEnum { A, B }
internal val serializers = SerializersModule {
polymorphic(Base::class) {
subclass(Derived::class)
}
}

如果我不改变type成员名,我得到这个错误:

线程"main"异常java.lang.IllegalArgumentException:类my.pack.Derived的多态序列化器具有属性"type"与JSON类鉴别器冲突。你可以改变在JsonConfiguration中使用类标识符来重命名属性@SerialName注释或退回到数组多态性

如果我更改了名称,我将得到这个JSON,它清楚地显示JSON类型标识符没有更改。

{
"type": "my.pack.Derived",
"typeChanged": "type",
"name": "name",
"source": "A"
}

Kotlinx序列化不允许对默认类型标识符进行重要的自定义-您只能更改字段的名称。

编码默认字段

在我进入解决方案之前,我想指出,在这些示例中使用@EncodeDefaultJson { encodeDefaults = true }是必需的,否则Kotlinx序列化将不会编码您的val source

@Serializable
data class Derived(
val type: String?,
val name: String?,
) : Base() {
@EncodeDefault
override val source = FooEnum.A
}

修改标识符字段

您可以使用@JsonClassDiscriminator来定义标识符的名称

(注意,您只需要父类Base上的@JsonClassDiscriminator,而不是两个)

然而,@JsonClassDiscriminator更像是一个"替代名称",而不是重写。要覆盖它,您可以在Json { }构建器

中设置classDiscriminator
val mapper = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
classDiscriminator = "source"
}

Discriminator值

你可以改变子类的type的值-使用@SerialName("...")在你的子类。

@Serializable
@SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()

在类中包含鉴别符

你也不能在你的类中包含鉴别符- https://github.com/Kotlin/kotlinx.serialization/issues/1664

所以有3个选项

关闭多态性h5> 代码更改为使用封闭多态性

由于Base是一个密封类,而不是enum,因此您可以对任何Base实例使用类型检查

fun main() {
val derived = Derived("type", "name")
val mapper = Json {
prettyPrint = true
encodeDefaults = true
classDiscriminator = "source"
}
val json = mapper.encodeToString(Base.serializer(), derived)
println(json)
val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
}
}
@Serializable
@SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()

@Serializable
sealed class Base

由于Base现在是密封的,它基本上与enum相同,因此不需要FooEnum.

val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
// no need for an 'else'
}

但是,您仍然需要Json { classDiscriminator= "source" }

基于内容的反序列化器

使用基于内容的反序列化器

这意味着您不需要将Base设置为密封类,如果鉴别符未知,您可以手动定义默认值。

object BaseSerializer : JsonContentPolymorphicSerializer<Base>(Base::class) {
override fun selectDeserializer(element: JsonElement) = when {
"source" in element.jsonObject -> {
val sourceContent = element.jsonObject["source"]?.jsonPrimitive?.contentOrNull
when (
val sourceEnum = FooEnum.values().firstOrNull { it.name == sourceContent }
) {
FooEnum.A -> Derived.serializer()
FooEnum.B -> error("no serializer for $sourceEnum")
else      -> error("'source' is null")
}
}
else                           -> error("no 'source' in JSON")
}
}

在某些情况下非常适合,特别是当您对源代码没有太多控制权时。然而,我认为这是相当粗糙的,并且很容易在选择序列化器时犯错误。

自定义序列化器

或者您可以编写自定义序列化器。

最终结果与基于内容的反序列化器没有太大不同。它仍然很复杂,仍然很容易犯错误。由于这些原因,我不会给出一个完整的例子。

这是有益的,因为如果你需要用非json格式编码/解码,它提供了更大的灵活性。

@Serializable(with = BaseSerializer::class)
@JsonClassDiscriminator("source")
sealed class Base {
abstract val source: FooEnum
}
object BaseSerializer : KSerializer<Base> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Base") {
// We have to write our own custom descriptor, because setting a custom serializer
// stops the plugin from generating one
}
override fun deserialize(decoder: Decoder): Base {
require(decoder is JsonDecoder) {"Base can only be deserialized as JSON"}

val sourceValue = decoder.decodeJsonElement().jsonObject["source"]?.jsonPrimitive?.contentOrNull
// same logic as the JsonContentPolymorphicSerializer...
}
override fun serialize(encoder: Encoder, value: Base) {
require(encoder is JsonEncoder) {"Base can only be serialized into JSON"}
when (value) {
is Derived -> encoder.encodeSerializableValue(Derived.serializer(), value)
}
}
}

最新更新