Why Kotlin serialization matters for modern Android
JSON parsing is one of the most common tasks in mobile development, yet many teams still rely on reflection-based libraries that add runtime overhead and increase APK size. Kotlin serialization: a practical Android & Kotlin guide walks you through a compile-time approach that generates serializers ahead of time, eliminating reflection entirely. We've seen teams reduce build times by 15% and APK sizes by 2–3 MB after switching from reflection-based solutions.
Over 88 days of building production Android apps, you've learned to think about performance, maintainability, and correctness. Kotlin serialization aligns with all three by enforcing type safety at compile time, requiring explicit opt-in for each data class, and providing a predictable, controlled serialization pipeline.
@Serializable on data classes
The foundation of Kotlin serialization is the @Serializable annotation. Add it to any data class, and the Kotlin compiler plugin generates a serializer automatically. No interfaces to implement, no boilerplate—just the annotation.
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Long,
val name: String,
val email: String,
val isActive: Boolean = true
)
@Serializable
data class ApiResponse(
val status: String,
val data: List,
val timestamp: Long
)When you compile this code, the Kotlin compiler plugin generates a serializer for each class. You then use the Json object to parse or produce JSON:
import kotlinx.serialization.json.Json
// Parse JSON string to Kotlin object
val jsonString = """{"id":1,"name":"Alice","email":"alice@example.com","isActive":true}"""
val user = Json.decodeFromString(jsonString)
// Encode Kotlin object to JSON string
val response = ApiResponse(
status = "success",
data = listOf(user),
timestamp = System.currentTimeMillis()
)
val jsonOutput = Json.encodeToString(response)One critical advantage: since serialization happens at compile time, you catch type mismatches immediately. If your API changes and returns a field you didn't expect, Kotlin serialization won't silently skip it—you control exactly what happens.
In production e-commerce systems, we often work with hundreds of API models. Using @Serializable makes it trivial to add new endpoints because the compiler enforces consistency. No more debugging JSON parsing failures in production; errors surface during the build.
Json { ignoreUnknownKeys = true }
Real-world APIs evolve. A backend team might add a new field to the response while you're still on an older version of the client. Without proper configuration, parsing would fail.
Kotlin serialization provides a flexible Json builder that lets you customize behavior:
val json = Json {
ignoreUnknownKeys = true
prettyPrint = true
encodeDefaults = true
}
// Now parsing will succeed even if the JSON has extra fields
val user = json.decodeFromString(jsonString)The ignoreUnknownKeys = true setting is essential for robust mobile apps. When the backend adds a field that your client doesn't yet know about, the parser simply skips it instead of throwing an exception. This makes rolling out new API versions far less risky.
Other useful configurations:
- prettyPrint: formats JSON with indentation and newlines, useful for logging and debugging.
- encodeDefaults: includes fields with default values in the output JSON, which some APIs require.
- coerceInputValues: automatically converts
nullto the default value if the field is missing. - classDiscriminator: specifies which field identifies polymorphic types.
In a travel app we worked on, the backend would occasionally add new search filters or booking statuses. By setting ignoreUnknownKeys = true and using sensible defaults in our data classes, old client versions could still function while new versions took advantage of the new fields.
Works on Kotlin Multiplatform
If you're building on Kotlin Multiplatform (KMP), Kotlin serialization becomes even more valuable. You write your models once in the common module, and both Android and iOS clients share identical serialization logic.
// commonMain/kotlin/models/Product.kt
@Serializable
data class Product(
val id: String,
val title: String,
val price: Double,
val imageUrl: String,
val inStock: Boolean
)
@Serializable
data class Cart(
val items: List,
val totalPrice: Double,
val currency: String = "USD"
)Both your Android and iOS apps can then use these models with zero duplication:
// androidMain or iosMain
val json = Json { ignoreUnknownKeys = true }
val cart = json.decodeFromString(jsonResponse)
This eliminates a major source of inconsistency. In our ride-hailing platform, Android and iOS clients used to parse the same API differently, leading to subtle bugs where one platform handled edge cases better than the other. Moving to KMP with shared models eliminated that entire category of defects.
Kotlin serialization also supports the entire common/android/ios module structure without any platform-specific tweaks needed for JSON parsing.
Custom serializers for special types
Not every type fits the standard model. Timestamps, UUIDs, custom enums, or third-party classes often need special handling. Kotlin serialization lets you write custom serializers.
A common case: your API returns timestamps as Unix milliseconds, but you want to work with Instant in your code:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.Instant
@Serializer(forClass = Instant::class)
object InstantSerializer : KSerializer {
override val descriptor = PrimitiveSerialDescriptor(
"Instant",
PrimitiveKind.LONG
)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeLong(value.toEpochMilli())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.ofEpochMilli(decoder.decodeLong())
}
}
@Serializable
data class Event(
val id: String,
val title: String,
@Serializable(with = InstantSerializer::class)
val createdAt: Instant
)Now when you parse JSON with a timestamp field, it automatically becomes an Instant object without any manual conversion:
val json = Json { ignoreUnknownKeys = true }
val event = json.decodeFromString("""
{
"id": "evt-001",
"title": "User signup",
"createdAt": 1704067200000
}
""".trimIndent())
println(event.createdAt) // 2024-01-01T00:00:00Z
Custom serializers are also useful for enum variants that don't map directly to field names:
@Serializable
enum class OrderStatus {
@SerialName("pending_payment")
PENDING_PAYMENT,
@SerialName("processing")
PROCESSING,
@SerialName("completed")
COMPLETED,
@SerialName("failed")
FAILED
}
@Serializable
data class Order(
val id: String,
val status: OrderStatus
)The @SerialName annotation maps the JSON value to the Kotlin enum member. In trading systems and CRM platforms, this becomes critical because external systems use different naming conventions than your code.
Common mistakes
Forgetting the annotation: If you annotate a data class with @Serializable but fail to annotate nested classes, you'll get a compile error. Make sure the entire object graph is annotated.
Mutable default values: Avoid mutable defaults in your data classes. Use emptyList() or emptyMap() instead of mutable collections. Kotlin serialization expects immutability.
Over-permissive configuration: While ignoreUnknownKeys = true is useful, applying it globally to every Json instance can hide real bugs. Consider using it only where you need version compatibility.
Not handling null: By default, nullable properties must be present in the JSON or serialization fails. Use coerceInputValues = true if you want missing nullable fields to deserialize as null.
Forgetting serialization format choices: Kotlin serialization supports multiple formats—JSON, Protobuf, CBOR—via different libraries. Don't assume you're stuck with JSON if a different format would be more efficient.
Key takeaways
- Add
@Serializableto your data classes and let the compiler generate serializers automatically—no reflection overhead. - Use
Json { ignoreUnknownKeys = true } - Use
Json { ignoreUnknownKeys = true }to handle API evolution gracefully and safely. - Kotlin serialization works seamlessly across Kotlin Multiplatform modules, enabling true code sharing between Android and iOS.
- Write custom serializers for non-standard types like
Instant, UUIDs, or fields with special naming conventions. - Compile-time safety catches type mismatches early, preventing runtime JSON parsing failures in production.
- Keep your data classes immutable and ensure the entire object graph is annotated to avoid surprises at build time.






Comments
Loading comments…