问题
I have a following sealed class:
sealed class ViewModel {
data class Loaded(val value : String) : ViewModel()
object Loading : ViewModel()
}
How can I serialize/deserialize instances of the ViewModel class, let's say to/from JSON format?
I've tried to use Genson serializer/deserializer library - it can handle Kotlin data classes, it's also possible to support polymorphic types (eg. using some metadata to specify concrete types).
However, the library fails on Kotlin object
types, as these are singletons without a public constructor. I guess I could write a custom Genson converter to handle it, but maybe there's an easier way to do it?
回答1:
You are probably right about the creating a custom serializer.
I have tried to serialize and de-serialize your class using the Jackson library and Kotlin.
These are the Maven dependencies for Jackson:
<!-- 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>
You can serialize the sealed class to JSON using this library with no extra custom serializers, but de-serialization requires a custom de-serializer.
Below is the toy code I have used to serialize and de-serialize your sealed class:
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
}
If you run this code this is the output:
{}
{"value":"test"}
ViewModel$Loading@1753acfe
Loaded(value=test)
回答2:
I had a similar problem recently (although using Jackson, not Genson.)
Assuming I have the following:
sealed class Parent(val name: String)
object ChildOne : Parent("ValOne")
object ChildTwo : Parent("ValTwo")
Then adding a JsonCreator
function to the sealed class:
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
}
}
}
Now you can deserialize using ChildOne
or ChildTwo
as key
in your json property.
回答3:
I ended up implementing a custom Converter plus a Factory to properly plug it into Genson.
It uses Genson's metadata convention to represent the object as:
{
"@class": "com.example.ViewModel.Loading"
}
The converter assumes useClassMetadata flag set, so serialization just needs to mark an empty object. For deserialization, it resolves class name from metadata, loads it and obtains 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() }
}
To make sure that this converter is applied only to actual objects, I register it using a factory, that tells Genson when to use it and when to fall back to the default implementation.
object KotlinConverterFactory : Factory<Converter<Any>> {
override fun create(type: Type, genson: Genson): Converter<Any>? =
if (TypeUtil.getRawClass(type).kotlin.objectInstance != null) KotlinObjectConverter
else null
}
The factory can be used to configure Genson via builder:
GensonBuilder()
.withConverterFactory(KotlinConverterFactory)
.useClassMetadata(true) // required to add metadata during serialization
// some other properties
.create()
The code probably could be even nicer with chained converters feature, but I didn't have time to check it out yet.
来源:https://stackoverflow.com/questions/50157468/how-to-serialize-deserialize-kotlin-sealed-class