Skip to Content
Course content

164: Custom Serializers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, kotlinx.serialization handles things automatically. You slap @Serializable on a data class, and it just works. But eventually, you'll hit a wall where the JSON representation the API expects doesn't match how you want to structure your data in Kotlin. I've run into this countless times when dealing with legacy APIs that use weird string formats for things that should be rich objects.

Let's look at a concrete example. Imagine we're building a theme engine. In our Kotlin code, we want a Color object with red, green, and blue components. However, the API we're talking to expects colors as a single Hex string, like "#FF5733".

The problem with default serialization

If we just do the standard thing, we get a nested object. I'll show you what I mean:

@Serializable
data class Color(val red: Int, val green: Int, val blue: Int)

@Serializable
data class UserTheme(val themeName: String, val primaryColor: Color)

If I serialize a UserTheme now, the JSON looks like this: {"themeName": "Midnight", "primaryColor": {"red": 255, "green": 87, "blue": 51}}. That's clean, but our API will reject it. It wants "primaryColor": "#FF5733". We need a custom serializer to bridge that gap.

Building the ColorSerializer

To do this, we need to implement the KSerializer interface. This requires us to define a descriptor (which tells the framework what the data "looks" like) and two methods: serialize and deserialize.

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

object ColorSerializer : KSerializer<Color> {
    override val descriptor: SerialDescriptor = 
        PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Color) {
        val hex = String.format("#%02X%02X%02X", value.red, value.green, value.blue)
        encoder.encodeString(hex)
    }

    override fun deserialize(decoder: Decoder): Color {
        val hex = decoder.decodeString()
        // I'll handle the parsing here...
        val r = hex.substring(1, 3).toInt(16)
        val g = hex.substring(3, 5).toInt(16)
        val b = hex.substring(5, 7).toInt(16)
        return Color(r, g, b)
    }
}

Wait, where did the '#' go?

I just ran this code, and it crashed with a StringIndexOutOfBoundsException. Why? Because I assumed the API would always send the hash symbol. In reality, some of our backend endpoints omit the # and just send "FF5733". My substring(1, 3) call was blindly skipping the first character, which is a classic rookie mistake in serialization logic.

I need to make the deserializer more robust. I'll check for the prefix before parsing. Here is the corrected deserialize method:

override fun deserialize(decoder: Decoder): Color {
    var hex = decoder.decodeString()
    if (hex.startsWith("#")) {
        hex = hex.substring(1)
    }
    
    val r = hex.substring(0, 2).toInt(16)
    val g = hex.substring(2, 4).toInt(16)
    val b = hex.substring(4, 6).toInt(16)
    return Color(r, g, b)
}

Wiring it into our data model

Now that the logic is solid, we have to tell Kotlin to actually use this serializer. You have two choices: you can annotate the class itself, or you can annotate the specific property. I usually prefer annotating the property if I only need this behavior for one specific API, but for a Color type, it makes more sense to make it the default everywhere.

By adding the @Serializable(with = ... ) annotation to the class, we don't have to remember to add it every time we use Color in a different data class.

@Serializable(with = ColorSerializer::class)
data class Color(val red: Int, val green: Int, val blue: Int)

@Serializable
data class UserTheme(val themeName: String, val primaryColor: Color)

Now, when I call Json.encodeToString(UserTheme("Midnight", Color(255, 87, 51))), I get exactly what the API wants: {"themeName": "Midnight", "primaryColor": "#FF5733"}.




📋 Practical Task

Exercise: Formatting International Phone Numbers for JSON

You are working with a telecom API. In your Kotlin code, you have a PhoneNumber data class that stores the countryCode and number as separate integers for easier validation. However, the API requires the phone number to be sent as a single formatted string: "+[countryCode][number]" (e.g., "+15551234567").

Your Task:

  • Create a PhoneNumber data class with countryCode: Int and number: Long.
  • Implement a PhoneNumberSerializer that implements KSerializer<PhoneNumber>.
  • In serialize, combine the two fields into the "+..." format.
  • In deserialize, parse the string by removing the '+', extracting the first 1-3 digits for the country code, and the rest for the number. (For the sake of this exercise, assume the country code is always the first 2 digits).
  • Annotate the PhoneNumber class to use your custom serializer.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.