Skip to content

R8 ProGuard

Maxwell edited this page Aug 2, 2026 · 1 revision

R8 / ProGuard

retrofit-graphql is R8-safe when using the recommended kotlinx.serialization path. This page documents the R8 configuration from the sample app and guidance for different serialization configurations.

R8 Configuration (Sample App)

Enable R8

// app/build.gradle.kts
android {
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
        }
    }
    testBuildType = "release"  // compile unit tests against the release variant
}

kotlinx.serialization: No Keep Rules Needed

The kotlinx.serialization consumer rules are shipped with the kotlinx-serialization-core/-json artifacts themselves and protect every @Serializable class on the consuming classpath. No custom keep rules are required for:

  • Generated response DTOs (GetCurrentUserData, GetMarketPlaceAppsData, etc.)
  • The legacy @Serializable models in :compat (GraphContainer<T>, GraphError and GraphError.Location, GraphQLRequest<TVariables>, PersistedQuery)
  • Any enum, variable, or input object class generated by the codegen plugin

EmptyGraphQLVariables is a plain object (not @Serializable) and needs no rules. The neutral GraphQLOperationRequest/GraphQLResponse contracts are serialized structurally by the explicit codec without reflection, so R8 is free to rename them -- the sample's verifyReleaseMapping asserts exactly that.

No library module ships keep rules. The historical :library consumer rule (-keep io.github.wax911.library.model.**) was removed because the io.github.wax911.library.* aliases emit only empty *Kt file-facade classes. R8 rules are module-owned (kotlinx artifact rules, Retrofit/OkHttp artifact rules) or consumer-owned (your Gson rules below).

Gson Upload Path: 2 Targeted Keep Rules

The sample app uses Gson for the multipart upload path (UploadMutationHelper). Gson derives JSON keys from java.lang.reflect.Field.getName(), so field names must survive R8 renaming:

# app/proguard-rules.pro

# GraphQLRequest: serialized by Gson in UploadMutationHelper.createOperationsPart()
-keep class co.anitrend.retrofit.graphql.model.GraphQLRequest {
    <fields>;
    <init>(...);
}

# UploadToStorageBucketVariables: serialized by Gson as nested variables
# within the operations payload of a multipart upload request.
-keep class co.anitrend.retrofit.graphql.sample.bucket.UploadToStorageBucketVariables {
    <fields>;
    <init>(...);
}

These rules are narrowly scoped to the exact classes used in the Gson upload path. The -keep directive preserves the class and all its members (fields, methods) so Gson's reflective serialization works correctly.

APQ + Gson Trap

If you use withPersistedQuery() in the Gson upload path, PersistedQuery is serialized as part of the request. Add:

-keep class co.anitrend.retrofit.graphql.model.request.PersistedQuery {
    <fields>;
}

This is only needed if your Gson path involves APQ. On the neutral path, GraphQLOperationRequest.withPersistedQuery() stores the extension structurally as GraphQLValue.ObjectValue, which the codec serializes without reflection -- no keep rule needed. The legacy typed kotlinx request path supports withPersistedQuery() because KotlinxGraphQLJson.encode() (:compat) merges supported extensions values into the outgoing JSON.

Verification

Release-variant JVM Unit Tests

./gradlew :app:testReleaseUnitTest

This runs JVM unit tests against release-variant classes. These tests do not execute minified R8 bytecode. The sample app has 11 unit tests (6 serialization + 5 mapper) that validate:

  • GetCurrentUserData can be deserialized from JSON
  • GetMarketPlaceAppsData can be deserialized from JSON
  • @SerialName annotations are present on release-variant generated classes
  • Error responses deserialize on the release JVM classpath
  • Empty data responses deserialize on the release JVM classpath

Instrumented Tests (R8 on Device)

./gradlew :app:releaseR8Verification -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect

This is the sample app's R8 runtime gate. It runs verifyReleaseMapping plus the managed-device pixel2api30ReleaseAndroidTest task. ReleaseSerializationTest validates serialization behavior on an actual emulator running the R8-optimized APK.

Mapping Inspection

# Check what R8 renamed
cat app/build/outputs/mapping/release/mapping.txt

Verify that no serialization-critical classes or fields are renamed:

  • Generated response DTO classes may be renamed, but their kotlinx serializers and wire-name literals must remain usable
  • GraphQLRequest fields (query, operationName, variables) should not be renamed -- this class is kept for the Gson upload path (legacy, :compat)
  • UploadToStorageBucketVariables field (upload) should not be renamed
  • GraphQLOperationRequest should be renamed -- the explicit codec does not use reflection, and the sample's verifyReleaseMapping asserts it is not accidentally kept

The sample release verification covers the kotlinx generated response runtime and the Gson multipart upload path. Concrete Gson response DTO support is covered by codegen functional tests; applications that deserialize generated response DTOs through Gson in release should add scoped keep rules and their own mapping or device verification.

What NOT to Keep

You do not need keep rules for:

  • Generated response DTOs (*Data classes) -- protected by the kotlinx artifact consumer rules when they are @Serializable
  • Legacy @Serializable models in :compat (GraphContainer, GraphError, GraphQLRequest, PersistedQuery) -- protected by the same kotlinx artifact rules
  • EmptyGraphQLVariables -- plain object, no serializer state
  • GraphQLOperationRequest / GraphQLResponse / GraphQLValue -- serialized structurally by the explicit codec, no reflection
  • Retrofit interfaces -- Retrofit ships its own consumer rules
  • OkHttp/Okio types -- they ship their own consumer rules

Full Gson Path

If your project uses Gson for the entire Retrofit path (no kotlinx), you need consumer-owned keep rules for every class serialized/deserialized by Gson. The library does not ship Gson keep rules for consumer model classes; the sample app keeps only the two classes of its own upload path:

# Your Gson-serialized model classes
-keep class com.your.app.model.** {
    <fields>;
}

If you deserialize generated @SerializedName response DTOs through Gson in release, add scoped rules for those DTOs and verify with your own mapping or device check (the sample release gate does not exercise Gson response DTO decoding).

See Also

Clone this wiki locally