Turning a JSON payload into a Kotlin data class looks trivial until you hit the details: which annotations, what about snake_case, what happens to missing fields? The answers differ meaningfully across the three mainstream libraries. Here's the same payload, three ways.
The JSON:
{
"user_id": "u-1042",
"display_name": "Yush",
"is_verified": true,
"follower_count": 1204,
"last_login": null
}
Gson
data class UserProfile(
@SerializedName("user_id") val userId: String,
@SerializedName("display_name") val displayName: String,
@SerializedName("is_verified") val isVerified: Boolean,
@SerializedName("follower_count") val followerCount: Int,
@SerializedName("last_login") val lastLogin: String?
)
The catch everyone learns the hard way: Gson ignores Kotlin null-safety. It uses reflection and will happily write null into a non-nullable val if the field is missing — you get the crash later, far from the parse site. Default values are also ignored unless every field has one. Gson is fine for legacy codebases, but it predates Kotlin and it shows.
Moshi (with KSP codegen)
@JsonClass(generateAdapter = true)
data class UserProfile(
@Json(name = "user_id") val userId: String,
@Json(name = "display_name") val displayName: String,
@Json(name = "is_verified") val isVerified: Boolean,
@Json(name = "follower_count") val followerCount: Int,
@Json(name = "last_login") val lastLogin: String?
)
With codegen (generateAdapter = true), Moshi enforces nullability at parse time: a missing non-nullable field throws a clear JsonDataException naming the field. Default values work as expected. This is the pragmatic choice for Android projects already on OkHttp/Retrofit.
kotlinx.serialization
@Serializable
data class UserProfile(
@SerialName("user_id") val userId: String,
@SerialName("display_name") val displayName: String,
@SerialName("is_verified") val isVerified: Boolean,
@SerialName("follower_count") val followerCount: Int,
@SerialName("last_login") val lastLogin: String? = null
)
Compiler-plugin based, zero reflection, multiplatform (JVM/JS/Native/Wasm). Two behaviors worth knowing: unknown JSON keys fail by default (turn on ignoreUnknownKeys = true in the Json {} config for API work), and fields with default values are skipped during serialization unless you set encodeDefaults = true. If you're starting fresh or targeting KMP, this is the one.
Quick comparison
| Gson | Moshi (codegen) | kotlinx.serialization | |
|---|---|---|---|
| Null-safety enforced | ❌ silently breaks | ✅ throws clearly | ✅ throws clearly |
| Default values | unreliable | ✅ | ✅ |
| Reflection at runtime | yes | no (codegen) | no (compiler plugin) |
| Multiplatform | ❌ | ❌ | ✅ |
| Best for | legacy code | Android + Retrofit | new code, KMP |
Generating the boilerplate
Writing @SerialName for thirty fields by hand is nobody's idea of fun. I use this JSON to Kotlin converter — paste the payload, and it derives the data class with nested classes and nullable types from the actual values, in the browser (nothing uploaded). Then switch the annotations to whichever library your project uses.
TL;DR
- Gson silently violates Kotlin null-safety; prefer Moshi or kotlinx.serialization.
- Moshi: best drop-in for Android/Retrofit stacks.
- kotlinx.serialization: default for new projects and multiplatform — remember
ignoreUnknownKeys.
Top comments (0)