Android Data Serialization Tutorial with the Kotlin Serialization Library
Learn how to use the Kotlin Serialization library in your Android app and how it differs from other data serialization libraries available out there. By Kshitij Chauhan.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Data Serialization Tutorial with the Kotlin Serialization Library
25 mins
- Getting Started
- Understanding Data Encoding and Serialization
- Understanding Data Decoding and Deserialization
- Kotlin Serialization
- The Compiler Plugin
- The JSON Encoder Module
- Comparing Kotlin Serialization Library to Moshi and Gson
- Modeling Data
- Encoding Data Manually
- Serializing Composite Types
- Customizing Property Names
- Marking Transient Data
- Integrating With Retrofit
- Adding the Retrofit Converter for Kotlin Serialization
- Switching to the Real Data Source
- Writing Serializers Manually
- Writing a Descriptor
- Writing the Serialize Method
- Writing the Deserialize Method
- Connecting the Serializer to the Class
- Bonus: Tests
- Limitations
- Where to Go From Here?
Switching to the Real Data Source
In the same Module.kt, you’ll find another Dagger module named DataModule
. It binds an instance of FakeDataSource
to BoredActivityDataSource
.
Modify boredActivityDataSource
to bind an instance of RealDataSource
instead:
@Binds fun boredActivityDataSource(realDataSource: RealDataSource): BoredActivityDataSource
Build and run. Now, you’ll see data from the real API. Pull down to refresh the list of activities to get new suggestions every time!
Writing Serializers Manually
While the auto-generated serializers work well in most cases, you can provide your own implementations if you wish to customize the serialization logic. In this section, you’ll learn how to write a serializer manually.
A serializer implements the KSerializer<T>
interface. It’s generic type parameter specifies the type of object serialized by the serializer.
Open BoredActivity.kt in data. Below BoredActivity
, add a new class:
import kotlinx.serialization.KSerializer // ... class BoredActivitySerializer: KSerializer<BoredActivity>
This new class implements KSerializer
.
The compiler will complain about missing methods in the class. You need to implement two methods, serialize
and deserialize
, and one property, descriptor
.
You’ll work on descriptor
first.
Writing a Descriptor
As you might guess from its name, descriptor
describes the structure of the object being serialized. It contains a description of the type and names of the properties to serialize.
Add this property to BoredActivitySerializer:
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.descriptors.element // ... class BoredActivitySerializer: KSerializer<BoredActivity> { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BoredActivity") { element<String>("activity") element<String>("type") element<Int>("participants") element<Double>("price") element<String>("link") element<String>("key") element<Double>("accessibility") } }
This descriptor describes the object as a collection of seven primitive properties. It specifies their types as well as their serialized names.
Next, you’ll work with serialize
.
Writing the Serialize Method
Now, you’ll add an implementation of serialize
below descriptor
:
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.encodeStructure // ... class BoredActivitySerializer: KSerializer<BoredActivity> { // ... override fun serialize(encoder: Encoder, value: BoredActivity) { encoder.encodeStructure(descriptor) { encodeStringElement(descriptor, 0, value.activity) encodeStringElement(descriptor, 1, value.type) encodeIntElement(descriptor, 2, value.participants) encodeDoubleElement(descriptor, 3, value.price) encodeStringElement(descriptor, 4, value.link) encodeStringElement(descriptor, 5, value.key) encodeDoubleElement(descriptor, 6, value.accessibility) } }
It accepts an encoder and an instance of BoredActivity
. Then it uses encodeXYZElement
to write the object’s properties one by one into the encoder, where XYZ
is a primitive type.
Note the integer values passed into each encodeXYZElement
. These values describe the order of properties. You use them when deserializing an object.
Finally, you’ll add deserialize
.
Writing the Deserialize Method
Add an implementation of the deserialize
right below serialize
:
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.decodeStructure import kotlinx.serialization.encoding.CompositeDecoder // ... class BoredActivitySerializer: KSerializer<BoredActivity> { // ... override fun deserialize(decoder: Decoder): BoredActivity = decoder.decodeStructure(descriptor) { var activity = "" var type = "" var participants = -1 var price = 0.0 var link = "" var key = "" var accessibility = 0.0 while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> activity = decodeStringElement(descriptor, 0) 1 -> type = decodeStringElement(descriptor, 1) 2 -> participants = decodeIntElement(descriptor, 2) 3 -> price = decodeDoubleElement(descriptor, 3) 4 -> link = decodeStringElement(descriptor, 4) 5 -> key = decodeStringElement(descriptor, 5) 6 -> accessibility = decodeDoubleElement(descriptor, 6) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } BoredActivity(activity, type, participants, price, link, key, accessibility) } }
It accepts a decoder, and returns an instance of BoredActivity
.
Note the iteration over index returned by the decoder, which you use to determine which property to decode next. Also, notice that the loop terminates whenever the index equals a special token called CompositeDecoder.DECODE_DONE
. This signals a decoder has no more properties to read.
Now that the serializer is complete, it’s time to wire it with the BoredActivity
class.
Connecting the Serializer to the Class
To use BoredActivitySerializer
, pass it as a parameter to @Serializable
as follows:
@Serializable(with = BoredActivitySerializer::class) data class BoredActivity( val activity: String, val type: String, val participants: Int, val price: Double, val link: String, val key: String, val accessibility: Double, )
Build and run. You won’t notice any changes, which indicates your serializer works correctly! Add a log statement in your serializer to confirm that it’s being used. For example, add the following to the top of deserializer
:
Log.d("BoredActivitySerializer","Using deserializer")
With this change, you complete Bored No More!. Don’t forget to try its suggestions the next time you’re feeling bored. :]
Bonus: Tests
The app ships with a few tests to ensure your serializers and viewmodels work correctly. Don’t forget to run them to ensure that everything is alright! To run the tests, in the Project pan in Android Studio, right click com.raywenderlich.android.borednomore (test). Then select Run Tests in com.raywenderlich…:
The results of the tests look like:
While the Kotlin Serialization library is great, every technology has its drawbacks. This tutorial would be incomplete if it didn’t highlight the library’s limitations. Keep reading to learn about them.
Limitations
The Kotlin Serialization library is opinionated about its approach to data serialization. As such, it imposes a few restrictions on how you write your code. Here’s a list of a few important limitations:
Deserializing the following JSON into a Project
object…
…produces a MissingFieldException
:
-
Non-class properties aren’t allowed in the primary constructor of a serializable class.
// Invalid code @Serializable class Project( path: String // Forbidden non-class property ) { val owner: String = path.substringBefore('/') val name: String = path.substringAfter('/') }
-
Only class properties with a backing field are serialized while the others are ignored.
@Serializable class Project( var name: String // Property with a backing field; allowed ) { var stars: Int = 0 // property with a backing field; allowed val path: String // no backing field; ignored by the serializer get() = "kotlin/$name" var id by ::name // delegated property; ignored by the serializer }
-
Optional properties must have a default value if they’re missing in the encoded data.
@Serializable data class Project(val name: String, val language: String)
Deserializing the following JSON into a
Project
object…{ "name" : "kotlinx.serialization" }
…produces a
MissingFieldException
:Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'Project', but it was missing.
-
All referenced objects in class properties must also be serializable.
class User(val name: String) @Serializable class Project( val name: String, val owner: User // Invalid code, User class is not serializable )
// Invalid code @Serializable class Project( path: String // Forbidden non-class property ) { val owner: String = path.substringBefore('/') val name: String = path.substringAfter('/') }
@Serializable class Project( var name: String // Property with a backing field; allowed ) { var stars: Int = 0 // property with a backing field; allowed val path: String // no backing field; ignored by the serializer get() = "kotlin/$name" var id by ::name // delegated property; ignored by the serializer }
@Serializable data class Project(val name: String, val language: String)
{ "name" : "kotlinx.serialization" }
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'Project', but it was missing.
class User(val name: String) @Serializable class Project( val name: String, val owner: User // Invalid code, User class is not serializable )
With that, you’re done with this tutorial!