将 JSON 反序列化为具有通用字段的 Serializable 类 - 错误:不允许类型参数中的星形投影

Posted

技术标签:

【中文标题】将 JSON 反序列化为具有通用字段的 Serializable 类 - 错误:不允许类型参数中的星形投影【英文标题】:Deserializing JSON into Serializable class with generic field - error: Star projections in type arguments are not allowed 【发布时间】:2022-01-07 23:02:20 【问题描述】:

简介

我在两个使用不同语言的后端服务器之间发送 JSON 消息。生产的 服务器创建各种 JSON 消息,用元数据包装在消息中。

包装类是Message,消费服务器必须确定它的消息类型 仅根据消息内容接收。

当我尝试使用 star-projection 来 反序列化消息,我得到一个错误。

示例

import kotlinx.serialization.json.Json

@Language("JSON")
val carJson = """
    
      "message_type": "some message",
      "data": 
        "info_type": "Car",
        "name": "Toyota"
      
    
  """.trimIndent()

// normally I wouldn't know what the Json message would be - so the type is Message<*>
val actualCarMessage = Json.decodeFromString<Message<*>>(carJson)

错误信息

Exception in thread "main" java.lang.IllegalArgumentException: Star projections in type arguments are not allowed, but Message<*>
    at kotlinx.serialization.SerializersKt__SerializersKt.serializerByKTypeImpl$SerializersKt__SerializersKt(Serializers.kt:81)
    at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:59)
    at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
    at ExampleKt.main(example.kt:96)
    at ExampleKt.main(example.kt)

类结构

我想将 JSON 反序列化为一个数据类 Message,它有一个泛型类型的字段。

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Message<out DataType : SpecificInformation>(
  @SerialName("message_type")
  val type: String,
  @SerialName("data")
  val data: DataType,
)

该字段受密封接口SpecificInformation 的约束,并具有一些实现。

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@JsonClassDiscriminator("info_type")
sealed interface SpecificInformation 
  @SerialName("info_type")
  val infoType: String


@Serializable
@SerialName("User")
data class UserInformation(
  @SerialName("info_type")
  override val infoType: String,
  val name: String,
) : SpecificInformation

// there are more implementations...

解决方法?

这是一个已知的 问题(kotlinx.serialization/issues/944) , 所以我正在寻找解决方法。

我可以控制 JSON 结构和库 - 尽管我更喜欢 kotlinx.序列化。

我不能改变有两个 JSON 对象,一个在另一个里面,而鉴别器是 在内部类中。

自定义序列化器会很棒。但我更愿意在类或文件上配置这个 (使用 @Serializable(with = ...)@file:UseSerializers(...))作为使用 自定义 SerializersModule 不是无缝的。

尝试:JsonContentPolymorphicSerializer

我编写了一个自定义序列化程序,只有在它被专门使用时才会使用它(这是我想要的 避免)。它也很笨重,如果数据类发生变化或添加新的类会中断,并且 不会从sealed interface 中受益。

这个可以改进吗

    可以通用吗? Json.decodeFromString&lt;Message&lt;*&gt;&gt;(carJson) 它没有任何硬编码字符串?
class MessageCustomSerializer : JsonContentPolymorphicSerializer<Message<*>>(Message::class) 
  override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Message<*>> 

    val discriminator = element
      .jsonObject["data"]
      ?.jsonObject?.get("info_type")
      ?.jsonPrimitive?.contentOrNull
    println("found discriminator $discriminator")

    val subclassSerializer = when (discriminator?.lowercase()) 
      "user" -> UserInformation.serializer()
      "car"  -> CarInformation.serializer()
      else   -> throw IllegalStateException("could not find serializer for $discriminator")
    
    println("found subclassSerializer $subclassSerializer")

    return Message.serializer(subclassSerializer)
  


fun main() 

  @Language("JSON")
  val carJson = """
      
        "message_type": "another message",
        "data": 
          "info_type": "Car",
          "brand": "Toyota"
        
      
    """.trimIndent()

  val actualCarMessage =
    Json.decodeFromString(MessageCustomSerializer(), carJson)
  val expectedCarMessage = Message("another message", CarInformation("Car", "Toyota"))

  require(actualCarMessage == expectedCarMessage) 
    println("car json parsing ❌")
  
  println("car json parsing ✅")

