Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
164: Custom Serializers
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
PhoneNumberdata class withcountryCode: Intandnumber: Long. - Implement a
PhoneNumberSerializerthat implementsKSerializer<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
PhoneNumberclass to use your custom serializer.
There are no comments for now.