Sparround

JSON serialization: Gson, Moshi, kotlinx.serialization

The three main libraries differ in null-safety and Kotlin support:

  • Gson — the oldest, reflection-based. It is unaware of Kotlin null-safety: it can write null into a non-null String field → an NPE later, exploding somewhere unexpected. Default values are lost via reflection.
  • Moshi — Square's modern library: in codegen mode (@JsonClass(generateAdapter = true)) it respects Kotlin null-safety — a null triggers a clear exception at parse time. A reflection mode also exists.
  • kotlinx.serialization — JetBrains' compiler-plugin solution: @Serializable, zero reflection, multiplatform, default values fully work. An official Retrofit converter exists.

The core interview probe: "what happens when the API returns null?" Gson — a silent null in a non-null field (a hidden bomb); Moshi codegen / kotlinx — an immediate parse error (fail fast). In banking, fail-fast wins: better than continuing with corrupt data.

kotlin
// Gson — annotasiya yalnız ad uyğunluğu üçün
data class TxDto(
    @SerializedName("tx_id") val txId: String,      // Gson null yaza bilər!
    @SerializedName("amount") val amount: Long = 0  // default itir
)

// Moshi codegen — null gələrsə JsonDataException
@JsonClass(generateAdapter = true)
data class TxDto(
    @Json(name = "tx_id") val txId: String,
    @Json(name = "amount") val amount: Long = 0     // default işləyir
)

// kotlinx.serialization
@Serializable
data class TxDto(
    @SerialName("tx_id") val txId: String,
    @SerialName("amount") val amount: Long = 0      // default işləyir
)

The same DTO in all three libraries

The ProGuard/R8 trap: reflection-based serialization (Gson, Moshi reflection) loses field names after obfuscation — parsing breaks in release builds while debug works. Fixes: @Keep/keep rules on DTOs, or moving to codegen modes (which need no rules). When you hear "works in debug, breaks in release", this is the first suspect.

🛠 Practice task

See the null bomb yourself: write data class UserDto(val name: String) and parse {"name": null} with Gson.

  • Confirm no exception is thrown, and that the NPE only arrives at name.length.
  • Parse the same JSON with Moshi codegen or kotlinx.serialization — compare where the failure surfaces.
  • Parse a money field as Double and print 0.1 + 0.2; then redo it with Long (minor units) and BigDecimal.

Done when: you can answer "why not store money in a Double" with concrete output.

📚 Sources and documentation