@Serializable(with = ... - 无限循环

我尝试将MessageCustomSerializer 直接应用到Message...

@Serializable(with = MessageCustomSerializer::class)
data class Message<out T : SpecificInformation>(
//...

但后来我无法访问插件生成的序列化程序,这会导致无限循环。

return Message.serializer(subclassSerializer) // calls 'MessageCustomSerializer', causes infinite loop

@Serializer(forClass = ...) - 不是通用的

除了用@Serializable(with = MessageCustomSerializer::class)注释Message,我 试过了 导出a plugin-generated serializer:

@Serializer(forClass = Message::class)
object MessagePluginGeneratedSerializer : KSerializer<Message<*>>

但是这个序列化器不是通用的,会导致错误

java.lang.AssertionError: No such value argument slot in IrConstructorCallImpl: 0 (total=0).
Symbol: MessageCustomSerializer.<init>|-5645683436151566731[0]
    at org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpressionKt.throwNoSuchArgumentSlotException(IrMemberAccessExpression.kt:66)
    at org.jetbrains.kotlin.ir.expressions.IrFunctionAccessExpression.putValueArgument(IrFunctionAccessExpression.kt:31)
    at org.jetbrains.kotlinx.serialization.compiler.backend.ir.IrBuilderExtension$DefaultImpls.irInvoke(GeneratorHelpers.kt:210)
    at org.jetbrains.kotlinx.serialization.compiler.backend.ir.SerializableCompanionIrGenerator.irInvoke(SerializableCompanionIrGenerator.kt:35)

【问题讨论】:

【参考方案1】:

您在这里问了很多事情,所以我将简单地尝试就您所犯的错误提供一些指示,这些错误似乎让您陷入困境。考虑到这些,并阅读我链接到的文档,我相信您应该能够自己解决其余的问题。

多态序列化

熟悉kotlinx.serializationpolymorphic serialization。当您尝试序列化 Message&lt;*&gt;DataType 时,您正在尝试使用多态序列化。

如果您将Message&lt;*&gt; 序列化为根对象,则明确指定PolymorphicSerializer(正如我在the bug report you link to 中发布的那样)应该可以工作。例如,Json.decodeFromString( PolymorphicSerializer( Message::class ), carJson )

附:我不能 100% 确定您在这里尝试做的事情与错误报告中的相同。无论哪种方式,明确指定序列化程序都应该有效,无论它是否是一个错误,您不应该被要求这样做。

MessageDataType 中的 message_typeinfo_type 字段分别为 class discriminators。您需要在 Json 设置中进行配置,并在具体类上设置正确的 SerialName 以使其正常工作。只有starting from kotlinx.serialization 1.3.0 使用@JsonClassDiscriminator 才能为每个层次结构使用不同的类鉴别器。

覆盖插件生成的序列化程序

但后来我无法访问插件生成的序列化程序,这会导致无限循环。

@Serializable(with = ...) 覆盖插件生成的序列化程序。如果要保留插件生成的序列化器,请不要申请with

当您直接序列化对象(作为根对象)时,您仍然可以将不同的序列化程序作为第一个参数传递给encode/decode。当您想要覆盖序列化程序以用于嵌套在根对象某处的特定属性时,use @Serializable on the property。

多态性和泛型类

"No such value argument slot in IrConstructorCallImpl: 0" 错误是意料之中的。

如果您想为多态泛型类指定序列化程序,请need to do more work。

【讨论】:

以上是关于将 JSON 反序列化为具有通用字段的 Serializable 类 - 错误:不允许类型参数中的星形投影的主要内容,如果未能解决你的问题,请参考以下文章

将特定 JSON 字段反序列化为 Unity 中的对象列表

使用spring boot将JSON反序列化为带有通用对象列表的POJO

如何将所有字段都是默认值的类型反序列化为 None ?

如何将具有嵌套属性的 JSON 对象反序列化为 Symfony 实体?

C#序列化及反序列化Json对象通用类JsonHelper

将 JSON 包反序列化为具有自定义属性名称的类