After chatting with Sebastiano Poggi about the issues with using Kotlin objects that I covered in a recent post, he made the interesting suggestion that possibly kotlinx.serialization might work better than the Java implementation because it is faster, more flexible (we can use different serialisation formats such as JSON or Protobuf), and is built with knowledge of the Kotlin language, so will possibly handle the singleton implementation of Kotlin objects better. This post covers some of the basics of Kotlin serialisation, and what was learned along the way.
As the name suggests, kotlinx.serialization is an equivalent to Java serialisation and there is much to like. One thing worth mentioning is that it is still very much a work in progress; It is still experimental and, as we shall see, is far from complete. At the time of writing (November 2019) I do not realistically see it hitting full release in the immediate future.
Let’s start by setting up our project to use kotlinx.serialization:
const val kotlinVersion = "1.3.50" object BuildPlugins { const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" const val kotlinSerializationPlugin = "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" const val detekt = "io.gitlab.arturbosch.detekt" const val ktlint = "org.jlleitschuh.gradle.ktlint" const val versions = "com.github.ben-manes.versions" const val kotlinxSerialization = "kotlinx-serialization" } object Libraries { private object Versions { const val jupiter = "5.6.0-M1" const val kotlinxSerializationRuntime = "0.13.0" } const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" const val kotlinxSerializationRuntime = "org.jetbrains.kotlinx:kotlinx-serialization-runtime:${Versions.kotlinxSerializationRuntime}" const val jupiter = "org.junit.jupiter:junit-jupiter:${Versions.jupiter}" }
buildscript { repositories { google() jcenter() } dependencies { classpath BuildPlugins.kotlinGradlePlugin classpath BuildPlugins.kotlinSerializationPlugin } } plugins { id 'com.github.ben-manes.versions' version '0.27.0' id 'io.gitlab.arturbosch.detekt' version "1.1.1" id 'org.jlleitschuh.gradle.ktlint' version '9.1.0' id 'org.jetbrains.kotlin.jvm' version '1.3.50' } apply plugin: BuildPlugins.versions apply plugin: BuildPlugins.ktlint apply plugin: BuildPlugins.detekt apply plugin: BuildPlugins.kotlinxSerialization . . . dependencies { implementation Libraries.kotlinStdLib implementation Libraries.kotlinxSerializationRuntime testImplementation Libraries.jupiter } . . .
I felt that the best way of checking it was to apply it to the same class hierarchy as in the previous article:
@Serializable sealed class KSerializerSealedClass { @Serializable object Object : KSerializerSealedClass() @Serializable object Object2 : KSerializerSealedClass() @Serializable data class DataClass(val data: String) : KSerializerSealedClass() }
One of the fundamental principles of kotlinx.serialization is that we should be able to tag classes as being Serializable
using the @Serializable
annotation and the necessary serialisation code will be generated at compile time and will avoid the runtime performance issues of reflection-based approaches. So the overhead compared to Java serialisation is pretty minimal.
IMPORTANT UPDATE: The following is no longer true. With Kotlin 1.3.60+
and kotlinx.serialization 0.14.0+
Kotlin object serialisation is now supported. More details can be found here. Although the code for the rest of this article in no longer necessary for Kotlin object serialisation, it may be useful to those who need to write custom serialisers, so I have decided to leave it in place.
However, at the time of writing, Kotlin objects are not actually supported yet, and if we try this we’ll get compiler errors on Object
and Object2
:
@Serializable annotation is ignored because it is impossible to serialize automatically interfaces or enums. Provide serializer manually via e.g. companion object
This has changed – if you see this error then you should check that you’re using Kotlin 1.3.60
or later and kotlinx.serialization 0.14.0
or later.
I must confess that, at this point, I almost abandoned this idea because it felt like it would be better to wait until kotlinx.serialization fully supports Kotlin object serialisation. However, my curiosity wanted to understand what was necessary to actually get this working. While what follows will be obsolete once kotlinx.serialization fully supports Kotlin object serialisation, some of the principles are still quite useful – particularly when it comes to writing customer serializers , so there is still value in covering this.
It is worth mentioning that once it reaches a full release kotlinx.serialization will probably be as simple to implement as the above code (with perhaps some small changes), it’s just not there yet.
I had an initial stab at writing a custom serializer for Kotlin objects and it seemed like quite a lot of code. I decided to check with Eugenio Marletti who is a Developer Advocate at JetBrains whose knowledge of Kotlin is far more nuanced than mine. Eugenio confirmed that my technique was correct, but offered some great suggestions of how the code could be improved and compacted. The code that follows is basically my initially verbose code which has been cleaned up / improved by Eugenio.
Let’s start by looking at the enabler code. While there is only 12 lines of code here, there’s an awful lot going on:
interface ObjectSerializer: KSerializer { fun serializer() = this } inline fun createObjectSerializer(crossinline instance: () -> T) = object : ObjectSerializer { override val descriptor: SerialDescriptor = SerialClassDescImpl(T::class.toString()) override fun deserialize(decoder: Decoder) = instance() override fun serialize(encoder: Encoder, obj: T) { encoder.beginStructure(descriptor, this).endStructure(descriptor) } }
The ObjectSerializer
interface is a generic extension of KSerializer
(part of kotlinx.serialization) which adds a serializer()
function. The value this gives us should be a little clearer once we see how it is implemented.
The createObjectSerializer
is where a lot happens. This is the place where we do the main work required by KSerializer
.
The descriptor
is used as a key to this KSerializer
implementation and needs to clearly identify that class that this KSerializer
instance is responsible for. This is what the framework uses to find the correct KSerializer
to use during deserialisation.
The deserialize()
method defers the object deserialisation to the instance
object passed in. This is actually doing the equivalent of the readResolve()
implementation that we needed to do for Java serialisation in the previous article – it will always return instance
when an object of this type is deserialised.
The serialise()
method is responsible to writing the object – beginStructure()
and endStructure()
can be envisaged as XML:
// T data goes here
The key thing here is the descriptor being written which enables the same deserializer to be called.
With these two enablers we can now update any / all objects within out project. In this case we update our class hierarchy:
sealed class KSerializerSealedClass { @Serializable(with = Object::class) object Object : KSerializerSealedClass(), ObjectSerializer
The @Serializable
annotation accepts an optional with
attribute which allows us to specify a custom serialiser which implements a method named serializer()
. This is where the ObjectSerializer
interface should make sense – it matches that signature.
Each object then implements ObjectSerializer
and delegates to an instance returned by createObjectSerializer()
. For the instance
argument it passes in itself – the singleton that needs to be be deserialised. This is the other side of the readResolve()
equivalence we saw earlier.
It is actually worth noting that we don’t have to do anything for DataClass
as this is fully supported when it contains only primitive types.
We’re not quite there yet because kotlinx.serialization does not generate the necessary code around sealed classes, but does have polymorphism support and we can expose what is necessary through that. For this we need to expose a couple of statics though a companion object:
sealed class KSerializerSealedClass { . . . companion object { val serializer = PolymorphicSerializer(KSerializerSealedClass::class) val serializersModule get() = SerializersModule { polymorphic(KSerializerSealedClass::class) { Object::class with Object.serializer() Object2::class with Object2.serializer() DataClass::class with DataClass.serializer() } } } }
serializer
exposes is a KSerialiser
interface that is actually a PolymorphicSerializer
implementation.
serializersModule
exposes the polymorphic instance serialisers for subclasses of KSerializerSealedClass
.
With these we can serialise and deserialise any subclass of KSerializerSealedClass
using 3 lines of code:
private fun serialize(input: KSerializerSealedClass): KSerializerSealedClass { val serializer = Json(JsonConfiguration.Stable, KSerializerSealedClass.serializersModule) val json = serializer.stringify(KSerializerSealedClass.serializer, input) return serializer.parse(KSerializerSealedClass.serializer, json) as KSerializerSealedClass }
This is not something you would want to do in production code: serialising and immediately deserialising makes no sense whatsoever. But it makes sense in test code, and this was taken from the unit tests that are in the sample project.
We first create a serializer
– in this case a JSON serialiser. This gets the serializersModule
that we just defined – so it knows there to defer serialisation of subclasses of KSerializerSealedClass
.
We can now serialise the object to JSON by calling stringify()
on the serializer
we just created, and passing in the serializer
that we exposed through the KSerializerSealedClass
companion object.
We can then deserialise using the same SerializerSealedClass.serializer
and the JSON output.
This does show one of the advantages of kotlinx.serialization – the custom serialization code is totally agnostic of the format being serialized to (in this case JSON). At present JSON, Cbor, and Protobuf are supported, but there are also additional plugins (some community contributions) for HOCON, Avro, Bson, XML, and YAML formats. These can be interchanged depending on requirements without requiring changes to the custom serializers that we created earlier for our Kotlin objects.
If we compare the final sealed class with the Java Serializeable
equivalent, it should be quite obvious that this is far more verbose, and that it simply because we are having to manage the Kotlin object serialisation ourselves rather than kotlinx.serialization doing that for us. Moreover we are actually having to do the equivalent of implementing readResolve()
in Java Serializable
– the lambda function we pass in to createObjectSerializer()
returns the singleton instance of the object, and this lambda is called by the deserialize()
method of the ObjectSerializer
object that we create for each Kotlin object. If we implement readResolve()
we should return the singleton instance, and we are effectively doing exactly the same thing here whenever the Kotlin object is deserialised.
That said, there’s still a lot to like about kotlinx.serialization and once it becomes more feature complete it will be a really nice, flexible, performant alternative to Java Serializable
with the added advantage of understanding the design patterns that Kotlin uses upon the JVM and being able to correctly apply them during serialization. Once we get close to a 1.0 release I’m hopeful that the implementation will be as simple as adding the @Serializable
annotation as we did in the first example which came with a warning that it does not work. Yet! – the importance of that final word should now be a little clearer. There may be slight differenced in syntax, but that’s about the level of complexity and effort that we should be looking at.
Once again my huge thanks to Seb, for planting the seed for this article, and to Eugenio for performing his Kotlin magician skills on my code.
The source code for this article is available here.
© 2019, Mark Allison. All rights reserved.
Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.