diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93cde7b55a..d04018591e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,3 +24,22 @@ jobs: - name: Build with Maven # This also runs javadoc:jar to detect any issues with the Javadoc generated during release run: mvn --batch-mode --update-snapshots --no-transfer-progress verify javadoc:jar + + native-image-test: + name: "GraalVM Native Image test" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: "Set up GraalVM" + uses: graalvm/setup-graalvm@v1 + with: + java-version: '17' + distribution: 'graalvm' + # According to documentation in graalvm/setup-graalvm this is used to avoid rate-limiting issues + github-token: ${{ secrets.GITHUB_TOKEN }} + cache: 'maven' + - name: Build and run tests + # Only run tests in `graal-native-image-test` (and implicitly build and run tests in `gson`), + # everything else is covered already by regular build job above + run: mvn test --batch-mode --update-snapshots --no-transfer-progress --activate-profiles native-image-test --projects graal-native-image-test --also-make diff --git a/.github/workflows/check-android-compatibility.yml b/.github/workflows/check-android-compatibility.yml new file mode 100644 index 0000000000..e71956fa2c --- /dev/null +++ b/.github/workflows/check-android-compatibility.yml @@ -0,0 +1,29 @@ +# For security reasons this is a separate GitHub workflow, see https://github.com/google/gson/issues/2429#issuecomment-1622522842 +# Once https://github.com/mojohaus/animal-sniffer/issues/252 or https://github.com/mojohaus/animal-sniffer/pull/253 +# are resolved, can consider adjusting pom.xml to include this as part of normal Maven build + +name: Check Android compatibility + +on: [push, pull_request] + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + check-android-compatibility: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + cache: 'maven' + + - name: Check Android compatibility + run: | + # Run 'test' phase because plugin normally expects to be executed after tests have been compiled + mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests diff --git a/.github/workflows/check-api-compatibility.yml b/.github/workflows/check-api-compatibility.yml index a446576408..79a793f564 100644 --- a/.github/workflows/check-api-compatibility.yml +++ b/.github/workflows/check-api-compatibility.yml @@ -1,3 +1,5 @@ +# This workflow makes sure that a pull request does not make any incompatible changes +# to the public API of Gson name: Check API compatibility on: pull_request diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 01d95bdf6f..76822e60f4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -4,9 +4,9 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] schedule: # Run every Monday at 16:10 - cron: '10 16 * * 1' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b620adfdf..7efc0da9de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -188,7 +188,7 @@ _2015-10-04_ * New: APIs to add primitives directly to `JsonArray` instances. * New: ISO 8601 date type adapter. Find this in _extras_. * Fix: `FieldNamingPolicy` now works properly when running on a device with a Turkish locale. - [autovalue]: https://github.com/google/auto/tree/master/value + [autovalue]: https://github.com/google/auto/tree/main/value ## Version 2.3.1 diff --git a/GsonDesignDocument.md b/GsonDesignDocument.md index 2c3702ee13..a80a81e627 100644 --- a/GsonDesignDocument.md +++ b/GsonDesignDocument.md @@ -1,57 +1,59 @@ # Gson Design Document -This document presents issues that we faced while designing Gson. It is meant for advanced users or developers working on Gson. If you are interested in learning how to use Gson, see its user guide. +This document presents issues that we faced while designing Gson. It is meant for advanced users or developers working on Gson. If you are interested in learning how to use Gson, see its user guide. -**Navigating the Json tree or the target Type Tree while deserializing** +Some information in this document is outdated and does not reflect the current state of Gson. This information can however still be relevant for understanding the history of Gson. -When you are deserializing a Json string into an object of desired type, you can either navigate the tree of the input, or the type tree of the desired type. Gson uses the latter approach of navigating the type of the target object. This keeps you in tight control of instantiating only the type of objects that you are expecting (essentially validating the input against the expected "schema"). By doing this, you also ignore any extra fields that the Json input has but were not expected. +## Navigating the Json tree or the target Type Tree while deserializing -As part of Gson, we wrote a general purpose ObjectNavigator that can take any object and navigate through its fields calling a visitor of your choice. +When you are deserializing a Json string into an object of desired type, you can either navigate the tree of the input, or the type tree of the desired type. Gson uses the latter approach of navigating the type of the target object. This keeps you in tight control of instantiating only the type of objects that you are expecting (essentially validating the input against the expected "schema"). By doing this, you also ignore any extra fields that the Json input has but were not expected. -**Supporting richer serialization semantics than deserialization semantics** +As part of Gson, we wrote a general purpose ObjectNavigator that can take any object and navigate through its fields calling a visitor of your choice. -Gson supports serialization of arbitrary collections, but can only deserialize genericized collections. this means that Gson can, in some cases, fail to deserialize Json that it wrote. This is primarily a limitation of the Java type system since when you encounter a Json array of arbitrary types there is no way to detect the types of individual elements. We could have chosen to restrict the serialization to support only generic collections, but chose not to.This is because often the user of the library are concerned with either serialization or deserialization, but not both. In such cases, there is no need to artificially restrict the serialization capabilities. +## Supporting richer serialization semantics than deserialization semantics -**Supporting serialization and deserialization of classes that are not under your control and hence can not be modified** +Gson supports serialization of arbitrary collections, but can only deserialize genericized collections. this means that Gson can, in some cases, fail to deserialize Json that it wrote. This is primarily a limitation of the Java type system since when you encounter a Json array of arbitrary types there is no way to detect the types of individual elements. We could have chosen to restrict the serialization to support only generic collections, but chose not to. This is because often the user of the library are concerned with either serialization or deserialization, but not both. In such cases, there is no need to artificially restrict the serialization capabilities. -Some Json libraries use annotations on fields or methods to indicate which fields should be used for Json serialization. That approach essentially precludes the use of classes from JDK or third-party libraries. We solved this problem by defining the notion of Custom serializers and deserializers. This approach is not new, and was used by the JAX-RPC technology to solve essentially the same problem. +## Supporting serialization and deserialization of classes that are not under your control and hence can not be modified -**Using Checked vs Unchecked exceptions to indicate a parsing error** +Some Json libraries use annotations on fields or methods to indicate which fields should be used for Json serialization. That approach essentially precludes the use of classes from JDK or third-party libraries. We solved this problem by defining the notion of custom serializers and deserializers. This approach is not new, and was used by the JAX-RPC technology to solve essentially the same problem. -We chose to use unchecked exceptions to indicate a parsing failure. This is primarily done because usually the client can not recover from bad input, and hence forcing them to catch a checked exception results in sloppy code in the catch() block. +## Using Checked vs Unchecked exceptions to indicate a parsing error -**Creating class instances for deserialization** +We chose to use unchecked exceptions to indicate a parsing failure. This is primarily done because usually the client can not recover from bad input, and hence forcing them to catch a checked exception results in sloppy code in the `catch()` block. -Gson needs to create a dummy class instance before it can deserialize Json data into its fields. We could have used Guice to get such an instance, but that would have resulted in a dependency on Guice. Moreover, it probably would have done the wrong thing since Guice is expected to return a valid instance, whereas we need to create a dummy one. Worse, Gson would overwrite the fields of that instance with the incoming data there by modifying the instance for all subsequent Guice injections. This is clearly not a desired behavior. Hence, we create class instances by invoking the parameterless constructor. We also handle the primitive types, enums, collections, sets, maps and trees as a special case. +## Creating class instances for deserialization -To solve the problem of supporting unmodifiable types, we use custom instance creators. So, if you want to use a library types that does not define a default constructor (for example, Money class), then you can register an instance creator that returns a dummy instance when asked. +Gson needs to create a dummy class instance before it can deserialize Json data into its fields. We could have used Guice to get such an instance, but that would have resulted in a dependency on Guice. Moreover, it probably would have done the wrong thing since Guice is expected to return a valid instance, whereas we need to create a dummy one. Worse, Gson would overwrite the fields of that instance with the incoming data thereby modifying the instance for all subsequent Guice injections. This is clearly not a desired behavior. Hence, we create class instances by invoking the parameterless constructor. We also handle the primitive types, enums, collections, sets, maps and trees as a special case. -**Using fields vs getters to indicate Json elements** +To solve the problem of supporting unmodifiable types, we use custom instance creators. So, if you want to use a library type that does not define a default constructor (for example, `Money` class), then you can register an instance creator that returns a dummy instance when asked. -Some Json libraries use the getters of a type to deduce the Json elements. We chose to use all fields (up the inheritance hierarchy) that are not transient, static, or synthetic. We did this because not all classes are written with suitably named getters. Moreover, getXXX or isXXX might be semantic rather than indicating properties. +## Using fields vs getters to indicate Json elements -However, there are good arguments to support properties as well. We intend to enhance Gson in a latter version to support properties as an alternate mapping for indicating Json fields. For now, Gson is fields-based. +Some Json libraries use the getters of a type to deduce the Json elements. We chose to use all fields (up the inheritance hierarchy) that are not transient, static, or synthetic. We did this because not all classes are written with suitably named getters. Moreover, `getXXX` or `isXXX` might be semantic rather than indicating properties. -**Why are most classes in Gson marked as final?** +However, there are good arguments to support properties as well. We intend to enhance Gson in a later version to support properties as an alternate mapping for indicating Json fields. For now, Gson is fields-based. -While Gson provides a fairly extensible architecture by providing pluggable serializers and deserializers, Gson classes were not specifically designed to be extensible. Providing non-final classes would have allowed a user to legitimately extend Gson classes, and then expect that behavior to work in all subsequent revisions. We chose to limit such use-cases by marking classes as final, and waiting until a good use-case emerges to allow extensibility. Marking a class final also has a minor benefit of providing additional optimization opportunities to Java compiler and virtual machine. +## Why are most classes in Gson marked as final? -**Why are inner interfaces and classes used heavily in Gson?** +While Gson provides a fairly extensible architecture by providing pluggable serializers and deserializers, Gson classes were not specifically designed to be extensible. Providing non-final classes would have allowed a user to legitimately extend Gson classes, and then expect that behavior to work in all subsequent revisions. We chose to limit such use-cases by marking classes as final, and waiting until a good use-case emerges to allow extensibility. Marking a class final also has a minor benefit of providing additional optimization opportunities to Java compiler and virtual machine. -Gson uses inner classes substantially. Many of the public interfaces are inner interfaces too (see JsonSerializer.Context or JsonDeserializer.Context as an example). These are primarily done as a matter of style. For example, we could have moved JsonSerializer.Context to be a top-level class JsonSerializerContext, but chose not to do so. However, if you can give us good reasons to rename it alternately, we are open to changing this philosophy. +## Why are inner interfaces and classes used heavily in Gson? -**Why do you provide two ways of constructing Gson?** +Gson uses inner classes substantially. Many of the public interfaces are inner interfaces too (see `JsonSerializer.Context` or `JsonDeserializer.Context` as an example). These are primarily done as a matter of style. For example, we could have moved `JsonSerializer.Context` to be a top-level class `JsonSerializerContext`, but chose not to do so. However, if you can give us good reasons to rename it alternately, we are open to changing this philosophy. -Gson can be constructed in two ways: by invoking new Gson() or by using a GsonBuilder. We chose to provide a simple no-args constructor to handle simple use-cases for Gson where you want to use default options, and quickly want to get going with writing code. For all other situations, where you need to configure Gson with options such as formatters, version controls etc, we use a builder pattern. The builder pattern allows a user to specify multiple optional settings for what essentially become constructor parameters for Gson. +## Why do you provide two ways of constructing Gson? -**Comparing Gson with Alternate Approaches** +Gson can be constructed in two ways: by invoking `new Gson()` or by using a `GsonBuilder`. We chose to provide a simple no-args constructor to handle simple use-cases for Gson where you want to use default options, and quickly want to get going with writing code. For all other situations, where you need to configure Gson with options such as formatters, version controls etc., we use a builder pattern. The builder pattern allows a user to specify multiple optional settings for what essentially become constructor parameters for Gson. + +## Comparing Gson with alternate approaches Note that these comparisons were done while developing Gson so these date back to mid to late 2007. -__Comparing Gson with org.json library__ +### Comparing Gson with org.json library -org.json is a much lower-level library that can be used to write a toJson() method in a class. If you can not use Gson directly (may be because of platform restrictions regarding reflection), you could use org.json to hand-code a toJson method in each object. +org.json is a much lower-level library that can be used to write a `toJson()` method in a class. If you can not use Gson directly (maybe because of platform restrictions regarding reflection), you could use org.json to hand-code a `toJson` method in each object. -__Comparing Gson with org.json.simple library__ +### Comparing Gson with org.json.simple library org.json.simple library is very similar to org.json library and hence fairly low level. The key issue with this library is that it does not handle exceptions very well. In some cases it appeared to just eat the exception while in other cases it throws an "Error" rather than an exception. diff --git a/README.md b/README.md index 04eb1c94e8..7418a27af7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ When this module is present, Gson can use the `Unsafe` class to create instances However, care should be taken when relying on this. `Unsafe` is not available in all environments and its usage has some pitfalls, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()). +#### Minimum Android API level + +- Gson 2.11.0 and newer: API level 21 +- Gson 2.10.1 and older: API level 19 + +Older Gson versions may also support lower API levels, however this has not been verified. + ### Documentation * [API Javadoc](https://www.javadoc.io/doc/com.google.code.gson/gson): Documentation for the current release * [User guide](UserGuide.md): This guide contains examples on how to use Gson in your code diff --git a/Troubleshooting.md b/Troubleshooting.md index 2f9185a38e..91fd22110a 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -2,7 +2,10 @@ This guide describes how to troubleshoot common issues when using Gson. -## `ClassCastException` when using deserialized object + + + +## `ClassCastException` when using deserialized object **Symptom:** `ClassCastException` is thrown when accessing an object deserialized by Gson @@ -14,9 +17,9 @@ This guide describes how to troubleshoot common issues when using Gson. See the [user guide](UserGuide.md#collections-examples) for more information. - When using `TypeToken` prefer the `Gson.fromJson` overloads with `TypeToken` parameter such as [`fromJson(Reader, TypeToken)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#fromJson(java.io.Reader,com.google.gson.reflect.TypeToken)). The overloads with `Type` parameter do not provide any type-safety guarantees. -- When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken>()` (where `T` is a type variable). Due to Java type erasure the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementClass)`. +- When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken>()` (where `T` is a type variable). Due to Java [type erasure](https://dev.java/learn/generics/type-erasure/) the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to unnamed module' is thrown @@ -30,7 +33,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to module com.google.gson' is thrown @@ -51,7 +54,7 @@ module mymodule { Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization). -## Android app not working in Release mode; random property names +## Android app not working in Release mode; random property names **Symptom:** Your Android app is working fine in Debug mode but fails in Release mode and the JSON properties have seemingly random names such as `a`, `b`, ... @@ -59,11 +62,11 @@ Or in case this occurs for a field in one of your classes which you did not actu **Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information. -## Android app unable to parse JSON after app update +## Android app unable to parse JSON after app update **Symptom:** You released a new version of your Android app and it fails to parse JSON data created by the previous version of your app -**Reason:** You probably have not configured ProGuard / R8 correctly; probably the fields names are being obfuscated and their naming changed between the versions of your app +**Reason:** You probably have not configured ProGuard / R8 correctly; probably the field names are being obfuscated and their naming changed between the versions of your app **Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information. @@ -71,7 +74,7 @@ If you want to preserve backward compatibility for you app you can use [`@Serial Normally ProGuard and R8 produce a mapping file, this makes it easier to find out the obfuscated field names instead of having to find them out through trial and error or other means. See the [Android Studio user guide](https://developer.android.com/studio/build/shrink-code.html#retracing) for more information. -## Default field values not present after deserialization +## Default field values not present after deserialization **Symptom:** You have assign default values to fields but after deserialization the fields have their standard value (such as `null` or `0`) @@ -84,7 +87,7 @@ Normally ProGuard and R8 produce a mapping file, this makes it easier to find ou Otherwise Gson will by default try to use JDK `Unsafe` or similar means to create an instance of your class without invoking the constructor and without running any initializers. You can also disable that behavior through [`GsonBuilder.disableJdkUnsafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) to notice such issues early on. -## `null` values for anonymous and local classes +## `null` values for anonymous and local classes **Symptom:** Objects of a class are always serialized as JSON `null` / always deserialized as Java `null` @@ -97,7 +100,7 @@ Notes: - "double brace-initialization" also creates anonymous classes - Local record classes (feature added in Java 16) are supported by Gson and are not affected by this -## Map keys having unexpected format in JSON +## Map keys having unexpected format in JSON **Symptom:** JSON output for `Map` keys is unexpected / cannot be deserialized again @@ -105,33 +108,53 @@ Notes: **Solution:** Use [`GsonBuilder.enableComplexMapKeySerialization()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#enableComplexMapKeySerialization()). See also the [user guide](UserGuide.md#maps-examples) for more information. -## Parsing JSON fails with `MalformedJsonException` +## Parsing JSON fails with `MalformedJsonException` **Symptom:** JSON parsing fails with `MalformedJsonException` **Reason:** The JSON data is actually malformed -**Solution:** During debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). +**Solution:** During debugging, log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). + +For example, let's assume you want to deserialize the following JSON data: + +```json +{ + "languages": [ + "English", + "French", + ] +} +``` -## Integral JSON number is parsed as `double` +This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 5 column 4 path $.languages[2]` +The problem here is the trailing comma (`,`) after `"French"`, trailing commas are not allowed by the JSON specification. The location information "line 5 column 4" points to the `]` in the JSON data (with some slight inaccuracies) because Gson expected another value after `,` instead of the closing `]`. The JSONPath `$.languages[2]` in the exception message also points there: `$.` refers to the root object, `languages` refers to its member of that name and `[2]` refers to the (missing) third value in the JSON array value of that member (numbering starts at 0, so it is `[2]` instead of `[3]`). +The proper solution here is to fix the malformed JSON data. + +To spot syntax errors in the JSON data easily you can open it in an editor with support for JSON, for example Visual Studio Code. It will highlight within the JSON data the error location and show why the JSON data is considered invalid. + +## Integral JSON number is parsed as `double` **Symptom:** JSON data contains an integral number such as `45` but Gson returns it as `double` -**Reason:** When parsing a JSON number as `Object`, Gson will by default create always return a `double` +**Reason:** When parsing a JSON number as `Object`, Gson will by default always return a `double` **Solution:** Use [`GsonBuilder.setObjectToNumberStrategy`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setObjectToNumberStrategy(com.google.gson.ToNumberStrategy)) to specify what type of number should be returned -## Malformed JSON not rejected +## Malformed JSON not rejected **Symptom:** Gson parses malformed JSON without throwing any exceptions **Reason:** Due to legacy reasons Gson performs parsing by default in lenient mode -**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html) section "Lenient JSON handling" +**Solution:** If you are using Gson 2.11.0 or newer, call [`GsonBuilder.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setStrictness(com.google.gson.Strictness)), +[`JsonReader.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setStrictness(com.google.gson.Strictness)) +and [`JsonWriter.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonWriter.html#setStrictness(com.google.gson.Strictness)) +with `Strictness.STRICT` to overwrite the default lenient behavior of `Gson` and make these classes strictly adhere to the JSON specification. +Otherwise if you are using an older Gson version, see the [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient) +section "JSON Strictness handling" for alternative solutions. -Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setLenient`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setLenient(boolean)) for more details. - -## `IllegalStateException`: "Expected ... but was ..." +## `IllegalStateException`: "Expected ... but was ..." **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was ..." is thrown @@ -139,13 +162,36 @@ Note: Even in non-lenient mode Gson deviates slightly from the JSON specificatio **Solution:** Make sure that your classes correctly model the JSON data. Also during debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Read the location information of the exception message, it indicates where exactly in the document the error occurred, including the [JSONPath](https://goessner.net/articles/JsonPath/). -## `IllegalStateException`: "Expected ... but was NULL" +For example, let's assume you have the following Java class: + +```java +class WebPage { + String languages; +} +``` + +And you want to deserialize the following JSON data: + +```json +{ + "languages": ["English", "French"] +} +``` + +This will fail with an exception similar to this one: `IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 17 path $.languages` +This means Gson expected a JSON string value but found the beginning of a JSON array (`[`). The location information "line 2 column 17" points to the `[` in the JSON data (with some slight inaccuracies), so does the JSONPath `$.languages` in the exception message. It refers to the `languages` member of the root object (`$.`). +The solution here is to change in the `WebPage` class the field `String languages` to `List languages`. + +## `IllegalStateException`: "Expected ... but was NULL" **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was NULL" is thrown -**Reason:** You have written a custom `TypeAdapter` which does not properly handle a JSON null value +**Reason:** + +- A built-in adapter does not support JSON null values +- You have written a custom `TypeAdapter` which does not properly handle JSON null values -**Solution:** Add code similar to the following at the beginning of the `read` method of your adapter: +**Solution:** If this occurs for a custom adapter you wrote, add code similar to the following at the beginning of its `read` method: ```java @Override @@ -154,14 +200,14 @@ public MyClass read(JsonReader in) throws IOException { in.nextNull(); return null; } - + ... } ``` Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html#nullSafe()) on the adapter instance you created. -## Properties missing in JSON +## Properties missing in JSON **Symptom:** Properties are missing in the JSON output @@ -171,7 +217,7 @@ Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google. Note: Gson does not support anonymous and local classes and will serialize them as JSON null, see the [related troubleshooting point](#null-values-for-anonymous-and-local-classes). -## JSON output changes for newer Android versions +## JSON output changes for newer Android versions **Symptom:** The JSON output differs when running on newer Android versions @@ -185,7 +231,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## JSON output contains values of `static` fields +## JSON output contains values of `static` fields **Symptom:** The JSON output contains values of `static` fields @@ -193,7 +239,7 @@ If you want to prevent using reflection on third-party classes in the future you **Solution:** When calling `GsonBuilder.excludeFieldsWithModifiers` you overwrite the default excluded modifiers. Therefore, you have to explicitly exclude `static` fields if desired. This can be done by adding `Modifier.STATIC` as additional argument. -## `NoSuchMethodError` when calling Gson methods +## `NoSuchMethodError` when calling Gson methods **Symptom:** A `java.lang.NoSuchMethodError` is thrown when trying to call certain Gson methods @@ -210,3 +256,102 @@ System.out.println(Gson.class.getProtectionDomain().getCodeSource().getLocation( ``` If that fails with a `NullPointerException` you have to try one of the other ways to find out where a class is loaded from. + +## `IllegalArgumentException`: 'Class ... declares multiple JSON fields named '...'' + +**Symptom:** An exception with the message 'Class ... declares multiple JSON fields named '...'' is thrown + +**Reason:** + +- The name you have specified with a [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation for a field collides with the name of another field +- The [`FieldNamingStrategy`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/FieldNamingStrategy.html) you have specified produces conflicting field names +- A field of your class has the same name as the field of a superclass + +Gson prevents multiple fields with the same name because during deserialization it would be ambiguous for which field the JSON data should be deserialized. For serialization it would cause the same field to appear multiple times in JSON. While the JSON specification permits this, it is likely that the application parsing the JSON data will not handle it correctly. + +**Solution:** First identify the fields with conflicting names based on the exception message. Then decide if you want to rename one of them using the [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation, or if you want to [exclude](UserGuide.md#excluding-fields-from-serialization-and-deserialization) one of them. When excluding one of the fields you have to include it for both serialization and deserialization (even if your application only performs one of these actions) because the duplicate field check cannot differentiate between these actions. + +## `UnsupportedOperationException` when serializing or deserializing `java.lang.Class` + +**Symptom:** An `UnsupportedOperationException` is thrown when trying to serialize or deserialize `java.lang.Class` + +**Reason:** Gson intentionally does not permit serializing and deserializing `java.lang.Class` for security reasons. Otherwise a malicious user could make your application load an arbitrary class from the classpath and, depending on what your application does with the `Class`, in the worst case perform a remote code execution attack. + +**Solution:** First check if you really need to serialize or deserialize a `Class`. Often it is possible to use string aliases and then map them to the known `Class`; you could write a custom [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) to do this. If the `Class` values are not known in advance, try to introduce a common base class or interface for all these classes and then verify that the deserialized class is a subclass. For example assuming the base class is called `MyBaseClass`, your custom `TypeAdapter` should load the class like this: + +```java +Class.forName(jsonString, false, getClass().getClassLoader()).asSubclass(MyBaseClass.class) +``` + +This will not initialize arbitrary classes, and it will throw a `ClassCastException` if the loaded class is not the same as or a subclass of `MyBaseClass`. + +## `IllegalStateException`: 'TypeToken must be created with a type argument'
`RuntimeException`: 'Missing type parameter' + +**Symptom:** An `IllegalStateException` with the message 'TypeToken must be created with a type argument' is thrown. +For older Gson versions a `RuntimeException` with message 'Missing type parameter' is thrown. + +**Reason:** + +- You created a `TypeToken` without type argument, for example `new TypeToken() {}` (note the missing `<...>`). You always have to provide the type argument, for example like this: `new TypeToken>() {}`. Normally the compiler will also emit a 'raw types' warning when you forget the `<...>`. +- You are using a code shrinking tool such as ProGuard or R8 (Android app builds normally have this enabled by default) but have not configured it correctly for usage with Gson. + +**Solution:** When you are using a code shrinking tool such as ProGuard or R8 you have to adjust your configuration to include the following rules: + +``` +# Keep generic signatures; needed for correct type resolution +-keepattributes Signature + +# Keep class TypeToken (respectively its generic signature) +-keep class com.google.gson.reflect.TypeToken { *; } + +# Keep any (anonymous) classes extending TypeToken +-keep class * extends com.google.gson.reflect.TypeToken +``` + +See also the [Android example](examples/android-proguard-example/README.md) for more information. + +Note: For newer Gson versions these rules might be applied automatically; make sure you are using the latest Gson version and the latest version of the code shrinking tool. + +## `JsonIOException`: 'Abstract classes can't be instantiated!' (R8) + +**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: + +``` +# Keep the no-args constructor of the deserialized class +-keepclassmembers class com.example.MyClass { + (); +} +``` + +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). + +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. + +## `IllegalArgumentException`: 'TypeToken type argument must not contain a type variable' + +**Symptom:** An exception with the message 'TypeToken type argument must not contain a type variable' is thrown + +**Reason:** This exception is thrown when you create an anonymous `TypeToken` subclass which captures a type variable, for example `new TypeToken>() {}` (where `T` is a type variable). At compile time such code looks safe and you can use the type `List` without any warnings. However, this code is not actually type-safe because at runtime due to [type erasure](https://dev.java/learn/generics/type-erasure/) only the upper bound of the type variable is available. For the previous example that would be `List`. When using such a `TypeToken` with any Gson methods performing deserialization this would lead to confusing and difficult to debug `ClassCastException`s. For serialization it can in some cases also lead to undesired results. + +Note: Earlier version of Gson unfortunately did not prevent capturing type variables, which caused many users to unwittingly write type-unsafe code. + +**Solution:** + +- Use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately. +- For Kotlin users: Use [`reified` type parameters](https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters), that means change `` to ``, if possible. If you have a chain of functions with type parameters you will probably have to make all of them `reified`. +- If you don't actually use Gson's `TypeToken` for any Gson method, use a general purpose 'type token' implementation provided by a different library instead, for example Guava's [`com.google.common.reflect.TypeToken`](https://javadoc.io/doc/com.google.guava/guava/latest/com/google/common/reflect/TypeToken.html). + +For backward compatibility it is possible to restore Gson's old behavior of allowing `TypeToken` to capture type variables by setting the [system property](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/System.html#setProperty(java.lang.String,java.lang.String)) `gson.allowCapturingTypeVariables` to `"true"`, **however**: + +- This does not solve any of the type-safety problems mentioned above; in the long term you should prefer one of the other solutions listed above. This system property might be removed in future Gson versions. +- You should only ever set the property to `"true"`, but never to any other value or manually clear it. Otherwise this might counteract any libraries you are using which might have deliberately set the system property because they rely on its behavior. diff --git a/UserGuide.md b/UserGuide.md index dfa76b043c..aaf948bc29 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -135,7 +135,7 @@ BagOfPrimitives obj = new BagOfPrimitives(); Gson gson = new Gson(); String json = gson.toJson(obj); -// ==> json is {"value1":1,"value2":"abc"} +// ==> {"value1":1,"value2":"abc"} ``` Note that you can not serialize objects with circular references since that will result in infinite recursion. @@ -222,7 +222,7 @@ Gson gson = new Gson(); Collection ints = Arrays.asList(1,2,3,4,5); // Serialization -String json = gson.toJson(ints); // ==> json is [1,2,3,4,5] +String json = gson.toJson(ints); // ==> [1,2,3,4,5] // Deserialization TypeToken> collectionType = new TypeToken>(){}; @@ -251,14 +251,14 @@ stringMap.put("key", "value"); stringMap.put(null, "null-entry"); // Serialization -String json = gson.toJson(stringMap); // ==> json is {"key":"value","null":"null-entry"} +String json = gson.toJson(stringMap); // ==> {"key":"value","null":"null-entry"} Map intMap = new LinkedHashMap<>(); intMap.put(2, 4); intMap.put(3, 6); // Serialization -String json = gson.toJson(intMap); // ==> json is {"2":4,"3":6} +String json = gson.toJson(intMap); // ==> {"2":4,"3":6} ``` For deserialization Gson uses the `read` method of the `TypeAdapter` registered for the Map key type. Similar to the Collection example shown above, for deserialization a `TypeToken` has to be used to tell Gson what types the Map keys and values have: @@ -297,12 +297,12 @@ complexMap.put(new PersonName("Jane", "Doe"), 35); // Serialization; complex map is serialized as a JSON array containing key-value pairs (as JSON arrays) String json = gson.toJson(complexMap); -// ==> json is [[{"firstName":"John","lastName":"Doe"},30],[{"firstName":"Jane","lastName":"Doe"},35]] +// ==> [[{"firstName":"John","lastName":"Doe"},30],[{"firstName":"Jane","lastName":"Doe"},35]] Map stringMap = new LinkedHashMap<>(); stringMap.put("key", "value"); // Serialization; non-complex map is serialized as a regular JSON object -String json = gson.toJson(stringMap); // json is {"key":"value"} +String json = gson.toJson(stringMap); // ==> {"key":"value"} ``` **Important:** Because Gson by default uses `toString()` to serialize Map keys, this can lead to malformed encoded keys or can cause mismatch between serialization and deserialization of the keys, for example when `toString()` is not properly implemented. A workaround for this can be to use `enableComplexMapKeySerialization()` to make sure the `TypeAdapter` registered for the Map key type is used for deserialization _and_ serialization. As shown in the example above, when none of the keys are serialized by the adapter as JSON array or JSON object, the Map is serialized as a regular JSON object, as desired. @@ -366,7 +366,7 @@ class Event { You can serialize the collection with Gson without doing anything specific: `toJson(collection)` would write out the desired output. -However, deserialization with `fromJson(json, Collection.class)` will not work since Gson has no way of knowing how to map the input to the types. Gson requires that you provide a genericised version of collection type in `fromJson()`. So, you have three options: +However, deserialization with `fromJson(json, Collection.class)` will not work since Gson has no way of knowing how to map the input to the types. Gson requires that you provide a genericized version of the collection type in `fromJson()`. So, you have three options: 1. Use Gson's parser API (low-level streaming parser or the DOM parser JsonParser) to parse the array elements and then use `Gson.fromJson()` on each of the array elements.This is the preferred approach. [Here is an example](extras/src/main/java/com/google/gson/extras/examples/rawcollections/RawCollectionsExample.java) that demonstrates how to do this. @@ -389,7 +389,7 @@ You can also find source code for some commonly used classes such as JodaTime at ### Custom Serialization and Deserialization -Sometimes default representation is not what you want. This is often the case when dealing with library classes (DateTime, etc). +Sometimes the default representation is not what you want. This is often the case when dealing with library classes (DateTime, etc.). Gson allows you to register your own custom serializers and deserializers. This is done by defining two parts: * JSON Serializers: Need to define custom serialization for an object @@ -405,7 +405,9 @@ gson.registerTypeAdapter(MyType.class, new MyDeserializer()); gson.registerTypeAdapter(MyType.class, new MyInstanceCreator()); ``` -`registerTypeAdapter` call checks if the type adapter implements more than one of these interfaces and register it for all of them. +`registerTypeAdapter` call checks +1. if the type adapter implements more than one of these interfaces, in that case it registers the adapter for all of them. +2. if the type adapter is for the Object class or JsonElement or any of its subclasses, in that case it throws IllegalArgumentException because overriding the built-in adapters for these types is not supported. #### Writing a Serializer @@ -741,7 +743,7 @@ In addition Gson's object model and data binding, you can use Gson to read from ## Issues in Designing Gson -See the [Gson design document](GsonDesignDocument.md "Gson design document") for a discussion of issues we faced while designing Gson. It also include a comparison of Gson with other Java libraries that can be used for JSON conversion. +See the [Gson design document](GsonDesignDocument.md "Gson design document") for a discussion of issues we faced while designing Gson. It also includes a comparison of Gson with other Java libraries that can be used for JSON conversion. ## Future Enhancements to Gson diff --git a/examples/android-proguard-example/README.md b/examples/android-proguard-example/README.md index 093c8eebca..902960fdfa 100644 --- a/examples/android-proguard-example/README.md +++ b/examples/android-proguard-example/README.md @@ -6,7 +6,15 @@ or remove them if they appear to be unused. This can cause issues for Gson which access the fields of a class. It is necessary to configure ProGuard to make sure that Gson works correctly. Also have a look at the [ProGuard manual](https://www.guardsquare.com/manual/configuration/usage#keepoverview) -for more details on how ProGuard can be configured. +and the [ProGuard Gson examples](https://www.guardsquare.com/manual/configuration/examples#gson) for more +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 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. 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/extras/pom.xml b/extras/pom.xml index 8600547700..d958bea74b 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -50,6 +50,7 @@ jsr250-api 1.0 + junit junit diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 87b522f0c4..29fbc69268 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -16,6 +16,7 @@ package com.google.gson.typeadapters; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -179,6 +180,7 @@ public static RuntimeTypeAdapterFactory of(Class baseType) { * Ensures that this factory will handle not just the given {@code baseType}, but any subtype * of that type. */ + @CanIgnoreReturnValue public RuntimeTypeAdapterFactory recognizeSubtypes() { this.recognizeSubtypes = true; return this; @@ -191,6 +193,7 @@ public RuntimeTypeAdapterFactory recognizeSubtypes() { * @throws IllegalArgumentException if either {@code type} or {@code label} * have already been registered on this type adapter. */ + @CanIgnoreReturnValue public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { if (type == null || label == null) { throw new NullPointerException(); @@ -210,6 +213,7 @@ public RuntimeTypeAdapterFactory registerSubtype(Class type, Str * @throws IllegalArgumentException if either {@code type} or its simple name * have already been registered on this type adapter. */ + @CanIgnoreReturnValue public RuntimeTypeAdapterFactory registerSubtype(Class type) { return registerSubtype(type, type.getSimpleName()); } diff --git a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java b/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java index 3b2425ec9e..9c9503ccfa 100644 --- a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java +++ b/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java @@ -129,8 +129,9 @@ public void testDeserializeListOfLists() { @Test public void testSerializationWithMultipleTypes() { Company google = new Company("Google"); - new Employee("Jesse", google); - new Employee("Joel", google); + // Employee constructor adds `this` to the given Company object + Employee unused1 = new Employee("Jesse", google); + Employee unused2 = new Employee("Joel", google); GsonBuilder gsonBuilder = new GsonBuilder(); new GraphAdapterBuilder() diff --git a/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java b/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java index ee475c8e2f..83fa8615bf 100644 --- a/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java +++ b/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java @@ -110,9 +110,9 @@ public void testCustomTypeAdapter() { @Override public User read(JsonReader in) throws IOException { in.beginObject(); - in.nextName(); + String unused1 = in.nextName(); String name = in.nextString(); - in.nextName(); + String unused2 = in.nextName(); String password = in.nextString(); in.endObject(); return new User(name, password); diff --git a/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java b/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java index 3d3262a336..23d5aeba6e 100644 --- a/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java +++ b/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java @@ -64,7 +64,7 @@ public void testUtcDatesOnJdkBefore1_7() { Gson gson = new GsonBuilder() .registerTypeAdapter(Date.class, new UtcDateTypeAdapter()) .create(); - gson.fromJson("'2014-12-05T04:00:00.000Z'", Date.class); + Date unused = gson.fromJson("'2014-12-05T04:00:00.000Z'", Date.class); } @Test diff --git a/graal-native-image-test/README.md b/graal-native-image-test/README.md new file mode 100644 index 0000000000..b3b2b353c2 --- /dev/null +++ b/graal-native-image-test/README.md @@ -0,0 +1,19 @@ +# graal-native-image-test + +This Maven module contains integration tests for using Gson in a GraalVM Native Image. + +Execution requires using GraalVM as JDK, and can be quite resource intensive. Native Image tests are therefore not enabled by default and the tests are only executed as regular unit tests. To run Native Image tests, make sure your `PATH` and `JAVA_HOME` environment variables point to GraalVM and then run: + +``` +mvn clean test --activate-profiles native-image-test +``` + +Technically it would also be possible to directly configure Native Image test execution for the `gson` module instead of having this separate Maven module. However, maintaining the reflection metadata for the unit tests would be quite cumbersome and would hinder future changes to the `gson` unit tests because many of them just happen to use reflection, without all of them being relevant for Native Image testing. + +## Reflection metadata + +Native Image creation requires configuring which class members are accessed using reflection, see the [GraalVM documentation](https://www.graalvm.org/22.3/reference-manual/native-image/metadata/#specifying-reflection-metadata-in-json). + +The file [`reflect-config.json`](./src/test/resources/META-INF/native-image/reflect-config.json) contains this reflection metadata. + +You can also run with `-Dagent=true` to let the Maven plugin automatically generate a metadata file, see the [plugin documentation](https://graalvm.github.io/native-build-tools/latest/maven-plugin.html#agent-support-running-tests). diff --git a/graal-native-image-test/pom.xml b/graal-native-image-test/pom.xml new file mode 100644 index 0000000000..6094092d79 --- /dev/null +++ b/graal-native-image-test/pom.xml @@ -0,0 +1,171 @@ + + + + 4.0.0 + + + com.google.code.gson + gson-parent + 2.10.2-SNAPSHOT + + graal-native-image-test + + + + 11 + **/Java17* + + + + + com.google.code.gson + gson + ${project.parent.version} + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + com.google.truth + truth + test + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + true + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + true + + + + org.apache.maven.plugins + maven-jar-plugin + + + true + + + + org.apache.maven.plugins + maven-install-plugin + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + default-testCompile + test-compile + + testCompile + + + + ${excludeTestCompilation} + + + + + + + + + + + JDK17 + + [17,) + + + 17 + + + + + + native-image-test + + false + + + + + org.graalvm.buildtools + native-maven-plugin + 0.9.28 + true + + + test-native + + test + + + true + + + + + + + + + diff --git a/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java b/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java new file mode 100644 index 0000000000..51ea11d199 --- /dev/null +++ b/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.native_test; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +class Java17RecordReflectionTest { + public record PublicRecord(int i) { + } + + @Test + void testPublicRecord() { + Gson gson = new Gson(); + PublicRecord r = gson.fromJson("{\"i\":1}", PublicRecord.class); + assertThat(r.i).isEqualTo(1); + } + + // Private record has implicit private canonical constructor + private record PrivateRecord(int i) { + } + + @Test + void testPrivateRecord() { + Gson gson = new Gson(); + PrivateRecord r = gson.fromJson("{\"i\":1}", PrivateRecord.class); + assertThat(r.i).isEqualTo(1); + } + + @Test + void testLocalRecord() { + record LocalRecordDeserialization(int i) { + } + + Gson gson = new Gson(); + LocalRecordDeserialization r = gson.fromJson("{\"i\":1}", LocalRecordDeserialization.class); + assertThat(r.i).isEqualTo(1); + } + + @Test + void testLocalRecordSerialization() { + record LocalRecordSerialization(int i) { + } + + Gson gson = new Gson(); + assertThat(gson.toJson(new LocalRecordSerialization(1))).isEqualTo("{\"i\":1}"); + } + + private record RecordWithSerializedName(@SerializedName("custom-name") int i) { + } + + @Test + void testSerializedName() { + Gson gson = new Gson(); + RecordWithSerializedName r = gson.fromJson("{\"custom-name\":1}", RecordWithSerializedName.class); + assertThat(r.i).isEqualTo(1); + + assertThat(gson.toJson(new RecordWithSerializedName(2))).isEqualTo("{\"custom-name\":2}"); + } + + private record RecordWithCustomConstructor(int i) { + @SuppressWarnings("unused") + RecordWithCustomConstructor { + i += 5; + } + } + + @Test + void testCustomConstructor() { + Gson gson = new Gson(); + RecordWithCustomConstructor r = gson.fromJson("{\"i\":1}", RecordWithCustomConstructor.class); + assertThat(r.i).isEqualTo(6); + } + + private record RecordWithCustomAccessor(int i) { + @SuppressWarnings("UnusedMethod") + @Override + public int i() { + return i + 5; + } + } + + @Test + void testCustomAccessor() { + Gson gson = new Gson(); + assertThat(gson.toJson(new RecordWithCustomAccessor(2))).isEqualTo("{\"i\":7}"); + } + + @JsonAdapter(RecordWithCustomClassAdapter.CustomAdapter.class) + private record RecordWithCustomClassAdapter(int i) { + private static class CustomAdapter extends TypeAdapter { + @Override + public RecordWithCustomClassAdapter read(JsonReader in) throws IOException { + return new RecordWithCustomClassAdapter(in.nextInt() + 5); + } + + @Override + public void write(JsonWriter out, RecordWithCustomClassAdapter value) throws IOException { + out.value(value.i + 6); + } + } + } + + @Test + void testCustomClassAdapter() { + Gson gson = new Gson(); + RecordWithCustomClassAdapter r = gson.fromJson("1", RecordWithCustomClassAdapter.class); + assertThat(r.i).isEqualTo(6); + + assertThat(gson.toJson(new RecordWithCustomClassAdapter(1))).isEqualTo("7"); + } + + private record RecordWithCustomFieldAdapter( + @JsonAdapter(RecordWithCustomFieldAdapter.CustomAdapter.class) + int i + ) { + private static class CustomAdapter extends TypeAdapter { + @Override + public Integer read(JsonReader in) throws IOException { + return in.nextInt() + 5; + } + + @Override + public void write(JsonWriter out, Integer value) throws IOException { + out.value(value + 6); + } + } + } + + @Test + void testCustomFieldAdapter() { + Gson gson = new Gson(); + RecordWithCustomFieldAdapter r = gson.fromJson("{\"i\":1}", RecordWithCustomFieldAdapter.class); + assertThat(r.i).isEqualTo(6); + + assertThat(gson.toJson(new RecordWithCustomFieldAdapter(1))).isEqualTo("{\"i\":7}"); + } + + private record RecordWithRegisteredAdapter(int i) { + } + + @Test + void testCustomAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(RecordWithRegisteredAdapter.class, new TypeAdapter() { + @Override + public RecordWithRegisteredAdapter read(JsonReader in) throws IOException { + return new RecordWithRegisteredAdapter(in.nextInt() + 5); + } + + @Override + public void write(JsonWriter out, RecordWithRegisteredAdapter value) throws IOException { + out.value(value.i + 6); + } + }) + .create(); + + RecordWithRegisteredAdapter r = gson.fromJson("1", RecordWithRegisteredAdapter.class); + assertThat(r.i).isEqualTo(6); + + assertThat(gson.toJson(new RecordWithRegisteredAdapter(1))).isEqualTo("7"); + } +} diff --git a/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java b/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java new file mode 100644 index 0000000000..3a2a5f48eb --- /dev/null +++ b/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.gson.native_test; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ReflectionTest { + private static class ClassWithDefaultConstructor { + private int i; + } + + @Test + void testDefaultConstructor() { + Gson gson = new Gson(); + + ClassWithDefaultConstructor c = gson.fromJson("{\"i\":1}", ClassWithDefaultConstructor.class); + assertThat(c.i).isEqualTo(1); + } + + private static class ClassWithCustomDefaultConstructor { + private int i; + + private ClassWithCustomDefaultConstructor() { + i = 1; + } + } + + @Test + void testCustomDefaultConstructor() { + Gson gson = new Gson(); + + ClassWithCustomDefaultConstructor c = gson.fromJson("{\"i\":2}", ClassWithCustomDefaultConstructor.class); + assertThat(c.i).isEqualTo(2); + + c = gson.fromJson("{}", ClassWithCustomDefaultConstructor.class); + assertThat(c.i).isEqualTo(1); + } + + private static class ClassWithoutDefaultConstructor { + private int i = -1; + + // Explicit constructor with args to remove implicit no-args default constructor + private ClassWithoutDefaultConstructor(int i) { + this.i = i; + } + } + + /** + * Tests deserializing a class without default constructor. + * + *

