如何序列化/反序列化Kotlin密封类?

18

我有一个以下封闭类:

sealed class ViewModel {

  data class Loaded(val value : String) : ViewModel()
  object Loading : ViewModel()

}
我该如何将ViewModel类的实例序列化/反序列化成JSON格式?我曾尝试过使用Genson序列化/反序列化库,它可以处理Kotlin数据类,并且也支持多态类型(例如使用一些元数据来指定具体类型)。但是,该库在处理Kotlin中的object类型时会失败,因为这些对象是没有公共构造函数的单例。我猜我可以编写一个自定义的Genson转换器来处理它,但也许有更简单的方法?

你为什么想要反序列化一个单例? - jrtapsell
2
@jrtapsell - 由于它不保存任何数据,因此没有必要拥有多个该类的实例。将其作为常规类是一种解决方法,但需要覆盖equals/hashcode并且总体上感觉不太对。 - Zbigniew Malinowski
5个回答

8

您关于创建自定义序列化器的想法可能是正确的。

我尝试使用Jackson库和Kotlin对您的类进行序列化和反序列化。

以下是Jackson的Maven依赖项:

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.8.8</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.8</version>
</dependency>

你可以使用这个库将封装类序列化为JSON,无需额外的自定义序列化程序,但反序列化需要一个自定义的反序列化程序。
以下是我用来序列化和反序列化你的封装类的玩具代码:
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule

sealed class ViewModel {
    data class Loaded(val value: String) : ViewModel()
    object Loading : ViewModel()
}

// Custom serializer
class ViewModelDeserializer : JsonDeserializer<ViewModel>() {
    override fun deserialize(jp: JsonParser?, p1: DeserializationContext?): ViewModel {
        val node: JsonNode? = jp?.getCodec()?.readTree(jp)
        val value = node?.get("value")
        return if (value != null) ViewModel.Loaded(value.asText()) else ViewModel.Loading
    }
}

fun main(args: Array<String>) {
    val m = createCustomMapper()
    val ser1 = m.writeValueAsString(ViewModel.Loading)
    println(ser1)
    val ser2 = m.writeValueAsString(ViewModel.Loaded("test"))
    println(ser2)
    val deserialized1 = m.readValue(ser1, ViewModel::class.java)
    val deserialized2 = m.readValue(ser2, ViewModel::class.java)
    println(deserialized1)
    println(deserialized2)
}

// Using mapper with custom serializer
private fun createCustomMapper(): ObjectMapper {
    val m = ObjectMapper()
    val sm = SimpleModule()
    sm.addDeserializer(ViewModel::class.java, ViewModelDeserializer())
    m.registerModule(sm)
    return m
}

如果您运行此代码,则输出如下:
{}
{"value":"test"}
ViewModel$Loading@1753acfe
Loaded(value=test)

1
谢谢你的回答。我知道自定义序列化器可以解决这个问题(我甚至认为它可以是通用转换器,不限于特定的“对象”类型)。但是,我想知道是否有现成的解决方案 :) - Zbigniew Malinowski
@ZbigniewMalinowski 我已经尝试使用Jackson找到一个开箱即用的解决方案,但是没有成功 - 可能有更聪明的库(或程序员)可以提供帮助。 - gil.fernandes

5

最近我遇到了类似的问题(尽管使用的是Jackson而不是Genson)。

假设我有以下内容:

sealed class Parent(val name: String)

object ChildOne : Parent("ValOne")
object ChildTwo : Parent("ValTwo")


然后在密封类中添加一个JsonCreator函数:
sealed class Parent(val name: String) {

    private companion object {
        @JsonCreator
        @JvmStatic
        fun findBySimpleClassName(simpleName: String): Parent? {
            return Parent::class.sealedSubclasses.first {
                it.simpleName == simpleName
            }.objectInstance
        }
    }
}

现在你可以使用ChildOneChildTwo作为key在你的json属性中进行反序列化。

