The release build crashes on JSON parsing while debug works fine
A screen parses a JSON response into a model class. It works in debug, but the release build either crashes or silently produces a model whose fields are all null / zero. Nothing references the model's properties from Kotlin — the JSON library reaches them reflectively. Assume the JSON is identical in both builds.
// app/build.gradle.kts
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}
}
}
// Response.kt — constructed only by the JSON library, via reflection
data class Response(val id: Long, val title: String, val price: Double)
val response: Response = gson.fromJson(body, Response::class.java)
Diagnose why only the release variant is affected, and fix it.
Only release sets isMinifyEnabled, so R8 runs only there. Nothing reaches the members statically, so R8 calls them dead and renames or drops them; the reflective lookup fails. Fix with a keep rule or @Keep, not by disabling minification.
- ✗Blaming the JSON payload or the network layer instead of the
release-onlyR8pass - ✗Fixing it by setting
isMinifyEnabled = false, which throws away the shrinking entirely - ✗Assuming
R8leaves a class alone because something reaches it reflectively at runtime
- →Why is a
@Keepannotation usually better than a hand-written wildcard keep rule? - →What would a mapping file have told you from the release stack trace?
Why only release breaks
isMinifyEnabled = true is set on release alone, so R8 runs only there. That is the first thing to check: when a bug lives in exactly one variant, look at what that variant does extra.
R8 walks the reachable graph from the keep rules and the entry points. Response is constructed only by the JSON library, by class name — nothing calls its constructor or reads its properties statically. To R8 that is dead code: the properties are renamed to a, b, c or removed outright. Reflection then looks for a field called title, does not find it, and leaves null behind.
// Option 1 — the @Keep annotation (androidx.annotation)
@Keep
data class Response(val id: Long, val title: String, val price: Double)
The other option is a keep rule in proguard-rules.pro covering the model package. Both preserve the names. Setting isMinifyEnabled = false also makes the symptom go away, but it gives up shrinking and obfuscation for the whole build — do not do that.