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?
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)
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.
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.
No need for #JsonCreator and sealdSubClass. Jackson has this support in its jackson-module-kotlin, just need one annotation #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"
}
The above example is copied from the its main repo README: https://github.com/FasterXML/jackson-module-kotlin
Related
Kotlin sealed interface serialization seems to be broken, unless using an explicitly registered serialization module. Am I missing something?
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlin.test.Test
sealed interface iFoo {
#Serializable
#SerialName("a")
class A() : iFoo//Foo(),
}
class EventTest {
#Test
fun testKotlinLang() {
// // this works
// val json = Json { serializersModule = SerializersModule {
// polymorphic(iFoo::class) {
// subclass(iFoo.A::class, iFoo.A.serializer())
// } } }
val json = Json
val a = iFoo.A() as iFoo
val s = json.encodeToString(a)
val a1: iFoo = json.decodeFromString(s)
}
}
Class 'A' is not registered for polymorphic serialization in the scope of 'iFoo'.
Mark the base class as 'sealed' or register the serializer explicitly.
kotlinx.serialization.SerializationException: Class 'A' is not registered for polymorphic serialization in the scope of 'iFoo'.
Mark the base class as 'sealed' or register the serializer explicitly.
at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:102)
at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:113)
at kotlinx.serialization.PolymorphicSerializerKt.findPolymorphicSerializer(PolymorphicSerializer.kt:109)
at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:224)
at kotlinx.serialization.json.Json.encodeToString(Json.kt:85)
at io.almer.EventTest.testKotlinLang(EventTest.kt:64)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
..............
Note that I am using the multiplatform mode but the JVM run fails.
Apparently it's not implemented yet :(
https://github.com/Kotlin/kotlinx.serialization/issues/1417
Possible work-around using sealed interface with #JvmInline: https://github.com/Kotlin/kotlinx.serialization/issues/1417#issuecomment-844300565
I'm trying to get a Kotlin JS app working, and when consuming data from a server using this code:
val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
#Serializable
data class Entry(val start: String, val end: String)
suspend fun loadData() {
val data = client.get<List<Entry>>("http://localhost:8080/data") {
accept(ContentType.Application.Json)
}
console.log(data)
}
I get exceptions like this:
Serializer for class 'Entry' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
On Kotlin/JS explicitly declared serializer should be used for interfaces and enums without #Serializable annotation
even though the class is marked as #Serializable.
If I change it to client.get<List<Map<String,String>>> then I get a valid result.
What am I doing wrong?
I needed to add this to build.gradle.kts:
kotlin("plugin.serialization") version "1.4.31"
I'm using Kotlin to write an AWS Lambda. I have a Kotlin data class
class MessageObject(
val id: String,
val name: String,
val otherId: String
)
This data class is used as the input to the required interface implementation
class Handler : RequestHandler<MessageObject, Output> {
...
override fun handleRequest(msg: MessageObject, ctx: Context) {
...
}
}
When I test this lambda in the aws console, and pass it a proper JSON message, I get this:
An error occurred during JSON parsing: java.lang.RuntimeException
java.lang.RuntimeException: An error occurred during JSON parsing
Caused by: java.io.UncheckedIOException:
com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of 'com.mycode.MessageObject'(no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
I'm almost certain this is fixed by saying:
ObjectMapper().registerModule(KotlinModule())
but in the world of AWS Lambda how do I edit the object mapper provided by AWS?
If you haven't gotten it to work with KotlinModule, since the problem you're having is that Jackson requires a default empty constructor and you currently don't have one. You could just change your MessageObject as follows and it should work:
data class MessageObject(
var id: String = "",
var name: String = "",
var otherId: String = ""
)
I created this repo with a fully functional kotlin lambda template using the Serverless Framework. Have a look for some other tidbits you might need: https://github.com/crafton/sls-aws-lambda-kotlin-gradlekt
You cannot use data class with provided RequestHandler<I, O> unfortunately, because you need register the kotlin module for your jackson mapper in order to work with data classes. But you can write you own RequestHandler, which will like this one.
Here's the code:
interface MyRequestStreamHandler<I : Any, O : Any?> : RequestStreamHandler {
val inputType: Class<I>
fun handleRequest(input: I, context: Context): O?
override fun handleRequest(inputStream: InputStream, outputStream: OutputStream, context: Context) {
handleRequest(inputStream.readJson(inputType), context).writeJsonNullable(outputStream)
}
interface MessageObjectRequestHandler : MyRequestStreamHandler< MessageObject, Output> {
override val inputType: Class<MessageObject >
get() = MessageObject::class.java
}
}
And jackson util:
private val objectMapper = jacksonObjectMapper()
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerKotlinModule()
private val writer: ObjectWriter = objectMapper.writer()
fun <T : Any> readJson(clazz: Class<T>, stream: InputStream): T =
objectMapper.readValue(stream, clazz)
fun <T : Any> InputStream.readJson(clazz: Class<T>): T =
readJson(clazz, this)
fun Any?.writeJsonNullable(outputStream: OutputStream) {
if (this != null) writer.writeValue(outputStream, this)
}
Now, you can keep your MessageObject class to be data class, and your handler will look something like:
class LambdaMain : MessageObjectRequestHandler {
override fun handleRequest(input: MessageObject, context: Context): Output {
//...
}
}
I have created a sealed class for the json field Value under CustomAttribute data class. This field can return String or Array of Strings.
How can we deserialize this sealed class from json?
data class CustomAttribute (
val attributeCode: String,
val value: Value
)
sealed class Value {
class StringArrayValue(val value: List<String>) : Value()
class StringValue(val value: String) : Value()
}
One solution is to use a RuntimeTypeAdapterFactory as per the instructions in this answer
val valueTypeAdapter = RuntimeTypeAdapter.of(Value::class.java)
.registerSubtype(StringArrayValue::class.java)
.registerSubtype(StringValue::class.java)
val gson = GsonBuilder().registerTypeAdapter(valueTypeAdapter).create()
RuntimeTypeAdapter is included in the source code for Gson but not exposed as a Maven artifact.
It is designed to be copy/pasted into your project from here
I created a TypeAdapterFactory implementation specifically to support sealed classes and their subtypes. This works similarly to the RuntimeTypeAdapterFactory (and I used it as a guide to write my class), but will specifically only support sealed types, and will deserialize using object instances of objects with a sealed class supertype (RuntimeTypeAdapterFactory will create a new instance of object types, which breaks equality checks when a single instance is the expectation).
private class SealedTypeAdapterFactory<T : Any> private constructor(
private val baseType: KClass<T>,
private val typeFieldName: String
) : TypeAdapterFactory {
private val subclasses = baseType.sealedSubclasses
private val nameToSubclass = subclasses.associateBy { it.simpleName!! }
init {
if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
}
override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null
val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
gson.getDelegateAdapter(this, TypeToken.get(it.java))
}
return object : TypeAdapter<R>() {
override fun write(writer: JsonWriter, value: R) {
val srcType = value::class
val label = srcType.simpleName!!
#Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
val jsonObject = delegate.toJsonTree(value).asJsonObject
if (jsonObject.has(typeFieldName)) {
throw JsonParseException("cannot serialize $label because it already defines a field named $typeFieldName")
}
val clone = JsonObject()
clone.add(typeFieldName, JsonPrimitive(label))
jsonObject.entrySet().forEach {
clone.add(it.key, it.value)
}
elementTypeAdapter.write(writer, clone)
}
override fun read(reader: JsonReader): R {
val element = elementTypeAdapter.read(reader)
val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
)
val name = labelElement.asString
val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
#Suppress("UNCHECKED_CAST")
return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
}
}
}
companion object {
fun <T : Any> of(clz: KClass<T>) = SealedTypeAdapterFactory(clz, "type")
}
}
Usage:
GsonBuilder().registerTypeAdapter(SealedTypeAdapterFactory.of(Value::class)).create()
I have successfully serialized and de-serialized a sealed class in the past, with a disclaimer of using Jackson, not Gson as my serialization engine.
My sealed class has been defined as:
#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, visible = true)
sealed class FlexibleResponseModel
class SnapshotResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class DifferentialResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class EventDrivenResponse(val collection: List<EntityEventModel>): FlexibleResponseModel()
class ErrorResponse(val error: String): FlexibleResponseModel()
With the annotations used, it required no further configuration for the Jackson instance to properly serialize and de-serialize instances of this sealed class granted that both sides of the communication possessed a uniform definition of the sealed class.
While I recognise that JsonTypeInfo is a Jackson-specific annotation, perhaps you might consider switching over from Gson if this feature is a must - or you might be able to find an equivalent configuration for Gson which would also include the class identifier in your serialized data.
I have the following Scala enum:
object RunMode extends Enumeration {
val CLIENT_MODE = Value("CLIENT")
val SERVER_MODE = Value("SERVER")
}
I have some JSON that my app takes in as input for example:
{
"version" : "0.1",
"runMode" : "CLIENT"
}
Here the JSON field "runMode" is really my RunMode enum, and its values will always be either "CLIENT" or "SERVER". I am trying to use GSON to deserialize this JSON into an AppConfig instance:
class AppConfig(version : String, runMode : RunMode) {
def version() : String = { this.version }
def runMode() : RunMode.Value = { this.runMode }
}
I have the following GSON code:
val gson = new Gson()
val text = Source.fromFile(jsonConfigFile).mkString
gson.fromJson(text, classOf[AppConfig])
When this runs:
java.lang.RuntimeException: Unable to invoke no-args constructor for class scala.Enumeration$Value. Register an InstanceCreator with Gson for this type may fix this problem.
> Buildiat com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:226)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:210)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
at com.google.gson.Gson.fromJson(Gson.java:887)
<rest of stacktrace omitted for brevity>
So clearly, GSON expects RunMode to have a no-arg constructor, and it doesn't, and so its unable to deserialize my JSON file at runtime.
I've tried a million different combos but can't quite seem to find the magical constructor definition. So I ask: How can I add a no-arg constructor to RunMode so that GSON can deserialize it into an AppConfig instance?
This doesn't directly answer why using Gson fails, but offers an alternative. Here is an example using argonaut:
RunMode enum definition:
object RunMode extends Enumeration {
type RunMode = Value
val CLIENT_MODE = Value("CLIENT")
val SERVER_MODE = Value("SERVER")
implicit def runModeCodec: CodecJson[RunMode.RunMode] = CodecJson({
case CLIENT_MODE => "CLIENT".asJson
case SERVER_MODE => "SERVER".asJson
}, c => c.focus.string match {
case Some("CLIENT") => DecodeResult.ok(CLIENT_MODE)
case Some("SERVER") => DecodeResult.ok(SERVER_MODE)
case _ => DecodeResult.fail("Could not decode RunMode", c.history)
})
}
Definition of Foo (matching the object you want to create):
case class Foo(version: String, runMode: RunMode)
object Foo {
implicit def codec: CodecJson[Foo] =
casecodec2(Foo.apply, Foo.unapply)("version", "runMode")
}
And now the decoding/encoding example:
object ArgonautEnumCodec {
def main(args: Array[String]): Unit = {
val res: String = Foo("0.1", RunMode.CLIENT_MODE).asJson.toString
println(res)
val foo: Foo = res.decodeOption[Foo].get
println(foo)
}
}
Yields:
{"version":"0.1","runMode":"CLIENT"}
Foo(0.1,CLIENT)
Since I'm not a Scala guy, but have some Gson background, peeking some insights into how Scala works was fun to me. The reason of why you're getting the exception is that Gson cannot instantiate an abstract class scala.Enumeration.Value. The AutoConfig class content is pretty much like the following class in vanilla Java:
final class AppConfig {
final String version;
// This is where ig gets failed
final scala.Enumeration.Value runMode;
AppConfig(final String version, final scala.Enumeration.Value runMode) {
this.version = version;
this.runMode = runMode;
}
}
As far as I understand how Scala enumerations are implemented, unlike Java enumerations, they do not have their type per se, and every Scala enumeration value seem to be an instance of scala.Enumeration$Val giving not enough "host" enumeration type information from its type (however instances seem to have their outer class references). That's why custom implementing custom type adapter is not that simple and requires some inspection on the real enum type (not sure how it can be implemented, though).
Gson provides a special annotation #JsonAdapter that can annotate a certain field including a type adapter to be applied. So the AppConfig.runMode from the class above can be annotated like:
#JsonAdapter(RunModeEnumTypeAdapter.class)
final scala.Enumeration.Value runMode;
Note that it has some hint on the target type in its name. This is because there's probably no other way to specify the target enumeration type. Now, how a generic scala.Enumeration type adapter can be implemented.
// E - a special generic type bound to associate a Scala enumeration with
// So any Scala enumeration can be processed with this type adapter
abstract class AbstractScalaEnumTypeAdapter<E extends scala.Enumeration>
extends TypeAdapter<scala.Enumeration.Value> {
private final E enumeration;
protected AbstractScalaEnumTypeAdapter(final E enumeration) {
this.enumeration = enumeration;
}
#Override
#SuppressWarnings("resource")
public final void write(final JsonWriter out, final scala.Enumeration.Value value)
throws IOException {
// If the given value is null, null must be written to the writer (however it depends on a particular Gson instance configuration)
if ( value == null ) {
out.nullValue();
} else {
// Does Scala provide something like java.lang.Enumeration#name?
out.value(value.toString());
}
}
#Override
public final scala.Enumeration.Value read(final JsonReader in)
throws IOException {
final JsonToken token = in.peek();
switch ( token ) {
case NULL:
// Consume the `null` JSON token
in.nextNull();
return null;
case STRING:
// Consume a JSON string value and lookup an appropriate Scala enumeration value by its name
final String rawValue = in.nextString();
return enumeration.withName(rawValue);
// These case labels are matter of style and cover the rest of possible Gson JSON tokens, and are not really necessary
case BEGIN_ARRAY:
case END_ARRAY:
case BEGIN_OBJECT:
case END_OBJECT:
case NAME:
case NUMBER:
case BOOLEAN:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
// Something else? Must never happen
default:
throw new AssertionError(token);
}
}
}
Now, RunMode can be bound to the type adapter above:
final class RunModeEnumTypeAdapter
extends AbstractScalaEnumTypeAdapter<RunMode$> {
// Gson can instantiate this itself
private RunModeEnumTypeAdapter() {
// This is how it looks like from the Java perspective
// And this is the "hint" I was talking about above
super(RunMode$.MODULE$);
}
}
Example of use:
final Gson gson = new Gson();
final AppConfig appConfig = gson.fromJson("{\"version\":\"0.1\",\"runMode\":\"CLIENT\"}", AppConfig.class);
System.out.println(appConfig.version);
System.out.println(appConfig.runMode);
System.out.println(gson.toJson(appConfig));
Output:
0.1
CLIENT
{"version":"0.1","runMode":"CLIENT"}
Probably not that nice and compact as Scala can do, but I hope the code above can be translated to Scala with no issues.