Skip to content

Commit ecb9f8c

Browse files
authored
Adjust ProGuard default rules and shrinking tests (#2420)
* Adjust ProGuard default rules and shrinking tests * Adjust comment * Add shrinking test for class without no-args constructor; improve docs * Improve Unsafe mention in Troubleshooting Guide * Improve comment for `-if class *`
1 parent 6d9c356 commit ecb9f8c

File tree

12 files changed

+129
-60
lines changed

12 files changed

+129
-60
lines changed

Troubleshooting.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ Note: For newer Gson versions these rules might be applied automatically; make s
313313

314314
**Symptom:** A `JsonIOException` with the message 'Abstract classes can't be instantiated!' is thrown; the class mentioned in the exception message is not actually `abstract` in your source code, and you are using the code shrinking tool R8 (Android app builds normally have this configured by default).
315315

316+
Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.
317+
316318
**Reason:** The code shrinking tool R8 performs optimizations where it removes the no-args constructor from a class and makes the class `abstract`. Due to this Gson cannot create an instance of the class.
317319

318320
**Solution:** Make sure the class has a no-args constructor, then adjust your R8 configuration file to keep the constructor of the class. For example:
@@ -324,6 +326,10 @@ Note: For newer Gson versions these rules might be applied automatically; make s
324326
}
325327
```
326328

329+
You can also use `<init>(...);` to keep all constructors of that class, but then you might actually rely on `sun.misc.Unsafe` on both JDK and Android to create classes without no-args constructor, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) for more information.
330+
327331
For Android you can add this rule to the `proguard-rules.pro` file, see also the [Android documentation](https://developer.android.com/build/shrink-code#keep-code). In case the class name in the exception message is obfuscated, see the Android documentation about [retracing](https://developer.android.com/build/shrink-code#retracing).
328332

329-
Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.
333+
For Android you can alternatively use the [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep) on the class or constructor you want to keep. That might be easier than having to maintain a custom R8 configuration.
334+
335+
Note that the latest Gson versions (> 2.10.1) specify a default R8 configuration. If your class is a top-level class or is `static`, has a no-args constructor and its fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional R8 configuration.

examples/android-proguard-example/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ details on how ProGuard can be configured.
1212
The R8 code shrinker uses the same rule format as ProGuard, but there are differences between these two
1313
tools. Have a look at R8's Compatibility FAQ, and especially at the [Gson section](https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#gson).
1414

15-
Note that newer Gson versions apply some of the rules shown in `proguard.cfg` automatically by default,
15+
Note that the latest Gson versions (> 2.10.1) apply some of the rules shown in `proguard.cfg` automatically by default,
1616
see the file [`gson/META-INF/proguard/gson.pro`](/gson/src/main/resources/META-INF/proguard/gson.pro) for
17-
the Gson version you are using.
17+
the Gson version you are using. In general if your classes are top-level classes or are `static`, have a no-args constructor and their fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional ProGuard or R8 configuration.
18+
19+
An alternative to writing custom keep rules for your classes in the ProGuard configuration can be to use
20+
Android's [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep).

gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,15 @@ static String checkInstantiable(Class<?> c) {
7676
if (Modifier.isAbstract(modifiers)) {
7777
// R8 performs aggressive optimizations where it removes the default constructor of a class
7878
// and makes the class `abstract`; check for that here explicitly
79-
if (c.getDeclaredConstructors().length == 0) {
80-
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
81-
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
82-
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
83-
}
84-
85-
return "Abstract classes can't be instantiated! Register an InstanceCreator"
86-
+ " or a TypeAdapter for this type. Class name: " + c.getName();
79+
/*
80+
* Note: Ideally should only show this R8-specific message when it is clear that R8 was
81+
* used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this
82+
* issue with R8 occurs most, R8 seems to keep some constructors for some reason while
83+
* still making the class abstract
84+
*/
85+
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
86+
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
87+
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
8788
}
8889
return null;
8990
}

gson/src/main/resources/META-INF/proguard/gson.pro

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
# Keep Gson annotations
1515
# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
16-
-keepattributes *Annotation*
16+
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
1717

1818

1919
### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
@@ -24,18 +24,20 @@
2424
-keep class com.google.gson.reflect.TypeToken { *; }
2525

2626
# Keep any (anonymous) classes extending TypeToken
27-
-keep class * extends com.google.gson.reflect.TypeToken
27+
-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
2828

2929
# Keep classes with @JsonAdapter annotation
30-
-keep @com.google.gson.annotations.JsonAdapter class *
30+
-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
3131

3232
# Keep fields with @SerializedName annotation, but allow obfuscation of their names
3333
-keepclassmembers,allowobfuscation class * {
3434
@com.google.gson.annotations.SerializedName <fields>;
3535
}
3636

3737
# Keep fields with any other Gson annotation
38-
-keepclassmembers class * {
38+
# Also allow obfuscation, assuming that users will additionally use @SerializedName or
39+
# other means to preserve the field names
40+
-keepclassmembers,allowobfuscation class * {
3941
@com.google.gson.annotations.Expose <fields>;
4042
@com.google.gson.annotations.JsonAdapter <fields>;
4143
@com.google.gson.annotations.Since <fields>;
@@ -44,15 +46,25 @@
4446

4547
# Keep no-args constructor of classes which can be used with @JsonAdapter
4648
# By default their no-args constructor is invoked to create an adapter instance
47-
-keep class * extends com.google.gson.TypeAdapter {
49+
-keepclassmembers class * extends com.google.gson.TypeAdapter {
4850
<init>();
4951
}
50-
-keep class * implements com.google.gson.TypeAdapterFactory {
52+
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
5153
<init>();
5254
}
53-
-keep class * implements com.google.gson.JsonSerializer {
55+
-keepclassmembers class * implements com.google.gson.JsonSerializer {
5456
<init>();
5557
}
56-
-keep class * implements com.google.gson.JsonDeserializer {
58+
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
5759
<init>();
5860
}
61+
62+
# If a class is used in some way by the application, and has fields annotated with @SerializedName
63+
# and a no-args constructor, keep those fields and the constructor
64+
# Based on https://issuetracker.google.com/issues/150189783#comment11
65+
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation
66+
-if class *
67+
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> {
68+
<init>();
69+
@com.google.gson.annotations.SerializedName <fields>;
70+
}

gson/src/test/java/com/google/gson/functional/Java17RecordTest.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public int i() {
8989

9090
var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
9191
assertThat(exception).hasMessageThat()
92-
.isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported");
92+
.isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported");
9393
}
9494

9595
@Test
@@ -154,7 +154,7 @@ record LocalRecord(String s) {
154154
// TODO: Adjust this once Gson throws more specific exception type
155155
catch (RuntimeException e) {
156156
assertThat(e).hasMessageThat()
157-
.isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]");
157+
.isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]");
158158
assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException);
159159
}
160160
}
@@ -227,7 +227,7 @@ public void testPrimitiveJsonNullValue() {
227227
String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
228228
var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
229229
assertThat(e).hasMessageThat()
230-
.isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte");
230+
.isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte");
231231
}
232232

233233
/**
@@ -384,8 +384,8 @@ record Blocked(int b) {}
384384

385385
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
386386
assertThat(exception).hasMessageThat()
387-
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
388-
". Register a TypeAdapter for this type or adjust the access filter.");
387+
.isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
388+
". Register a TypeAdapter for this type or adjust the access filter.");
389389
}
390390

391391
@Test
@@ -396,15 +396,15 @@ public void testReflectionFilterBlockInaccessible() {
396396

397397
var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
398398
assertThat(exception).hasMessageThat()
399-
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
400-
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
401-
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
399+
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
400+
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
401+
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
402402

403403
exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
404404
assertThat(exception).hasMessageThat()
405-
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
406-
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
407-
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
405+
.isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
406+
+ " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
407+
+ " type, adjust the access filter or increase the visibility of the element and its declaring type.");
408408

409409
assertThat(gson.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}");
410410
assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2));
@@ -427,7 +427,8 @@ record LocalRecord(int i) {}
427427

428428
var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
429429
assertThat(exception).hasMessageThat()
430-
.isEqualTo("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
431-
+ " this type. Class name: java.lang.Record");
430+
.isEqualTo("Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
431+
+ " or a TypeAdapter for this type. Class name: java.lang.Record"
432+
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
432433
}
433434
}

gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,14 @@
1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.fail;
2121

22-
import com.google.gson.InstanceCreator;
23-
import com.google.gson.ReflectionAccessFilter;
2422
import com.google.gson.reflect.TypeToken;
25-
import java.lang.reflect.Type;
2623
import java.util.Collections;
2724
import org.junit.Test;
2825

2926
public class ConstructorConstructorTest {
3027
private ConstructorConstructor constructorConstructor = new ConstructorConstructor(
31-
Collections.<Type, InstanceCreator<?>>emptyMap(), true,
32-
Collections.<ReflectionAccessFilter>emptyList()
28+
Collections.emptyMap(), true,
29+
Collections.emptyList()
3330
);
3431

3532
private abstract static class AbstractClass {
@@ -39,7 +36,7 @@ public AbstractClass() { }
3936
private interface Interface { }
4037

4138
/**
42-
* Verify that ConstructorConstructor does not try to invoke no-arg constructor
39+
* Verify that ConstructorConstructor does not try to invoke no-args constructor
4340
* of abstract class.
4441
*/
4542
@Test
@@ -49,9 +46,10 @@ public void testGet_AbstractClassNoArgConstructor() {
4946
constructor.construct();
5047
fail("Expected exception");
5148
} catch (RuntimeException exception) {
52-
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated! "
53-
+ "Register an InstanceCreator or a TypeAdapter for this type. "
54-
+ "Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass");
49+
assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated!"
50+
+ " Adjust the R8 configuration or register an InstanceCreator or a TypeAdapter for this type."
51+
+ " Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass"
52+
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
5553
}
5654
}
5755

@@ -62,9 +60,9 @@ public void testGet_Interface() {
6260
constructor.construct();
6361
fail("Expected exception");
6462
} catch (RuntimeException exception) {
65-
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated! "
66-
+ "Register an InstanceCreator or a TypeAdapter for this type. "
67-
+ "Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
63+
assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated!"
64+
+ " Register an InstanceCreator or a TypeAdapter for this type."
65+
+ " Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface");
6866
}
6967
}
7068
}

shrinker-test/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@
199199
<dependencies>
200200
<dependency>
201201
<!-- R8 dependency used above -->
202+
<!-- Note: For some reason Maven shows the warning "Missing POM for com.android.tools:r8:jar",
203+
but it appears that can be ignored -->
202204
<groupId>com.android.tools</groupId>
203205
<artifactId>r8</artifactId>
204206
<version>8.0.40</version>

shrinker-test/proguard.pro

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
-keep class com.example.DefaultConstructorMain {
1919
public static java.lang.String runTest();
2020
public static java.lang.String runTestNoJdkUnsafe();
21+
public static java.lang.String runTestNoDefaultConstructor();
2122
}
2223

2324

@@ -27,3 +28,21 @@
2728
-keepclassmembers class com.example.ClassWithNamedFields {
2829
!transient <fields>;
2930
}
31+
32+
-keepclassmembernames class com.example.ClassWithExposeAnnotation {
33+
<fields>;
34+
}
35+
-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation {
36+
** f;
37+
}
38+
-keepclassmembernames class com.example.ClassWithVersionAnnotations {
39+
<fields>;
40+
}
41+
42+
43+
-keepclassmembernames class com.example.DefaultConstructorMain$TestClass {
44+
<fields>;
45+
}
46+
-keepclassmembernames class com.example.DefaultConstructorMain$TestClassNotAbstract {
47+
<fields>;
48+
}

shrinker-test/r8.pro

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,21 @@
44
### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard
55
### See https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode
66

7-
# Keep the no-args constructor of deserialized classes
8-
-keepclassmembers class com.example.ClassWithDefaultConstructor {
9-
<init>();
10-
}
11-
-keepclassmembers class com.example.GenericClasses$GenericClass {
12-
<init>();
13-
}
14-
-keepclassmembers class com.example.GenericClasses$UsingGenericClass {
15-
<init>();
16-
}
17-
-keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass {
18-
<init>();
19-
}
20-
217
# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to
228
# preserve the generic signature
239
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
2410
-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass
2511

2612
# Don't obfuscate class name, to check it in exception message
2713
-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass
14+
-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor
15+
2816
# This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract
2917
-keep class com.example.DefaultConstructorMain$TestClassNotAbstract {
3018
@com.google.gson.annotations.SerializedName <fields>;
3119
}
3220

3321
# Keep enum constants which are not explicitly used in code
34-
-keep class com.example.EnumClass {
22+
-keepclassmembers class com.example.EnumClass {
3523
** SECOND;
3624
}

shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
public class ClassWithJsonAdapterAnnotation {
2525
// For this field don't use @SerializedName and ignore it for deserialization
26+
// Has custom ProGuard rule to keep the field name
2627
@JsonAdapter(value = Adapter.class, nullSafe = false)
2728
DummyClass f;
2829

0 commit comments

Comments
 (0)