This should use JDK Unsafe, and would normally require specifying {@code "unsafeAllocated": true} + * in the reflection metadata for GraalVM, though for some reason it also seems to work without it? Possibly + * because GraalVM seems to have special support for Gson, see its class {@code com.oracle.svm.thirdparty.gson.GsonFeature}. + */ + @Test + void testClassWithoutDefaultConstructor() { + Gson gson = new Gson(); + + ClassWithoutDefaultConstructor c = gson.fromJson("{\"i\":1}", ClassWithoutDefaultConstructor.class); + assertThat(c.i).isEqualTo(1); + + c = gson.fromJson("{}", ClassWithoutDefaultConstructor.class); + // Class is instantiated with JDK Unsafe, so field keeps its default value instead of assigned -1 + assertThat(c.i).isEqualTo(0); + } + + @Test + void testInstanceCreator() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(ClassWithoutDefaultConstructor.class, new InstanceCreator() { + @Override + public ClassWithoutDefaultConstructor createInstance(Type type) { + return new ClassWithoutDefaultConstructor(-2); + } + }) + .create(); + + ClassWithoutDefaultConstructor c = gson.fromJson("{\"i\":1}", ClassWithoutDefaultConstructor.class); + assertThat(c.i).isEqualTo(1); + + c = gson.fromJson("{}", ClassWithoutDefaultConstructor.class); + // Uses default value specified by InstanceCreator + assertThat(c.i).isEqualTo(-2); + } + + private static class ClassWithFinalField { + // Initialize with value which is not inlined by compiler + private final int i = nonConstant(); + + private static int nonConstant() { + return "a".length(); // = 1 + } + } + + @Test + void testFinalField() { + Gson gson = new Gson(); + + ClassWithFinalField c = gson.fromJson("{\"i\":2}", ClassWithFinalField.class); + assertThat(c.i).isEqualTo(2); + + c = gson.fromJson("{}", ClassWithFinalField.class); + assertThat(c.i).isEqualTo(1); + } + + private static class ClassWithSerializedName { + @SerializedName("custom-name") + private int i; + } + + @Test + void testSerializedName() { + Gson gson = new Gson(); + ClassWithSerializedName c = gson.fromJson("{\"custom-name\":1}", ClassWithSerializedName.class); + assertThat(c.i).isEqualTo(1); + + c = new ClassWithSerializedName(); + c.i = 2; + assertThat(gson.toJson(c)).isEqualTo("{\"custom-name\":2}"); + } + + @JsonAdapter(ClassWithCustomClassAdapter.CustomAdapter.class) + private static class ClassWithCustomClassAdapter { + private static class CustomAdapter extends TypeAdapter { + @Override + public ClassWithCustomClassAdapter read(JsonReader in) throws IOException { + return new ClassWithCustomClassAdapter(in.nextInt() + 5); + } + + @Override + public void write(JsonWriter out, ClassWithCustomClassAdapter value) throws IOException { + out.value(value.i + 6); + } + } + + private int i; + + private ClassWithCustomClassAdapter(int i) { + this.i = i; + } + } + + @Test + void testCustomClassAdapter() { + Gson gson = new Gson(); + ClassWithCustomClassAdapter c = gson.fromJson("1", ClassWithCustomClassAdapter.class); + assertThat(c.i).isEqualTo(6); + + assertThat(gson.toJson(new ClassWithCustomClassAdapter(1))).isEqualTo("7"); + } + + private static class ClassWithCustomFieldAdapter { + private static class CustomAdapter extends TypeAdapter { + @Override + public Integer read(JsonReader in) throws IOException { + return in.nextInt() + 5; + } + + @Override + public void write(JsonWriter out, Integer value) throws IOException { + out.value(value + 6); + } + } + + @JsonAdapter(ClassWithCustomFieldAdapter.CustomAdapter.class) + private int i; + + private ClassWithCustomFieldAdapter(int i) { + this.i = i; + } + + private ClassWithCustomFieldAdapter() { + this(-1); + } + } + + @Test + void testCustomFieldAdapter() { + Gson gson = new Gson(); + ClassWithCustomFieldAdapter c = gson.fromJson("{\"i\":1}", ClassWithCustomFieldAdapter.class); + assertThat(c.i).isEqualTo(6); + + assertThat(gson.toJson(new ClassWithCustomFieldAdapter(1))).isEqualTo("{\"i\":7}"); + } + + private static class ClassWithRegisteredAdapter { + private int i; + + private ClassWithRegisteredAdapter(int i) { + this.i = i; + } + } + + @Test + void testCustomAdapter() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(ClassWithRegisteredAdapter.class, new TypeAdapter() { + @Override + public ClassWithRegisteredAdapter read(JsonReader in) throws IOException { + return new ClassWithRegisteredAdapter(in.nextInt() + 5); + } + + @Override + public void write(JsonWriter out, ClassWithRegisteredAdapter value) throws IOException { + out.value(value.i + 6); + } + }) + .create(); + + ClassWithRegisteredAdapter c = gson.fromJson("1", ClassWithRegisteredAdapter.class); + assertThat(c.i).isEqualTo(6); + + assertThat(gson.toJson(new ClassWithRegisteredAdapter(1))).isEqualTo("7"); + } + + @Test + void testGenerics() { + Gson gson = new Gson(); + + List list = gson.fromJson("[{\"i\":1}]", new TypeToken>() {}); + assertThat(list).hasSize(1); + assertThat(list.get(0).i).isEqualTo(1); + + @SuppressWarnings("unchecked") + List list2 = (List) gson.fromJson("[{\"i\":1}]", TypeToken.getParameterized(List.class, ClassWithDefaultConstructor.class)); + assertThat(list2).hasSize(1); + assertThat(list2.get(0).i).isEqualTo(1); + } +} diff --git a/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json b/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 0000000000..e5220767d3 --- /dev/null +++ b/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,101 @@ +[ +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithDefaultConstructor", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomDefaultConstructor", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithoutDefaultConstructor", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithFinalField", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithSerializedName", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomFieldAdapter", + "allDeclaredFields":true, + "allDeclaredConstructors": true +}, + +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomClassAdapter$CustomAdapter", + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomFieldAdapter$CustomAdapter", + "allDeclaredConstructors": true +}, + + + +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$PublicRecord", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$PrivateRecord", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithSerializedName", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomConstructor", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomAccessor", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomFieldAdapter", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$1LocalRecordDeserialization", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$1LocalRecordSerialization", + "allDeclaredFields":true, + "allPublicMethods": true, + "allDeclaredConstructors": true +}, + +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomClassAdapter$CustomAdapter", + "allDeclaredConstructors": true +}, +{ + "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomFieldAdapter$CustomAdapter", + "allDeclaredConstructors": true +} +] diff --git a/gson/pom.xml b/gson/pom.xml index 8af1ecdee3..d7c7ffcd3e 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -46,7 +46,7 @@ com.google.errorprone error_prone_annotations - 2.18.0 + 2.23.0 @@ -57,13 +57,12 @@ com.google.truth truth - 1.1.3 test com.google.guava guava-testlib - 31.1-jre + 32.1.3-jre test @@ -84,7 +83,7 @@ filter-sources - ${basedir}/src/main/java-templates + ${project.basedir}/src/main/java-templates ${project.build.directory}/generated-sources/java-templates @@ -94,6 +93,7 @@ org.apache.maven.plugins maven-compiler-plugin + default-compile @@ -103,6 +103,7 @@ + default-testCompile test-compile @@ -136,7 +137,6 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M9 true + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + true + + org.apache.maven.plugins maven-deploy-plugin diff --git a/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java b/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java index 80881dc8e6..2c74b39dc2 100644 --- a/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java +++ b/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java @@ -66,7 +66,7 @@ public void timeBagOfPrimitivesStreaming(int reps) throws IOException { int intValue = 0; boolean booleanValue = false; String stringValue = null; - while(jr.hasNext()) { + while (jr.hasNext()) { String name = jr.nextName(); if (name.equals("longValue")) { longValue = jr.nextLong(); @@ -96,7 +96,7 @@ public void timeBagOfPrimitivesReflectionStreaming(int reps) throws Exception { JsonReader jr = new JsonReader(reader); jr.beginObject(); BagOfPrimitives bag = new BagOfPrimitives(); - while(jr.hasNext()) { + while (jr.hasNext()) { String name = jr.nextName(); for (Field field : BagOfPrimitives.class.getDeclaredFields()) { if (field.getName().equals(name)) { diff --git a/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java b/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java index c10202db8d..49868c1516 100644 --- a/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java +++ b/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java @@ -56,7 +56,7 @@ void setUp() throws Exception { * Benchmark to measure Gson performance for deserializing an object */ public void timeCollectionsDefault(int reps) { - for (int i=0; i bags = new ArrayList<>(); - while(jr.hasNext()) { + while (jr.hasNext()) { jr.beginObject(); long longValue = 0; int intValue = 0; boolean booleanValue = false; String stringValue = null; - while(jr.hasNext()) { + while (jr.hasNext()) { String name = jr.nextName(); if (name.equals("longValue")) { longValue = jr.nextLong(); @@ -105,15 +105,15 @@ public void timeCollectionsStreaming(int reps) throws IOException { */ @SuppressWarnings("ModifiedButNotUsed") public void timeCollectionsReflectionStreaming(int reps) throws Exception { - for (int i=0; i bags = new ArrayList<>(); - while(jr.hasNext()) { + while (jr.hasNext()) { jr.beginObject(); BagOfPrimitives bag = new BagOfPrimitives(); - while(jr.hasNext()) { + while (jr.hasNext()) { String name = jr.nextName(); for (Field field : BagOfPrimitives.class.getDeclaredFields()) { if (field.getName().equals(name)) { diff --git a/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java b/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java index 63522af2e7..c366a0d129 100644 --- a/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java +++ b/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java @@ -270,7 +270,7 @@ public void parse(char[] data, Document document) throws Exception { } private static class GsonBindParser implements Parser { - private static Gson gson = new GsonBuilder() + private static final Gson gson = new GsonBuilder() .setDateFormat("EEE MMM dd HH:mm:ss Z yyyy") .create(); @@ -351,7 +351,7 @@ static class User { @JsonProperty boolean geo_enabled; @JsonProperty boolean verified; @JsonProperty String profile_background_image_url; - @JsonProperty boolean defalut_profile_image; + @JsonProperty boolean default_profile_image; @JsonProperty int friends_count; @JsonProperty int statuses_count; @JsonProperty String screen_name; diff --git a/pom.xml b/pom.xml index 93abb76ed5..e25f4c4910 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,8 @@ gson + graal-native-image-test + shrinker-test extras metrics proto @@ -83,7 +85,12 @@ junit junit 4.13.2 - test + + + + com.google.truth + truth + 1.1.5 @@ -93,7 +100,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.2.1 + 3.4.1 enforce-versions @@ -132,7 +139,10 @@ -XDcompilePolicy=simple - -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xplugin:ErrorProne + -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xep:NotJavadoc:OFF + -Xlint:all,-options @@ -141,7 +151,7 @@ com.google.errorprone error_prone_core - 2.18.0 + 2.23.0 @@ -149,7 +159,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.0 @@ -174,25 +184,35 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.1 + org.apache.maven.plugins maven-jar-plugin 3.3.0 + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + org.apache.maven.plugins maven-source-plugin - 3.2.1 + 3.3.0 org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 org.apache.maven.plugins maven-release-plugin - 3.0.0 + 3.0.1 true @@ -279,7 +299,7 @@ com.github.siom79.japicmp japicmp-maven-plugin - 0.17.2 + 0.18.2 @@ -306,6 +326,33 @@ + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.23 + + + check-android-compatibility + + check + + + + + net.sf.androidscents.signature + android-api-level-21 + 5.0.1_r2 + + + + + @@ -331,6 +378,7 @@ + release diff --git a/proto/pom.xml b/proto/pom.xml index 9225ff2239..524e822a7c 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 31.1-jre + 32.1.3-jre @@ -62,7 +62,6 @@ com.google.truth truth - 1.1.3 test diff --git a/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java b/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java index 9aa166fc68..6b4003f07f 100644 --- a/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java +++ b/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java @@ -20,6 +20,7 @@ import com.google.common.base.CaseFormat; import com.google.common.collect.MapMaker; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; @@ -102,6 +103,7 @@ private Builder(EnumSerialization enumSerialization, CaseFormat fromFieldNameFor setFieldNameSerializationFormat(fromFieldNameFormat, toFieldNameFormat); } + @CanIgnoreReturnValue public Builder setEnumSerialization(EnumSerialization enumSerialization) { this.enumSerialization = requireNonNull(enumSerialization); return this; @@ -122,6 +124,7 @@ public Builder setEnumSerialization(EnumSerialization enumSerialization) { * n__id_ct nIdCt * } */ + @CanIgnoreReturnValue public Builder setFieldNameSerializationFormat(CaseFormat fromFieldNameFormat, CaseFormat toFieldNameFormat) { this.protoFormat = fromFieldNameFormat; @@ -141,6 +144,7 @@ public Builder setFieldNameSerializationFormat(CaseFormat fromFieldNameFormat, * ...the adapter will serialize the field using '{@code appId}' instead of the default ' * {@code clientAppId}'. This lets you customize the name serialization of any proto field. */ + @CanIgnoreReturnValue public Builder addSerializedNameExtension( Extension serializedNameExtension) { serializedNameExtensions.add(requireNonNull(serializedNameExtension)); @@ -166,6 +170,7 @@ public Builder addSerializedNameExtension( * Note that you need to set the enum serialization of this adapter to * {@link EnumSerialization#NAME}, otherwise these annotations will be ignored. */ + @CanIgnoreReturnValue public Builder addSerializedEnumValueExtension( Extension serializedEnumValueExtension) { serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension)); diff --git a/shrinker-test/README.md b/shrinker-test/README.md new file mode 100644 index 0000000000..f9b674d143 --- /dev/null +++ b/shrinker-test/README.md @@ -0,0 +1,9 @@ +# shrinker-test + +This Maven module contains integration tests which check the behavior of Gson when used in combination with code shrinking and obfuscation tools, such as ProGuard or R8. + +The code which is shrunken is under `src/main/java`; it should not contain any important assertions in case the code shrinking tools affect these assertions in any way. The test code under `src/test/java` executes the shrunken and obfuscated JAR and verifies that it behaves as expected. + +The tests might be a bit brittle, especially the R8 test setup. Future ProGuard and R8 versions might cause the tests to behave differently. In case tests fail the ProGuard and R8 mapping files created in the `target` directory can help with debugging. If necessary rewrite tests or even remove them if they cannot be implemented anymore for newer ProGuard or R8 versions. + +**Important:** Because execution of the code shrinking tools is performed during the Maven build, trying to directly run the integration tests from the IDE might not work, or might use stale results if you changed the configuration in between. Run `mvn clean verify` before trying to run the integration tests from the IDE. diff --git a/shrinker-test/common.pro b/shrinker-test/common.pro new file mode 100644 index 0000000000..9995925e9d --- /dev/null +++ b/shrinker-test/common.pro @@ -0,0 +1,44 @@ +### Common rules for ProGuard and R8 +### Should only contains rules needed specifically for the integration test; +### any general rules which are relevant for all users should not be here but in `META-INF/proguard` of Gson + +-allowaccessmodification + +# On Windows mixed case class names might cause problems +-dontusemixedcaseclassnames + +# Ignore notes about duplicate JDK classes +-dontnote module-info,jdk.internal.** + + +# Keep test entrypoints +-keep class com.example.Main { + public static void runTests(...); +} +-keep class com.example.NoSerializedNameMain { + public static java.lang.String runTestNoArgsConstructor(); + public static java.lang.String runTestNoJdkUnsafe(); + public static java.lang.String runTestHasArgsConstructor(); +} + + +### Test data setup + +# Keep fields without annotations which should be preserved +-keepclassmembers class com.example.ClassWithNamedFields { + !transient ; +} + +-keepclassmembernames class com.example.ClassWithExposeAnnotation { + ; +} +-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation { + ** f; +} +-keepclassmembernames class com.example.ClassWithVersionAnnotations { + ; +} + +# Keep the name of the class to allow using reflection to check if this class still exists +# after shrinking +-keepnames class com.example.UnusedClass diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml new file mode 100644 index 0000000000..d4ea9115c5 --- /dev/null +++ b/shrinker-test/pom.xml @@ -0,0 +1,228 @@ + + + + 4.0.0 + + + com.google.code.gson + gson-parent + 2.10.2-SNAPSHOT + + + shrinker-test + + + 8 + + + + + + google + https://maven.google.com + + + + + + com.google.code.gson + gson + ${project.parent.version} + + + + junit + junit + test + + + com.google.truth + truth + test + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + true + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + + + + + + com.github.wvengen + proguard-maven-plugin + 2.6.0 + + + package + + proguard + + + + + true + ${project.basedir}/proguard.pro + + + + + + ${java.home}/jmods/java.base.jmod + + ${java.home}/jmods/java.sql.jmod + + ${java.home}/jmods/java.compiler.jmod + + + true + proguard-output.jar + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + false + + false + + + *:* + + + META-INF/MANIFEST.MF + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + r8 + package + + java + + + + false + false + + true + + + com.android.tools + r8 + + + + com.android.tools.r8.R8 + + --release + + --classfile + --lib${java.home} + --pg-conf${project.basedir}/r8.pro + + --pg-map-output${project.build.directory}/r8_map.txt + --output${project.build.directory}/r8-output.jar + ${project.build.directory}/${project.build.finalName}.jar + + + + + + + + + com.android.tools + r8 + 8.1.56 + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.1 + + + + integration-test + verify + + + + + + + diff --git a/shrinker-test/proguard.pro b/shrinker-test/proguard.pro new file mode 100644 index 0000000000..7922bc0967 --- /dev/null +++ b/shrinker-test/proguard.pro @@ -0,0 +1,17 @@ +# Include common rules +-include common.pro + +### ProGuard specific rules + +# Unlike R8, ProGuard does not perform aggressive optimization which makes classes abstract, +# therefore for ProGuard can successfully perform deserialization, and for that need to +# preserve the field names +-keepclassmembernames class com.example.NoSerializedNameMain$TestClassNoArgsConstructor { + ; +} +-keepclassmembernames class com.example.NoSerializedNameMain$TestClassNotAbstract { + ; +} +-keepclassmembernames class com.example.NoSerializedNameMain$TestClassHasArgsConstructor { + ; +} diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro new file mode 100644 index 0000000000..642334d56f --- /dev/null +++ b/shrinker-test/r8.pro @@ -0,0 +1,24 @@ +# Include common rules +-include common.pro + +### 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 + +# 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 +-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass + +# Don't obfuscate class name, to check it in exception message +-keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassNoArgsConstructor +-keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassHasArgsConstructor + +# 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.NoSerializedNameMain$TestClassNotAbstract { + @com.google.gson.annotations.SerializedName ; +} + +# Keep enum constants which are not explicitly used in code +-keepclassmembers class com.example.EnumClass { + ** SECOND; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithAdapter.java b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java new file mode 100644 index 0000000000..aa7f08da6b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java @@ -0,0 +1,44 @@ +package com.example; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +@JsonAdapter(ClassWithAdapter.Adapter.class) +public class ClassWithAdapter { + static class Adapter extends TypeAdapter { + @Override + public ClassWithAdapter read(JsonReader in) throws IOException { + in.beginObject(); + String name = in.nextName(); + if (!name.equals("custom")) { + throw new IllegalArgumentException("Unexpected name: " + name); + } + int i = in.nextInt(); + in.endObject(); + + return new ClassWithAdapter(i); + } + + @Override + public void write(JsonWriter out, ClassWithAdapter value) throws IOException { + out.beginObject(); + out.name("custom"); + out.value(value.i); + out.endObject(); + } + } + + public Integer i; + + public ClassWithAdapter(int i) { + this.i = i; + } + + @Override + public String toString() { + return "ClassWithAdapter[" + i + "]"; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java new file mode 100644 index 0000000000..30a61fa921 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java @@ -0,0 +1,13 @@ +package com.example; + +import com.google.gson.annotations.Expose; + +/** + * Uses {@link Expose} annotation. + */ +public class ClassWithExposeAnnotation { + @Expose + int i; + + int i2; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java new file mode 100644 index 0000000000..c65281e08e --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java @@ -0,0 +1,17 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +/** + * Class without no-args constructor, but with field annotated with + * {@link SerializedName}. + */ +public class ClassWithHasArgsConstructor { + @SerializedName("myField") + public int i; + + // Specify explicit constructor with args to remove implicit no-args default constructor + public ClassWithHasArgsConstructor(int i) { + this.i = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java new file mode 100644 index 0000000000..4ea37b3d91 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java @@ -0,0 +1,127 @@ +package com.example; + +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * Uses {@link JsonAdapter} annotation on fields. + */ +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; + + @SerializedName("f1") + @JsonAdapter(Adapter.class) + DummyClass f1; + + @SerializedName("f2") + @JsonAdapter(Factory.class) + DummyClass f2; + + @SerializedName("f3") + @JsonAdapter(Serializer.class) + DummyClass f3; + + @SerializedName("f4") + @JsonAdapter(Deserializer.class) + DummyClass f4; + + public ClassWithJsonAdapterAnnotation() { + } + + // Note: R8 seems to make this constructor the no-args constructor and initialize fields + // by default; currently this is not visible in the deserialization test because the JSON data + // contains values for all fields; but it is noticeable once the JSON data is missing fields + public ClassWithJsonAdapterAnnotation(int i1, int i2, int i3, int i4) { + f1 = new DummyClass(Integer.toString(i1)); + f2 = new DummyClass(Integer.toString(i2)); + f3 = new DummyClass(Integer.toString(i3)); + f4 = new DummyClass(Integer.toString(i4)); + + // Note: Deliberately don't initialize field `f` here to not refer to it anywhere in code + } + + @Override + public String toString() { + return "ClassWithJsonAdapterAnnotation[f1=" + f1 + ", f2=" + f2 + ", f3=" + f3 + ", f4=" + f4 + "]"; + } + + static class Adapter extends TypeAdapter { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("adapter-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + out.value("adapter-" + value); + } + } + + static class Factory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + @SuppressWarnings("unchecked") // the code below is not type-safe, but does not matter for this test + TypeAdapter r = (TypeAdapter) new TypeAdapter() { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("factory-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + out.value("factory-" + value.s); + } + }; + + return r; + } + } + + static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(DummyClass src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer-" + src.s); + } + } + + static class Deserializer implements JsonDeserializer { + @Override + public DummyClass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new DummyClass("deserializer-" + json.getAsInt()); + } + } + + // Use this separate class mainly to work around incorrect delegation behavior for JsonSerializer + // and JsonDeserializer used with @JsonAdapter, see https://github.com/google/gson/issues/1783 + static class DummyClass { + @SerializedName("s") + String s; + + DummyClass(String s) { + this.s = s; + } + + @Override + public String toString() { + return s; + } + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java new file mode 100644 index 0000000000..0a68da9c25 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java @@ -0,0 +1,10 @@ +package com.example; + +public class ClassWithNamedFields { + public int myField; + public short notAccessedField = -1; + + public ClassWithNamedFields(int i) { + myField = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java new file mode 100644 index 0000000000..f211daf91c --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java @@ -0,0 +1,16 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +/** + * Class with no-args constructor and with field annotated with + * {@link SerializedName}. + */ +public class ClassWithNoArgsConstructor { + @SerializedName("myField") + public int i; + + public ClassWithNoArgsConstructor() { + i = -3; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java new file mode 100644 index 0000000000..ce982215ca --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java @@ -0,0 +1,15 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public class ClassWithSerializedName { + @SerializedName("myField") + public int i; + + @SerializedName("notAccessed") + public short notAccessedField = -1; + + public ClassWithSerializedName(int i) { + this.i = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java new file mode 100644 index 0000000000..b0178bf613 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java @@ -0,0 +1,19 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +/** + * Class without no-args constructor, but with field annotated with + * {@link SerializedName}. The constructor should not actually be used in the + * code, but this shouldn't lead to R8 concluding that values of the type are + * not constructible and therefore must be null. + */ +public class ClassWithUnreferencedHasArgsConstructor { + @SerializedName("myField") + public int i; + + // Specify explicit constructor with args to remove implicit no-args default constructor + public ClassWithUnreferencedHasArgsConstructor(int i) { + this.i = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java new file mode 100644 index 0000000000..2d43032140 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java @@ -0,0 +1,18 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +/** + * Class with no-args constructor and with field annotated with + * {@link SerializedName}. The constructor should not actually be used in the + * code, but this shouldn't lead to R8 concluding that values of the type are + * not constructible and therefore must be null. + */ +public class ClassWithUnreferencedNoArgsConstructor { + @SerializedName("myField") + public int i; + + public ClassWithUnreferencedNoArgsConstructor() { + i = -3; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java new file mode 100644 index 0000000000..9c554f7e9b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java @@ -0,0 +1,21 @@ +package com.example; + +import com.google.gson.annotations.Since; +import com.google.gson.annotations.Until; + +/** + * Uses {@link Since} and {@link Until} annotations. + */ +public class ClassWithVersionAnnotations { + @Since(1) + int i1; + + @Until(1) // will be ignored with GsonBuilder.setVersion(1) + int i2; + + @Since(2) // will be ignored with GsonBuilder.setVersion(1) + int i3; + + @Until(2) + int i4; +} diff --git a/shrinker-test/src/main/java/com/example/EnumClass.java b/shrinker-test/src/main/java/com/example/EnumClass.java new file mode 100644 index 0000000000..36688887bb --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClass.java @@ -0,0 +1,6 @@ +package com.example; + +public enum EnumClass { + FIRST, + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java new file mode 100644 index 0000000000..a127a8be13 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java @@ -0,0 +1,10 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public enum EnumClassWithSerializedName { + @SerializedName("one") + FIRST, + @SerializedName("two") + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/GenericClasses.java b/shrinker-test/src/main/java/com/example/GenericClasses.java new file mode 100644 index 0000000000..cd91149be4 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/GenericClasses.java @@ -0,0 +1,66 @@ +package com.example; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +public class GenericClasses { + static class GenericClass { + @SerializedName("t") + T t; + + @Override + public String toString() { + return "{t=" + t + "}"; + } + } + + static class UsingGenericClass { + @SerializedName("g") + GenericClass g; + + @Override + public String toString() { + return "{g=" + g + "}"; + } + } + + static class GenericUsingGenericClass { + @SerializedName("g") + GenericClass g; + + @Override + public String toString() { + return "{g=" + g + "}"; + } + } + + @JsonAdapter(DummyClass.Adapter.class) + static class DummyClass { + String s; + + DummyClass(String s) { + this.s = s; + } + + @Override + public String toString() { + return s; + } + + static class Adapter extends TypeAdapter { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("read-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/shrinker-test/src/main/java/com/example/Main.java b/shrinker-test/src/main/java/com/example/Main.java new file mode 100644 index 0000000000..d2d88021e1 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/Main.java @@ -0,0 +1,204 @@ +package com.example; + +import static com.example.TestExecutor.same; + +import com.example.GenericClasses.DummyClass; +import com.example.GenericClasses.GenericClass; +import com.example.GenericClasses.GenericUsingGenericClass; +import com.example.GenericClasses.UsingGenericClass; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +public class Main { + /** + * Main entrypoint, called by {@code ShrinkingIT.test()}. + * + *

To be safe let all tests put their output to the consumer and let integration test verify it; + * don't perform any relevant assertions in this code because code shrinkers could affect it. + * + * @param outputConsumer consumes the test output: {@code name, content} pairs + */ + public static void runTests(BiConsumer outputConsumer) { + // Create the TypeToken instances on demand because creation of them can fail when + // generic signatures were erased + testTypeTokenWriteRead(outputConsumer, "anonymous", () -> new TypeToken>() {}); + testTypeTokenWriteRead(outputConsumer, "manual", () -> TypeToken.getParameterized(List.class, ClassWithAdapter.class)); + + testNamedFields(outputConsumer); + testSerializedName(outputConsumer); + + testConstructorNoArgs(outputConsumer); + testConstructorHasArgs(outputConsumer); + testUnreferencedConstructorNoArgs(outputConsumer); + testUnreferencedConstructorHasArgs(outputConsumer); + + testNoJdkUnsafe(outputConsumer); + + testEnum(outputConsumer); + testEnumSerializedName(outputConsumer); + + testExposeAnnotation(outputConsumer); + testVersionAnnotations(outputConsumer); + testJsonAdapterAnnotation(outputConsumer); + + testGenericClasses(outputConsumer); + } + + private static void testTypeTokenWriteRead(BiConsumer outputConsumer, String description, Supplier> typeTokenSupplier) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + TestExecutor.run(outputConsumer, "Write: TypeToken " + description, + () -> gson.toJson(Arrays.asList(new ClassWithAdapter(1)), typeTokenSupplier.get().getType())); + TestExecutor.run(outputConsumer, "Read: TypeToken " + description, () -> { + Object deserialized = gson.fromJson("[{\"custom\": 3}]", typeTokenSupplier.get()); + return deserialized.toString(); + }); + } + + /** + * Calls {@link Gson#toJson}, but (hopefully) in a way which prevents code shrinkers + * from understanding that reflection is used for {@code obj}. + */ + private static String toJson(Gson gson, Object obj) { + return gson.toJson(same(obj)); + } + + /** + * Calls {@link Gson#fromJson}, but (hopefully) in a way which prevents code shrinkers + * from understanding that reflection is used for {@code c}. + */ + private static T fromJson(Gson gson, String json, Class c) { + return gson.fromJson(json, same(c)); + } + + private static void testNamedFields(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Named fields", () -> toJson(gson, new ClassWithNamedFields(2))); + TestExecutor.run(outputConsumer, "Read: Named fields", () -> { + ClassWithNamedFields deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithNamedFields.class); + return Integer.toString(deserialized.myField); + }); + } + + private static void testSerializedName(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: SerializedName", () -> toJson(gson, new ClassWithSerializedName(2))); + TestExecutor.run(outputConsumer, "Read: SerializedName", () -> { + ClassWithSerializedName deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithSerializedName.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testConstructorNoArgs(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: No args constructor", () -> toJson( + gson, new ClassWithNoArgsConstructor())); + TestExecutor.run(outputConsumer, "Read: No args constructor; initial constructor value", () -> { + ClassWithNoArgsConstructor deserialized = fromJson(gson, "{}", ClassWithNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + TestExecutor.run(outputConsumer, "Read: No args constructor; custom value", () -> { + ClassWithNoArgsConstructor deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testConstructorHasArgs(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Constructor with args", () -> toJson( + gson, new ClassWithHasArgsConstructor(2))); + // This most likely relies on JDK Unsafe (unless the shrinker rewrites the constructor in some way) + TestExecutor.run(outputConsumer, "Read: Constructor with args", () -> { + ClassWithHasArgsConstructor deserialized = fromJson( + gson, "{\"myField\": 3}", ClassWithHasArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testUnreferencedConstructorNoArgs(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + // No write because we're not referencing this class's constructor. + + // This runs the no-args constructor. + TestExecutor.run(outputConsumer, "Read: Unreferenced no args constructor; initial constructor value", () -> { + ClassWithUnreferencedNoArgsConstructor deserialized = fromJson( + gson, "{}", ClassWithUnreferencedNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + TestExecutor.run(outputConsumer, "Read: Unreferenced no args constructor; custom value", () -> { + ClassWithUnreferencedNoArgsConstructor deserialized = fromJson( + gson, "{\"myField\": 3}", ClassWithUnreferencedNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testUnreferencedConstructorHasArgs(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + // No write because we're not referencing this class's constructor. + + // This most likely relies on JDK Unsafe (unless the shrinker rewrites the constructor in some way) + TestExecutor.run(outputConsumer, "Read: Unreferenced constructor with args", () -> { + ClassWithUnreferencedHasArgsConstructor deserialized = fromJson( + gson, "{\"myField\": 3}", ClassWithUnreferencedHasArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testNoJdkUnsafe(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().disableJdkUnsafe().create(); + TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; initial constructor value", () -> { + ClassWithNoArgsConstructor deserialized = fromJson( + gson, "{}", ClassWithNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; custom value", () -> { + ClassWithNoArgsConstructor deserialized = fromJson( + gson, "{\"myField\": 3}", ClassWithNoArgsConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testEnum(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Enum", () -> toJson(gson, EnumClass.FIRST)); + TestExecutor.run(outputConsumer, "Read: Enum", () -> fromJson(gson, "\"SECOND\"", EnumClass.class).toString()); + } + + private static void testEnumSerializedName(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Enum SerializedName", + () -> toJson(gson, EnumClassWithSerializedName.FIRST)); + TestExecutor.run(outputConsumer, "Read: Enum SerializedName", + () -> fromJson(gson, "\"two\"", EnumClassWithSerializedName.class).toString()); + } + + private static void testExposeAnnotation(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + TestExecutor.run(outputConsumer, "Write: @Expose", () -> toJson(gson, new ClassWithExposeAnnotation())); + } + + private static void testVersionAnnotations(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setVersion(1).create(); + TestExecutor.run(outputConsumer, "Write: Version annotations", () -> toJson(gson, new ClassWithVersionAnnotations())); + } + + private static void testJsonAdapterAnnotation(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: JsonAdapter on fields", () -> toJson(gson, new ClassWithJsonAdapterAnnotation(1, 2, 3, 4))); + + String json = "{\"f1\": 1, \"f2\": 2, \"f3\": {\"s\": \"3\"}, \"f4\": 4}"; + TestExecutor.run(outputConsumer, "Read: JsonAdapter on fields", () -> fromJson(gson, json, ClassWithJsonAdapterAnnotation.class).toString()); + } + + private static void testGenericClasses(BiConsumer outputConsumer) { + Gson gson = new Gson(); + TestExecutor.run(outputConsumer, "Read: Generic TypeToken", () -> gson.fromJson("{\"t\": 1}", new TypeToken>() {}).toString()); + TestExecutor.run(outputConsumer, "Read: Using Generic", () -> fromJson(gson, "{\"g\": {\"t\": 1}}", UsingGenericClass.class).toString()); + TestExecutor.run(outputConsumer, "Read: Using Generic TypeToken", () -> gson.fromJson("{\"g\": {\"t\": 1}}", new TypeToken>() {}).toString()); + } +} diff --git a/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java b/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java new file mode 100644 index 0000000000..a43fc0bdef --- /dev/null +++ b/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java @@ -0,0 +1,59 @@ +package com.example; + +import static com.example.TestExecutor.same; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Covers cases of classes which don't use {@code @SerializedName} on their fields, and are + * therefore not matched by the default {@code gson.pro} rules. + */ +public class NoSerializedNameMain { + static class TestClassNoArgsConstructor { + // Has a no-args default constructor. + public String s; + } + + // R8 test rule in r8.pro for this class still removes no-args constructor, but doesn't make class abstract + static class TestClassNotAbstract { + public String s; + } + + static class TestClassHasArgsConstructor { + public String s; + + // Specify explicit constructor with args to remove implicit no-args default constructor + public TestClassHasArgsConstructor(String s) { + this.s = s; + } + } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testNoSerializedName_NoArgsConstructor()}. + */ + public static String runTestNoArgsConstructor() { + TestClassNoArgsConstructor deserialized = new Gson().fromJson( + "{\"s\":\"value\"}", same(TestClassNoArgsConstructor.class)); + return deserialized.s; + } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testNoSerializedName_NoArgsConstructorNoJdkUnsafe()}. + */ + public static String runTestNoJdkUnsafe() { + Gson gson = new GsonBuilder().disableJdkUnsafe().create(); + TestClassNotAbstract deserialized = gson.fromJson( + "{\"s\": \"value\"}", same(TestClassNotAbstract.class)); + return deserialized.s; + } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testNoSerializedName_HasArgsConstructor()}. + */ + public static String runTestHasArgsConstructor() { + TestClassHasArgsConstructor deserialized = new Gson().fromJson( + "{\"s\":\"value\"}", same(TestClassHasArgsConstructor.class)); + return deserialized.s; + } +} diff --git a/shrinker-test/src/main/java/com/example/TestExecutor.java b/shrinker-test/src/main/java/com/example/TestExecutor.java new file mode 100644 index 0000000000..6ea9b9b927 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/TestExecutor.java @@ -0,0 +1,34 @@ +package com.example; + +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +public class TestExecutor { + /** + * Helper method for running individual tests. In case of an exception wraps it and + * includes the {@code name} of the test to make debugging issues with the obfuscated + * JARs a bit easier. + */ + public static void run(BiConsumer outputConsumer, String name, Supplier resultSupplier) { + String result; + try { + result = resultSupplier.get(); + } catch (Throwable t) { + throw new RuntimeException("Test failed: " + name, t); + } + outputConsumer.accept(name, result); + } + + /** + * Returns {@code t}, but in a way which (hopefully) prevents code shrinkers from + * simplifying this. + */ + public static T same(T t) { + // This is essentially `return t`, but contains some redundant code to try + // prevent the code shrinkers from simplifying this + return Optional.of(t) + .map(v -> Optional.of(v).get()) + .orElseThrow(() -> new AssertionError("unreachable")); + } +} diff --git a/shrinker-test/src/main/java/com/example/UnusedClass.java b/shrinker-test/src/main/java/com/example/UnusedClass.java new file mode 100644 index 0000000000..18d452f949 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/UnusedClass.java @@ -0,0 +1,17 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +/** + * Class with no-args constructor and field annotated with {@code @SerializedName}, + * but which is not actually used anywhere in the code. + * + *

The default ProGuard rules should not keep this class. + */ +public class UnusedClass { + public UnusedClass() { + } + + @SerializedName("i") + public int i; +} 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 new file mode 100644 index 0000000000..a017c2c33b --- /dev/null +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gson.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import com.example.UnusedClass; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Integration test verifying behavior of shrunken and obfuscated JARs. + */ +@RunWith(Parameterized.class) +public class ShrinkingIT { + // These JAR files are prepared by the Maven build + public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar"); + public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar"); + + @Parameters(name = "{index}: {0}") + public static List jarsToTest() { + return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH); + } + + @Parameter + public Path jarToTest; + + @Before + public void verifyJarExists() { + if (!Files.isRegularFile(jarToTest)) { + fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`"); + } + } + + @FunctionalInterface + interface TestAction { + void run(Class c) throws Exception; + } + + private void runTest(String className, TestAction testAction) throws Exception { + // Use bootstrap class loader; load all custom classes from JAR and not + // from dependencies of this test + ClassLoader classLoader = null; + + // Load the shrunken and obfuscated JARs with a separate class loader, then load + // the main test class from it and let the test action invoke its test methods + try (URLClassLoader loader = new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) { + Class c = loader.loadClass(className); + testAction.run(c); + } + } + + @Test + public void test() throws Exception { + StringBuilder output = new StringBuilder(); + + runTest("com.example.Main", c -> { + Method m = c.getMethod("runTests", BiConsumer.class); + m.invoke(null, (BiConsumer) (name, content) -> output.append(name + "\n" + content + "\n===\n")); + }); + + assertThat(output.toString()).isEqualTo(String.join("\n", + "Write: TypeToken anonymous", + "[", + " {", + " \"custom\": 1", + " }", + "]", + "===", + "Read: TypeToken anonymous", + "[ClassWithAdapter[3]]", + "===", + "Write: TypeToken manual", + "[", + " {", + " \"custom\": 1", + " }", + "]", + "===", + "Read: TypeToken manual", + "[ClassWithAdapter[3]]", + "===", + "Write: Named fields", + "{", + " \"myField\": 2,", + " \"notAccessedField\": -1", + "}", + "===", + "Read: Named fields", + "3", + "===", + "Write: SerializedName", + "{", + " \"myField\": 2,", + " \"notAccessed\": -1", + "}", + "===", + "Read: SerializedName", + "3", + "===", + "Write: No args constructor", + "{", + " \"myField\": -3", + "}", + "===", + "Read: No args constructor; initial constructor value", + "-3", + "===", + "Read: No args constructor; custom value", + "3", + "===", + "Write: Constructor with args", + "{", + " \"myField\": 2", + "}", + "===", + "Read: Constructor with args", + "3", + "===", + "Read: Unreferenced no args constructor; initial constructor value", + "-3", + "===", + "Read: Unreferenced no args constructor; custom value", + "3", + "===", + "Read: Unreferenced constructor with args", + "3", + "===", + "Read: No JDK Unsafe; initial constructor value", + "-3", + "===", + "Read: No JDK Unsafe; custom value", + "3", + "===", + "Write: Enum", + "\"FIRST\"", + "===", + "Read: Enum", + "SECOND", + "===", + "Write: Enum SerializedName", + "\"one\"", + "===", + "Read: Enum SerializedName", + "SECOND", + "===", + "Write: @Expose", + "{\"i\":0}", + "===", + "Write: Version annotations", + "{\"i1\":0,\"i4\":0}", + "===", + "Write: JsonAdapter on fields", + "{", + " \"f\": \"adapter-null\",", + " \"f1\": \"adapter-1\",", + " \"f2\": \"factory-2\",", + " \"f3\": \"serializer-3\",", + // For f4 only a JsonDeserializer is registered, so serialization falls back to reflection + " \"f4\": {", + " \"s\": \"4\"", + " }", + "}", + "===", + "Read: JsonAdapter on fields", + // For f3 only a JsonSerializer is registered, so for deserialization value is read as is using reflection + "ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3, f4=deserializer-4]", + "===", + "Read: Generic TypeToken", + "{t=read-1}", + "===", + "Read: Using Generic", + "{g={t=read-1}}", + "===", + "Read: Using Generic TypeToken", + "{g={t=read-1}}", + "===", + "" + )); + } + + @Test + public void testNoSerializedName_NoArgsConstructor() throws Exception { + runTest("com.example.NoSerializedNameMain", c -> { + Method m = c.getMethod("runTestNoArgsConstructor"); + + 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.NoSerializedNameMain$TestClassNoArgsConstructor" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class" + ); + } + }); + } + + @Test + public void testNoSerializedName_NoArgsConstructorNoJdkUnsafe() throws Exception { + runTest("com.example.NoSerializedNameMain", c -> { + Method m = c.getMethod("runTestNoJdkUnsafe"); + + 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( + "Unable to create instance of class com.example.NoSerializedNameMain$TestClassNotAbstract;" + + " usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this type," + + " adding a no-args constructor, or enabling usage of JDK Unsafe may fix this problem. Or adjust" + + " your R8 configuration to keep the no-args constructor of the class." + ); + } + }); + } + + @Test + public void testNoSerializedName_HasArgsConstructor() throws Exception { + runTest("com.example.NoSerializedNameMain", c -> { + Method m = c.getMethod("runTestHasArgsConstructor"); + + 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.NoSerializedNameMain$TestClassHasArgsConstructor" + + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class" + ); + } + }); + } + + @Test + public void testUnusedClassRemoved() throws Exception { + // For some reason this test only works for R8 but not for ProGuard; ProGuard keeps the unused class + assumeTrue(jarToTest.equals(R8_RESULT_PATH)); + + String className = UnusedClass.class.getName(); + ClassNotFoundException e = assertThrows(ClassNotFoundException.class, () -> { + runTest(className, c -> { + fail("Class should have been removed during shrinking: " + c); + }); + }); + assertThat(e).hasMessageThat().contains(className); + } +}