diff --git a/Troubleshooting.md b/Troubleshooting.md index 525df0154e..e6f21034fe 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -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: @@ -324,6 +326,10 @@ Note: For newer Gson versions these rules might be applied automatically; make s } ``` +You can also use `(...);` 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. diff --git a/examples/android-proguard-example/README.md b/examples/android-proguard-example/README.md index 942477d71a..902960fdfa 100644 --- a/examples/android-proguard-example/README.md +++ b/examples/android-proguard-example/README.md @@ -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). diff --git a/gson/pom.xml b/gson/pom.xml index 0e120d3e44..9a48ef4ded 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -62,7 +62,7 @@ com.google.guava guava-testlib - 32.0.1-jre + 32.1.1-jre test diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 217beebb6e..68eb7d718b 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -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.

* - *

The following is an example shows how to use the {@code GsonBuilder} to construct a Gson - * instance: + *

The following example shows how to use the {@code GsonBuilder} to construct a Gson instance: * *

  * Gson gson = new GsonBuilder()
@@ -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;
@@ -279,7 +278,7 @@ public GsonBuilder serializeNulls() {
    * {"x":2,"y":3}}.
    *
    * 

Given the assumption above, a {@code Map} 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). * *

Below is an example of serializing complex types as JSON arrays: *

 {@code
@@ -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.
    *
@@ -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.
    *
diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
index 7d2dc9b622..0f488a9bb9 100644
--- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
+++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
@@ -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;
   }
diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java
index 4a695666c8..3921a70b64 100644
--- a/gson/src/main/java/com/google/gson/reflect/TypeToken.java
+++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java
@@ -337,9 +337,12 @@ public static  TypeToken get(Class 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.
    *
+   * 

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); @@ -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 @@ -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); diff --git a/gson/src/main/resources/META-INF/proguard/gson.pro b/gson/src/main/resources/META-INF/proguard/gson.pro index c9f235e95b..59d3bb441d 100644 --- a/gson/src/main/resources/META-INF/proguard/gson.pro +++ b/gson/src/main/resources/META-INF/proguard/gson.pro @@ -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 @@ -24,10 +24,10 @@ -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 * { @@ -35,7 +35,9 @@ } # 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 ; @com.google.gson.annotations.JsonAdapter ; @com.google.gson.annotations.Since ; @@ -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 { (); } --keep class * implements com.google.gson.TypeAdapterFactory { +-keepclassmembers class * implements com.google.gson.TypeAdapterFactory { (); } --keep class * implements com.google.gson.JsonSerializer { +-keepclassmembers class * implements com.google.gson.JsonSerializer { (); } --keep class * implements com.google.gson.JsonDeserializer { +-keepclassmembers class * implements com.google.gson.JsonDeserializer { (); } + +# 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> { + (); + @com.google.gson.annotations.SerializedName ; +} diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index 9929c0495b..a8e8e88b98 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -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; @@ -109,12 +109,8 @@ private static final class TestTypeAdapter extends TypeAdapter { @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 @@ -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}"); @@ -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(); } diff --git a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java index 4998737931..99ffaef76d 100644 --- a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java +++ b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java @@ -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.common.MoreAsserts; import java.util.Arrays; @@ -37,17 +37,8 @@ public void testGet() { List list = a.asList(); assertThat(list.get(0)).isEqualTo(new JsonPrimitive(1)); - try { - list.get(-1); - fail(); - } catch (IndexOutOfBoundsException e) { - } - - try { - list.get(2); - fail(); - } catch (IndexOutOfBoundsException e) { - } + assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(2)); a.add((JsonElement) null); assertThat(list.get(1)).isEqualTo(JsonNull.INSTANCE); @@ -75,24 +66,11 @@ public void testSet() { assertThat(list.get(0)).isEqualTo(new JsonPrimitive(2)); assertThat(a.get(0)).isEqualTo(new JsonPrimitive(2)); - try { - list.set(-1, new JsonPrimitive(1)); - fail(); - } catch (IndexOutOfBoundsException e) { - } - - try { - list.set(2, new JsonPrimitive(1)); - fail(); - } catch (IndexOutOfBoundsException e) { - } - - try { - list.set(0, null); - fail(); - } catch (NullPointerException e) { - assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); - } + assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1))); + assertThrows(IndexOutOfBoundsException.class, () -> list.set(2, new JsonPrimitive(1))); + + NullPointerException e = assertThrows(NullPointerException.class, () -> list.set(0, null)); + assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); } @Test @@ -115,30 +93,14 @@ public void testAdd() { ); assertThat(list).isEqualTo(expectedList); - try { - list.set(-1, new JsonPrimitive(1)); - fail(); - } catch (IndexOutOfBoundsException e) { - } - - try { - list.set(list.size(), new JsonPrimitive(1)); - fail(); - } catch (IndexOutOfBoundsException e) { - } - - try { - list.add(0, null); - fail(); - } catch (NullPointerException e) { - assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); - } - try { - list.add(null); - fail(); - } catch (NullPointerException e) { - assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); - } + assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1))); + assertThrows(IndexOutOfBoundsException.class, () -> list.set(list.size(), new JsonPrimitive(1))); + + NullPointerException e = assertThrows(NullPointerException.class, () -> list.add(0, null)); + assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); + + e = assertThrows(NullPointerException.class, () -> list.add(null)); + assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); } @Test @@ -157,18 +119,11 @@ public void testAddAll() { assertThat(list).isEqualTo(expectedList); assertThat(list).isEqualTo(expectedList); - try { - list.addAll(0, Collections.singletonList(null)); - fail(); - } catch (NullPointerException e) { - assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); - } - try { - list.addAll(Collections.singletonList(null)); - fail(); - } catch (NullPointerException e) { - assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); - } + NullPointerException e = assertThrows(NullPointerException.class, () -> list.addAll(0, Collections.singletonList(null))); + assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); + + e = assertThrows(NullPointerException.class, () -> list.addAll(Collections.singletonList(null))); + assertThat(e).hasMessageThat().isEqualTo("Element must be non-null"); } @Test @@ -180,11 +135,8 @@ public void testRemoveIndex() { assertThat(list.remove(0)).isEqualTo(new JsonPrimitive(1)); assertThat(list).hasSize(0); assertThat(a).hasSize(0); - try { - list.remove(0); - fail(); - } catch (IndexOutOfBoundsException e) { - } + + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(0)); } @Test diff --git a/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/src/test/java/com/google/gson/JsonParserTest.java index 37ba3f1375..7c9f360264 100644 --- a/gson/src/test/java/com/google/gson/JsonParserTest.java +++ b/gson/src/test/java/com/google/gson/JsonParserTest.java @@ -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.common.TestTypes.BagOfPrimitives; import com.google.gson.internal.Streams; @@ -37,10 +37,7 @@ public class JsonParserTest { @Test public void testParseInvalidJson() { - try { - JsonParser.parseString("[[]"); - fail(); - } catch (JsonSyntaxException expected) { } + assertThrows(JsonSyntaxException.class, () -> JsonParser.parseString("[[]")); } @Test @@ -81,11 +78,7 @@ public void testParseUnquotedSingleWordStringFails() { @Test public void testParseUnquotedMultiWordStringFails() { - String unquotedSentence = "Test is a test..blah blah"; - try { - JsonParser.parseString(unquotedSentence); - fail(); - } catch (JsonSyntaxException expected) { } + assertThrows(JsonSyntaxException.class, () -> JsonParser.parseString("Test is a test..blah blah")); } @Test diff --git a/gson/src/test/java/com/google/gson/JsonStreamParserTest.java b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java index 2fec1f1767..807b93f89d 100644 --- a/gson/src/test/java/com/google/gson/JsonStreamParserTest.java +++ b/gson/src/test/java/com/google/gson/JsonStreamParserTest.java @@ -16,7 +16,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 java.io.EOFException; import java.util.NoSuchElementException; @@ -72,57 +72,34 @@ public void testNoSideEffectForHasNext() { public void testCallingNextBeyondAvailableInput() { JsonElement unused1 = parser.next(); JsonElement unused2 = parser.next(); - try { - parser.next(); - fail("Parser should not go beyond available input"); - } catch (NoSuchElementException expected) { - } + // Parser should not go beyond available input + assertThrows(NoSuchElementException.class, parser::next); } @Test public void testEmptyInput() { JsonStreamParser parser = new JsonStreamParser(""); - try { - parser.next(); - fail(); - } catch (JsonIOException e) { - assertThat(e.getCause()).isInstanceOf(EOFException.class); - } + JsonIOException e = assertThrows(JsonIOException.class, parser::next); + assertThat(e).hasCauseThat().isInstanceOf(EOFException.class); parser = new JsonStreamParser(""); - try { - parser.hasNext(); - fail(); - } catch (JsonIOException e) { - assertThat(e.getCause()).isInstanceOf(EOFException.class); - } + e = assertThrows(JsonIOException.class, parser::hasNext); + assertThat(e).hasCauseThat().isInstanceOf(EOFException.class); } @Test public void testIncompleteInput() { JsonStreamParser parser = new JsonStreamParser("["); assertThat(parser.hasNext()).isTrue(); - try { - parser.next(); - fail(); - } catch (JsonSyntaxException e) { - } + assertThrows(JsonSyntaxException.class, parser::next); } @Test public void testMalformedInput() { JsonStreamParser parser = new JsonStreamParser(":"); - try { - parser.hasNext(); - fail(); - } catch (JsonSyntaxException e) { - } + assertThrows(JsonSyntaxException.class, parser::hasNext); parser = new JsonStreamParser(":"); - try { - parser.next(); - fail(); - } catch (JsonSyntaxException e) { - } + assertThrows(JsonSyntaxException.class, parser::next); } } diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java index 014be170a6..50a85441b6 100644 --- a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java @@ -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.internal.LazilyParsedNumber; import com.google.gson.stream.JsonReader; @@ -33,19 +33,14 @@ public void testDouble() throws IOException { ToNumberStrategy strategy = ToNumberPolicy.DOUBLE; assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1); assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(3.141592653589793D); - try { - strategy.readNumber(fromString("1e400")); - fail(); - } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo( - "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); - } - try { - strategy.readNumber(fromString("\"not-a-number\"")); - fail(); - } catch (NumberFormatException expected) { - } + + MalformedJsonException e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400"))); + assertThat(e).hasMessageThat().isEqualTo( + "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json" + ); + + assertThrows(NumberFormatException.class, () -> strategy.readNumber(fromString("\"not-a-number\""))); } @Test @@ -62,46 +57,31 @@ public void testLongOrDouble() throws IOException { assertThat(strategy.readNumber(fromString("10"))).isEqualTo(10L); assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1); assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(3.141592653589793D); - try { - strategy.readNumber(fromString("1e400")); - fail(); - } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity; at path $"); - } - try { - strategy.readNumber(fromString("\"not-a-number\"")); - fail(); - } catch (JsonParseException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $"); - } + + Exception e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400"))); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity; at path $"); + + e = assertThrows(JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\""))); + assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $"); assertThat(strategy.readNumber(fromStringLenient("NaN"))).isEqualTo(Double.NaN); assertThat(strategy.readNumber(fromStringLenient("Infinity"))).isEqualTo(Double.POSITIVE_INFINITY); assertThat(strategy.readNumber(fromStringLenient("-Infinity"))).isEqualTo(Double.NEGATIVE_INFINITY); - try { - strategy.readNumber(fromString("NaN")); - fail(); - } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo( - "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); - } - try { - strategy.readNumber(fromString("Infinity")); - fail(); - } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo( - "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); - } - try { - strategy.readNumber(fromString("-Infinity")); - fail(); - } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo( - "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); - } + + e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("NaN"))); + assertThat(e).hasMessageThat().isEqualTo( + "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); + + e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("Infinity"))); + assertThat(e).hasMessageThat().isEqualTo( + "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); + + e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("-Infinity"))); + assertThat(e).hasMessageThat().isEqualTo( + "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); } @Test @@ -111,44 +91,27 @@ public void testBigDecimal() throws IOException { assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279"))).isEqualTo(new BigDecimal("3.141592653589793238462643383279")); assertThat(strategy.readNumber(fromString("1e400"))).isEqualTo(new BigDecimal("1e400")); - try { - strategy.readNumber(fromString("\"not-a-number\"")); - fail(); - } catch (JsonParseException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $"); - } + JsonParseException e = assertThrows(JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\""))); + assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $"); } @Test public void testNullsAreNeverExpected() throws IOException { - try { - ToNumberPolicy.DOUBLE.readNumber(fromString("null")); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); - } - try { - ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); - } - try { - ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); - } - try { - ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")); - fail(); - } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" - + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); - } + IllegalStateException e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.DOUBLE.readNumber(fromString("null"))); + assertThat(e).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); + + e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null"))); + assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); + + e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null"))); + assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); + + e = assertThrows(IllegalStateException.class, () -> ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null"))); + assertThat(e).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); } private static JsonReader fromString(String json) { diff --git a/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/src/test/java/com/google/gson/TypeAdapterTest.java index 18af4c4bfa..ea3042eb08 100644 --- a/gson/src/test/java/com/google/gson/TypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/TypeAdapterTest.java @@ -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.stream.JsonReader; import com.google.gson.stream.JsonWriter; @@ -59,19 +59,11 @@ public void testToJson_ThrowingIOException() { } }; - try { - adapter.toJson(1); - fail(); - } catch (JsonIOException e) { - assertThat(e.getCause()).isEqualTo(exception); - } + JsonIOException e = assertThrows(JsonIOException.class, () -> adapter.toJson(1)); + assertThat(e).hasCauseThat().isEqualTo(exception); - try { - adapter.toJsonTree(1); - fail(); - } catch (JsonIOException e) { - assertThat(e.getCause()).isEqualTo(exception); - } + e = assertThrows(JsonIOException.class, () -> adapter.toJsonTree(1)); + assertThat(e).hasCauseThat().isEqualTo(exception); } private static final TypeAdapter adapter = new TypeAdapter() { diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index e70c175b3c..e858062dd1 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -384,11 +384,7 @@ public void testBitSetDeserialization() { public void testDefaultDateSerialization() { Date now = new Date(1315806903103L); String json = gson.toJson(now); - if (JavaVersion.isJava9OrLater()) { - assertThat(json).isEqualTo("\"Sep 11, 2011, 10:55:03 PM\""); - } else { - assertThat(json).isEqualTo("\"Sep 11, 2011 10:55:03 PM\""); - } + assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\""); } @Test @@ -420,11 +416,7 @@ public void testDefaultDateSerializationUsingBuilder() { Gson gson = new GsonBuilder().create(); Date now = new Date(1315806903103L); String json = gson.toJson(now); - if (JavaVersion.isJava9OrLater()) { - assertThat(json).isEqualTo("\"Sep 11, 2011, 10:55:03 PM\""); - } else { - assertThat(json).isEqualTo("\"Sep 11, 2011 10:55:03 PM\""); - } + assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\""); } @Test diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java index 0b9a6c078c..bc9e8f06b6 100644 --- a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java @@ -89,7 +89,7 @@ public int i() { var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class)); assertThat(exception).hasMessageThat() - .isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported"); + .isEqualTo("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported"); } @Test @@ -154,7 +154,7 @@ record LocalRecord(String s) { // TODO: Adjust this once Gson throws more specific exception type catch (RuntimeException e) { assertThat(e).hasMessageThat() - .isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]"); + .isEqualTo("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]"); assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException); } } @@ -227,7 +227,7 @@ public void testPrimitiveJsonNullValue() { String s = "{'aString': 's', 'aByte': null, 'aShort': 0}"; var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class)); assertThat(e).hasMessageThat() - .isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte"); + .isEqualTo("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte"); } /** @@ -384,8 +384,8 @@ record Blocked(int b) {} var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1))); assertThat(exception).hasMessageThat() - .isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() + - ". Register a TypeAdapter for this type or adjust the access filter."); + .isEqualTo("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() + + ". Register a TypeAdapter for this type or adjust the access filter."); } @Test @@ -396,15 +396,15 @@ public void testReflectionFilterBlockInaccessible() { var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1))); assertThat(exception).hasMessageThat() - .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" - + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" - + " type, adjust the access filter or increase the visibility of the element and its declaring type."); + .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type."); exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class)); assertThat(exception).hasMessageThat() - .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" - + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" - + " type, adjust the access filter or increase the visibility of the element and its declaring type."); + .isEqualTo("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type."); assertThat(gson.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}"); assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2)); @@ -427,7 +427,8 @@ record LocalRecord(int i) {} var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class)); assertThat(exception).hasMessageThat() - .isEqualTo("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for" - + " this type. Class name: java.lang.Record"); + .isEqualTo("Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator" + + " or a TypeAdapter for this type. Class name: java.lang.Record" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"); } } diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index b0de9da48d..e0efd94dee 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -570,11 +570,8 @@ static final class Product { public void testDateAsMapObjectField() { HasObjectMap a = new HasObjectMap(); a.map.put("date", new Date(0)); - if (JavaVersion.isJava9OrLater()) { - assertThat(gson.toJson(a)).isEqualTo("{\"map\":{\"date\":\"Dec 31, 1969, 4:00:00 PM\"}}"); - } else { - assertThat(gson.toJson(a)).isEqualTo("{\"map\":{\"date\":\"Dec 31, 1969 4:00:00 PM\"}}"); - } + assertThat(gson.toJson(a)) + .matches("\\{\"map\":\\{\"date\":\"Dec 31, 1969,? 4:00:00\\hPM\"\\}\\}"); } static class HasObjectMap { diff --git a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java index 602ba074e9..e582ad08ac 100644 --- a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java +++ b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java @@ -19,17 +19,14 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import com.google.gson.InstanceCreator; -import com.google.gson.ReflectionAccessFilter; import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; import java.util.Collections; import org.junit.Test; public class ConstructorConstructorTest { private ConstructorConstructor constructorConstructor = new ConstructorConstructor( - Collections.>emptyMap(), true, - Collections.emptyList() + Collections.emptyMap(), true, + Collections.emptyList() ); private abstract static class AbstractClass { @@ -39,7 +36,7 @@ public AbstractClass() { } private interface Interface { } /** - * Verify that ConstructorConstructor does not try to invoke no-arg constructor + * Verify that ConstructorConstructor does not try to invoke no-args constructor * of abstract class. */ @Test @@ -49,9 +46,10 @@ public void testGet_AbstractClassNoArgConstructor() { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { - assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated! " - + "Register an InstanceCreator or a TypeAdapter for this type. " - + "Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass"); + assertThat(exception).hasMessageThat().isEqualTo("Abstract classes can't be instantiated!" + + " Adjust the R8 configuration or register an InstanceCreator or a TypeAdapter for this type." + + " Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class"); } } @@ -62,9 +60,9 @@ public void testGet_Interface() { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { - assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated! " - + "Register an InstanceCreator or a TypeAdapter for this type. " - + "Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface"); + assertThat(exception).hasMessageThat().isEqualTo("Interfaces can't be instantiated!" + + " Register an InstanceCreator or a TypeAdapter for this type." + + " Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface"); } } } diff --git a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java index 2034e38fc6..835bc9dd49 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java @@ -58,21 +58,22 @@ private void assertFormattingAlwaysEmitsUsLocale(Locale locale) { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(locale); try { - String afterYearSep = JavaVersion.isJava9OrLater() ? ", " : " "; - String afterYearLongSep = JavaVersion.isJava9OrLater() ? " at " : " "; - String utcFull = JavaVersion.isJava9OrLater() ? "Coordinated Universal Time" : "UTC"; - assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep), - DateType.DATE.createDefaultsAdapterFactory()); + // The patterns here attempt to accommodate minor date-time formatting differences between JDK + // versions. Ideally Gson would serialize in a way that is independent of the JDK version. + // Note: \h means "horizontal space", because some JDK versions use Narrow No Break Space + // (U+202F) before the AM or PM indication. + String utcFull = "(Coordinated Universal Time|UTC)"; + assertFormatted("Jan 1, 1970,? 12:00:00\\hAM", DateType.DATE.createDefaultsAdapterFactory()); assertFormatted("1/1/70", DateType.DATE.createAdapterFactory(DateFormat.SHORT)); assertFormatted("Jan 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM)); assertFormatted("January 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.LONG)); - assertFormatted(String.format("1/1/70%s12:00 AM", afterYearSep), + assertFormatted("1/1/70,? 12:00\\hAM", DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT)); - assertFormatted(String.format("Jan 1, 1970%s12:00:00 AM", afterYearSep), + assertFormatted("Jan 1, 1970,? 12:00:00\\hAM", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM)); - assertFormatted(String.format("January 1, 1970%s12:00:00 AM UTC", afterYearLongSep), + assertFormatted("January 1, 1970(,| at)? 12:00:00\\hAM UTC", DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG)); - assertFormatted(String.format("Thursday, January 1, 1970%s12:00:00 AM %s", afterYearLongSep, utcFull), + assertFormatted("Thursday, January 1, 1970(,| at)? 12:00:00\\hAM " + utcFull, DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL)); } finally { TimeZone.setDefault(defaultTimeZone); @@ -150,9 +151,7 @@ public void testFormatUsesDefaultTimezone() throws Exception { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(Locale.US); try { - String afterYearSep = JavaVersion.isJava9OrLater() ? ", " : " "; - assertFormatted(String.format("Dec 31, 1969%s4:00:00 PM", afterYearSep), - DateType.DATE.createDefaultsAdapterFactory()); + assertFormatted("Dec 31, 1969,? 4:00:00\\hPM", DateType.DATE.createDefaultsAdapterFactory()); assertParsed("Dec 31, 1969 4:00:00 PM", DateType.DATE.createDefaultsAdapterFactory()); } finally { TimeZone.setDefault(defaultTimeZone); @@ -222,9 +221,10 @@ private static TypeAdapter dateAdapter(TypeAdapterFactory adapterFactory) return adapter; } - private static void assertFormatted(String formatted, TypeAdapterFactory adapterFactory) { + private static void assertFormatted(String formattedPattern, TypeAdapterFactory adapterFactory) { TypeAdapter adapter = dateAdapter(adapterFactory); - assertThat(adapter.toJson(new Date(0))).isEqualTo(toLiteral(formatted)); + String json = adapter.toJson(new Date(0)); + assertThat(json).matches(toLiteral(formattedPattern)); } @SuppressWarnings("UndefinedEquals") diff --git a/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java b/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java index b04c56ab34..8233275c37 100644 --- a/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java +++ b/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java @@ -113,11 +113,10 @@ public void testDefaultSqlTimeDeserialization() { public void testDefaultSqlTimestampSerialization() { Timestamp now = new java.sql.Timestamp(1259875082000L); String json = gson.toJson(now); - if (JavaVersion.isJava9OrLater()) { - assertThat(json).isEqualTo("\"Dec 3, 2009, 1:18:02 PM\""); - } else { - assertThat(json).isEqualTo("\"Dec 3, 2009 1:18:02 PM\""); - } + // The exact format of the serialized date-time string depends on the JDK version. The pattern + // here allows for an optional comma after the date, and what might be U+202F (Narrow No-Break + // Space) before "PM". + assertThat(json).matches("\"Dec 3, 2009,? 1:18:02\\hPM\""); } @Test diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java index d38f05b1ec..4c1ccf0d6b 100644 --- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -146,6 +146,9 @@ public void testParameterizedFactory() { class LocalGenericClass {} TypeToken expectedLocalType = new TypeToken>() {}; assertThat(TypeToken.getParameterized(LocalGenericClass.class, Integer.class)).isEqualTo(expectedLocalType); + + // For legacy reasons, if requesting parameterized type for non-generic class, create a `TypeToken(Class)` + assertThat(TypeToken.getParameterized(String.class)).isEqualTo(TypeToken.get(String.class)); } @Test @@ -158,7 +161,7 @@ public void testParameterizedFactory_Invalid() { assertThat(e).hasMessageThat().isEqualTo("rawType must be of type Class, but was java.lang.String[]"); e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(String.class, Number.class)); - assertThat(e).hasMessageThat().isEqualTo("java.lang.String is not a generic type"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.String requires 0 type arguments, but got 1"); e = assertThrows(IllegalArgumentException.class, () -> TypeToken.getParameterized(List.class, new Type[0])); assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0"); diff --git a/proto/pom.xml b/proto/pom.xml index fbf87b17b0..dfd2024bf6 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 32.0.1-jre + 32.1.1-jre diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index e72d26e60d..65de7c7f12 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -199,6 +199,8 @@ + com.android.tools r8 8.0.40 diff --git a/shrinker-test/proguard.pro b/shrinker-test/proguard.pro index 0cb35048c9..3e8a812041 100644 --- a/shrinker-test/proguard.pro +++ b/shrinker-test/proguard.pro @@ -18,6 +18,7 @@ -keep class com.example.DefaultConstructorMain { public static java.lang.String runTest(); public static java.lang.String runTestNoJdkUnsafe(); + public static java.lang.String runTestNoDefaultConstructor(); } @@ -27,3 +28,21 @@ -keepclassmembers class com.example.ClassWithNamedFields { !transient ; } + +-keepclassmembernames class com.example.ClassWithExposeAnnotation { + ; +} +-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation { + ** f; +} +-keepclassmembernames class com.example.ClassWithVersionAnnotations { + ; +} + + +-keepclassmembernames class com.example.DefaultConstructorMain$TestClass { + ; +} +-keepclassmembernames class com.example.DefaultConstructorMain$TestClassNotAbstract { + ; +} diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro index 690fe339b5..392f0f0d9c 100644 --- a/shrinker-test/r8.pro +++ b/shrinker-test/r8.pro @@ -4,20 +4,6 @@ ### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard ### See https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode -# Keep the no-args constructor of deserialized classes --keepclassmembers class com.example.ClassWithDefaultConstructor { - (); -} --keepclassmembers class com.example.GenericClasses$GenericClass { - (); -} --keepclassmembers class com.example.GenericClasses$UsingGenericClass { - (); -} --keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass { - (); -} - # For classes with generic type parameter R8 in "full mode" requires to have a keep rule to # preserve the generic signature -keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass @@ -25,12 +11,14 @@ # Don't obfuscate class name, to check it in exception message -keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass +-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor + # This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract -keep class com.example.DefaultConstructorMain$TestClassNotAbstract { @com.google.gson.annotations.SerializedName ; } # Keep enum constants which are not explicitly used in code --keep class com.example.EnumClass { +-keepclassmembers class com.example.EnumClass { ** SECOND; } diff --git a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java index 238ee1818e..0cb05a25a6 100644 --- a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java +++ b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java @@ -23,6 +23,7 @@ */ public class ClassWithJsonAdapterAnnotation { // For this field don't use @SerializedName and ignore it for deserialization + // Has custom ProGuard rule to keep the field name @JsonAdapter(value = Adapter.class, nullSafe = false) DummyClass f; diff --git a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java index e570866bec..4f0152f7d1 100644 --- a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java +++ b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java @@ -8,14 +8,24 @@ public class DefaultConstructorMain { static class TestClass { - @SerializedName("s") public String s; } // R8 rule for this class still removes no-args constructor, but doesn't make class abstract static class TestClassNotAbstract { + public String s; + } + + // Current Gson ProGuard rules only keep default constructor (and only then prevent R8 from + // making class abstract); other constructors are ignored to suggest to user adding default + // constructor instead of implicitly relying on JDK Unsafe + static class TestClassWithoutDefaultConstructor { @SerializedName("s") public String s; + + public TestClassWithoutDefaultConstructor(String s) { + this.s = s; + } } /** @@ -34,4 +44,12 @@ public static String runTestNoJdkUnsafe() { TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class)); return deserialized.s; } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testNoDefaultConstructor()}. + */ + public static String runTestNoDefaultConstructor() { + TestClassWithoutDefaultConstructor deserialized = new Gson().fromJson("{\"s\":\"value\"}", same(TestClassWithoutDefaultConstructor.class)); + return deserialized.s; + } } diff --git a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java index 9c32f1aa57..3304570197 100644 --- a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -220,4 +220,24 @@ public void testDefaultConstructorNoJdkUnsafe() throws Exception { } }); } + + @Test + public void testNoDefaultConstructor() throws Exception { + runTest("com.example.DefaultConstructorMain", c -> { + Method m = c.getMethod("runTestNoDefaultConstructor"); + + if (jarToTest.equals(PROGUARD_RESULT_PATH)) { + Object result = m.invoke(null); + assertThat(result).isEqualTo("value"); + } else { + // R8 performs more aggressive optimizations + Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null)); + assertThat(e).hasCauseThat().hasMessageThat().isEqualTo( + "Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator" + + " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClassWithoutDefaultConstructor" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class" + ); + } + }); + } }