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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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!

Image showing BoredActivity objects fetched from the API

Image showing the pull-to-refresh functionality of the app

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")

Image showing debug statements printed by the custom serializer

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…:
A screenshot of right clicking on the tests folder and selecting to run the tests.

The results of the tests look like:

Imaging showing all tests passed for the sample project

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)
    
    { "name" : "kotlinx.serialization" }
    
    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!