这是目前最好的解决方案。但我猜它不能处理嵌套的密封类? - Carson Holzheimer
@CarsonHolzheimer 我还没有尝试过,但我不明白为什么它不能像处理嵌套的密封类一样处理。 - SergioLeone
2
我在这里肯定缺少某些基本知识。我可以使用此方法序列化封闭类实例,但我无法使用jackson对象映射程序对字符串进行反序列化:“无法从对象值(没有委托或基于属性的创建者)进行反序列化”。你能提供一个将字符串反序列化为对象实例的示例吗? - Bastian Stein
我不认为我还有它,但我会尝试查找并更新答案@BastianStein 也许你可以打开另一个问题并发布你那里的代码。这样更容易看出你是否遗漏了什么。 - SergioLeone

3
不需要使用 @JsonCreator 和 sealdSubClass。Jackson 在其 jackson-module-kotlin 中支持此功能,只需一个注释 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME):
  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
  sealed class SuperClass{
      class A: SuperClass()
      class B: SuperClass()
  }

...
val mapper = jacksonObjectMapper()
val root: SuperClass = mapper.readValue(json)
when(root){
    is A -> "It's A"
    is B -> "It's B"
}

上面的例子摘自其主要存储库README:https://github.com/FasterXML/jackson-module-kotlin


1
你也可以使用 JsonTypeInfo.Id.DEDUCTION 进行更自然的反序列化。你需要确保密封类型是可推导的。 - Asad-ullah Khan

3
我最终实现了一个自定义转换器和工厂,以使其正确地插入到Genson中。
它使用了Genson的元数据约定来表示对象,如下所示:
{ 
  "@class": "com.example.ViewModel.Loading" 
}

转换器假定设置了"useClassMetadata"标志,因此序列化只需要标记一个空对象。对于反序列化,它从元数据中解析类名,加载它并获取"objectInstance"。
object KotlinObjectConverter : Converter<Any> {

override fun serialize(objectData: Any, writer: ObjectWriter, ctx: Context) {
    with(writer) {
        // just empty JSON object, class name will be automatically added as metadata
        beginObject()
        endObject()
    }
}

override fun deserialize(reader: ObjectReader, ctx: Context): Any? =
    Class.forName(reader.nextObjectMetadata().metadata("class"))
        .kotlin.objectInstance
        .also { reader.endObject() }
}

为了确保该转换器只应用于实际的 对象,我使用工厂进行注册,告诉Genson何时使用它以及何时回退到默认实现。
object KotlinConverterFactory : Factory<Converter<Any>> {

    override fun create(type: Type, genson: Genson): Converter<Any>? =
        if (TypeUtil.getRawClass(type).kotlin.objectInstance != null) KotlinObjectConverter
        else null

}

可以使用工厂模式通过构建器来配置Genson:

GensonBuilder()
        .withConverterFactory(KotlinConverterFactory)
        .useClassMetadata(true) // required to add metadata during serialization
        // some other properties
        .create()

代码可能可以使用链接的转换器功能更加简洁,但我还没有时间去验证。

0
我遇到了类似的问题,花了一天的时间来解决它,其中一个@X.Y的答案https://dev59.com/J1UL5IYBdhLWcg3wk4vK#71315804对我有所帮助,但是它有点不完整。
要解决你面临的问题,你需要两件事情:
1. 你的jackson `objectMapper` 需要一个配置为支持单例对象的 KotlinModule,下面是代码片段:
```kotlin val objectMapper: ObjectMapper = JsonMapper.builder().build() objectMapper.registerModule(KotlinModule.Builder() .enable(KotlinFeature.SingletonSupport).build()) ```
2. 第二件事是由上面的答案提供的 -> https://dev59.com/J1UL5IYBdhLWcg3wk4vK#71315804,即 `@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)` 注解。
通过执行上述两个步骤,Jackson会使用类名对对象进行序列化,并使用相同的类名对其进行反序列化,从而保持单例行为完整。
如果你只用第二步解决了问题,你可能会发现问题已经解决了,但是每次反序列化后你会得到不同的实例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接