Skip to content

Commit

Permalink
Merge branch 'main' into marcono1234/strictness2-fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcono1234 committed Jul 29, 2023
2 parents f00f357 + 3d241ca commit a657c16
Show file tree
Hide file tree
Showing 27 changed files with 272 additions and 346 deletions.
8 changes: 7 additions & 1 deletion Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ Note: For newer Gson versions these rules might be applied automatically; make s

**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).

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.

**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.

**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:
Expand All @@ -324,6 +326,10 @@ Note: For newer Gson versions these rules might be applied automatically; make s
}
```

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.

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).

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.
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.

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.
7 changes: 5 additions & 2 deletions examples/android-proguard-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ details on how ProGuard can be configured.
The R8 code shrinker uses the same rule format as ProGuard, but there are differences between these two
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).

Note that newer Gson versions apply some of the rules shown in `proguard.cfg` automatically by default,
Note that the latest Gson versions (> 2.10.1) apply some of the rules shown in `proguard.cfg` automatically by default,
see the file [`gson/META-INF/proguard/gson.pro`](/gson/src/main/resources/META-INF/proguard/gson.pro) for
the Gson version you are using.
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.

An alternative to writing custom keep rules for your classes in the ProGuard configuration can be to use
Android's [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep).
2 changes: 1 addition & 1 deletion gson/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava-testlib</artifactId>
<version>32.0.1-jre</version>
<version>32.1.1-jre</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
11 changes: 5 additions & 6 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@
* use {@code new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its
* various configuration methods, and finally calling create.</p>
*
* <p>The following is an example shows how to use the {@code GsonBuilder} to construct a Gson
* instance:
* <p>The following example shows how to use the {@code GsonBuilder} to construct a Gson instance:
*
* <pre>
* Gson gson = new GsonBuilder()
Expand Down Expand Up @@ -125,7 +124,7 @@ public GsonBuilder() {
* Constructs a GsonBuilder instance from a Gson instance. The newly constructed GsonBuilder
* has the same configuration as the previously built Gson instance.
*
* @param gson the gson instance whose configuration should by applied to a new GsonBuilder.
* @param gson the gson instance whose configuration should be applied to a new GsonBuilder.
*/
GsonBuilder(Gson gson) {
this.excluder = gson.excluder;
Expand Down Expand Up @@ -279,7 +278,7 @@ public GsonBuilder serializeNulls() {
* {"x":2,"y":3}}.
*
* <p>Given the assumption above, a {@code Map<Point, String>} will be
* serialize as an array of arrays (can be viewed as an entry set of pairs).
* serialized as an array of arrays (can be viewed as an entry set of pairs).
*
* <p>Below is an example of serializing complex types as JSON arrays:
* <pre> {@code
Expand Down Expand Up @@ -601,7 +600,7 @@ public GsonBuilder setDateFormat(String pattern) {
}

/**
* Configures Gson to to serialize {@code Date} objects according to the style value provided.
* Configures Gson to serialize {@code Date} objects according to the style value provided.
* You can call this method or {@link #setDateFormat(String)} multiple times, but only the last
* invocation will be used to decide the serialization format.
*
Expand All @@ -622,7 +621,7 @@ public GsonBuilder setDateFormat(int style) {
}

/**
* Configures Gson to to serialize {@code Date} objects according to the style value provided.
* Configures Gson to serialize {@code Date} objects according to the style value provided.
* You can call this method or {@link #setDateFormat(String)} multiple times, but only the last
* invocation will be used to decide the serialization format.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ static String checkInstantiable(Class<?> c) {
if (Modifier.isAbstract(modifiers)) {
// R8 performs aggressive optimizations where it removes the default constructor of a class
// and makes the class `abstract`; check for that here explicitly
if (c.getDeclaredConstructors().length == 0) {
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
}

return "Abstract classes can't be instantiated! Register an InstanceCreator"
+ " or a TypeAdapter for this type. Class name: " + c.getName();
/*
* Note: Ideally should only show this R8-specific message when it is clear that R8 was
* used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this
* issue with R8 occurs most, R8 seems to keep some constructors for some reason while
* still making the class abstract
*/
return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+ " an InstanceCreator or a TypeAdapter for this type. Class name: " + c.getName()
+ "\nSee " + TroubleshootingGuide.createUrl("r8-abstract-class");
}
return null;
}
Expand Down
28 changes: 15 additions & 13 deletions gson/src/main/java/com/google/gson/reflect/TypeToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,12 @@ public static <T> TypeToken<T> get(Class<T> type) {
* As seen here the result is a {@code TypeToken<?>}; this method cannot provide any type safety,
* and care must be taken to pass in the correct number of type arguments.
*
* <p>If {@code rawType} is a non-generic class and no type arguments are provided, this method
* simply delegates to {@link #get(Class)} and creates a {@code TypeToken(Class)}.
*
* @throws IllegalArgumentException
* If {@code rawType} is not of type {@code Class}, if it is not a generic type, or if the
* type arguments are invalid for the raw type
* If {@code rawType} is not of type {@code Class}, or if the type arguments are invalid for
* the raw type
*/
public static TypeToken<?> getParameterized(Type rawType, Type... typeArguments) {
Objects.requireNonNull(rawType);
Expand All @@ -354,10 +357,16 @@ public static TypeToken<?> getParameterized(Type rawType, Type... typeArguments)
Class<?> rawClass = (Class<?>) rawType;
TypeVariable<?>[] typeVariables = rawClass.getTypeParameters();

// Note: Does not check if owner type of rawType is generic because this factory method
// does not support specifying owner type
if (typeVariables.length == 0) {
throw new IllegalArgumentException(rawClass.getName() + " is not a generic type");
int expectedArgsCount = typeVariables.length;
int actualArgsCount = typeArguments.length;
if (actualArgsCount != expectedArgsCount) {
throw new IllegalArgumentException(rawClass.getName() + " requires " + expectedArgsCount +
" type arguments, but got " + actualArgsCount);
}

// For legacy reasons create a TypeToken(Class) if the type is not generic
if (typeArguments.length == 0) {
return get(rawClass);
}

// Check for this here to avoid misleading exception thrown by ParameterizedTypeImpl
Expand All @@ -366,13 +375,6 @@ public static TypeToken<?> getParameterized(Type rawType, Type... typeArguments)
+ " it requires specifying an owner type");
}

int expectedArgsCount = typeVariables.length;
int actualArgsCount = typeArguments.length;
if (actualArgsCount != expectedArgsCount) {
throw new IllegalArgumentException(rawClass.getName() + " requires " + expectedArgsCount +
" type arguments, but got " + actualArgsCount);
}

for (int i = 0; i < expectedArgsCount; i++) {
Type typeArgument = Objects.requireNonNull(typeArguments[i], "Type argument must not be null");
Class<?> rawTypeArgument = $Gson$Types.getRawType(typeArgument);
Expand Down
28 changes: 20 additions & 8 deletions gson/src/main/resources/META-INF/proguard/gson.pro
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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


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

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

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

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

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

# Keep no-args constructor of classes which can be used with @JsonAdapter
# By default their no-args constructor is invoked to create an adapter instance
-keep class * extends com.google.gson.TypeAdapter {
-keepclassmembers class * extends com.google.gson.TypeAdapter {
<init>();
}
-keep class * implements com.google.gson.TypeAdapterFactory {
-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
<init>();
}
-keep class * implements com.google.gson.JsonSerializer {
-keepclassmembers class * implements com.google.gson.JsonSerializer {
<init>();
}
-keep class * implements com.google.gson.JsonDeserializer {
-keepclassmembers class * implements com.google.gson.JsonDeserializer {
<init>();
}

# If a class is used in some way by the application, and has fields annotated with @SerializedName
# and a no-args constructor, keep those fields and the constructor
# Based on https://issuetracker.google.com/issues/150189783#comment11
# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 for a more detailed explanation
-if class *
-keepclasseswithmembers,allowobfuscation,allowoptimization class <1> {
<init>();
@com.google.gson.annotations.SerializedName <fields>;
}
26 changes: 7 additions & 19 deletions gson/src/test/java/com/google/gson/GsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.google.gson;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertThrows;

import com.google.gson.Gson.FutureTypeAdapter;
import com.google.gson.internal.Excluder;
Expand Down Expand Up @@ -109,12 +109,8 @@ private static final class TestTypeAdapter extends TypeAdapter<Object> {
@Test
public void testGetAdapter_Null() {
Gson gson = new Gson();
try {
gson.getAdapter((TypeToken<?>) null);
fail();
} catch (NullPointerException e) {
assertThat(e).hasMessageThat().isEqualTo("type must not be null");
}
NullPointerException e = assertThrows(NullPointerException.class, () -> gson.getAdapter((TypeToken<?>) null));
assertThat(e).hasMessageThat().isEqualTo("type must not be null");
}

@Test
Expand Down Expand Up @@ -287,13 +283,9 @@ public void testNewJsonWriter_Default() throws IOException {
jsonWriter.value(true);
jsonWriter.endObject();

try {
// Additional top-level value
jsonWriter.value(1);
fail();
} catch (IllegalStateException expected) {
assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
}
// Additional top-level value
IllegalStateException e = assertThrows(IllegalStateException.class, () -> jsonWriter.value(1));
assertThat(e).hasMessageThat().isEqualTo("JSON must have only one top-level value.");

jsonWriter.close();
assertThat(writer.toString()).isEqualTo("{\"\\u003ctest2\":true}");
Expand Down Expand Up @@ -329,11 +321,7 @@ public void testNewJsonWriter_Custom() throws IOException {
public void testNewJsonReader_Default() throws IOException {
String json = "test"; // String without quotes
JsonReader jsonReader = new Gson().newJsonReader(new StringReader(json));
try {
jsonReader.nextString();
fail();
} catch (MalformedJsonException expected) {
}
assertThrows(MalformedJsonException.class, jsonReader::nextString);
jsonReader.close();
}

Expand Down
Loading

0 comments on commit a657c16

Please sign in to comment.