What this usually means
ProGuard obfuscation renames classes, methods, and fields to short meaningless names. If your code uses reflection (e.g., Gson, Retrofit, Dagger, view binding, or custom serialization), those reflective accesses break because they refer to the original names. The common causes are: not keeping serialization model fields, not keeping entry points for reflection, and missing mapping files for deobfuscation. The stack trace becomes useless without the mapping file. Often the crash appears only in production because debug builds typically disable ProGuard.
The first ten minutes — establish facts before touching code.
- 1Check if the crash only happens on release build: `adb logcat | grep -E 'AndroidRuntime|FATAL'` on the release APK
- 2Save the `mapping.txt` file from the build (located at `build/outputs/mapping/release/mapping.txt`) — this is critical for deobfuscation
- 3Deobfuscate the stack trace: `retrace.sh -verbose mapping.txt stacktrace.txt` (from Android SDK tools/proguard/bin/)
- 4Look for reflection patterns: if the deobfuscated trace shows calls to `Class.forName()`, `Method.invoke()`, or `Field.set()` on obfuscated classes, that's the root cause
- 5Check ProGuard rules: verify if rules for reflection-heavy libraries (Gson, Retrofit, Dagger) are present in `proguard-rules.pro`
- 6Test with a minimal reproduction: create a release APK with ProGuard enabled and run unit tests that exercise the crashing path
The specific files, logs, configs, and dashboards that usually own this bug.
- search`app/build/outputs/mapping/release/mapping.txt` — the mapping file for obfuscated names
- search`app/proguard-rules.pro` — ProGuard rules file; check for missing -keep rules
- search`gradle.properties` — ensure `android.enableR8.fullMode=false` if using R8 with compatibility issues
- search`app/src/main/AndroidManifest.xml` — verify all components are listed (ProGuard might remove unused ones if not kept)
- searchCrashlytics or Firebase console — look for missing mapping file upload; ensure mapping.txt is uploaded with the release
- searchBuild output logs: `./gradlew assembleRelease --info | grep -i proguard` to see which classes are obfuscated
- searchAPK decompiled: `unzip -o app-release.apk -d apk_decompiled && tree apk_decompiled/smali` to see actual class names
Practical causes, not theory. These are the things you will actually find.
- warningMissing `-keep` rules for classes used in reflection (Gson models, Retrofit interfaces, Dagger components)
- warningMapping file not preserved or not uploaded to crash reporting service (Crashlytics, Sentry)
- warningUsing `@Keep` annotation but the annotation itself is obfuscated (need to keep the annotation class)
- warningSerialization (Parcelable, Serializable) classes not kept — ProGuard renames fields and breaks deserialization
- warningProGuard removes unused methods/classes that are actually used via reflection from libraries or generated code
- warningR8 full mode aggressively removes classes and methods; need to add `-keep` rules for entry points
Concrete fix directions. Pick the one that matches your root cause.
- buildAdd `-keep class com.example.models.** { *; }` to keep all fields and methods for serialization models
- buildFor Gson: `-keepattributes Signature`, `-keepattributes *Annotation*`, and `-keep class com.google.gson.** { *; }`
- buildFor Retrofit: `-keep,allowobfuscation interface com.example.api.**`
- buildFor Dagger: `-keep class dagger.** { *; }` and `-keep class * extends dagger.Component { *; }`
- buildUpload mapping.txt to Crashlytics: `firebase crashlytics:mapping mapping.txt` or set `firebaseCrashlyticsMappingFileUploadEnabled true` in Gradle
- buildTest with `android.enableR8.fullMode=false` in gradle.properties to disable R8 full mode if rules are complex
A fix you cannot prove is a guess. Close the loop.
- verifiedRun release build again and check that the crash is gone: `adb install app-release.apk` and navigate to the crashing path
- verifiedDeobfuscate a fresh stack trace: confirm that it now shows clear class names
- verifiedCheck the mapping file for the specific class that was crashing: `grep 'com.example.MyClass' mapping.txt` should show original name
- verifiedEnable ProGuard verbose logging: `-verbose` in proguard-rules.pro and look for warnings about removed classes
- verifiedInspect the APK with `apkanalyzer` or `dexdump` to verify that kept classes are present with original names
- verifiedRun a smoke test suite on the release APK using instrumentation tests or Robolectric with ProGuard rules
Things that make this bug worse or harder to find.
- warningDon't blindly add `-keep class * { *; }` — this disables obfuscation entirely and defeats the purpose
- warningDon't ignore warnings about `-keep` rules that conflict; they may cause unexpected removal
- warningDon't forget to upload mapping.txt after every release; without it, crash reports are useless
- warningDon't assume debug build behavior matches release; always test with ProGuard enabled
- warningDon't use `@Keep` on inner classes without also keeping the outer class (ProGuard may remove the outer)
- warningDon't rely solely on default ProGuard rules from libraries; review and customize them for your app
Gson deserialization fails after ProGuard obfuscation
Timeline
- 08:00Release build triggers on CI, starts uploading to Play Store
- 08:15Production crash reports spike in Crashlytics: Fatal Exception: com.google.gson.JsonSyntaxException
- 08:20Stack trace shows only obfuscated names: a.b.c$d.e() — no mapping file found in Crashlytics
- 08:25I check the CI artifacts; mapping.txt is missing from the build output because the upload step failed silently
- 08:30I find a local copy of mapping.txt from my machine (same git tag) and deobfuscate: `retrace.sh -verbose mapping.txt crash.txt`
- 08:35Deobfuscated trace reveals NullPointerException in `UserResponse.toJson()` — field `email` is null in JSON but not nullable in model
- 08:40I check the model class: `data class UserResponse(val email: String)` — but the API now returns `email` as `null` for some users
- 08:45Fix: change field to `val email: String?` and add `@SerializedName("email")` to avoid mismatch. Also add `-keep class com.example.models.** { *; }` to prevent field renaming
- 08:55Release new build, upload mapping.txt, monitor — crash rate drops to zero
The morning started with a routine release build. By 8:15, Crashlytics was lighting up with a new crash affecting ~5% of users. The stack trace was completely obfuscated — `a.b.c$d.e()` — meaningless. My first instinct was to find the mapping file. The CI had failed to upload it due to a permission issue in the Gradle task, a problem I'd ignored for weeks.
I managed to locate a local mapping file from the same git tag. Using retrace.sh, I deobfuscated the trace and saw the real culprit: `UserResponse.toJson()` was throwing a NullPointerException. The JSON response had a null `email` field, but the Kotlin data class declared it as non-null `String`. ProGuard had also renamed the field, but that was secondary.
The fix was straightforward: make `email` nullable (`String?`) and add `@SerializedName("email")` to be safe. I also added a broad `-keep` rule for all model classes to prevent future field renaming issues. After re-releasing with the mapping file properly uploaded, the crash disappeared. The lesson: never assume your CI is uploading artifacts correctly, and always test with a production-like ProGuard configuration before releasing.
Root cause
Non-nullable field `email` in `UserResponse` data class when API can return null, combined with ProGuard renaming the field (though the rename was not the primary cause).
The fix
Changed `val email: String` to `val email: String?` and added `@SerializedName("email")`. Also added `-keep class com.example.models.** { *; }` to ProGuard rules.
The lesson
Always make data classes nullable for optional fields, and ensure mapping.txt is uploaded to crash reporting on every release. ProGuard obfuscation can hide the real issue; deobfuscate the stack trace before debugging.
ProGuard (and its successor R8) obfuscates your code by renaming classes, methods, and fields to short, meaningless names. The mapping file (`mapping.txt`) records the original-to-obfuscated name mapping. Without it, a stack trace from a release build is nearly useless. The mapping file is generated in `build/outputs/mapping/release/` during a release build. You must preserve this file and upload it to your crash reporting service (Crashlytics, Sentry, etc.).
ProGuard also performs optimization (dead code removal) and shrinking (removing unused classes). This can cause crashes if code that is only used via reflection (e.g., Gson, Retrofit, Dagger) is removed. The solution is to add `-keep` rules to prevent specific classes or members from being removed or renamed.
Gson/JSON serialization: Gson uses reflection to set fields. If a field is renamed by ProGuard, Gson cannot match the JSON key to the field. Solution: use `@SerializedName` annotations and add `-keep class com.example.models.** { *; }`. Also keep `-keepattributes Signature` to preserve generic type information.
Retrofit: Retrofit interfaces are proxied dynamically. ProGuard may rename the interface methods, causing the proxy to fail. Solution: `-keep,allowobfuscation interface com.example.api.**` to keep the interface names but allow obfuscation of method bodies.
Dagger/Hilt: Dagger generates code that accesses inject fields via reflection. If those fields are removed or renamed, injection fails. Solution: add `-keep class dagger.** { *; }` and `-keep class * extends dagger.Component { *; }`.
View binding: `@BindView` or `viewBinding` use reflection to find views. If the binding class is obfuscated, it won't find the view ID. Solution: keep the R class and binding classes with `-keep class **.R { *; }` and `-keep class **.databinding.** { *; }`.
To deobfuscate a stack trace, use the `retrace.sh` script (or `retrace.bat` on Windows) located in `$ANDROID_HOME/tools/proguard/bin/`. Run: `retrace.sh -verbose mapping.txt stacktrace.txt`. The `-verbose` flag shows both obfuscated and deobfuscated lines. You can also use Android Studio's built-in deobfuscation: open the stack trace in the Logcat window and click on the obfuscated line — it will prompt you to load a mapping file.
If you don't have the mapping file, you can decompile the APK to see the obfuscated names. Use `unzip -o app-release.apk -d apk_decompiled` then `tree apk_decompiled/smali` to see the directory structure. Tools like `jadx` or `apktool` can convert smali back to readable Java. However, this is time-consuming; always preserve the mapping file.
R8 is the default shrinker/obfuscator since Android Gradle Plugin 3.4. It has a 'full mode' that is more aggressive: it assumes all code that is not kept is dead, even if it might be used via reflection from libraries. This can break things like Kotlin metadata, enum valuesOf() calls, and service loaders. Symptoms include `NoSuchMethodError` or `ClassNotFoundException` for classes that are present in the APK but not kept.
To disable full mode, add `android.enableR8.fullMode=false` to `gradle.properties`. Better yet, audit your code and add explicit `-keep` rules for any code that uses reflection. The R8 documentation recommends testing with full mode enabled to catch missing rules early.
Use specific `-keep` rules rather than broad ones. For example, instead of `-keep class * { *; }`, use `-keep class com.example.models.** { *; }` for your model classes. Broad rules can prevent optimization and bloat the APK. For libraries, follow their official ProGuard rules. For example, Gson recommends: `-keepattributes Signature`, `-keepattributes *Annotation*`, and `-keep class com.google.gson.** { *; }`.
Test ProGuard rules with a release build and run a smoke test suite. Use Android's test utilities like `testOptions.unitTests.all { systemProperty 'robolectric.enabled', 'true' }` to run unit tests on release build. Also consider using the Android Studio profiler to check that all classes are correctly kept.
Frequently asked questions
What is the difference between ProGuard and R8?
ProGuard is the traditional obfuscator, while R8 is its replacement introduced by Google. R8 is faster and more aggressive. Both perform shrinking, optimization, and obfuscation. R8 is enabled by default in Android Gradle Plugin 3.4+. You can switch back to ProGuard by adding `android.enableR8=false` in gradle.properties, but it's not recommended as R8 is the future.
How do I fix a crash that only happens on release builds?
First, get the mapping.txt from the release build and deobfuscate the stack trace. Identify if the crash is due to reflection, serialization, or missing class. Add appropriate `-keep` rules to prevent the problematic class or member from being obfuscated. Test by building a release APK and running the specific scenario. Also ensure mapping.txt is uploaded to your crash reporting service.
Why does my app crash with 'ClassNotFoundException' even though the class exists in the APK?
This can happen if the class is removed by ProGuard because it's considered unused. Even if the class file exists in the APK, ProGuard may have removed it from the DEX. Check the `mapping.txt` to see if the class was removed (it won't appear in the mapping). Add a `-keep class` rule for that class. Also, ensure that the class is not being loaded via reflection without a keep rule.
How do I preserve field names for Gson serialization?
Use `@SerializedName` annotations on each field to specify the JSON key. Additionally, add `-keepattributes Signature` and `-keep class com.yourpackage.models.** { *; }` to prevent field renaming. If you don't want to keep all fields, you can keep specific fields with `-keepclassmembers class * { @com.google.gson.annotations.SerializedName <fields>; }`.
Should I use @Keep annotation or ProGuard rules?
Both work, but `@Keep` is more convenient for individual classes/methods. However, `@Keep` only prevents the annotated element from being removed or renamed; it does not affect its inner classes or methods. For broad protection, use ProGuard rules. Also, note that `@Keep` is only available in AndroidX (`androidx.annotation.Keep`) and is not recognized by ProGuard by default — you need to keep the annotation itself: `-keep class androidx.annotation.Keep { *; }`.