DEV Community

yuus_company
yuus_company

Posted on

JSON to Kotlin data class: Gson vs Moshi vs kotlinx.serialization annotations

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
}
Enter fullscreen mode Exit fullscreen mode

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?
)
Enter fullscreen mode Exit fullscreen mode

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?
)
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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)