Switching from Moshi to Kotlinx Serialization
Reading Time: 6 minutesIntroduction
With Kotlin Multiplatform officially in Alpha, we should now be seriously considering it for current and future projects. Regarding existing projects, this will typically mean switching from a number of popular libraries, to their KMM counterparts. Regarding Json deserialisation this most likely means going from Gson, Jackson or Moshi to Kotlinx Serialization.
To practically look at this, I’ve taken a small sample of json from the Sky News app feed and deserialised it using Moshi. The data is a polymorphic list that’s in a more verbose format than desired as it isn’t explicitly designed for mobile.
The goal is to take a basic Moshi implementation, and then replace it with a KotlinX implementation without changing any other details.
Sample json and desired data classes
The sample data is effectively a news category in the Sky News app, the category has a title and a list of News Items. The News Items can comform to a number of different types. In this example I’ve stripped the data right back to almost nothing, just keeping the structure and keeping two distinct types; a story and a video.
{
"name": "UK",
"_embedded": {
"contents": [
{
"headline": "Article Headline",
"type": "story",
"teaserImage": {
"_links": {
"url": {
"href": "article/image/url"
}
},
"altText": "Image 1 altText",
"caption": "Image 1 caption"
}
},
{
"videoUrl": "video/url",
"headline": "Video Headline",
"type": "video",
"teaserImage": {
"_links": {
"url": {
"href": "video/image/url"
}
},
"altText": "Image 2 altText",
"caption": null
}
},
{
"key": "value",
"type": "something_else"
}
]
}
}
The classes we want to decode the above json into are as follows:
interface ListItem { }
data class NewsList(val name: String, val data: List<ListItem>)
data class Image(
val imageUrl: String,
val caption: String,
val altText: String,
)
data class Article(
val headline: String,
val teaserImage: Image
) : ListItem
data class Video(
val headline: String,
val teaserImage: Image,
val videoUrl: String
) : ListItem
The main thing to note here is that we’re transforming the data to remove the irrelevant parts in Image and in the NewsList
Moshi Implementation
The moshi implementation is reasonably straight forward. We have to create a couple of custom type adapters to transform the data to the desired structure, and for the type adapters we need to create some intermediary classes to map between the json data and our desired data structure.
internal data class NewsFeedTeaserImage(val _links: NewsFeedImageLinks, val altText: String, val caption: String?)
internal data class NewsFeedImageLinks(val url: NewsFeedImageUrl)
internal data class NewsFeedImageUrl(val href: String)
internal class NewsFeedImageAdapter {
@FromJson
fun fromJson(json: NewsFeedTeaserImage): Image {
return Image(
json._links.url.href,
json.caption ?: json.altText,
json.altText
)
}
@ToJson
fun toJson(image: Image): NewsFeedTeaserImage {
val links = NewsFeedImageLinks(
NewsFeedImageUrl(image.imageUrl)
)
return NewsFeedTeaserImage(
links,
image.altText,
image.caption
)
}
}
Once we have our custom adapters, we then have to set up our polymorphic adapter. Thankfully this comes out of the box with the moshi adapters dependency.
implementation 'com.squareup.moshi:moshi-adapters:1.9.3'
private val polymorphicJsonAdapterFactory = PolymorphicJsonAdapterFactory.of(
ListItem::class.java,
"type"
)
.withSubtype(Article::class.java, "story")
.withSubtype(Video::class.java, "video")
.withDefaultValue(Invalid)
With this adapter, we have registered the known subtypes Article and Video, as well as assigning anything we don’t know about to an Invalid object, which we can then filter out.
With all that done we set up a Moshi instance and create a wrapper around it, to make it easier when we want to switch it out to Kotlinx serialization.
val moshi =
Moshi.Builder()
.add(NewsFeedImageAdapter())
.add(NewsFeedAdapter())
.add(polymorphicJsonAdapterFactory)
.add(KotlinJsonAdapterFactory())
.build()
class MoshiWrapper(moshi: Moshi) : NewsListJsonWrapper {
private val jsonAdapter: JsonAdapter<NewsList> =
moshi.adapter(NewsList::class.java)
override fun fromJson(json: String): NewsList? {
return jsonAdapter.fromJson(json)
}
}
And finally, to validate that it’s all working as expected, we can write a simple unit test, passing in our sample json data from earlier, deserializing it, and asserting it’s as expected. See the full code here:
class JsonWrapperTest : TestCase() {
private val moshiWrapper: NewsListJsonWrapper = MoshiWrapper(moshi)
private val expectedData = NewsList("UK", listOf(expectedArticle, expectedVideo, Invalid))
// Test that the JsonWrapper is deserializing our sample data as we intend
fun `test moshi json deserialization`() {
val deserializedJson = moshiWrapper.fromJson(sampleJson)
assertEquals(expectedData, deserializedJson)
}
}
Kotlinx Implementation
The first thing I noticed when trying to use Kotlinx is that the bundled version of kotlin in Android Studio needs to be version 4.0 or above otherwise you get lint errors on the Serialization annotations.
With Kotlinx we have to annotate all of our classes that are part of our json with the @Serialization annotation. This is typical with other Json libraries, however in the moshi example I had avoided it.
With that done we can start looking at custom deserializers, similar to the TypeAdapters in Moshi. Kotlinx names the intermediary classes we used for json deserialising Surrogates. So we have to create a Surrogate for the Image class and a Surrogate for the NewsFeed class. To achieve this, we can simply take the surrogate classes we created for the Moshi adapters and annotate them using the Kotlinx annotations like so:
@Serializable
@SerialName("Image")
internal data class NewsFeedTeaserImage(val _links: NewsFeedImageLinks, val altText: String, val caption: String?)
@Serializable
internal data class NewsFeedImageLinks(val url: NewsFeedImageUrl)
@Serializable
internal data class NewsFeedImageUrl(val href: String)
With that done a custom Serializer will look like this:
object ImageSerializer : KSerializer<Image> {
override val descriptor: SerialDescriptor = NewsFeedTeaserImage.serializer().descriptor
override fun deserialize(decoder: Decoder): Image {
val surrogate = decoder.decodeSerializableValue(NewsFeedTeaserImage.serializer())
return Image(
surrogate._links.url.href,
surrogate.caption ?: surrogate.altText,
surrogate.altText
)
}
override fun serialize(encoder: Encoder, value: Image) {
val links = NewsFeedImageLinks(
NewsFeedImageUrl(value.imageUrl)
)
val surrogate = NewsFeedTeaserImage(
links,
value.altText,
value.caption
)
encoder.encodeSerializableValue(NewsFeedTeaserImage.serializer(), surrogate)
}
}
@Serializable(with = ImageSerializer::class)
Handling the polymorphism is also a breeze. As we are using an interface as our base class, the type of polymorphism in the kotlinx documentation is open polymorphism as in theory we can add more subtypes anywhere we want. This is opposed to closed polymorphism which would be a sealed class, which is more strict. We need to tell our classes what type they are, otherwise they will use the full class name, for example we simply annotate Article like this, to match the json feed:
@Serializable
@SerialName("story")
data class Article(
Then to enable the polymorphism we simply tell kotlinx about it using a Json function like so:
val module = SerializersModule {
polymorphic(ListItem::class, Article::class, Article.serializer())
polymorphic(ListItem::class, Video::class, Video.serializer())
polymorphicDefault(ListItem::class) { Invalid.serializer() }
}
val format = Json {
serializersModule = module
ignoreUnknownKeys = true
}
With that done, Kotlinx hopefully knows how to parse our feed. We simply create the wrapper and unit test it, like we did for the Moshi wrapper.
class KotlinxWrapper : NewsListJsonWrapper {
override fun fromJson(json: String): NewsList? {
return format.decodeFromString(NewsList.serializer(), json)
}
}
class JsonWrapperTest : TestCase() {
private val kotlinxWrapper: NewsListJsonWrapper = KotlinxWrapper()
private val expectedData = NewsList("UK", listOf(expectedArticle, expectedVideo, Invalid))
// Test that the JsonWrapper is deserializing our sample data as we intend
fun `test kotlinx json deserialization`() {
val deserializedJson = kotlinxWrapper.fromJson(sampleJson)
assertEquals(expectedData, deserializedJson)
}
}
And everything is as expected.
Conclusion
So that’s one hurdle down on the track to KMM. For the most part it is absolutely no more difficult than Moshi, most things translate pretty closely. The documentation for Kotlinx is very good and seems to get updated regularly. I only found a few discrepancies in the docs which were very easy to solve.
Github repo and resources used
Github Repo: https://github.com/danieljonker/moshi-to-kotlinx
Moshi Docs: https://github.com/square/moshi
Kotlinx Docs: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md