diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..7041029d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ + + +### Purpose + + + + +### Description + + + + + +### Checklist + + +- [ ] New code follows the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) +- [ ] If necessary, new public API validates arguments, for example rejects `null` +- [ ] New public API has Javadoc + - [ ] Javadoc uses `@since $next-version$` + (`$next-version$` is a special placeholder which is automatically replaced during release) +- [ ] If necessary, new unit tests have been added + - [ ] Assertions in unit tests use [Truth](https://truth.dev/), see existing tests + - [ ] No JUnit 3 features are used (such as extending class `TestCase`) + - [ ] If this pull request fixes a bug, a new test was added for a situation which failed previously and is now fixed +- [ ] `mvn clean verify javadoc:jar` passes without errors diff --git a/.github/workflows/check-android-compatibility.yml b/.github/workflows/check-android-compatibility.yml new file mode 100644 index 00000000..e71956fa --- /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/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 00000000..3ed5dea4 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b620adf..7efc0da9 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/README.md b/README.md index 4ee9f76e..989b891e 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,18 @@ 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 * [Troubleshooting guide](Troubleshooting.md): Describes how to solve common issues when using Gson - * [Change log](CHANGELOG.md): Changes in the recent versions + * [Releases and change log](https://github.com/google/gson/releases): Latest releases and changes in these versions; for older releases see [`CHANGELOG.md`](CHANGELOG.md) * [Design document](GsonDesignDocument.md): This document discusses issues we faced while designing Gson. It also includes a comparison of Gson with other Java libraries that can be used for Json conversion Please use the ['gson' tag on StackOverflow](https://stackoverflow.com/questions/tagged/gson) or the [google-gson Google group](https://groups.google.com/group/google-gson) to discuss Gson or to post questions. @@ -95,6 +102,22 @@ Please use the ['gson' tag on StackOverflow](https://stackoverflow.com/questions * [Gson Tutorial Series](https://futurestud.io/tutorials/gson-getting-started-with-java-json-serialization-deserialization) by `Future Studio` * [Gson API Report](https://abi-laboratory.pro/java/tracker/timeline/gson/) +### Building + +Gson uses Maven to build the project: +``` +mvn clean verify +``` + +JDK 11 or newer is required for building, JDK 17 is recommended. + +### Contributing + +See the [contributing guide](https://github.com/google/.github/blob/master/CONTRIBUTING.md). +Please perform a quick search to check if there are already existing issues or pull requests related to your contribution. + +Keep in mind that Gson is in maintenance mode. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback. + ### License Gson is released under the [Apache 2.0 license](LICENSE). diff --git a/ReleaseProcess.md b/ReleaseProcess.md index eaa0e7c5..35103557 100644 --- a/ReleaseProcess.md +++ b/ReleaseProcess.md @@ -13,7 +13,7 @@ The following is a step-by-step procedure for releasing a new version of Google- 1. [Log in to Nexus repository manager](https://oss.sonatype.org/index.html#welcome) at Sonatype and close the staging repository for Gson. 1. Download and sanity check all downloads. Do not skip this step! Once you release the staging repository, there is no going back. It will get synced with Maven Central and you will not be able to update or delete anything. Your only recourse will be to release a new version of Gson and hope that no one uses the old one. 1. Release the staging repository for Gson. Gson will now get synced to Maven Central with-in the next hour. For issues consult [Sonatype Guide](https://central.sonatype.org/publish/release/). -1. Update [Gson Changelog](CHANGELOG.md). Also, look at all bugs that were fixed and add a few lines describing what changed in the release. +1. Create a [GitHub release](https://github.com/google/gson/releases) for the new version. You can let GitHub [automatically generate the description for the release](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes), but you should edit it manually to point out the most important changes and potentially incompatible changes. 1. Update version references in (version might be referenced multiple times): - [`README.md`](README.md) - [`UserGuide.md`](UserGuide.md) diff --git a/Troubleshooting.md b/Troubleshooting.md index 2f9185a3..94f8391e 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,7 +62,7 @@ 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 @@ -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,15 +108,32 @@ 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/). -## Integral JSON number is parsed as `double` +For example, let's assume you want to deserialize the following JSON data: + +```json +{ + "languages": [ + "English", + "French", + ] +} +``` + +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` @@ -121,17 +141,20 @@ Notes: **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:** -**Solution:** Add code similar to the following at the beginning of the `read` method of your adapter: +- 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:** 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 05380671..f5198654 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. @@ -651,7 +651,6 @@ public class SampleObjectForTest { @Foo private final int annotatedField; private final String stringField; private final long longField; - private final Class clazzField; public SampleObjectForTest() { annotatedField = 5; diff --git a/examples/android-proguard-example/README.md b/examples/android-proguard-example/README.md index 093c8eeb..902960fd 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/gson/pom.xml b/gson/pom.xml index 71ed3b82..9a74975f 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -34,6 +34,17 @@ + + + com.google.errorprone + error_prone_annotations + 2.21.1 + + junit junit @@ -42,13 +53,12 @@ com.google.truth truth - 1.1.3 test com.google.guava guava-testlib - 31.1-jre + 32.1.2-jre test @@ -69,7 +79,7 @@ filter-sources - ${basedir}/src/main/java-templates + ${project.basedir}/src/main/java-templates ${project.build.directory}/generated-sources/java-templates @@ -79,6 +89,7 @@ org.apache.maven.plugins maven-compiler-plugin + default-compile @@ -88,6 +99,7 @@ + default-testCompile test-compile @@ -125,7 +137,6 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M9 - + 4.0.0 io.gitlab.jfronny @@ -35,7 +36,9 @@ 11 - + + https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git @@ -85,40 +88,69 @@ junit junit 4.13.2 - test + + + + com.google.truth + truth + 1.1.5 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-versions + + enforce + + + + + + [3.3.1,) + + + + + + [11,) + + + + + + + + org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.11.0 true true - true + true -Xlint:all,-options - - [11,) - org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 + 3.6.0 - - [11,) - 11 @@ -129,6 +161,7 @@ false https://docs.oracle.com/en/java/javase/11/docs/api/ + https://errorprone.info/api/latest/ true + true + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + 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