aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--.github/workflows/build.yml16
-rw-r--r--.github/workflows/check-api-compatibility.yml50
-rw-r--r--.github/workflows/codeql-analysis.yml4
-rw-r--r--Android.bp2
-rw-r--r--CHANGELOG.md14
-rw-r--r--METADATA18
-rw-r--r--README.md4
-rw-r--r--ReleaseProcess.md27
-rw-r--r--UserGuide.md15
-rw-r--r--extras/pom.xml3
-rw-r--r--extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java11
-rw-r--r--extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java10
-rw-r--r--extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java9
-rw-r--r--gson/pom.xml176
-rw-r--r--gson/src/main/java/com/google/gson/ExclusionStrategy.java9
-rw-r--r--gson/src/main/java/com/google/gson/FieldAttributes.java5
-rw-r--r--gson/src/main/java/com/google/gson/FieldNamingPolicy.java10
-rw-r--r--gson/src/main/java/com/google/gson/Gson.java421
-rw-r--r--gson/src/main/java/com/google/gson/GsonBuilder.java189
-rw-r--r--gson/src/main/java/com/google/gson/JsonArray.java259
-rw-r--r--gson/src/main/java/com/google/gson/JsonDeserializer.java12
-rw-r--r--gson/src/main/java/com/google/gson/JsonElement.java171
-rw-r--r--gson/src/main/java/com/google/gson/JsonNull.java16
-rw-r--r--gson/src/main/java/com/google/gson/JsonObject.java87
-rw-r--r--gson/src/main/java/com/google/gson/JsonParser.java3
-rw-r--r--gson/src/main/java/com/google/gson/JsonPrimitive.java122
-rw-r--r--gson/src/main/java/com/google/gson/JsonSerializer.java6
-rw-r--r--gson/src/main/java/com/google/gson/ReflectionAccessFilter.java6
-rw-r--r--gson/src/main/java/com/google/gson/ToNumberPolicy.java6
-rw-r--r--gson/src/main/java/com/google/gson/ToNumberStrategy.java4
-rw-r--r--gson/src/main/java/com/google/gson/TypeAdapter.java20
-rw-r--r--gson/src/main/java/com/google/gson/TypeAdapterFactory.java3
-rw-r--r--gson/src/main/java/com/google/gson/annotations/JsonAdapter.java14
-rw-r--r--gson/src/main/java/com/google/gson/annotations/Since.java12
-rw-r--r--gson/src/main/java/com/google/gson/annotations/Until.java18
-rw-r--r--gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java8
-rw-r--r--gson/src/main/java/com/google/gson/internal/$Gson$Types.java14
-rw-r--r--gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java39
-rw-r--r--gson/src/main/java/com/google/gson/internal/Excluder.java8
-rw-r--r--gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java1
-rw-r--r--gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java46
-rw-r--r--gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java98
-rw-r--r--gson/src/main/java/com/google/gson/internal/Streams.java46
-rw-r--r--gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java26
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java39
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java27
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java17
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java47
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java9
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java4
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java2
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java347
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java14
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java26
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java41
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java44
-rw-r--r--gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java246
-rw-r--r--gson/src/main/java/com/google/gson/reflect/TypeToken.java62
-rw-r--r--gson/src/main/java/com/google/gson/stream/JsonReader.java140
-rw-r--r--gson/src/main/java/com/google/gson/stream/JsonScope.java2
-rw-r--r--gson/src/main/java/com/google/gson/stream/JsonWriter.java51
-rw-r--r--gson/src/test/java/com/google/gson/GsonBuilderTest.java162
-rw-r--r--gson/src/test/java/com/google/gson/GsonTest.java213
-rw-r--r--gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java6
-rw-r--r--gson/src/test/java/com/google/gson/JsonArrayAsListTest.java285
-rw-r--r--gson/src/test/java/com/google/gson/JsonArrayTest.java220
-rw-r--r--gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java287
-rw-r--r--gson/src/test/java/com/google/gson/JsonObjectTest.java146
-rw-r--r--gson/src/test/java/com/google/gson/JsonPrimitiveTest.java26
-rw-r--r--gson/src/test/java/com/google/gson/MixedStreamTest.java2
-rw-r--r--gson/src/test/java/com/google/gson/TypeAdapterTest.java33
-rw-r--r--gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java70
-rw-r--r--gson/src/test/java/com/google/gson/common/MoreAsserts.java57
-rw-r--r--gson/src/test/java/com/google/gson/common/TestTypes.java16
-rw-r--r--gson/src/test/java/com/google/gson/functional/ArrayTest.java42
-rw-r--r--gson/src/test/java/com/google/gson/functional/CollectionTest.java42
-rw-r--r--gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java11
-rw-r--r--gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java8
-rw-r--r--gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java2
-rw-r--r--gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java14
-rw-r--r--gson/src/test/java/com/google/gson/functional/InternationalizationTest.java42
-rw-r--r--gson/src/test/java/com/google/gson/functional/Java17RecordTest.java430
-rw-r--r--gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java18
-rw-r--r--gson/src/test/java/com/google/gson/functional/JsonArrayTest.java162
-rw-r--r--gson/src/test/java/com/google/gson/functional/MapTest.java32
-rw-r--r--gson/src/test/java/com/google/gson/functional/ObjectTest.java107
-rw-r--r--gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java106
-rw-r--r--gson/src/test/java/com/google/gson/functional/PrimitiveTest.java60
-rw-r--r--gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java9
-rw-r--r--gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java61
-rw-r--r--gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java81
-rw-r--r--gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java10
-rw-r--r--gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java19
-rw-r--r--gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java193
-rw-r--r--gson/src/test/java/com/google/gson/functional/TypeVariableTest.java10
-rw-r--r--gson/src/test/java/com/google/gson/functional/VersioningTest.java80
-rw-r--r--gson/src/test/java/com/google/gson/internal/JavaVersionTest.java5
-rw-r--r--gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java60
-rw-r--r--gson/src/test/java/com/google/gson/internal/StreamsTest.java68
-rw-r--r--gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java9
-rw-r--r--gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java6
-rw-r--r--gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java81
-rw-r--r--gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java10
-rw-r--r--gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java82
-rw-r--r--gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java27
-rw-r--r--gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java83
-rw-r--r--gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java108
-rw-r--r--gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java103
-rw-r--r--gson/src/test/java/com/google/gson/stream/JsonReaderTest.java71
-rw-r--r--metrics/pom.xml14
-rw-r--r--metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java9
-rw-r--r--metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java4
-rw-r--r--pom.xml241
-rw-r--r--proto/pom.xml3
-rw-r--r--proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java9
116 files changed, 5777 insertions, 1435 deletions
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index b8a47ca2..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-gson/docs/javadocs/* linguist-documentation
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0008892e..ef1b23d0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,16 +4,20 @@ on: [push, pull_request]
jobs:
build:
+ name: "Build on JDK ${{ matrix.java }}"
+ strategy:
+ matrix:
+ java: [ 11, 17 ]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 11
- uses: actions/setup-java@v2
+ - uses: actions/checkout@v3
+ - name: "Set up JDK ${{ matrix.java }}"
+ uses: actions/setup-java@v3
with:
distribution: 'temurin'
- java-version: '11'
+ java-version: ${{ matrix.java }}
cache: 'maven'
- name: Build with Maven
- # This also runs javadoc:javadoc to detect any issues with the Javadoc
- run: mvn --batch-mode --update-snapshots verify javadoc:javadoc
+ # 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
diff --git a/.github/workflows/check-api-compatibility.yml b/.github/workflows/check-api-compatibility.yml
new file mode 100644
index 00000000..a4465764
--- /dev/null
+++ b/.github/workflows/check-api-compatibility.yml
@@ -0,0 +1,50 @@
+name: Check API compatibility
+
+on: pull_request
+
+jobs:
+ check-api-compatibility:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout old version
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ path: 'gson-old-japicmp'
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '11'
+ cache: 'maven'
+
+ - name: Build old version
+ run: |
+ cd gson-old-japicmp
+ # Set dummy version
+ mvn --batch-mode --no-transfer-progress org.codehaus.mojo:versions-maven-plugin:2.11.0:set -DnewVersion=JAPICMP-OLD
+ # Install artifacts with dummy version in local repository; used later by Maven plugin for comparison
+ mvn --batch-mode --no-transfer-progress install -DskipTests
+
+ - name: Checkout new version
+ uses: actions/checkout@v3
+
+ - name: Check API compatibility
+ id: check-compatibility
+ run: |
+ mvn --batch-mode --fail-at-end --no-transfer-progress package japicmp:cmp -DskipTests
+
+ - name: Upload API differences artifacts
+ uses: actions/upload-artifact@v3
+ # Run on workflow success (in that case differences report might include added methods and classes)
+ # or when API compatibility check failed
+ if: success() || ( failure() && steps.check-compatibility.outcome == 'failure' )
+ with:
+ name: api-differences
+ path: |
+ **/japicmp/default-cli.html
+ **/japicmp/default-cli.diff
+ # Plugin should always have created report files (though they might be empty)
+ if-no-files-found: error
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 17ed734a..01d95bdf 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning
- name: Initialize CodeQL
@@ -48,7 +48,7 @@ jobs:
# Can replace this with github/codeql-action/autobuild action to run complete build
- name: Compile sources
run: |
- mvn compile --batch-mode
+ mvn compile --batch-mode --no-transfer-progress
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
diff --git a/Android.bp b/Android.bp
index 6e9b498f..0eef4dc6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -35,6 +35,8 @@ java_library {
":GsonBuildConfig.java",
],
sdk_version: "current",
+ // b/267831518: Pin tradefed and dependencies to Java 11.
+ java_version: "11",
}
python_binary_host {
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 374faf37..b0790fcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,20 @@
Change Log
==========
+## Version 2.9.1
+
+* Make `Object` and `JsonElement` deserialization iterative rather than
+ recursive (#1912)
+* Added parsing support for enum that has overridden toString() method (#1950)
+* Removed support for building Gson with Gradle (#2081)
+* Removed obsolete `codegen` hierarchy (#2099)
+* Add support for reflection access filter (#1905)
+* Improve `TypeToken` creation validation (#2072)
+* Add explicit support for `float` in `JsonWriter` (#2130, #2132)
+* Fail when parsing invalid local date (#2134)
+
+Also many small improvements to javadoc.
+
## Version 2.9.0
**The minimum supported Java version changes from 6 to 7.**
diff --git a/METADATA b/METADATA
index 71e90854..ce36b887 100644
--- a/METADATA
+++ b/METADATA
@@ -1,7 +1,9 @@
-name: "gson"
-description:
- "A Java serialization/deserialization library to convert Java Objects into JSON and back"
+# This project was upgraded with external_updater.
+# Usage: tools/external_updater/updater.sh update gson
+# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md
+name: "gson"
+description: "A Java serialization/deserialization library to convert Java Objects into JSON and back"
third_party {
url {
type: HOMEPAGE
@@ -11,10 +13,14 @@ third_party {
type: GIT
value: "https://github.com/google/gson.git"
}
- version: "2.9.1"
- last_upgrade_date { year: 2022 month: 8 day: 9 }
+ version: "gson-parent-2.10"
license_type: NOTICE
security {
tag: "NVD-CPE2.3:cpe:/a:google:gson:2.9.1"
}
-} \ No newline at end of file
+ last_upgrade_date {
+ year: 2022
+ month: 11
+ day: 10
+ }
+}
diff --git a/README.md b/README.md
index b3c5b550..dddaf95f 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ There are a few open-source projects that can convert Java objects to JSON. Howe
Gradle:
```gradle
dependencies {
- implementation 'com.google.code.gson:gson:2.9.0'
+ implementation 'com.google.code.gson:gson:2.10'
}
```
@@ -28,7 +28,7 @@ Maven:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
- <version>2.9.0</version>
+ <version>2.10</version>
</dependency>
```
diff --git a/ReleaseProcess.md b/ReleaseProcess.md
index 4b85f4dc..eaa0e7c5 100644
--- a/ReleaseProcess.md
+++ b/ReleaseProcess.md
@@ -6,19 +6,23 @@ The following is a step-by-step procedure for releasing a new version of Google-
1. Ensure all changelists are code-reviewed and have +1
1. `cd gson` to the parent directory; ensure there are no open files and all changes are committed.
1. Run `mvn release:clean`
-1. Do a dry run: `mvn release:prepare -DdryRun=true`
1. Start the release: `mvn release:prepare`
- * Answer questions: usually the defaults are fine.
- * This will do a full build, change version from `-SNAPSHOT` to the released version, commit and create the tags. It will then change the version to `-SNAPSHOT` for the next release.
+ - Answer questions: usually the defaults are fine. Try to follow [Semantic Versioning](https://semver.org/) when choosing the release version number.
+ - This will do a full build, change version from `-SNAPSHOT` to the released version, commit and create the tags. It will then change the version to `-SNAPSHOT` for the next release.
1. Complete the release: `mvn release:perform`
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://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide#SonatypeOSSMavenRepositoryUsageGuide-8.ReleaseIt).
+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. Update version references in (version might be referenced multiple times):
+ - [`README.md`](README.md)
+ - [`UserGuide.md`](UserGuide.md)
-1. Update the version in the [Using Gson with Maven2 page](https://github.com/google/gson/blob/master/UserGuide.md#TOC-Gson-With-Maven)
-1. Update [Gson Changelog](https://github.com/google/gson/blob/master/CHANGELOG.md). Also, look at all bugs that were fixed and add a few lines describing what changed in the release.
-1. Create a post on the [Gson Discussion Forum](https://groups.google.com/group/google-gson)
-1. Update the release version in [Wikipedia](https://en.wikipedia.org/wiki/GSON) and update the current "stable" release.
+ Note: When using the Maven Release Plugin as described above, these version references should have been replaced automatically, but verify this manually nonetheless to be on the safe side.
+1. Optional: Create a post on the [Gson Discussion Forum](https://groups.google.com/group/google-gson).
+1. Optional: Update the release version in [Wikipedia](https://en.wikipedia.org/wiki/Gson) and update the current "stable" release.
+
+Important: When aborting a release / rolling back release preparations, make sure to also revert all changes to files which were done during the release (e.g. automatic replacement of version references).
## Configuring a machine for deployment to Sonatype Repository
@@ -31,10 +35,7 @@ This section was borrowed heavily from [Doclava release process](https://code.go
## Getting Maven Publishing Privileges
-Based on [Gson group thread](https://groups.google.com/d/topic/google-gson/DHWJHVFpIBg/discussion):
-
-1. [Sign up for a Sonatype account](https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide) following instructions under (2) on that page
-1. Ask one of the existing members of the repository to create a JIRA ticket (Step 3 of above document) to add you to the publisher list.
+See [OSSRH Publish Guide](https://central.sonatype.org/publish/publish-guide/).
## Running Benchmarks or Tests on Android
diff --git a/UserGuide.md b/UserGuide.md
index 3764b828..b82bd725 100644
--- a/UserGuide.md
+++ b/UserGuide.md
@@ -76,7 +76,7 @@ The Gson instance does not maintain any state while invoking JSON operations. So
```gradle
dependencies {
- implementation 'com.google.code.gson:gson:2.9.0'
+ implementation 'com.google.code.gson:gson:2.10'
}
```
@@ -90,7 +90,7 @@ To use Gson with Maven2/3, you can use the Gson version available in Maven Centr
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
- <version>2.9.0</version>
+ <version>2.10</version>
<scope>compile</scope>
</dependency>
</dependencies>
@@ -155,7 +155,8 @@ BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
* While serializing, a null field is omitted from the output.
* While deserializing, a missing entry in JSON results in setting the corresponding field in the object to its default value: null for object types, zero for numeric types, and false for booleans.
* If a field is _synthetic_, it is ignored and not included in JSON serialization or deserialization.
-* Fields corresponding to the outer classes in inner classes, anonymous classes, and local classes are ignored and not included in serialization or deserialization.
+* Fields corresponding to the outer classes in inner classes are ignored and not included in serialization or deserialization.
+* Anonymous and local classes are excluded. They will be serialized as JSON `null` and when deserialized their JSON value is ignored and `null` is returned. Convert the classes to `static` nested classes to enable serialization and deserialization for them.
### <a name="TOC-Nested-Classes-including-Inner-Classes-"></a>Nested Classes (including Inner Classes)
@@ -224,7 +225,9 @@ Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
String json = gson.toJson(ints); // ==> json is [1,2,3,4,5]
// Deserialization
-Type collectionType = new TypeToken<Collection<Integer>>(){}.getType();
+TypeToken<Collection<Integer>> collectionType = new TypeToken<Collection<Integer>>(){};
+// Note: For older Gson versions it is necessary to use `collectionType.getType()` as argument below,
+// this is however not type-safe and care must be taken to specify the correct type for the local variable
Collection<Integer> ints2 = gson.fromJson(json, collectionType);
// ==> ints2 is same as ints
```
@@ -262,10 +265,12 @@ For deserialization Gson uses the `read` method of the `TypeAdapter` registered
```java
Gson gson = new Gson();
-Type mapType = new TypeToken<Map<String, String>>(){}.getType();
+TypeToken<Map<String, String>> mapType = new TypeToken<Map<String, String>>(){};
String json = "{\"key\": \"value\"}";
// Deserialization
+// Note: For older Gson versions it is necessary to use `mapType.getType()` as argument below,
+// this is however not type-safe and care must be taken to specify the correct type for the local variable
Map<String, String> stringMap = gson.fromJson(json, mapType);
// ==> stringMap is {key=value}
```
diff --git a/extras/pom.xml b/extras/pom.xml
index da752f10..533f78fa 100644
--- a/extras/pom.xml
+++ b/extras/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
- <version>2.9.1</version>
+ <version>2.10</version>
</parent>
<artifactId>gson-extras</artifactId>
@@ -47,7 +47,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
- <version>3.0.0</version>
<configuration>
<!-- Currently not deployed -->
<skip>true</skip>
diff --git a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
index e6a07f14..c48c3cd9 100644
--- a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
+++ b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
@@ -42,7 +42,6 @@ import java.util.Queue;
* Writes a graph of objects as a list of named nodes.
*/
// TODO: proper documentation
-@SuppressWarnings("rawtypes")
public final class GraphAdapterBuilder {
private final Map<Type, InstanceCreator<?>> instanceCreators;
private final ConstructorConstructor constructorConstructor;
@@ -78,7 +77,7 @@ public final class GraphAdapterBuilder {
}
}
- static class Factory implements TypeAdapterFactory, InstanceCreator {
+ static class Factory implements TypeAdapterFactory, InstanceCreator<Object> {
private final Map<Type, InstanceCreator<?>> instanceCreators;
private final ThreadLocal<Graph> graphThreadLocal = new ThreadLocal<>();
@@ -215,7 +214,6 @@ public final class GraphAdapterBuilder {
* <p>Gson should only ever call this method when we're expecting it to;
* that is only when we've called back into Gson to deserialize a tree.
*/
- @SuppressWarnings("unchecked")
@Override
public Object createInstance(Type type) {
Graph graph = graphThreadLocal.get();
@@ -242,14 +240,14 @@ public final class GraphAdapterBuilder {
* The queue of elements to write during serialization. Unused during
* deserialization.
*/
- private final Queue<Element> queue = new LinkedList<>();
+ private final Queue<Element<?>> queue = new LinkedList<>();
/**
* The instance currently being deserialized. Used as a backdoor between
* the graph traversal (which needs to know instances) and instance creators
* which create them.
*/
- private Element nextCreate;
+ private Element<Object> nextCreate;
private Graph(Map<Object, Element<?>> map) {
this.map = map;
@@ -299,11 +297,12 @@ public final class GraphAdapterBuilder {
typeAdapter.write(out, value);
}
+ @SuppressWarnings("unchecked")
void read(Graph graph) throws IOException {
if (graph.nextCreate != null) {
throw new IllegalStateException("Unexpected recursive call to read() for " + id);
}
- graph.nextCreate = this;
+ graph.nextCreate = (Element<Object>) this;
value = typeAdapter.fromJsonTree(element);
if (value == null) {
throw new IllegalStateException("non-null value deserialized to null: " + element);
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 502ad4ec..87b522f0 100644
--- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
+++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
@@ -91,7 +91,7 @@ import java.util.Map;
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
- * <h3>Registering Types</h3>
+ * <h2>Registering Types</h2>
* Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
* name to the {@link #of} factory method. If you don't supply an explicit type
* field name, {@code "type"} will be used. <pre> {@code
@@ -119,7 +119,7 @@ import java.util.Map;
* .registerSubtype(Diamond.class);
* }</pre>
*
- * <h3>Serialization and deserialization</h3>
+ * <h2>Serialization and deserialization</h2>
* In order to serialize and deserialize a polymorphic object,
* you must specify the base type explicitly.
* <pre> {@code
@@ -158,7 +158,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
}
-
+
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
@@ -244,7 +244,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
} else {
labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
}
-
+
if (labelJsonElement == null) {
throw new JsonParseException("cannot deserialize " + baseType
+ " because it does not define a field named " + typeFieldName);
@@ -282,7 +282,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
+ " because it already defines a field named " + typeFieldName);
}
clone.add(typeFieldName, new JsonPrimitive(label));
-
+
for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
diff --git a/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java b/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java
index e3574bbc..4ade154f 100644
--- a/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java
+++ b/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java
@@ -16,15 +16,12 @@
package com.google.gson.typeadapters;
-import javax.annotation.PostConstruct;
-
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-
-import junit.framework.TestCase;
-
import java.util.Arrays;
import java.util.List;
+import javax.annotation.PostConstruct;
+import junit.framework.TestCase;
public class PostConstructAdapterFactoryTest extends TestCase {
public void test() throws Exception {
@@ -55,6 +52,7 @@ public class PostConstructAdapterFactoryTest extends TestCase {
assertEquals(sandwiches, sandwichesFromJson);
}
+ @SuppressWarnings("overrides") // for missing hashCode() override
static class Sandwich {
public String bread;
public String cheese;
@@ -89,6 +87,7 @@ public class PostConstructAdapterFactoryTest extends TestCase {
}
}
+ @SuppressWarnings("overrides") // for missing hashCode() override
static class MultipleSandwiches {
public List<Sandwich> sandwiches;
diff --git a/gson/pom.xml b/gson/pom.xml
index 04af473f..277ab598 100644
--- a/gson/pom.xml
+++ b/gson/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
- <version>2.9.1</version>
+ <version>2.10</version>
</parent>
<artifactId>gson</artifactId>
@@ -17,6 +17,10 @@
</license>
</licenses>
+ <properties>
+ <excludeTestCompilation>**/Java17*</excludeTestCompilation>
+ </properties>
+
<dependencies>
<dependency>
<groupId>junit</groupId>
@@ -27,6 +31,26 @@
<build>
<plugins>
+ <!--
+ Plugins for source generation and compilation
+ -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>templating-maven-plugin</artifactId>
+ <version>1.0.0</version>
+ <executions>
+ <execution>
+ <id>filtering-java-templates</id>
+ <goals>
+ <goal>filter-sources</goal>
+ </goals>
+ <configuration>
+ <sourceDirectory>${basedir}/src/main/java-templates</sourceDirectory>
+ <outputDirectory>${project.build.directory}/generated-sources/java-templates</outputDirectory>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
@@ -40,54 +64,16 @@
</excludes>
</configuration>
</execution>
- </executions>
- </plugin>
- <!-- Note: Javadoc plugin has to be run in combination with >= `package`
- phase, e.g. `mvn package javadoc:javadoc`, otherwise it fails with
- "Aggregator report contains named and unnamed modules" -->
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>3.0.0-M7</version>
- <configuration>
- <!-- Deny illegal access, this is required for ReflectionAccessTest -->
- <!-- Requires Java >= 9; Important: In case future Java versions
- don't support this flag anymore, don't remove it unless CI also runs with
- that Java version. Ideally would use toolchain to specify that this should
- run with e.g. Java 11, but Maven toolchain requirements (unlike Gradle ones)
- don't seem to be portable (every developer would have to set up toolchain
- configuration locally). -->
- <argLine>--illegal-access=deny</argLine>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-javadoc-plugin</artifactId>
- <configuration>
- <excludePackageNames>com.google.gson.internal:com.google.gson.internal.bind</excludePackageNames>
- </configuration>
- </plugin>
- <!-- Add module-info to JAR, see https://github.com/moditect/moditect#adding-module-descriptors-to-existing-jar-files -->
- <!-- Uses ModiTect instead of separate maven-compiler-plugin executions
- for better Eclipse IDE support, see https://github.com/eclipse-m2e/m2e-core/issues/393 -->
- <plugin>
- <groupId>org.moditect</groupId>
- <artifactId>moditect-maven-plugin</artifactId>
- <version>1.0.0.RC2</version>
- <executions>
<execution>
- <id>add-module-info</id>
- <phase>package</phase>
+ <id>default-testCompile</id>
+ <phase>test-compile</phase>
<goals>
- <goal>add-module-info</goal>
+ <goal>testCompile</goal>
</goals>
<configuration>
- <jvmVersion>9</jvmVersion>
- <module>
- <moduleInfoFile>${project.build.sourceDirectory}/module-info.java</moduleInfoFile>
- </module>
- <!-- Overwrite the previously generated JAR file, if any -->
- <overwriteExistingFiles>true</overwriteExistingFiles>
+ <testExcludes>
+ <exclude>${excludeTestCompilation}</exclude>
+ </testExcludes>
</configuration>
</execution>
</executions>
@@ -104,34 +90,26 @@
</execution>
</executions>
</plugin>
+
+ <!--
+ Plugins for test execution
+ -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>3.0.0-M7</version>
<configuration>
- <archive>
- <!-- Use existing manifest generated by BND plugin -->
- <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
- </archive>
+ <!-- Deny illegal access, this is required for ReflectionAccessTest -->
+ <!-- Requires Java >= 9; Important: In case future Java versions
+ don't support this flag anymore, don't remove it unless CI also runs with
+ that Java version. Ideally would use toolchain to specify that this should
+ run with e.g. Java 11, but Maven toolchain requirements (unlike Gradle ones)
+ don't seem to be portable (every developer would have to set up toolchain
+ configuration locally). -->
+ <argLine>--illegal-access=deny</argLine>
</configuration>
</plugin>
<plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>templating-maven-plugin</artifactId>
- <version>1.0.0</version>
- <executions>
- <execution>
- <id>filtering-java-templates</id>
- <goals>
- <goal>filter-sources</goal>
- </goals>
- <configuration>
- <sourceDirectory>${basedir}/src/main/java-templates</sourceDirectory>
- <outputDirectory>${project.build.directory}/generated-sources/java-templates</outputDirectory>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
<groupId>com.coderplus.maven.plugins</groupId>
<artifactId>copy-rename-maven-plugin</artifactId>
<version>1.0.1</version>
@@ -163,6 +141,7 @@
<version>2.6.0</version>
<executions>
<execution>
+ <id>obfuscate-test-class</id>
<phase>process-test-classes</phase>
<goals>
<goal>proguard</goal>
@@ -206,6 +185,69 @@
</execution>
</executions>
</plugin>
+
+ <!--
+ Plugins for building / modifying artifacts
+ -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <!-- Use existing manifest generated by BND plugin -->
+ <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+ </archive>
+ </configuration>
+ </plugin>
+ <!-- Add module-info to JAR, see https://github.com/moditect/moditect#adding-module-descriptors-to-existing-jar-files -->
+ <!-- Uses ModiTect instead of separate maven-compiler-plugin executions
+ for better Eclipse IDE support, see https://github.com/eclipse-m2e/m2e-core/issues/393 -->
+ <!-- Note: For some reason this has to be executed before javadoc plugin; otherwise `javadoc:jar` goal fails
+ to find source files -->
+ <plugin>
+ <groupId>org.moditect</groupId>
+ <artifactId>moditect-maven-plugin</artifactId>
+ <version>1.0.0.RC2</version>
+ <executions>
+ <execution>
+ <id>add-module-info</id>
+ <phase>package</phase>
+ <goals>
+ <goal>add-module-info</goal>
+ </goals>
+ <configuration>
+ <jvmVersion>9</jvmVersion>
+ <module>
+ <moduleInfoFile>${project.build.sourceDirectory}/module-info.java</moduleInfoFile>
+ </module>
+ <!-- Overwrite the previously generated JAR file, if any -->
+ <overwriteExistingFiles>true</overwriteExistingFiles>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Note: Javadoc plugin has to be run in combination with >= `package` phase,
+ e.g. `mvn package javadoc:javadoc`, otherwise it fails with
+ "Aggregator report contains named and unnamed modules" -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <excludePackageNames>com.google.gson.internal:com.google.gson.internal.bind</excludePackageNames>
+ </configuration>
+ </plugin>
</plugins>
</build>
+ <profiles>
+ <profile>
+ <id>JDK17</id>
+ <activation>
+ <jdk>[17,)</jdk>
+ </activation>
+ <properties>
+ <maven.compiler.testRelease>17</maven.compiler.testRelease>
+ <excludeTestCompilation />
+ </properties>
+ </profile>
+ </profiles>
</project>
diff --git a/gson/src/main/java/com/google/gson/ExclusionStrategy.java b/gson/src/main/java/com/google/gson/ExclusionStrategy.java
index bc0dc74a..557eddb2 100644
--- a/gson/src/main/java/com/google/gson/ExclusionStrategy.java
+++ b/gson/src/main/java/com/google/gson/ExclusionStrategy.java
@@ -17,11 +17,8 @@
package com.google.gson;
/**
- * A strategy (or policy) definition that is used to decide whether or not a field or top-level
- * class should be serialized or deserialized as part of the JSON output/input. For serialization,
- * if the {@link #shouldSkipClass(Class)} method returns true then that class or field type
- * will not be part of the JSON output. For deserialization, if {@link #shouldSkipClass(Class)}
- * returns true, then it will not be set as part of the Java object structure.
+ * A strategy (or policy) definition that is used to decide whether or not a field or
+ * class should be serialized or deserialized as part of the JSON output/input.
*
* <p>The following are a few examples that shows how you can use this exclusion mechanism.
*
@@ -64,7 +61,7 @@ package com.google.gson;
*
* <p>Now if you want to configure {@code Gson} to use a user defined exclusion strategy, then
* the {@code GsonBuilder} is required. The following is an example of how you can use the
- * {@code GsonBuilder} to configure Gson to use one of the above sample:
+ * {@code GsonBuilder} to configure Gson to use one of the above samples:
* <pre class="code">
* ExclusionStrategy excludeStrings = new UserDefinedExclusionStrategy(String.class);
* Gson gson = new GsonBuilder()
diff --git a/gson/src/main/java/com/google/gson/FieldAttributes.java b/gson/src/main/java/com/google/gson/FieldAttributes.java
index 9fb93f7b..4b9e16ab 100644
--- a/gson/src/main/java/com/google/gson/FieldAttributes.java
+++ b/gson/src/main/java/com/google/gson/FieldAttributes.java
@@ -16,12 +16,12 @@
package com.google.gson;
-import com.google.gson.internal.$Gson$Preconditions;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Objects;
/**
* A data object that stores attributes of a field.
@@ -42,8 +42,7 @@ public final class FieldAttributes {
* @param f the field to pull attributes from
*/
public FieldAttributes(Field f) {
- $Gson$Preconditions.checkNotNull(f);
- this.field = f;
+ this.field = Objects.requireNonNull(f);
}
/**
diff --git a/gson/src/main/java/com/google/gson/FieldNamingPolicy.java b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
index a4fa7c27..cd42f42c 100644
--- a/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
+++ b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
@@ -86,6 +86,8 @@ public enum FieldNamingPolicy implements FieldNamingStrategy {
* <li>aStringField ---&gt; A_STRING_FIELD</li>
* <li>aURL ---&gt; A_U_R_L</li>
* </ul>
+ *
+ * @since 2.9.0
*/
UPPER_CASE_WITH_UNDERSCORES() {
@Override public String translateName(Field f) {
@@ -125,7 +127,8 @@ public enum FieldNamingPolicy implements FieldNamingStrategy {
* Using dashes in JavaScript is not recommended since dash is also used for a minus sign in
* expressions. This requires that a field named with dashes is always accessed as a quoted
* property like {@code myobject['my-field']}. Accessing it as an object field
- * {@code myobject.my-field} will result in an unintended javascript expression.
+ * {@code myobject.my-field} will result in an unintended JavaScript expression.
+ *
* @since 1.4
*/
LOWER_CASE_WITH_DASHES() {
@@ -148,8 +151,9 @@ public enum FieldNamingPolicy implements FieldNamingStrategy {
* Using dots in JavaScript is not recommended since dot is also used for a member sign in
* expressions. This requires that a field named with dots is always accessed as a quoted
* property like {@code myobject['my.field']}. Accessing it as an object field
- * {@code myobject.my.field} will result in an unintended javascript expression.
- * @since 2.8
+ * {@code myobject.my.field} will result in an unintended JavaScript expression.
+ *
+ * @since 2.8.4
*/
LOWER_CASE_WITH_DOTS() {
@Override public String translateName(Field f) {
diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
index 666e5f8b..262cd175 100644
--- a/gson/src/main/java/com/google/gson/Gson.java
+++ b/gson/src/main/java/com/google/gson/Gson.java
@@ -32,6 +32,7 @@ import com.google.gson.internal.bind.MapTypeAdapterFactory;
import com.google.gson.internal.bind.NumberTypeAdapter;
import com.google.gson.internal.bind.ObjectTypeAdapter;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
+import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.internal.sql.SqlTypesSupport;
import com.google.gson.reflect.TypeToken;
@@ -54,7 +55,9 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongArray;
@@ -74,26 +77,32 @@ import java.util.concurrent.atomic.AtomicLongArray;
* <pre>
* Gson gson = new Gson(); // Or use new GsonBuilder().create();
* MyType target = new MyType();
- * String json = gson.toJson(target); // serializes target to Json
+ * String json = gson.toJson(target); // serializes target to JSON
* MyType target2 = gson.fromJson(json, MyType.class); // deserializes json into target2
* </pre>
*
- * <p>If the object that your are serializing/deserializing is a {@code ParameterizedType}
- * (i.e. contains at least one type parameter and may be an array) then you must use the
- * {@link #toJson(Object, Type)} or {@link #fromJson(String, Type)} method. Here is an
- * example for serializing and deserializing a {@code ParameterizedType}:
- *
+ * <p>If the type of the object that you are converting is a {@code ParameterizedType}
+ * (i.e. has at least one type argument, for example {@code List<MyType>}) then for
+ * deserialization you must use a {@code fromJson} method with {@link Type} or {@link TypeToken}
+ * parameter to specify the parameterized type. For serialization specifying a {@code Type}
+ * or {@code TypeToken} is optional, otherwise Gson will use the runtime type of the object.
+ * {@link TypeToken} is a class provided by Gson which helps creating parameterized types.
+ * Here is an example showing how this can be done:
* <pre>
- * Type listType = new TypeToken&lt;List&lt;String&gt;&gt;() {}.getType();
- * List&lt;String&gt; target = new LinkedList&lt;String&gt;();
- * target.add("blah");
+ * TypeToken&lt;List&lt;MyType&gt;&gt; listType = new TypeToken&lt;List&lt;MyType&gt;&gt;() {};
+ * List&lt;MyType&gt; target = new LinkedList&lt;MyType&gt;();
+ * target.add(new MyType(1, "abc"));
*
* Gson gson = new Gson();
- * String json = gson.toJson(target, listType);
- * List&lt;String&gt; target2 = gson.fromJson(json, listType);
+ * // For serialization you normally do not have to specify the type, Gson will use
+ * // the runtime type of the objects, however you can also specify it explicitly
+ * String json = gson.toJson(target, listType.getType());
+ *
+ * // But for deserialization you have to specify the type
+ * List&lt;MyType&gt; target2 = gson.fromJson(json, listType);
* </pre>
*
- * <p>See the <a href="https://sites.google.com/site/gson/gson-user-guide">Gson User Guide</a>
+ * <p>See the <a href="https://github.com/google/gson/blob/master/UserGuide.md">Gson User Guide</a>
* for a more complete set of examples.</p>
*
* <h2>Lenient JSON handling</h2>
@@ -123,7 +132,7 @@ import java.util.concurrent.atomic.AtomicLongArray;
* to make sure there is no trailing data
* </ol>
*
- * @see com.google.gson.reflect.TypeToken
+ * @see TypeToken
*
* @author Inderjeet Singh
* @author Joel Leitch
@@ -143,7 +152,6 @@ public final class Gson {
static final ToNumberStrategy DEFAULT_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE;
static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER;
- private static final TypeToken<?> NULL_KEY_SURROGATE = TypeToken.get(Object.class);
private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
/**
@@ -156,7 +164,7 @@ public final class Gson {
private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls
= new ThreadLocal<>();
- private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();
+ private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<>();
private final ConstructorConstructor constructorConstructor;
private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
@@ -209,9 +217,9 @@ public final class Gson {
* through {@link GsonBuilder#excludeFieldsWithoutExposeAnnotation()}. </li>
* <li>By default, Gson ignores the {@link com.google.gson.annotations.Since} annotation. You
* can enable Gson to use this annotation through {@link GsonBuilder#setVersion(double)}.</li>
- * <li>The default field naming policy for the output Json is same as in Java. So, a Java class
+ * <li>The default field naming policy for the output JSON is same as in Java. So, a Java class
* field <code>versionNumber</code> will be output as <code>&quot;versionNumber&quot;</code> in
- * Json. The same rules are applied for mapping incoming Json to the Java classes. You can
+ * JSON. The same rules are applied for mapping incoming JSON to the Java classes. You can
* change this policy through {@link GsonBuilder#setFieldNamingPolicy(FieldNamingPolicy)}.</li>
* <li>By default, Gson excludes <code>transient</code> or <code>static</code> fields from
* consideration for serialization and deserialization. You can change this behavior through
@@ -336,6 +344,7 @@ public final class Gson {
* instance.
*
* @return a GsonBuilder instance.
+ * @since 2.8.3
*/
public GsonBuilder newBuilder() {
return new GsonBuilder(this);
@@ -398,7 +407,7 @@ public final class Gson {
}
double doubleValue = value.doubleValue();
checkValidFloatingPoint(doubleValue);
- out.value(value);
+ out.value(doubleValue);
}
};
}
@@ -422,7 +431,10 @@ public final class Gson {
}
float floatValue = value.floatValue();
checkValidFloatingPoint(floatValue);
- out.value(value);
+ // For backward compatibility don't call `JsonWriter.value(float)` because that method has
+ // been newly added and not all custom JsonWriter implementations might override it yet
+ Number floatNumber = value instanceof Float ? value : floatValue;
+ out.value(floatNumber);
}
};
}
@@ -502,11 +514,13 @@ public final class Gson {
* @throws IllegalArgumentException if this GSON cannot serialize and
* deserialize {@code type}.
*/
- @SuppressWarnings("unchecked")
public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
- TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
+ Objects.requireNonNull(type, "type must not be null");
+ TypeAdapter<?> cached = typeTokenCache.get(type);
if (cached != null) {
- return (TypeAdapter<T>) cached;
+ @SuppressWarnings("unchecked")
+ TypeAdapter<T> adapter = (TypeAdapter<T>) cached;
+ return adapter;
}
Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
@@ -518,6 +532,7 @@ public final class Gson {
}
// the key and value type parameters always agree
+ @SuppressWarnings("unchecked")
FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
@@ -530,8 +545,14 @@ public final class Gson {
for (TypeAdapterFactory factory : factories) {
TypeAdapter<T> candidate = factory.create(this, type);
if (candidate != null) {
+ @SuppressWarnings("unchecked")
+ TypeAdapter<T> existingAdapter = (TypeAdapter<T>) typeTokenCache.putIfAbsent(type, candidate);
+ // If other thread concurrently added adapter prefer that one instead
+ if (existingAdapter != null) {
+ candidate = existingAdapter;
+ }
+
call.setDelegate(candidate);
- typeTokenCache.put(type, candidate);
return candidate;
}
}
@@ -634,13 +655,15 @@ public final class Gson {
* {@link JsonElement}s. This method should be used when the specified object is not a generic
* type. This method uses {@link Class#getClass()} to get the type for the specified object, but
* the {@code getClass()} loses the generic type information because of the Type Erasure feature
- * of Java. Note that this method works fine if the any of the object fields are of generic type,
+ * of Java. Note that this method works fine if any of the object fields are of generic type,
* just the object itself should not be of a generic type. If the object is of generic type, use
* {@link #toJsonTree(Object, Type)} instead.
*
- * @param src the object for which Json representation is to be created setting for Gson
- * @return Json representation of {@code src}.
+ * @param src the object for which JSON representation is to be created
+ * @return JSON representation of {@code src}.
* @since 1.4
+ *
+ * @see #toJsonTree(Object, Type)
*/
public JsonElement toJsonTree(Object src) {
if (src == null) {
@@ -664,6 +687,8 @@ public final class Gson {
* </pre>
* @return Json representation of {@code src}
* @since 1.4
+ *
+ * @see #toJsonTree(Object)
*/
public JsonElement toJsonTree(Object src, Type typeOfSrc) {
JsonTreeWriter writer = new JsonTreeWriter();
@@ -672,17 +697,20 @@ public final class Gson {
}
/**
- * This method serializes the specified object into its equivalent Json representation.
+ * This method serializes the specified object into its equivalent JSON representation.
* This method should be used when the specified object is not a generic type. This method uses
* {@link Class#getClass()} to get the type for the specified object, but the
* {@code getClass()} loses the generic type information because of the Type Erasure feature
- * of Java. Note that this method works fine if the any of the object fields are of generic type,
+ * of Java. Note that this method works fine if any of the object fields are of generic type,
* just the object itself should not be of a generic type. If the object is of generic type, use
* {@link #toJson(Object, Type)} instead. If you want to write out the object to a
* {@link Writer}, use {@link #toJson(Object, Appendable)} instead.
*
- * @param src the object for which Json representation is to be created setting for Gson
+ * @param src the object for which JSON representation is to be created
* @return Json representation of {@code src}.
+ *
+ * @see #toJson(Object, Appendable)
+ * @see #toJson(Object, Type)
*/
public String toJson(Object src) {
if (src == null) {
@@ -693,7 +721,7 @@ public final class Gson {
/**
* This method serializes the specified object, including those of generic types, into its
- * equivalent Json representation. This method must be used if the specified object is a generic
+ * equivalent JSON representation. This method must be used if the specified object is a generic
* type. For non-generic objects, use {@link #toJson(Object)} instead. If you want to write out
* the object to a {@link Appendable}, use {@link #toJson(Object, Type, Appendable)} instead.
*
@@ -704,7 +732,10 @@ public final class Gson {
* <pre>
* Type typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
* </pre>
- * @return Json representation of {@code src}
+ * @return JSON representation of {@code src}
+ *
+ * @see #toJson(Object, Type, Appendable)
+ * @see #toJson(Object)
*/
public String toJson(Object src, Type typeOfSrc) {
StringWriter writer = new StringWriter();
@@ -713,18 +744,22 @@ public final class Gson {
}
/**
- * This method serializes the specified object into its equivalent Json representation.
+ * This method serializes the specified object into its equivalent JSON representation and
+ * writes it to the writer.
* This method should be used when the specified object is not a generic type. This method uses
* {@link Class#getClass()} to get the type for the specified object, but the
* {@code getClass()} loses the generic type information because of the Type Erasure feature
- * of Java. Note that this method works fine if the any of the object fields are of generic type,
+ * of Java. Note that this method works fine if any of the object fields are of generic type,
* just the object itself should not be of a generic type. If the object is of generic type, use
* {@link #toJson(Object, Type, Appendable)} instead.
*
- * @param src the object for which Json representation is to be created setting for Gson
- * @param writer Writer to which the Json representation needs to be written
+ * @param src the object for which JSON representation is to be created
+ * @param writer Writer to which the JSON representation needs to be written
* @throws JsonIOException if there was a problem writing to the writer
* @since 1.2
+ *
+ * @see #toJson(Object)
+ * @see #toJson(Object, Type, Appendable)
*/
public void toJson(Object src, Appendable writer) throws JsonIOException {
if (src != null) {
@@ -736,8 +771,9 @@ public final class Gson {
/**
* This method serializes the specified object, including those of generic types, into its
- * equivalent Json representation. This method must be used if the specified object is a generic
- * type. For non-generic objects, use {@link #toJson(Object, Appendable)} instead.
+ * equivalent JSON representation and writes it to the writer.
+ * This method must be used if the specified object is a generic type. For non-generic objects,
+ * use {@link #toJson(Object, Appendable)} instead.
*
* @param src the object for which JSON representation is to be created
* @param typeOfSrc The specific genericized type of src. You can obtain
@@ -746,9 +782,12 @@ public final class Gson {
* <pre>
* Type typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
* </pre>
- * @param writer Writer to which the Json representation of src needs to be written.
+ * @param writer Writer to which the JSON representation of src needs to be written.
* @throws JsonIOException if there was a problem writing to the writer
* @since 1.2
+ *
+ * @see #toJson(Object, Type)
+ * @see #toJson(Object, Appendable)
*/
public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOException {
try {
@@ -773,9 +812,9 @@ public final class Gson {
*
* @throws JsonIOException if there was a problem writing to the writer
*/
- @SuppressWarnings("unchecked")
public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException {
- TypeAdapter<?> adapter = getAdapter(TypeToken.get(typeOfSrc));
+ @SuppressWarnings("unchecked")
+ TypeAdapter<Object> adapter = (TypeAdapter<Object>) getAdapter(TypeToken.get(typeOfSrc));
boolean oldLenient = writer.isLenient();
writer.setLenient(true);
boolean oldHtmlSafe = writer.isHtmlSafe();
@@ -783,7 +822,7 @@ public final class Gson {
boolean oldSerializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(serializeNulls);
try {
- ((TypeAdapter<Object>) adapter).write(writer, src);
+ adapter.write(writer, src);
} catch (IOException e) {
throw new JsonIOException(e);
} catch (AssertionError e) {
@@ -814,7 +853,7 @@ public final class Gson {
* Writes out the equivalent JSON for a tree of {@link JsonElement}s.
*
* @param jsonElement root of a tree of {@link JsonElement}s
- * @param writer Writer to which the Json representation needs to be written
+ * @param writer Writer to which the JSON representation needs to be written
* @throws JsonIOException if there was a problem writing to the writer
* @since 1.4
*/
@@ -903,17 +942,17 @@ public final class Gson {
}
/**
- * This method deserializes the specified Json into an object of the specified class. It is not
+ * This method deserializes the specified JSON into an object of the specified class. It is not
* suitable to use if the specified class is a generic type since it will not have the generic
* type information because of the Type Erasure feature of Java. Therefore, this method should not
* be used if the desired type is a generic type. Note that this method works fine if the any of
* the fields of the specified object are generics, just the object itself should not be a
* generic type. For the cases when the object is of generic type, invoke
- * {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of
+ * {@link #fromJson(String, TypeToken)}. If you have the JSON in a {@link Reader} instead of
* a String, use {@link #fromJson(Reader, Class)} instead.
*
- * <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
- * or if there is trailing data.
+ * <p>An exception is thrown if the JSON string has multiple top-level JSON elements, or if there
+ * is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is not desired.
*
* @param <T> the type of the desired object
* @param json the string from which the object is to be deserialized
@@ -922,98 +961,167 @@ public final class Gson {
* or if {@code json} is empty.
* @throws JsonSyntaxException if json is not a valid representation for an object of type
* classOfT
+ *
+ * @see #fromJson(Reader, Class)
+ * @see #fromJson(String, TypeToken)
*/
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
- Object object = fromJson(json, (Type) classOfT);
+ T object = fromJson(json, TypeToken.get(classOfT));
return Primitives.wrap(classOfT).cast(object);
}
/**
- * This method deserializes the specified Json into an object of the specified type. This method
+ * This method deserializes the specified JSON into an object of the specified type. This method
* is useful if the specified object is a generic type. For non-generic objects, use
- * {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of
+ * {@link #fromJson(String, Class)} instead. If you have the JSON in a {@link Reader} instead of
* a String, use {@link #fromJson(Reader, Type)} instead.
*
+ * <p>Since {@code Type} is not parameterized by T, this method is not type-safe and
+ * should be used carefully. If you are creating the {@code Type} from a {@link TypeToken},
+ * prefer using {@link #fromJson(String, TypeToken)} instead since its return type is based
+ * on the {@code TypeToken} and is therefore more type-safe.
+ *
* <p>An exception is thrown if the JSON string has multiple top-level JSON elements,
- * or if there is trailing data.
+ * or if there is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is
+ * not desired.
+ *
+ * @param <T> the type of the desired object
+ * @param json the string from which the object is to be deserialized
+ * @param typeOfT The specific genericized type of src
+ * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}
+ * or if {@code json} is empty.
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+ *
+ * @see #fromJson(Reader, Type)
+ * @see #fromJson(String, Class)
+ * @see #fromJson(String, TypeToken)
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
+ return (T) fromJson(json, TypeToken.get(typeOfT));
+ }
+
+ /**
+ * This method deserializes the specified JSON into an object of the specified type. This method
+ * is useful if the specified object is a generic type. For non-generic objects, use
+ * {@link #fromJson(String, Class)} instead. If you have the JSON in a {@link Reader} instead of
+ * a String, use {@link #fromJson(Reader, TypeToken)} instead.
+ *
+ * <p>An exception is thrown if the JSON string has multiple top-level JSON elements, or if there
+ * is trailing data. Use {@link #fromJson(JsonReader, TypeToken)} if this behavior is not desired.
*
* @param <T> the type of the desired object
* @param json the string from which the object is to be deserialized
- * @param typeOfT The specific genericized type of src. You can obtain this type by using the
- * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for
+ * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of
+ * {@code TypeToken} with the specific generic type arguments. For example, to get the type for
* {@code Collection<Foo>}, you should use:
* <pre>
- * Type typeOfT = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
+ * new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}
* </pre>
* @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null}
* or if {@code json} is empty.
- * @throws JsonParseException if json is not a valid representation for an object of type typeOfT
- * @throws JsonSyntaxException if json is not a valid representation for an object of type
+ * @throws JsonSyntaxException if json is not a valid representation for an object of the type typeOfT
+ *
+ * @see #fromJson(Reader, TypeToken)
+ * @see #fromJson(String, Class)
+ * @since 2.10
*/
- @SuppressWarnings("unchecked")
- public <T> T fromJson(String json, Type typeOfT) throws JsonSyntaxException {
+ public <T> T fromJson(String json, TypeToken<T> typeOfT) throws JsonSyntaxException {
if (json == null) {
return null;
}
StringReader reader = new StringReader(json);
- T target = (T) fromJson(reader, typeOfT);
- return target;
+ return fromJson(reader, typeOfT);
}
/**
- * This method deserializes the Json read from the specified reader into an object of the
+ * This method deserializes the JSON read from the specified reader into an object of the
* specified class. It is not suitable to use if the specified class is a generic type since it
* will not have the generic type information because of the Type Erasure feature of Java.
* Therefore, this method should not be used if the desired type is a generic type. Note that
- * this method works fine if the any of the fields of the specified object are generics, just the
+ * this method works fine if any of the fields of the specified object are generics, just the
* object itself should not be a generic type. For the cases when the object is of generic type,
- * invoke {@link #fromJson(Reader, Type)}. If you have the Json in a String form instead of a
+ * invoke {@link #fromJson(Reader, TypeToken)}. If you have the JSON in a String form instead of a
* {@link Reader}, use {@link #fromJson(String, Class)} instead.
*
- * <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
- * or if there is trailing data.
+ * <p>An exception is thrown if the JSON data has multiple top-level JSON elements, or if there
+ * is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is not desired.
*
* @param <T> the type of the desired object
- * @param json the reader producing the Json from which the object is to be deserialized.
+ * @param json the reader producing the JSON from which the object is to be deserialized.
* @param classOfT the class of T
- * @return an object of type T from the string. Returns {@code null} if {@code json} is at EOF.
+ * @return an object of type T from the Reader. Returns {@code null} if {@code json} is at EOF.
* @throws JsonIOException if there was a problem reading from the Reader
- * @throws JsonSyntaxException if json is not a valid representation for an object of type
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
* @since 1.2
+ *
+ * @see #fromJson(String, Class)
+ * @see #fromJson(Reader, TypeToken)
*/
public <T> T fromJson(Reader json, Class<T> classOfT) throws JsonSyntaxException, JsonIOException {
- JsonReader jsonReader = newJsonReader(json);
- Object object = fromJson(jsonReader, classOfT);
- assertFullConsumption(object, jsonReader);
+ T object = fromJson(json, TypeToken.get(classOfT));
return Primitives.wrap(classOfT).cast(object);
}
/**
- * This method deserializes the Json read from the specified reader into an object of the
+ * This method deserializes the JSON read from the specified reader into an object of the
* specified type. This method is useful if the specified object is a generic type. For
- * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the Json in a
+ * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the JSON in a
* String form instead of a {@link Reader}, use {@link #fromJson(String, Type)} instead.
*
- * <p>An exception is thrown if the JSON data has multiple top-level JSON elements,
- * or if there is trailing data.
+ * <p>Since {@code Type} is not parameterized by T, this method is not type-safe and
+ * should be used carefully. If you are creating the {@code Type} from a {@link TypeToken},
+ * prefer using {@link #fromJson(Reader, TypeToken)} instead since its return type is based
+ * on the {@code TypeToken} and is therefore more type-safe.
+ *
+ * <p>An exception is thrown if the JSON data has multiple top-level JSON elements, or if there
+ * is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is not desired.
*
* @param <T> the type of the desired object
- * @param json the reader producing Json from which the object is to be deserialized
- * @param typeOfT The specific genericized type of src. You can obtain this type by using the
- * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for
- * {@code Collection<Foo>}, you should use:
- * <pre>
- * Type typeOfT = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
- * </pre>
- * @return an object of type T from the json. Returns {@code null} if {@code json} is at EOF.
+ * @param json the reader producing JSON from which the object is to be deserialized
+ * @param typeOfT The specific genericized type of src
+ * @return an object of type T from the Reader. Returns {@code null} if {@code json} is at EOF.
* @throws JsonIOException if there was a problem reading from the Reader
- * @throws JsonSyntaxException if json is not a valid representation for an object of type
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
* @since 1.2
+ *
+ * @see #fromJson(String, Type)
+ * @see #fromJson(Reader, Class)
+ * @see #fromJson(Reader, TypeToken)
*/
@SuppressWarnings("unchecked")
public <T> T fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException {
+ return (T) fromJson(json, TypeToken.get(typeOfT));
+ }
+
+ /**
+ * This method deserializes the JSON read from the specified reader into an object of the
+ * specified type. This method is useful if the specified object is a generic type. For
+ * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the JSON in a
+ * String form instead of a {@link Reader}, use {@link #fromJson(String, TypeToken)} instead.
+ *
+ * <p>An exception is thrown if the JSON data has multiple top-level JSON elements, or if there
+ * is trailing data. Use {@link #fromJson(JsonReader, TypeToken)} if this behavior is not desired.
+ *
+ * @param <T> the type of the desired object
+ * @param json the reader producing JSON from which the object is to be deserialized
+ * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of
+ * {@code TypeToken} with the specific generic type arguments. For example, to get the type for
+ * {@code Collection<Foo>}, you should use:
+ * <pre>
+ * new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}
+ * </pre>
+ * @return an object of type T from the Reader. Returns {@code null} if {@code json} is at EOF.
+ * @throws JsonIOException if there was a problem reading from the Reader
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type of typeOfT
+ *
+ * @see #fromJson(String, TypeToken)
+ * @see #fromJson(Reader, Class)
+ * @since 2.10
+ */
+ public <T> T fromJson(Reader json, TypeToken<T> typeOfT) throws JsonIOException, JsonSyntaxException {
JsonReader jsonReader = newJsonReader(json);
- T object = (T) fromJson(jsonReader, typeOfT);
+ T object = fromJson(jsonReader, typeOfT);
assertFullConsumption(object, jsonReader);
return object;
}
@@ -1030,10 +1138,18 @@ public final class Gson {
}
}
+ // fromJson(JsonReader, Class) is unfortunately missing and cannot be added now without breaking
+ // source compatibility in certain cases, see https://github.com/google/gson/pull/1700#discussion_r973764414
+
/**
- * Reads the next JSON value from {@code reader} and convert it to an object
+ * Reads the next JSON value from {@code reader} and converts it to an object
* of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF.
- * Since Type is not parameterized by T, this method is type unsafe and should be used carefully.
+ *
+ * <p>Since {@code Type} is not parameterized by T, this method is not type-safe and
+ * should be used carefully. If you are creating the {@code Type} from a {@link TypeToken},
+ * prefer using {@link #fromJson(JsonReader, TypeToken)} instead since its return type is based
+ * on the {@code TypeToken} and is therefore more type-safe. If the provided type is a
+ * {@code Class} the {@code TypeToken} can be created with {@link TypeToken#get(Class)}.
*
* <p>Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has
* multiple top-level JSON elements, or if there is trailing data.
@@ -1042,19 +1158,59 @@ public final class Gson {
* regardless of the lenient mode setting of the provided reader. The lenient mode setting
* of the reader is restored once this method returns.
*
- * @throws JsonIOException if there was a problem writing to the Reader
- * @throws JsonSyntaxException if json is not a valid representation for an object of type
+ * @param <T> the type of the desired object
+ * @param reader the reader whose next JSON value should be deserialized
+ * @param typeOfT The specific genericized type of src
+ * @return an object of type T from the JsonReader. Returns {@code null} if {@code reader} is at EOF.
+ * @throws JsonIOException if there was a problem reading from the JsonReader
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+ *
+ * @see #fromJson(Reader, Type)
+ * @see #fromJson(JsonReader, TypeToken)
*/
@SuppressWarnings("unchecked")
public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException {
+ return (T) fromJson(reader, TypeToken.get(typeOfT));
+ }
+
+ /**
+ * Reads the next JSON value from {@code reader} and converts it to an object
+ * of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF.
+ * This method is useful if the specified object is a generic type. For non-generic objects,
+ * {@link #fromJson(JsonReader, Type)} can be called, or {@link TypeToken#get(Class)} can
+ * be used to create the type token.
+ *
+ * <p>Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has
+ * multiple top-level JSON elements, or if there is trailing data.
+ *
+ * <p>The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode},
+ * regardless of the lenient mode setting of the provided reader. The lenient mode setting
+ * of the reader is restored once this method returns.
+ *
+ * @param <T> the type of the desired object
+ * @param reader the reader whose next JSON value should be deserialized
+ * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of
+ * {@code TypeToken} with the specific generic type arguments. For example, to get the type for
+ * {@code Collection<Foo>}, you should use:
+ * <pre>
+ * new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}
+ * </pre>
+ * @return an object of type T from the JsonReader. Returns {@code null} if {@code reader} is at EOF.
+ * @throws JsonIOException if there was a problem reading from the JsonReader
+ * @throws JsonSyntaxException if json is not a valid representation for an object of the type typeOfT
+ *
+ * @see #fromJson(Reader, TypeToken)
+ * @see #fromJson(JsonReader, Type)
+ * @since 2.10
+ */
+ public <T> T fromJson(JsonReader reader, TypeToken<T> typeOfT) throws JsonIOException, JsonSyntaxException {
boolean isEmpty = true;
boolean oldLenient = reader.isLenient();
reader.setLenient(true);
try {
reader.peek();
isEmpty = false;
- TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
- TypeAdapter<T> typeAdapter = getAdapter(typeToken);
+ TypeAdapter<T> typeAdapter = getAdapter(typeOfT);
T object = typeAdapter.read(reader);
return object;
} catch (EOFException e) {
@@ -1081,55 +1237,89 @@ public final class Gson {
}
/**
- * This method deserializes the Json read from the specified parse tree into an object of the
+ * This method deserializes the JSON read from the specified parse tree into an object of the
* specified type. It is not suitable to use if the specified class is a generic type since it
* will not have the generic type information because of the Type Erasure feature of Java.
* Therefore, this method should not be used if the desired type is a generic type. Note that
- * this method works fine if the any of the fields of the specified object are generics, just the
+ * this method works fine if any of the fields of the specified object are generics, just the
* object itself should not be a generic type. For the cases when the object is of generic type,
- * invoke {@link #fromJson(JsonElement, Type)}.
+ * invoke {@link #fromJson(JsonElement, TypeToken)}.
+ *
* @param <T> the type of the desired object
* @param json the root of the parse tree of {@link JsonElement}s from which the object is to
* be deserialized
* @param classOfT The class of T
- * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null}
+ * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null}
* or if {@code json} is empty.
- * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type classOfT
* @since 1.3
+ *
+ * @see #fromJson(Reader, Class)
+ * @see #fromJson(JsonElement, TypeToken)
*/
public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException {
- Object object = fromJson(json, (Type) classOfT);
+ T object = fromJson(json, TypeToken.get(classOfT));
return Primitives.wrap(classOfT).cast(object);
}
/**
- * This method deserializes the Json read from the specified parse tree into an object of the
+ * This method deserializes the JSON read from the specified parse tree into an object of the
* specified type. This method is useful if the specified object is a generic type. For
* non-generic objects, use {@link #fromJson(JsonElement, Class)} instead.
*
+ * <p>Since {@code Type} is not parameterized by T, this method is not type-safe and
+ * should be used carefully. If you are creating the {@code Type} from a {@link TypeToken},
+ * prefer using {@link #fromJson(JsonElement, TypeToken)} instead since its return type is based
+ * on the {@code TypeToken} and is therefore more type-safe.
+ *
* @param <T> the type of the desired object
* @param json the root of the parse tree of {@link JsonElement}s from which the object is to
* be deserialized
- * @param typeOfT The specific genericized type of src. You can obtain this type by using the
- * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for
- * {@code Collection<Foo>}, you should use:
- * <pre>
- * Type typeOfT = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
- * </pre>
- * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null}
+ * @param typeOfT The specific genericized type of src
+ * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null}
* or if {@code json} is empty.
* @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
* @since 1.3
+ *
+ * @see #fromJson(Reader, Type)
+ * @see #fromJson(JsonElement, Class)
+ * @see #fromJson(JsonElement, TypeToken)
*/
@SuppressWarnings("unchecked")
public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException {
+ return (T) fromJson(json, TypeToken.get(typeOfT));
+ }
+
+ /**
+ * This method deserializes the JSON read from the specified parse tree into an object of the
+ * specified type. This method is useful if the specified object is a generic type. For
+ * non-generic objects, use {@link #fromJson(JsonElement, Class)} instead.
+ *
+ * @param <T> the type of the desired object
+ * @param json the root of the parse tree of {@link JsonElement}s from which the object is to
+ * be deserialized
+ * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of
+ * {@code TypeToken} with the specific generic type arguments. For example, to get the type for
+ * {@code Collection<Foo>}, you should use:
+ * <pre>
+ * new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}
+ * </pre>
+ * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null}
+ * or if {@code json} is empty.
+ * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+ *
+ * @see #fromJson(Reader, TypeToken)
+ * @see #fromJson(JsonElement, Class)
+ * @since 2.10
+ */
+ public <T> T fromJson(JsonElement json, TypeToken<T> typeOfT) throws JsonSyntaxException {
if (json == null) {
return null;
}
- return (T) fromJson(new JsonTreeReader(json), typeOfT);
+ return fromJson(new JsonTreeReader(json), typeOfT);
}
- static class FutureTypeAdapter<T> extends TypeAdapter<T> {
+ static class FutureTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private TypeAdapter<T> delegate;
public void setDelegate(TypeAdapter<T> typeAdapter) {
@@ -1139,18 +1329,23 @@ public final class Gson {
delegate = typeAdapter;
}
- @Override public T read(JsonReader in) throws IOException {
+ private TypeAdapter<T> delegate() {
if (delegate == null) {
- throw new IllegalStateException();
+ throw new IllegalStateException("Delegate has not been set yet");
}
- return delegate.read(in);
+ return delegate;
+ }
+
+ @Override public TypeAdapter<T> getSerializationDelegate() {
+ return delegate();
+ }
+
+ @Override public T read(JsonReader in) throws IOException {
+ return delegate().read(in);
}
@Override public void write(JsonWriter out, T value) throws IOException {
- if (delegate == null) {
- throw new IllegalStateException();
- }
- delegate.write(out, value);
+ delegate().write(out, value);
}
}
diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
index 4c540ac1..8b04430f 100644
--- a/gson/src/main/java/com/google/gson/GsonBuilder.java
+++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
@@ -16,26 +16,6 @@
package com.google.gson;
-import java.lang.reflect.Type;
-import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import com.google.gson.internal.$Gson$Preconditions;
-import com.google.gson.internal.Excluder;
-import com.google.gson.internal.bind.DefaultDateTypeAdapter;
-import com.google.gson.internal.bind.TreeTypeAdapter;
-import com.google.gson.internal.bind.TypeAdapters;
-import com.google.gson.internal.sql.SqlTypesSupport;
-import com.google.gson.reflect.TypeToken;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-
import static com.google.gson.Gson.DEFAULT_COMPLEX_MAP_KEYS;
import static com.google.gson.Gson.DEFAULT_DATE_PATTERN;
import static com.google.gson.Gson.DEFAULT_ESCAPE_HTML;
@@ -48,6 +28,28 @@ import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS;
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;
+import com.google.gson.annotations.Since;
+import com.google.gson.annotations.Until;
+import com.google.gson.internal.$Gson$Preconditions;
+import com.google.gson.internal.Excluder;
+import com.google.gson.internal.bind.DefaultDateTypeAdapter;
+import com.google.gson.internal.bind.TreeTypeAdapter;
+import com.google.gson.internal.bind.TypeAdapters;
+import com.google.gson.internal.sql.SqlTypesSupport;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
/**
* <p>Use this builder to construct a {@link Gson} instance when you need to set configuration
* options other than the default. For {@link Gson} with default configuration, it is simpler to
@@ -143,21 +145,35 @@ public final class GsonBuilder {
}
/**
- * Configures Gson to enable versioning support.
+ * Configures Gson to enable versioning support. Versioning support works based on the
+ * annotation types {@link Since} and {@link Until}. It allows including or excluding fields
+ * and classes based on the specified version. See the documentation of these annotation
+ * types for more information.
+ *
+ * <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until}
+ * has no effect.
*
- * @param ignoreVersionsAfter any field or type marked with a version higher than this value
- * are ignored during serialization or deserialization.
+ * @param version the version number to use.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * @throws IllegalArgumentException if the version number is NaN or negative
+ * @see Since
+ * @see Until
*/
- public GsonBuilder setVersion(double ignoreVersionsAfter) {
- excluder = excluder.withVersion(ignoreVersionsAfter);
+ public GsonBuilder setVersion(double version) {
+ if (Double.isNaN(version) || version < 0.0) {
+ throw new IllegalArgumentException("Invalid version: " + version);
+ }
+ excluder = excluder.withVersion(version);
return this;
}
/**
* Configures Gson to excludes all class fields that have the specified modifiers. By default,
- * Gson will exclude all fields marked transient or static. This method will override that
- * behavior.
+ * Gson will exclude all fields marked {@code transient} or {@code static}. This method will
+ * override that behavior.
+ *
+ * <p>This is a convenience method which behaves as if an {@link ExclusionStrategy} which
+ * excludes these fields was {@linkplain #setExclusionStrategies(ExclusionStrategy...) registered with this builder}.
*
* @param modifiers the field modifiers. You must use the modifiers specified in the
* {@link java.lang.reflect.Modifier} class. For example,
@@ -166,6 +182,7 @@ public final class GsonBuilder {
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
*/
public GsonBuilder excludeFieldsWithModifiers(int... modifiers) {
+ Objects.requireNonNull(modifiers);
excluder = excluder.withModifiers(modifiers);
return this;
}
@@ -185,9 +202,12 @@ public final class GsonBuilder {
}
/**
- * Configures Gson to exclude all fields from consideration for serialization or deserialization
+ * Configures Gson to exclude all fields from consideration for serialization and deserialization
* that do not have the {@link com.google.gson.annotations.Expose} annotation.
*
+ * <p>This is a convenience method which behaves as if an {@link ExclusionStrategy} which excludes
+ * these fields was {@linkplain #setExclusionStrategies(ExclusionStrategy...) registered with this builder}.
+ *
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
*/
public GsonBuilder excludeFieldsWithoutExposeAnnotation() {
@@ -214,8 +234,9 @@ public final class GsonBuilder {
* on the key; however, when this is called then one of the following cases
* apply:
*
- * <h3>Maps as JSON objects</h3>
- * For this case, assume that a type adapter is registered to serialize and
+ * <p><b>Maps as JSON objects</b>
+ *
+ * <p>For this case, assume that a type adapter is registered to serialize and
* deserialize some {@code Point} class, which contains an x and y coordinate,
* to/from the JSON Primitive string value {@code "(x,y)"}. The Java map would
* then be serialized as a {@link JsonObject}.
@@ -239,11 +260,12 @@ public final class GsonBuilder {
* }
* }</pre>
*
- * <h3>Maps as JSON arrays</h3>
- * For this case, assume that a type adapter was NOT registered for some
+ * <p><b>Maps as JSON arrays</b>
+ *
+ * <p>For this case, assume that a type adapter was NOT registered for some
* {@code Point} class, but rather the default Gson serialization is applied.
* In this case, some {@code new Point(2,3)} would serialize as {@code
- * {"x":2,"y":5}}.
+ * {"x":2,"y":3}}.
*
* <p>Given the assumption above, a {@code Map<Point, String>} will be
* serialize as an array of arrays (can be viewed as an entry set of pairs).
@@ -290,7 +312,20 @@ public final class GsonBuilder {
}
/**
- * Configures Gson to exclude inner classes during serialization.
+ * Configures Gson to exclude inner classes (= non-{@code static} nested classes) during serialization
+ * and deserialization. This is a convenience method which behaves as if an {@link ExclusionStrategy}
+ * which excludes inner classes was {@linkplain #setExclusionStrategies(ExclusionStrategy...) registered with this builder}.
+ * This means inner classes will be serialized as JSON {@code null}, and will be deserialized as
+ * Java {@code null} with their JSON data being ignored. And fields with an inner class as type will
+ * be ignored during serialization and deserialization.
+ *
+ * <p>By default Gson serializes and deserializes inner classes, but ignores references to the
+ * enclosing instance. Deserialization might not be possible at all when {@link #disableJdkUnsafe()}
+ * is used (and no custom {@link InstanceCreator} is registered), or it can lead to unexpected
+ * {@code NullPointerException}s when the deserialized instance is used afterwards.
+ *
+ * <p>In general using inner classes with Gson should be avoided; they should be converted to {@code static}
+ * nested classes if possible.
*
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.3
@@ -309,33 +344,34 @@ public final class GsonBuilder {
* @since 1.3
*/
public GsonBuilder setLongSerializationPolicy(LongSerializationPolicy serializationPolicy) {
- this.longSerializationPolicy = serializationPolicy;
+ this.longSerializationPolicy = Objects.requireNonNull(serializationPolicy);
return this;
}
/**
- * Configures Gson to apply a specific naming policy to an object's field during serialization
+ * Configures Gson to apply a specific naming policy to an object's fields during serialization
* and deserialization.
*
- * @param namingConvention the JSON field naming convention to use for serialization and
- * deserialization.
- * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * <p>This method just delegates to {@link #setFieldNamingStrategy(FieldNamingStrategy)}.
*/
public GsonBuilder setFieldNamingPolicy(FieldNamingPolicy namingConvention) {
- this.fieldNamingPolicy = namingConvention;
- return this;
+ return setFieldNamingStrategy(namingConvention);
}
/**
- * Configures Gson to apply a specific naming policy strategy to an object's field during
+ * Configures Gson to apply a specific naming strategy to an object's fields during
* serialization and deserialization.
*
- * @param fieldNamingStrategy the actual naming strategy to apply to the fields
+ * <p>The created Gson instance might only use the field naming strategy once for a
+ * field and cache the result. It is not guaranteed that the strategy will be used
+ * again every time the value of a field is serialized or deserialized.
+ *
+ * @param fieldNamingStrategy the naming strategy to apply to the fields
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.3
*/
public GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrategy) {
- this.fieldNamingPolicy = fieldNamingStrategy;
+ this.fieldNamingPolicy = Objects.requireNonNull(fieldNamingStrategy);
return this;
}
@@ -345,9 +381,10 @@ public final class GsonBuilder {
* @param objectToNumberStrategy the actual object-to-number strategy
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @see ToNumberPolicy#DOUBLE The default object-to-number strategy
+ * @since 2.8.9
*/
public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) {
- this.objectToNumberStrategy = objectToNumberStrategy;
+ this.objectToNumberStrategy = Objects.requireNonNull(objectToNumberStrategy);
return this;
}
@@ -357,9 +394,10 @@ public final class GsonBuilder {
* @param numberToNumberStrategy the actual number-to-number strategy
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @see ToNumberPolicy#LAZILY_PARSED_NUMBER The default number-to-number strategy
+ * @since 2.8.9
*/
public GsonBuilder setNumberToNumberStrategy(ToNumberStrategy numberToNumberStrategy) {
- this.numberToNumberStrategy = numberToNumberStrategy;
+ this.numberToNumberStrategy = Objects.requireNonNull(numberToNumberStrategy);
return this;
}
@@ -368,12 +406,29 @@ public final class GsonBuilder {
* deserialization. Each of the {@code strategies} will be applied as a disjunction rule.
* This means that if one of the {@code strategies} suggests that a field (or class) should be
* skipped then that field (or object) is skipped during serialization/deserialization.
+ * The strategies are added to the existing strategies (if any); the existing strategies
+ * are not replaced.
+ *
+ * <p>Fields are excluded for serialization and deserialization when
+ * {@link ExclusionStrategy#shouldSkipField(FieldAttributes) shouldSkipField} returns {@code true},
+ * or when {@link ExclusionStrategy#shouldSkipClass(Class) shouldSkipClass} returns {@code true}
+ * for the field type. Gson behaves as if the field did not exist; its value is not serialized
+ * and on deserialization if a JSON member with this name exists it is skipped by default.<br>
+ * When objects of an excluded type (as determined by
+ * {@link ExclusionStrategy#shouldSkipClass(Class) shouldSkipClass}) are serialized a
+ * JSON null is written to output, and when deserialized the JSON value is skipped and
+ * {@code null} is returned.
+ *
+ * <p>The created Gson instance might only use an exclusion strategy once for a field or
+ * class and cache the result. It is not guaranteed that the strategy will be used again
+ * every time the value of a field or a class is serialized or deserialized.
*
* @param strategies the set of strategy object to apply during object (de)serialization.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.4
*/
public GsonBuilder setExclusionStrategies(ExclusionStrategy... strategies) {
+ Objects.requireNonNull(strategies);
for (ExclusionStrategy strategy : strategies) {
excluder = excluder.withExclusionStrategy(strategy, true, true);
}
@@ -388,11 +443,15 @@ public final class GsonBuilder {
* class) should be skipped then that field (or object) is skipped during its
* serialization.
*
+ * <p>See the documentation of {@link #setExclusionStrategies(ExclusionStrategy...)}
+ * for a detailed description of the effect of exclusion strategies.
+ *
* @param strategy an exclusion strategy to apply during serialization.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.7
*/
public GsonBuilder addSerializationExclusionStrategy(ExclusionStrategy strategy) {
+ Objects.requireNonNull(strategy);
excluder = excluder.withExclusionStrategy(strategy, true, false);
return this;
}
@@ -405,11 +464,15 @@ public final class GsonBuilder {
* class) should be skipped then that field (or object) is skipped during its
* deserialization.
*
+ * <p>See the documentation of {@link #setExclusionStrategies(ExclusionStrategy...)}
+ * for a detailed description of the effect of exclusion strategies.
+ *
* @param strategy an exclusion strategy to apply during deserialization.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.7
*/
public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strategy) {
+ Objects.requireNonNull(strategy);
excluder = excluder.withExclusionStrategy(strategy, false, true);
return this;
}
@@ -527,26 +590,34 @@ public final class GsonBuilder {
* types! For example, applications registering {@code boolean.class} should also register {@code
* Boolean.class}.
*
+ * <p>{@link JsonSerializer} and {@link JsonDeserializer} are made "{@code null}-safe". This
+ * means when trying to serialize {@code null}, Gson will write a JSON {@code null} and the
+ * serializer is not called. Similarly when deserializing a JSON {@code null}, Gson will emit
+ * {@code null} without calling the deserializer. If it is desired to handle {@code null} values,
+ * a {@link TypeAdapter} should be used instead.
+ *
* @param type the type definition for the type adapter being registered
* @param typeAdapter This object must implement at least one of the {@link TypeAdapter},
* {@link InstanceCreator}, {@link JsonSerializer}, and a {@link JsonDeserializer} interfaces.
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
*/
- @SuppressWarnings({"unchecked", "rawtypes"})
public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {
+ Objects.requireNonNull(type);
$Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?>
|| typeAdapter instanceof JsonDeserializer<?>
|| typeAdapter instanceof InstanceCreator<?>
|| typeAdapter instanceof TypeAdapter<?>);
if (typeAdapter instanceof InstanceCreator<?>) {
- instanceCreators.put(type, (InstanceCreator) typeAdapter);
+ instanceCreators.put(type, (InstanceCreator<?>) typeAdapter);
}
if (typeAdapter instanceof JsonSerializer<?> || typeAdapter instanceof JsonDeserializer<?>) {
TypeToken<?> typeToken = TypeToken.get(type);
factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter));
}
if (typeAdapter instanceof TypeAdapter<?>) {
- factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter));
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ TypeAdapterFactory factory = TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter);
+ factories.add(factory);
}
return this;
}
@@ -557,9 +628,14 @@ public final class GsonBuilder {
* is designed to handle a large number of factories, so you should consider registering
* them to be at par with registering an individual type adapter.
*
+ * <p>The created Gson instance might only use the factory once to create an adapter for
+ * a specific type and cache the result. It is not guaranteed that the factory will be used
+ * again every time the type is serialized or deserialized.
+ *
* @since 2.1
*/
public GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory) {
+ Objects.requireNonNull(factory);
factories.add(factory);
return this;
}
@@ -578,8 +654,8 @@ public final class GsonBuilder {
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
* @since 1.7
*/
- @SuppressWarnings({"unchecked", "rawtypes"})
public GsonBuilder registerTypeHierarchyAdapter(Class<?> baseType, Object typeAdapter) {
+ Objects.requireNonNull(baseType);
$Gson$Preconditions.checkArgument(typeAdapter instanceof JsonSerializer<?>
|| typeAdapter instanceof JsonDeserializer<?>
|| typeAdapter instanceof TypeAdapter<?>);
@@ -587,7 +663,9 @@ public final class GsonBuilder {
hierarchyFactories.add(TreeTypeAdapter.newTypeHierarchyFactory(baseType, typeAdapter));
}
if (typeAdapter instanceof TypeAdapter<?>) {
- factories.add(TypeAdapters.newTypeHierarchyFactory(baseType, (TypeAdapter)typeAdapter));
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ TypeAdapterFactory factory = TypeAdapters.newTypeHierarchyFactory(baseType, (TypeAdapter)typeAdapter);
+ factories.add(factory);
}
return this;
}
@@ -631,6 +709,7 @@ public final class GsonBuilder {
* disabling usage of {@code Unsafe}.
*
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * @since 2.9.0
*/
public GsonBuilder disableJdkUnsafe() {
this.useJdkUnsafe = false;
@@ -649,12 +728,16 @@ public final class GsonBuilder {
* all classes for which no {@link TypeAdapter} has been registered, and for which no
* built-in Gson {@code TypeAdapter} exists.
*
+ * <p>The created Gson instance might only use an access filter once for a class or its
+ * members and cache the result. It is not guaranteed that the filter will be used again
+ * every time a class or its members are accessed during serialization or deserialization.
+ *
* @param filter filter to add
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * @since 2.9.1
*/
public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) {
- if (filter == null) throw new NullPointerException();
-
+ Objects.requireNonNull(filter);
reflectionFilters.addFirst(filter);
return this;
}
diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java
index fe8b686d..370b323f 100644
--- a/gson/src/main/java/com/google/gson/JsonArray.java
+++ b/gson/src/main/java/com/google/gson/JsonArray.java
@@ -16,6 +16,7 @@
package com.google.gson;
+import com.google.gson.internal.NonNullElementWrapperList;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
@@ -23,29 +24,44 @@ import java.util.Iterator;
import java.util.List;
/**
- * A class representing an array type in Json. An array is a list of {@link JsonElement}s each of
+ * A class representing an array type in JSON. An array is a list of {@link JsonElement}s each of
* which can be of a different type. This is an ordered list, meaning that the order in which
- * elements are added is preserved.
+ * elements are added is preserved. This class does not support {@code null} values. If {@code null}
+ * is provided as element argument to any of the methods, it is converted to a {@link JsonNull}.
+ *
+ * <p>{@code JsonArray} only implements the {@link Iterable} interface but not the {@link List}
+ * interface. A {@code List} view of it can be obtained with {@link #asList()}.
*
* @author Inderjeet Singh
* @author Joel Leitch
*/
public final class JsonArray extends JsonElement implements Iterable<JsonElement> {
- private final List<JsonElement> elements;
+ private final ArrayList<JsonElement> elements;
/**
* Creates an empty JsonArray.
*/
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonArray() {
elements = new ArrayList<>();
}
-
+
+ /**
+ * Creates an empty JsonArray with the desired initial capacity.
+ *
+ * @param capacity initial capacity.
+ * @throws IllegalArgumentException if the {@code capacity} is
+ * negative
+ * @since 2.8.1
+ */
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonArray(int capacity) {
elements = new ArrayList<>(capacity);
}
/**
- * Creates a deep copy of this element and all its children
+ * Creates a deep copy of this element and all its children.
+ *
* @since 2.8.2
*/
@Override
@@ -64,6 +80,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
* Adds the specified boolean to self.
*
* @param bool the boolean that needs to be added to the array.
+ * @since 2.4
*/
public void add(Boolean bool) {
elements.add(bool == null ? JsonNull.INSTANCE : new JsonPrimitive(bool));
@@ -73,6 +90,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
* Adds the specified character to self.
*
* @param character the character that needs to be added to the array.
+ * @since 2.4
*/
public void add(Character character) {
elements.add(character == null ? JsonNull.INSTANCE : new JsonPrimitive(character));
@@ -82,6 +100,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
* Adds the specified number to self.
*
* @param number the number that needs to be added to the array.
+ * @since 2.4
*/
public void add(Number number) {
elements.add(number == null ? JsonNull.INSTANCE : new JsonPrimitive(number));
@@ -91,6 +110,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
* Adds the specified string to self.
*
* @param string the string that needs to be added to the array.
+ * @since 2.4
*/
public void add(String string) {
elements.add(string == null ? JsonNull.INSTANCE : new JsonPrimitive(string));
@@ -119,19 +139,20 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
/**
* Replaces the element at the specified position in this array with the specified element.
- * Element can be null.
+ *
* @param index index of the element to replace
* @param element element to be stored at the specified position
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException if the specified index is outside the array bounds
*/
public JsonElement set(int index, JsonElement element) {
- return elements.set(index, element);
+ return elements.set(index, element == null ? JsonNull.INSTANCE : element);
}
/**
* Removes the first occurrence of the specified element from this array, if it is present.
* If the array does not contain the element, it is unchanged.
+ *
* @param element element to be removed from this array, if present
* @return true if this array contained the specified element, false otherwise
* @since 2.3
@@ -144,6 +165,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
* Removes the element at the specified position in this array. Shifts any subsequent elements
* to the left (subtracts one from their indices). Returns the element that was removed from
* the array.
+ *
* @param index index the index of the element to be removed
* @return the element previously at the specified position
* @throws IndexOutOfBoundsException if the specified index is outside the array bounds
@@ -155,6 +177,7 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
/**
* Returns true if this array contains the specified element.
+ *
* @return true if this array contains the specified element.
* @param element whose presence in this array is to be tested
* @since 2.3
@@ -171,11 +194,12 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
public int size() {
return elements.size();
}
-
+
/**
- * Returns true if the array is empty
+ * Returns true if the array is empty.
*
- * @return true if the array is empty
+ * @return true if the array is empty.
+ * @since 2.8.7
*/
public boolean isEmpty() {
return elements.isEmpty();
@@ -193,10 +217,10 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
}
/**
- * Returns the ith element of the array.
+ * Returns the i-th element of the array.
*
* @param i the index of the element that is being sought.
- * @return the element present at the ith index.
+ * @return the element present at the i-th index.
* @throws IndexOutOfBoundsException if i is negative or greater than or equal to the
* {@link #size()} of the array.
*/
@@ -204,191 +228,204 @@ public final class JsonArray extends JsonElement implements Iterable<JsonElement
return elements.get(i);
}
+ private JsonElement getAsSingleElement() {
+ int size = elements.size();
+ if (size == 1) {
+ return elements.get(0);
+ }
+ throw new IllegalStateException("Array must have size 1, but has size " + size);
+ }
+
/**
- * convenience method to get this array as a {@link Number} if it contains a single element.
+ * Convenience method to get this array as a {@link Number} if it contains a single element.
+ * This method calls {@link JsonElement#getAsNumber()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a number if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid Number.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a number if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public Number getAsNumber() {
- if (elements.size() == 1) {
- return elements.get(0).getAsNumber();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsNumber();
}
/**
- * convenience method to get this array as a {@link String} if it contains a single element.
+ * Convenience method to get this array as a {@link String} if it contains a single element.
+ * This method calls {@link JsonElement#getAsString()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a String if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid String.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a String if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public String getAsString() {
- if (elements.size() == 1) {
- return elements.get(0).getAsString();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsString();
}
/**
- * convenience method to get this array as a double if it contains a single element.
+ * Convenience method to get this array as a double if it contains a single element.
+ * This method calls {@link JsonElement#getAsDouble()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a double if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid double.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a double if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public double getAsDouble() {
- if (elements.size() == 1) {
- return elements.get(0).getAsDouble();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsDouble();
}
/**
- * convenience method to get this array as a {@link BigDecimal} if it contains a single element.
+ * Convenience method to get this array as a {@link BigDecimal} if it contains a single element.
+ * This method calls {@link JsonElement#getAsBigDecimal()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a {@link BigDecimal} if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive}.
- * @throws NumberFormatException if the element at index 0 is not a valid {@link BigDecimal}.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a {@link BigDecimal} if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
* @since 1.2
*/
@Override
public BigDecimal getAsBigDecimal() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBigDecimal();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBigDecimal();
}
/**
- * convenience method to get this array as a {@link BigInteger} if it contains a single element.
+ * Convenience method to get this array as a {@link BigInteger} if it contains a single element.
+ * This method calls {@link JsonElement#getAsBigInteger()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a {@link BigInteger} if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive}.
- * @throws NumberFormatException if the element at index 0 is not a valid {@link BigInteger}.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a {@link BigInteger} if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
* @since 1.2
*/
@Override
public BigInteger getAsBigInteger() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBigInteger();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBigInteger();
}
/**
- * convenience method to get this array as a float if it contains a single element.
+ * Convenience method to get this array as a float if it contains a single element.
+ * This method calls {@link JsonElement#getAsFloat()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a float if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid float.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a float if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public float getAsFloat() {
- if (elements.size() == 1) {
- return elements.get(0).getAsFloat();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsFloat();
}
/**
- * convenience method to get this array as a long if it contains a single element.
+ * Convenience method to get this array as a long if it contains a single element.
+ * This method calls {@link JsonElement#getAsLong()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a long if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid long.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a long if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public long getAsLong() {
- if (elements.size() == 1) {
- return elements.get(0).getAsLong();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsLong();
}
/**
- * convenience method to get this array as an integer if it contains a single element.
+ * Convenience method to get this array as an integer if it contains a single element.
+ * This method calls {@link JsonElement#getAsInt()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as an integer if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid integer.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as an integer if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public int getAsInt() {
- if (elements.size() == 1) {
- return elements.get(0).getAsInt();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsInt();
}
+ /**
+ * Convenience method to get this array as a primitive byte if it contains a single element.
+ * This method calls {@link JsonElement#getAsByte()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
+ *
+ * @return this element as a primitive byte if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ */
@Override
public byte getAsByte() {
- if (elements.size() == 1) {
- return elements.get(0).getAsByte();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsByte();
}
+ /**
+ * Convenience method to get this array as a character if it contains a single element.
+ * This method calls {@link JsonElement#getAsCharacter()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
+ *
+ * @return this element as a primitive short if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
+ * @deprecated This method is misleading, as it does not get this element as a char but rather as
+ * a string's first character.
+ */
@Deprecated
@Override
public char getAsCharacter() {
- if (elements.size() == 1) {
- JsonElement element = elements.get(0);
- return element.getAsCharacter();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsCharacter();
}
/**
- * convenience method to get this array as a primitive short if it contains a single element.
+ * Convenience method to get this array as a primitive short if it contains a single element.
+ * This method calls {@link JsonElement#getAsShort()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a primitive short if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid short.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a primitive short if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public short getAsShort() {
- if (elements.size() == 1) {
- return elements.get(0).getAsShort();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsShort();
}
/**
- * convenience method to get this array as a boolean if it contains a single element.
+ * Convenience method to get this array as a boolean if it contains a single element.
+ * This method calls {@link JsonElement#getAsBoolean()} on the element, therefore any
+ * of the exceptions declared by that method can occur.
*
- * @return get this element as a boolean if it is single element array.
- * @throws ClassCastException if the element in the array is of not a {@link JsonPrimitive} and
- * is not a valid boolean.
- * @throws IllegalStateException if the array has more than one element.
+ * @return this element as a boolean if it is single element array.
+ * @throws IllegalStateException if the array is empty or has more than one element.
*/
@Override
public boolean getAsBoolean() {
- if (elements.size() == 1) {
- return elements.get(0).getAsBoolean();
- }
- throw new IllegalStateException();
+ return getAsSingleElement().getAsBoolean();
}
+ /**
+ * Returns a mutable {@link List} view of this {@code JsonArray}. Changes to the {@code List}
+ * are visible in this {@code JsonArray} and the other way around.
+ *
+ * <p>The {@code List} does not permit {@code null} elements. Unlike {@code JsonArray}'s
+ * {@code null} handling, a {@link NullPointerException} is thrown when trying to add {@code null}.
+ * Use {@link JsonNull} for JSON null values.
+ *
+ * @return mutable {@code List} view
+ * @since 2.10
+ */
+ public List<JsonElement> asList() {
+ return new NonNullElementWrapperList<>(elements);
+ }
+
+ /**
+ * Returns whether the other object is equal to this. This method only considers
+ * the other object to be equal if it is an instance of {@code JsonArray} and has
+ * equal elements in the same order.
+ */
@Override
public boolean equals(Object o) {
return (o == this) || (o instanceof JsonArray && ((JsonArray) o).elements.equals(elements));
}
+ /**
+ * Returns the hash code of this array. This method calculates the hash code based
+ * on the elements of this array.
+ */
@Override
public int hashCode() {
return elements.hashCode();
diff --git a/gson/src/main/java/com/google/gson/JsonDeserializer.java b/gson/src/main/java/com/google/gson/JsonDeserializer.java
index 0589eb28..6462d45c 100644
--- a/gson/src/main/java/com/google/gson/JsonDeserializer.java
+++ b/gson/src/main/java/com/google/gson/JsonDeserializer.java
@@ -19,7 +19,7 @@ package com.google.gson;
import java.lang.reflect.Type;
/**
- * <p>Interface representing a custom deserializer for Json. You should write a custom
+ * <p>Interface representing a custom deserializer for JSON. You should write a custom
* deserializer, if you are not happy with the default deserialization done by Gson. You will
* also need to register this deserializer through
* {@link GsonBuilder#registerTypeAdapter(Type, Object)}.</p>
@@ -42,17 +42,19 @@ import java.lang.reflect.Type;
* </pre>
*
* <p>The default deserialization of {@code Id(com.foo.MyObject.class, 20L)} will require the
- * Json string to be <code>{"clazz":com.foo.MyObject,"value":20}</code>. Suppose, you already know
+ * JSON string to be <code>{"clazz":"com.foo.MyObject","value":20}</code>. Suppose, you already know
* the type of the field that the {@code Id} will be deserialized into, and hence just want to
- * deserialize it from a Json string {@code 20}. You can achieve that by writing a custom
+ * deserialize it from a JSON string {@code 20}. You can achieve that by writing a custom
* deserializer:</p>
*
* <pre>
- * class IdDeserializer implements JsonDeserializer&lt;Id&gt;() {
+ * class IdDeserializer implements JsonDeserializer&lt;Id&gt; {
* public Id deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
* throws JsonParseException {
- * return new Id((Class)typeOfT, id.getValue());
+ * long idValue = json.getAsJsonPrimitive().getAsLong();
+ * return new Id((Class) typeOfT, idValue);
* }
+ * }
* </pre>
*
* <p>You will also need to register {@code IdDeserializer} with Gson as follows:</p>
diff --git a/gson/src/main/java/com/google/gson/JsonElement.java b/gson/src/main/java/com/google/gson/JsonElement.java
index fb6a5f1a..640f8818 100644
--- a/gson/src/main/java/com/google/gson/JsonElement.java
+++ b/gson/src/main/java/com/google/gson/JsonElement.java
@@ -24,7 +24,7 @@ import java.math.BigDecimal;
import java.math.BigInteger;
/**
- * A class representing an element of Json. It could either be a {@link JsonObject}, a
+ * A class representing an element of JSON. It could either be a {@link JsonObject}, a
* {@link JsonArray}, a {@link JsonPrimitive} or a {@link JsonNull}.
*
* @author Inderjeet Singh
@@ -32,14 +32,24 @@ import java.math.BigInteger;
*/
public abstract class JsonElement {
/**
+ * @deprecated Creating custom {@code JsonElement} subclasses is highly discouraged
+ * and can lead to undefined behavior.<br>
+ * This constructor is only kept for backward compatibility.
+ */
+ @Deprecated
+ public JsonElement() {
+ }
+
+ /**
* Returns a deep copy of this element. Immutable elements like primitives
* and nulls are not copied.
+ *
* @since 2.8.2
*/
public abstract JsonElement deepCopy();
/**
- * provides check for verifying if this element is an array or not.
+ * Provides a check for verifying if this element is a JSON array or not.
*
* @return true if this element is of type {@link JsonArray}, false otherwise.
*/
@@ -48,7 +58,7 @@ public abstract class JsonElement {
}
/**
- * provides check for verifying if this element is a Json object or not.
+ * Provides a check for verifying if this element is a JSON object or not.
*
* @return true if this element is of type {@link JsonObject}, false otherwise.
*/
@@ -57,7 +67,7 @@ public abstract class JsonElement {
}
/**
- * provides check for verifying if this element is a primitive or not.
+ * Provides a check for verifying if this element is a primitive or not.
*
* @return true if this element is of type {@link JsonPrimitive}, false otherwise.
*/
@@ -66,7 +76,7 @@ public abstract class JsonElement {
}
/**
- * provides check for verifying if this element represents a null value or not.
+ * Provides a check for verifying if this element represents a null value or not.
*
* @return true if this element is of type {@link JsonNull}, false otherwise.
* @since 1.2
@@ -76,13 +86,13 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link JsonObject}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
+ * Convenience method to get this element as a {@link JsonObject}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJsonObject()}
* first.
*
- * @return get this element as a {@link JsonObject}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link JsonObject}.
+ * @throws IllegalStateException if this element is of another type.
*/
public JsonObject getAsJsonObject() {
if (isJsonObject()) {
@@ -92,13 +102,13 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link JsonArray}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
+ * Convenience method to get this element as a {@link JsonArray}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJsonArray()}
* first.
*
- * @return get this element as a {@link JsonArray}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link JsonArray}.
+ * @throws IllegalStateException if this element is of another type.
*/
public JsonArray getAsJsonArray() {
if (isJsonArray()) {
@@ -108,13 +118,13 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link JsonPrimitive}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
+ * Convenience method to get this element as a {@link JsonPrimitive}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJsonPrimitive()}
* first.
*
- * @return get this element as a {@link JsonPrimitive}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link JsonPrimitive}.
+ * @throws IllegalStateException if this element is of another type.
*/
public JsonPrimitive getAsJsonPrimitive() {
if (isJsonPrimitive()) {
@@ -124,13 +134,13 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link JsonNull}. If the element is of some
- * other type, a {@link IllegalStateException} will result. Hence it is best to use this method
+ * Convenience method to get this element as a {@link JsonNull}. If this element is of some
+ * other type, an {@link IllegalStateException} will result. Hence it is best to use this method
* after ensuring that this element is of the desired type by calling {@link #isJsonNull()}
* first.
*
- * @return get this element as a {@link JsonNull}.
- * @throws IllegalStateException if the element is of another type.
+ * @return this element as a {@link JsonNull}.
+ * @throws IllegalStateException if this element is of another type.
* @since 1.2
*/
public JsonNull getAsJsonNull() {
@@ -141,12 +151,11 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a boolean value.
+ * Convenience method to get this element as a boolean value.
*
- * @return get this element as a primitive boolean value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * boolean value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive boolean value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public boolean getAsBoolean() {
@@ -154,12 +163,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link Number}.
+ * Convenience method to get this element as a {@link Number}.
*
- * @return get this element as a {@link Number}.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * number.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a {@link Number}.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray},
+ * or cannot be converted to a number.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public Number getAsNumber() {
@@ -167,12 +176,11 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a string value.
+ * Convenience method to get this element as a string value.
*
- * @return get this element as a string value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * string value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a string value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public String getAsString() {
@@ -180,12 +188,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive double value.
+ * Convenience method to get this element as a primitive double value.
*
- * @return get this element as a primitive double value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * double value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive double value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid double.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public double getAsDouble() {
@@ -193,12 +201,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive float value.
+ * Convenience method to get this element as a primitive float value.
*
- * @return get this element as a primitive float value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * float value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive float value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid float.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public float getAsFloat() {
@@ -206,12 +214,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive long value.
+ * Convenience method to get this element as a primitive long value.
*
- * @return get this element as a primitive long value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * long value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive long value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid long.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public long getAsLong() {
@@ -219,12 +227,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive integer value.
+ * Convenience method to get this element as a primitive integer value.
*
- * @return get this element as a primitive integer value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * integer value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive integer value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid integer.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public int getAsInt() {
@@ -232,12 +240,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive byte value.
+ * Convenience method to get this element as a primitive byte value.
*
- * @return get this element as a primitive byte value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * byte value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive byte value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid byte.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
* @since 1.3
*/
@@ -246,13 +254,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get the first character of this element as a string or the first
- * character of this array's first element as a string.
+ * Convenience method to get the first character of the string value of this element.
*
- * @return the first character of the string.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * string value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return the first character of the string value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray},
+ * or if its string value is empty.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
* @since 1.3
* @deprecated This method is misleading, as it does not get this element as a char but rather as
@@ -264,12 +271,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link BigDecimal}.
+ * Convenience method to get this element as a {@link BigDecimal}.
*
- * @return get this element as a {@link BigDecimal}.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive}.
- * @throws NumberFormatException if the element is not a valid {@link BigDecimal}.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a {@link BigDecimal}.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if this element is not a valid {@link BigDecimal}.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
* @since 1.2
*/
@@ -278,12 +285,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a {@link BigInteger}.
+ * Convenience method to get this element as a {@link BigInteger}.
*
- * @return get this element as a {@link BigInteger}.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive}.
- * @throws NumberFormatException if the element is not a valid {@link BigInteger}.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a {@link BigInteger}.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if this element is not a valid {@link BigInteger}.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
* @since 1.2
*/
@@ -292,12 +299,12 @@ public abstract class JsonElement {
}
/**
- * convenience method to get this element as a primitive short value.
+ * Convenience method to get this element as a primitive short value.
*
- * @return get this element as a primitive short value.
- * @throws ClassCastException if the element is of not a {@link JsonPrimitive} and is not a valid
- * short value.
- * @throws IllegalStateException if the element is of the type {@link JsonArray} but contains
+ * @return this element as a primitive short value.
+ * @throws UnsupportedOperationException if this element is not a {@link JsonPrimitive} or {@link JsonArray}.
+ * @throws NumberFormatException if the value contained is not a valid short.
+ * @throws IllegalStateException if this element is of the type {@link JsonArray} but contains
* more than a single element.
*/
public short getAsShort() {
diff --git a/gson/src/main/java/com/google/gson/JsonNull.java b/gson/src/main/java/com/google/gson/JsonNull.java
index 67cb9325..b14fd3f1 100644
--- a/gson/src/main/java/com/google/gson/JsonNull.java
+++ b/gson/src/main/java/com/google/gson/JsonNull.java
@@ -17,7 +17,7 @@
package com.google.gson;
/**
- * A class representing a Json {@code null} value.
+ * A class representing a JSON {@code null} value.
*
* @author Inderjeet Singh
* @author Joel Leitch
@@ -25,15 +25,16 @@ package com.google.gson;
*/
public final class JsonNull extends JsonElement {
/**
- * singleton for JsonNull
+ * Singleton for {@code JsonNull}.
*
* @since 1.8
*/
public static final JsonNull INSTANCE = new JsonNull();
/**
- * Creates a new JsonNull object.
- * Deprecated since Gson version 1.8. Use {@link #INSTANCE} instead
+ * Creates a new {@code JsonNull} object.
+ *
+ * @deprecated Deprecated since Gson version 1.8, use {@link #INSTANCE} instead.
*/
@Deprecated
public JsonNull() {
@@ -41,7 +42,8 @@ public final class JsonNull extends JsonElement {
}
/**
- * Returns the same instance since it is an immutable value
+ * Returns the same instance since it is an immutable value.
+ *
* @since 2.8.2
*/
@Override
@@ -50,7 +52,7 @@ public final class JsonNull extends JsonElement {
}
/**
- * All instances of JsonNull have the same hash code since they are indistinguishable
+ * All instances of {@code JsonNull} have the same hash code since they are indistinguishable.
*/
@Override
public int hashCode() {
@@ -58,7 +60,7 @@ public final class JsonNull extends JsonElement {
}
/**
- * All instances of JsonNull are the same
+ * All instances of {@code JsonNull} are considered equal.
*/
@Override
public boolean equals(Object other) {
diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java
index 285a8429..60dac41c 100644
--- a/gson/src/main/java/com/google/gson/JsonObject.java
+++ b/gson/src/main/java/com/google/gson/JsonObject.java
@@ -17,7 +17,6 @@
package com.google.gson;
import com.google.gson.internal.LinkedTreeMap;
-
import java.util.Map;
import java.util.Set;
@@ -25,15 +24,28 @@ import java.util.Set;
* A class representing an object type in Json. An object consists of name-value pairs where names
* are strings, and values are any other type of {@link JsonElement}. This allows for a creating a
* tree of JsonElements. The member elements of this object are maintained in order they were added.
+ * This class does not support {@code null} values. If {@code null} is provided as value argument
+ * to any of the methods, it is converted to a {@link JsonNull}.
+ *
+ * <p>{@code JsonObject} does not implement the {@link Map} interface, but a {@code Map} view
+ * of it can be obtained with {@link #asMap()}.
*
* @author Inderjeet Singh
* @author Joel Leitch
*/
public final class JsonObject extends JsonElement {
- private final LinkedTreeMap<String, JsonElement> members = new LinkedTreeMap<>();
+ private final LinkedTreeMap<String, JsonElement> members = new LinkedTreeMap<>(false);
+
+ /**
+ * Creates an empty JsonObject.
+ */
+ @SuppressWarnings("deprecation") // superclass constructor
+ public JsonObject() {
+ }
/**
- * Creates a deep copy of this element and all its children
+ * Creates a deep copy of this element and all its children.
+ *
* @since 2.8.2
*/
@Override
@@ -47,7 +59,7 @@ public final class JsonObject extends JsonElement {
/**
* Adds a member, which is a name-value pair, to self. The name must be a String, but the value
- * can be an arbitrary JsonElement, thereby allowing you to build a full tree of JsonElements
+ * can be an arbitrary {@link JsonElement}, thereby allowing you to build a full tree of JsonElements
* rooted at this node.
*
* @param property name of the member.
@@ -58,10 +70,11 @@ public final class JsonObject extends JsonElement {
}
/**
- * Removes the {@code property} from this {@link JsonObject}.
+ * Removes the {@code property} from this object.
*
* @param property name of the member that should be removed.
- * @return the {@link JsonElement} object that is being removed.
+ * @return the {@link JsonElement} object that is being removed, or {@code null} if no
+ * member with this name exists.
* @since 1.3
*/
public JsonElement remove(String property) {
@@ -69,8 +82,8 @@ public final class JsonObject extends JsonElement {
}
/**
- * Convenience method to add a primitive member. The specified value is converted to a
- * JsonPrimitive of String.
+ * Convenience method to add a string member. The specified value is converted to a
+ * {@link JsonPrimitive} of String.
*
* @param property name of the member.
* @param value the string value associated with the member.
@@ -80,8 +93,8 @@ public final class JsonObject extends JsonElement {
}
/**
- * Convenience method to add a primitive member. The specified value is converted to a
- * JsonPrimitive of Number.
+ * Convenience method to add a number member. The specified value is converted to a
+ * {@link JsonPrimitive} of Number.
*
* @param property name of the member.
* @param value the number value associated with the member.
@@ -92,10 +105,10 @@ public final class JsonObject extends JsonElement {
/**
* Convenience method to add a boolean member. The specified value is converted to a
- * JsonPrimitive of Boolean.
+ * {@link JsonPrimitive} of Boolean.
*
* @param property name of the member.
- * @param value the number value associated with the member.
+ * @param value the boolean value associated with the member.
*/
public void addProperty(String property, Boolean value) {
add(property, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value));
@@ -103,10 +116,10 @@ public final class JsonObject extends JsonElement {
/**
* Convenience method to add a char member. The specified value is converted to a
- * JsonPrimitive of Character.
+ * {@link JsonPrimitive} of Character.
*
* @param property name of the member.
- * @param value the number value associated with the member.
+ * @param value the char value associated with the member.
*/
public void addProperty(String property, Character value) {
add(property, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value));
@@ -136,6 +149,7 @@ public final class JsonObject extends JsonElement {
* Returns the number of key/value pairs in the object.
*
* @return the number of key/value pairs in the object.
+ * @since 2.7
*/
public int size() {
return members.size();
@@ -155,48 +169,79 @@ public final class JsonObject extends JsonElement {
* Returns the member with the specified name.
*
* @param memberName name of the member that is being requested.
- * @return the member matching the name. Null if no such member exists.
+ * @return the member matching the name, or {@code null} if no such member exists.
*/
public JsonElement get(String memberName) {
return members.get(memberName);
}
/**
- * Convenience method to get the specified member as a JsonPrimitive element.
+ * Convenience method to get the specified member as a {@link JsonPrimitive}.
*
* @param memberName name of the member being requested.
- * @return the JsonPrimitive corresponding to the specified member.
+ * @return the {@code JsonPrimitive} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code JsonPrimitive}.
*/
public JsonPrimitive getAsJsonPrimitive(String memberName) {
return (JsonPrimitive) members.get(memberName);
}
/**
- * Convenience method to get the specified member as a JsonArray.
+ * Convenience method to get the specified member as a {@link JsonArray}.
*
* @param memberName name of the member being requested.
- * @return the JsonArray corresponding to the specified member.
+ * @return the {@code JsonArray} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code JsonArray}.
*/
public JsonArray getAsJsonArray(String memberName) {
return (JsonArray) members.get(memberName);
}
/**
- * Convenience method to get the specified member as a JsonObject.
+ * Convenience method to get the specified member as a {@link JsonObject}.
*
* @param memberName name of the member being requested.
- * @return the JsonObject corresponding to the specified member.
+ * @return the {@code JsonObject} corresponding to the specified member, or {@code null} if no
+ * member with this name exists.
+ * @throws ClassCastException if the member is not of type {@code JsonObject}.
*/
public JsonObject getAsJsonObject(String memberName) {
return (JsonObject) members.get(memberName);
}
+ /**
+ * Returns a mutable {@link Map} view of this {@code JsonObject}. Changes to the {@code Map}
+ * are visible in this {@code JsonObject} and the other way around.
+ *
+ * <p>The {@code Map} does not permit {@code null} keys or values. Unlike {@code JsonObject}'s
+ * {@code null} handling, a {@link NullPointerException} is thrown when trying to add {@code null}.
+ * Use {@link JsonNull} for JSON null values.
+ *
+ * @return mutable {@code Map} view
+ * @since 2.10
+ */
+ public Map<String, JsonElement> asMap() {
+ // It is safe to expose the underlying map because it disallows null keys and values
+ return members;
+ }
+
+ /**
+ * Returns whether the other object is equal to this. This method only considers
+ * the other object to be equal if it is an instance of {@code JsonObject} and has
+ * equal members, ignoring order.
+ */
@Override
public boolean equals(Object o) {
return (o == this) || (o instanceof JsonObject
&& ((JsonObject) o).members.equals(members));
}
+ /**
+ * Returns the hash code of this object. This method calculates the hash code based
+ * on the members of this object, ignoring order.
+ */
@Override
public int hashCode() {
return members.hashCode();
diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java
index d3508c10..5b800420 100644
--- a/gson/src/main/java/com/google/gson/JsonParser.java
+++ b/gson/src/main/java/com/google/gson/JsonParser.java
@@ -45,6 +45,7 @@ public final class JsonParser {
* @param json JSON text
* @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
* @throws JsonParseException if the specified text is not valid JSON
+ * @since 2.8.6
*/
public static JsonElement parseString(String json) throws JsonSyntaxException {
return parseReader(new StringReader(json));
@@ -61,6 +62,7 @@ public final class JsonParser {
* @return a parse tree of {@link JsonElement}s corresponding to the specified JSON
* @throws JsonParseException if there is an IOException or if the specified
* text is not valid JSON
+ * @since 2.8.6
*/
public static JsonElement parseReader(Reader reader) throws JsonIOException, JsonSyntaxException {
try {
@@ -90,6 +92,7 @@ public final class JsonParser {
*
* @throws JsonParseException if there is an IOException or if the specified
* text is not valid JSON
+ * @since 2.8.6
*/
public static JsonElement parseReader(JsonReader reader)
throws JsonIOException, JsonSyntaxException {
diff --git a/gson/src/main/java/com/google/gson/JsonPrimitive.java b/gson/src/main/java/com/google/gson/JsonPrimitive.java
index 5e95d5a8..92a8df15 100644
--- a/gson/src/main/java/com/google/gson/JsonPrimitive.java
+++ b/gson/src/main/java/com/google/gson/JsonPrimitive.java
@@ -16,14 +16,13 @@
package com.google.gson;
-import com.google.gson.internal.$Gson$Preconditions;
+import com.google.gson.internal.LazilyParsedNumber;
import java.math.BigDecimal;
import java.math.BigInteger;
-
-import com.google.gson.internal.LazilyParsedNumber;
+import java.util.Objects;
/**
- * A class representing a Json primitive value. A primitive value
+ * A class representing a JSON primitive value. A primitive value
* is either a String, a Java primitive, or a Java primitive
* wrapper type.
*
@@ -39,8 +38,9 @@ public final class JsonPrimitive extends JsonElement {
*
* @param bool the value to create the primitive with.
*/
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonPrimitive(Boolean bool) {
- value = $Gson$Preconditions.checkNotNull(bool);
+ value = Objects.requireNonNull(bool);
}
/**
@@ -48,8 +48,9 @@ public final class JsonPrimitive extends JsonElement {
*
* @param number the value to create the primitive with.
*/
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonPrimitive(Number number) {
- value = $Gson$Preconditions.checkNotNull(number);
+ value = Objects.requireNonNull(number);
}
/**
@@ -57,24 +58,27 @@ public final class JsonPrimitive extends JsonElement {
*
* @param string the value to create the primitive with.
*/
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonPrimitive(String string) {
- value = $Gson$Preconditions.checkNotNull(string);
+ value = Objects.requireNonNull(string);
}
/**
* Create a primitive containing a character. The character is turned into a one character String
- * since Json only supports String.
+ * since JSON only supports String.
*
* @param c the value to create the primitive with.
*/
+ @SuppressWarnings("deprecation") // superclass constructor
public JsonPrimitive(Character c) {
// convert characters to strings since in JSON, characters are represented as a single
// character string
- value = $Gson$Preconditions.checkNotNull(c).toString();
+ value = Objects.requireNonNull(c).toString();
}
/**
* Returns the same value as primitives are immutable.
+ *
* @since 2.8.2
*/
@Override
@@ -92,9 +96,10 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a boolean value.
- *
- * @return get this element as a primitive boolean value.
+ * Convenience method to get this element as a boolean value.
+ * If this primitive {@linkplain #isBoolean() is not a boolean}, the string value
+ * is parsed using {@link Boolean#parseBoolean(String)}. This means {@code "true"} (ignoring
+ * case) is considered {@code true} and any other value is considered {@code false}.
*/
@Override
public boolean getAsBoolean() {
@@ -115,14 +120,21 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a Number.
+ * Convenience method to get this element as a {@link Number}.
+ * If this primitive {@linkplain #isString() is a string}, a lazily parsed {@code Number}
+ * is constructed which parses the string when any of its methods are called (which can
+ * lead to a {@link NumberFormatException}).
*
- * @return get this element as a Number.
- * @throws NumberFormatException if the value contained is not a valid Number.
+ * @throws UnsupportedOperationException if this primitive is neither a number nor a string.
*/
@Override
public Number getAsNumber() {
- return value instanceof String ? new LazilyParsedNumber((String) value) : (Number) value;
+ if (value instanceof Number) {
+ return (Number) value;
+ } else if (value instanceof String) {
+ return new LazilyParsedNumber((String) value);
+ }
+ throw new UnsupportedOperationException("Primitive is neither a number nor a string");
}
/**
@@ -134,27 +146,21 @@ public final class JsonPrimitive extends JsonElement {
return value instanceof String;
}
- /**
- * convenience method to get this element as a String.
- *
- * @return get this element as a String.
- */
+ // Don't add Javadoc, inherit it from super implementation; no exceptions are thrown here
@Override
public String getAsString() {
- if (isNumber()) {
+ if (value instanceof String) {
+ return (String) value;
+ } else if (isNumber()) {
return getAsNumber().toString();
} else if (isBoolean()) {
return ((Boolean) value).toString();
- } else {
- return (String) value;
}
+ throw new AssertionError("Unexpected value type: " + value.getClass());
}
/**
- * convenience method to get this element as a primitive double.
- *
- * @return get this element as a primitive double.
- * @throws NumberFormatException if the value contained is not a valid double.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public double getAsDouble() {
@@ -162,33 +168,24 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a {@link BigDecimal}.
- *
- * @return get this element as a {@link BigDecimal}.
- * @throws NumberFormatException if the value contained is not a valid {@link BigDecimal}.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public BigDecimal getAsBigDecimal() {
- return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(value.toString());
+ return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(getAsString());
}
/**
- * convenience method to get this element as a {@link BigInteger}.
- *
- * @return get this element as a {@link BigInteger}.
- * @throws NumberFormatException if the value contained is not a valid {@link BigInteger}.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public BigInteger getAsBigInteger() {
return value instanceof BigInteger ?
- (BigInteger) value : new BigInteger(value.toString());
+ (BigInteger) value : new BigInteger(getAsString());
}
/**
- * convenience method to get this element as a float.
- *
- * @return get this element as a float.
- * @throws NumberFormatException if the value contained is not a valid float.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public float getAsFloat() {
@@ -196,10 +193,10 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a primitive long.
+ * Convenience method to get this element as a primitive long.
*
- * @return get this element as a primitive long.
- * @throws NumberFormatException if the value contained is not a valid long.
+ * @return this element as a primitive long.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public long getAsLong() {
@@ -207,10 +204,7 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a primitive short.
- *
- * @return get this element as a primitive short.
- * @throws NumberFormatException if the value contained is not a valid short value.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public short getAsShort() {
@@ -218,26 +212,41 @@ public final class JsonPrimitive extends JsonElement {
}
/**
- * convenience method to get this element as a primitive integer.
- *
- * @return get this element as a primitive integer.
- * @throws NumberFormatException if the value contained is not a valid integer.
+ * @throws NumberFormatException {@inheritDoc}
*/
@Override
public int getAsInt() {
return isNumber() ? getAsNumber().intValue() : Integer.parseInt(getAsString());
}
+ /**
+ * @throws NumberFormatException {@inheritDoc}
+ */
@Override
public byte getAsByte() {
return isNumber() ? getAsNumber().byteValue() : Byte.parseByte(getAsString());
}
+ /**
+ * @throws UnsupportedOperationException if the string value of this
+ * primitive is empty.
+ * @deprecated This method is misleading, as it does not get this element as a char but rather as
+ * a string's first character.
+ */
+ @Deprecated
@Override
public char getAsCharacter() {
- return getAsString().charAt(0);
+ String s = getAsString();
+ if (s.isEmpty()) {
+ throw new UnsupportedOperationException("String value is empty");
+ } else {
+ return s.charAt(0);
+ }
}
+ /**
+ * Returns the hash code of this object.
+ */
@Override
public int hashCode() {
if (value == null) {
@@ -255,6 +264,11 @@ public final class JsonPrimitive extends JsonElement {
return value.hashCode();
}
+ /**
+ * Returns whether the other object is equal to this. This method only considers
+ * the other object to be equal if it is an instance of {@code JsonPrimitive} and
+ * has an equal value.
+ */
@Override
public boolean equals(Object obj) {
if (this == obj) {
diff --git a/gson/src/main/java/com/google/gson/JsonSerializer.java b/gson/src/main/java/com/google/gson/JsonSerializer.java
index 19eaf17d..2bdb8fb2 100644
--- a/gson/src/main/java/com/google/gson/JsonSerializer.java
+++ b/gson/src/main/java/com/google/gson/JsonSerializer.java
@@ -19,7 +19,7 @@ package com.google.gson;
import java.lang.reflect.Type;
/**
- * Interface representing a custom serializer for Json. You should write a custom serializer, if
+ * Interface representing a custom serializer for JSON. You should write a custom serializer, if
* you are not happy with the default serialization done by Gson. You will also need to register
* this serializer through {@link com.google.gson.GsonBuilder#registerTypeAdapter(Type, Object)}.
*
@@ -43,12 +43,12 @@ import java.lang.reflect.Type;
* </pre>
*
* <p>The default serialization of {@code Id(com.foo.MyObject.class, 20L)} will be
- * <code>{"clazz":com.foo.MyObject,"value":20}</code>. Suppose, you just want the output to be
+ * <code>{"clazz":"com.foo.MyObject","value":20}</code>. Suppose, you just want the output to be
* the value instead, which is {@code 20} in this case. You can achieve that by writing a custom
* serializer:</p>
*
* <pre>
- * class IdSerializer implements JsonSerializer&lt;Id&gt;() {
+ * class IdSerializer implements JsonSerializer&lt;Id&gt; {
* public JsonElement serialize(Id id, Type typeOfId, JsonSerializationContext context) {
* return new JsonPrimitive(id.getValue());
* }
diff --git a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
index b787ae89..254d2e5d 100644
--- a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
+++ b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
@@ -1,8 +1,7 @@
package com.google.gson;
-import java.lang.reflect.AccessibleObject;
-
import com.google.gson.internal.ReflectionAccessFilterHelper;
+import java.lang.reflect.AccessibleObject;
/**
* Filter for determining whether reflection based serialization and
@@ -28,10 +27,13 @@ import com.google.gson.internal.ReflectionAccessFilterHelper;
* fields and classes.
*
* @see GsonBuilder#addReflectionAccessFilter(ReflectionAccessFilter)
+ * @since 2.9.1
*/
public interface ReflectionAccessFilter {
/**
* Result of a filter check.
+ *
+ * @since 2.9.1
*/
enum FilterResult {
/**
diff --git a/gson/src/main/java/com/google/gson/ToNumberPolicy.java b/gson/src/main/java/com/google/gson/ToNumberPolicy.java
index bd70213b..86892984 100644
--- a/gson/src/main/java/com/google/gson/ToNumberPolicy.java
+++ b/gson/src/main/java/com/google/gson/ToNumberPolicy.java
@@ -16,12 +16,11 @@
package com.google.gson;
-import java.io.IOException;
-import java.math.BigDecimal;
-
import com.google.gson.internal.LazilyParsedNumber;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.MalformedJsonException;
+import java.io.IOException;
+import java.math.BigDecimal;
/**
* An enumeration that defines two standard number reading strategies and a couple of
@@ -29,6 +28,7 @@ import com.google.gson.stream.MalformedJsonException;
* {@link Object} and {@link Number}.
*
* @see ToNumberStrategy
+ * @since 2.8.9
*/
public enum ToNumberPolicy implements ToNumberStrategy {
diff --git a/gson/src/main/java/com/google/gson/ToNumberStrategy.java b/gson/src/main/java/com/google/gson/ToNumberStrategy.java
index 3cd84fa5..ae742219 100644
--- a/gson/src/main/java/com/google/gson/ToNumberStrategy.java
+++ b/gson/src/main/java/com/google/gson/ToNumberStrategy.java
@@ -16,9 +16,8 @@
package com.google.gson;
-import java.io.IOException;
-
import com.google.gson.stream.JsonReader;
+import java.io.IOException;
/**
* A strategy that is used to control how numbers should be deserialized for {@link Object} and {@link Number}
@@ -56,6 +55,7 @@ import com.google.gson.stream.JsonReader;
* @see ToNumberPolicy
* @see GsonBuilder#setObjectToNumberStrategy(ToNumberStrategy)
* @see GsonBuilder#setNumberToNumberStrategy(ToNumberStrategy)
+ * @since 2.8.9
*/
public interface ToNumberStrategy {
diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java
index ba798537..98e1668a 100644
--- a/gson/src/main/java/com/google/gson/TypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/TypeAdapter.java
@@ -30,7 +30,7 @@ import java.io.Writer;
/**
* Converts Java objects to and from JSON.
*
- * <h3>Defining a type's JSON form</h3>
+ * <h2>Defining a type's JSON form</h2>
* By default Gson converts application classes to JSON using its built-in type
* adapters. If Gson's default JSON conversion isn't appropriate for a type,
* extend this class to customize the conversion. Here's an example of a type
@@ -96,7 +96,7 @@ import java.io.Writer;
*/
// non-Javadoc:
//
-// <h3>JSON Conversion</h3>
+// <h2>JSON Conversion</h2>
// <p>A type adapter registered with Gson is automatically invoked while serializing
// or deserializing JSON. However, you can also use type adapters directly to serialize
// and deserialize JSON. Here is an example for deserialization: <pre> {@code
@@ -118,6 +118,9 @@ import java.io.Writer;
//
public abstract class TypeAdapter<T> {
+ public TypeAdapter() {
+ }
+
/**
* Writes one JSON value (an array, object, string, number, boolean or null)
* for {@code value}.
@@ -131,8 +134,7 @@ public abstract class TypeAdapter<T> {
* Unlike Gson's similar {@link Gson#toJson(JsonElement, Appendable) toJson}
* method, this write is strict. Create a {@link
* JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call
- * {@link #write(com.google.gson.stream.JsonWriter, Object)} for lenient
- * writing.
+ * {@link #write(JsonWriter, Object)} for lenient writing.
*
* @param value the Java object to convert. May be null.
* @since 2.2
@@ -205,9 +207,9 @@ public abstract class TypeAdapter<T> {
* Converts {@code value} to a JSON document. Unlike Gson's similar {@link
* Gson#toJson(Object) toJson} method, this write is strict. Create a {@link
* JsonWriter#setLenient(boolean) lenient} {@code JsonWriter} and call
- * {@link #write(com.google.gson.stream.JsonWriter, Object)} for lenient
- * writing.
+ * {@link #write(JsonWriter, Object)} for lenient writing.
*
+ * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #write(JsonWriter, Object)}
* @param value the Java object to convert. May be null.
* @since 2.2
*/
@@ -216,7 +218,7 @@ public abstract class TypeAdapter<T> {
try {
toJson(stringWriter, value);
} catch (IOException e) {
- throw new AssertionError(e); // No I/O writing to a StringWriter.
+ throw new JsonIOException(e);
}
return stringWriter.toString();
}
@@ -226,6 +228,7 @@ public abstract class TypeAdapter<T> {
*
* @param value the Java object to convert. May be null.
* @return the converted JSON tree. May be {@link JsonNull}.
+ * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #write(JsonWriter, Object)}
* @since 2.2
*/
public final JsonElement toJsonTree(T value) {
@@ -248,7 +251,7 @@ public abstract class TypeAdapter<T> {
/**
* Converts the JSON document in {@code in} to a Java object. Unlike Gson's
- * similar {@link Gson#fromJson(java.io.Reader, Class) fromJson} method, this
+ * similar {@link Gson#fromJson(Reader, Class) fromJson} method, this
* read is strict. Create a {@link JsonReader#setLenient(boolean) lenient}
* {@code JsonReader} and call {@link #read(JsonReader)} for lenient reading.
*
@@ -284,6 +287,7 @@ public abstract class TypeAdapter<T> {
*
* @param jsonTree the JSON element to convert. May be {@link JsonNull}.
* @return the converted Java object. May be null.
+ * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #read(JsonReader)}
* @since 2.2
*/
public final T fromJsonTree(JsonElement jsonTree) {
diff --git a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
index c12429e9..75fdddbf 100644
--- a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
+++ b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
@@ -22,6 +22,7 @@ import com.google.gson.reflect.TypeToken;
* Creates type adapters for set of related types. Type adapter factories are
* most useful when several types share similar structure in their JSON form.
*
+ * <h2>Examples</h2>
* <h3>Example: Converting enums to lowercase</h3>
* In this example, we implement a factory that creates type adapters for all
* enums. The type adapters will write enums in lowercase, despite the fact
@@ -90,7 +91,7 @@ import com.google.gson.reflect.TypeToken;
* If multiple factories support the same type, the factory registered earlier
* takes precedence.
*
- * <h3>Example: composing other type adapters</h3>
+ * <h3>Example: Composing other type adapters</h3>
* In this example we implement a factory for Guava's {@code Multiset}
* collection type. The factory can be used to create type adapters for
* multisets of any element type: the type adapter for {@code
diff --git a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
index a01d77a6..d1685759 100644
--- a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
+++ b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
@@ -16,6 +16,7 @@
package com.google.gson.annotations;
+import com.google.gson.Gson;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
@@ -60,7 +61,7 @@ import java.lang.annotation.Target;
* </pre>
*
* Since User class specified UserJsonAdapter.class in &#64;JsonAdapter annotation, it
- * will automatically be invoked to serialize/deserialize User instances. <br>
+ * will automatically be invoked to serialize/deserialize User instances.
*
* <p> Here is an example of how to apply this annotation to a field.
* <pre>
@@ -80,9 +81,14 @@ import java.lang.annotation.Target;
*
* <p>The class referenced by this annotation must be either a {@link
* TypeAdapter} or a {@link TypeAdapterFactory}, or must implement one
- * or both of {@link JsonDeserializer} or {@link JsonSerializer}.
- * Using {@link TypeAdapterFactory} makes it possible to delegate
- * to the enclosing {@code Gson} instance.
+ * or both of {@link JsonDeserializer} or {@link JsonSerializer}.
+ * Using {@link TypeAdapterFactory} makes it possible to delegate
+ * to the enclosing {@link Gson} instance.
+ *
+ * <p>{@code Gson} instances might cache the adapter they create for
+ * a {@code @JsonAdapter} annotation. It is not guaranteed that a new
+ * adapter is created every time the annotated class or field is serialized
+ * or deserialized.
*
* @since 2.3
*
diff --git a/gson/src/main/java/com/google/gson/annotations/Since.java b/gson/src/main/java/com/google/gson/annotations/Since.java
index e23b6ec9..a7e51fc1 100644
--- a/gson/src/main/java/com/google/gson/annotations/Since.java
+++ b/gson/src/main/java/com/google/gson/annotations/Since.java
@@ -16,6 +16,7 @@
package com.google.gson.annotations;
+import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -24,12 +25,11 @@ import java.lang.annotation.Target;
/**
* An annotation that indicates the version number since a member or a type has been present.
- * This annotation is useful to manage versioning of your Json classes for a web-service.
+ * This annotation is useful to manage versioning of your JSON classes for a web-service.
*
* <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
- * {@link com.google.gson.GsonBuilder} and invoke
- * {@link com.google.gson.GsonBuilder#setVersion(double)} method.
+ * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
@@ -50,14 +50,16 @@ import java.lang.annotation.Target;
*
* @author Inderjeet Singh
* @author Joel Leitch
+ * @see GsonBuilder#setVersion(double)
+ * @see Until
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Since {
/**
- * the value indicating a version number since this member
- * or type has been present.
+ * The value indicating a version number since this member or type has been present.
+ * The number is inclusive; annotated elements will be included if {@code gsonVersion >= value}.
*/
double value();
}
diff --git a/gson/src/main/java/com/google/gson/annotations/Until.java b/gson/src/main/java/com/google/gson/annotations/Until.java
index 7c61d104..a5fcabd4 100644
--- a/gson/src/main/java/com/google/gson/annotations/Until.java
+++ b/gson/src/main/java/com/google/gson/annotations/Until.java
@@ -16,6 +16,7 @@
package com.google.gson.annotations;
+import com.google.gson.GsonBuilder;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -24,14 +25,13 @@ import java.lang.annotation.Target;
/**
* An annotation that indicates the version number until a member or a type should be present.
- * Basically, if Gson is created with a version number that exceeds the value stored in the
- * {@code Until} annotation then the field will be ignored from the JSON output. This annotation
- * is useful to manage versioning of your JSON classes for a web-service.
+ * Basically, if Gson is created with a version number that is equal to or exceeds the value
+ * stored in the {@code Until} annotation then the field will be ignored from the JSON output.
+ * This annotation is useful to manage versioning of your JSON classes for a web-service.
*
* <p>
* This annotation has no effect unless you build {@link com.google.gson.Gson} with a
- * {@link com.google.gson.GsonBuilder} and invoke
- * {@link com.google.gson.GsonBuilder#setVersion(double)} method.
+ * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
*
* <p>Here is an example of how this annotation is meant to be used:</p>
* <pre>
@@ -47,12 +47,14 @@ import java.lang.annotation.Target;
* methods will use all the fields for serialization and deserialization. However, if you created
* Gson with {@code Gson gson = new GsonBuilder().setVersion(1.2).create()} then the
* {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code emailAddress}
- * and {@code password} fields from the example above, because the version number passed to the
+ * and {@code password} fields from the example above, because the version number passed to the
* GsonBuilder, {@code 1.2}, exceeds the version number set on the {@code Until} annotation,
* {@code 1.1}, for those fields.
*
* @author Inderjeet Singh
* @author Joel Leitch
+ * @see GsonBuilder#setVersion(double)
+ * @see Since
* @since 1.3
*/
@Documented
@@ -61,8 +63,8 @@ import java.lang.annotation.Target;
public @interface Until {
/**
- * the value indicating a version number until this member
- * or type should be ignored.
+ * The value indicating a version number until this member or type should be be included.
+ * The number is exclusive; annotated elements will be included if {@code gsonVersion < value}.
*/
double value();
}
diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java b/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java
index f0e7d3fa..f1056efc 100644
--- a/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java
+++ b/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java
@@ -16,6 +16,8 @@
package com.google.gson.internal;
+import java.util.Objects;
+
/**
* A simple utility class used to check method Preconditions.
*
@@ -34,6 +36,12 @@ public final class $Gson$Preconditions {
throw new UnsupportedOperationException();
}
+ /**
+ * @deprecated
+ * This is an internal Gson method. Use {@link Objects#requireNonNull(Object)} instead.
+ */
+ // Only deprecated for now because external projects might be using this by accident
+ @Deprecated
public static <T> T checkNotNull(T obj) {
if (obj == null) {
throw new NullPointerException();
diff --git a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java
index 9891154a..965e010f 100644
--- a/gson/src/main/java/com/google/gson/internal/$Gson$Types.java
+++ b/gson/src/main/java/com/google/gson/internal/$Gson$Types.java
@@ -16,6 +16,9 @@
package com.google.gson.internal;
+import static com.google.gson.internal.$Gson$Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
@@ -32,9 +35,6 @@ import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
-import static com.google.gson.internal.$Gson$Preconditions.checkArgument;
-import static com.google.gson.internal.$Gson$Preconditions.checkNotNull;
-
/**
* Static methods for working with types.
*
@@ -486,6 +486,7 @@ public final class $Gson$Types {
private final Type[] typeArguments;
public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
+ requireNonNull(rawType);
// require an owner type if the raw type needs it
if (rawType instanceof Class<?>) {
Class<?> rawTypeAsClass = (Class<?>) rawType;
@@ -498,7 +499,7 @@ public final class $Gson$Types {
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0, length = this.typeArguments.length; t < length; t++) {
- checkNotNull(this.typeArguments[t]);
+ requireNonNull(this.typeArguments[t]);
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
@@ -552,6 +553,7 @@ public final class $Gson$Types {
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
+ requireNonNull(componentType);
this.componentType = canonicalize(componentType);
}
@@ -590,14 +592,14 @@ public final class $Gson$Types {
checkArgument(upperBounds.length == 1);
if (lowerBounds.length == 1) {
- checkNotNull(lowerBounds[0]);
+ requireNonNull(lowerBounds[0]);
checkNotPrimitive(lowerBounds[0]);
checkArgument(upperBounds[0] == Object.class);
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
- checkNotNull(upperBounds[0]);
+ requireNonNull(upperBounds[0]);
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
index 68b2bd64..115a2a09 100644
--- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
+++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
@@ -61,6 +61,25 @@ public final class ConstructorConstructor {
this.reflectionFilters = reflectionFilters;
}
+ /**
+ * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers
+ * return an exception message.
+ * @param c instance of the class to be checked
+ * @return if instantiable {@code null}, else a non-{@code null} exception message
+ */
+ static String checkInstantiable(Class<?> c) {
+ int modifiers = c.getModifiers();
+ if (Modifier.isInterface(modifiers)) {
+ return "Interfaces can't be instantiated! Register an InstanceCreator "
+ + "or a TypeAdapter for this type. Interface name: " + c.getName();
+ }
+ if (Modifier.isAbstract(modifiers)) {
+ return "Abstract classes can't be instantiated! Register an InstanceCreator "
+ + "or a TypeAdapter for this type. Class name: " + c.getName();
+ }
+ return null;
+ }
+
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();
@@ -110,7 +129,7 @@ public final class ConstructorConstructor {
// Check whether type is instantiable; otherwise ReflectionAccessFilter recommendation
// of adjusting filter suggested below is irrelevant since it would not solve the problem
- final String exceptionMessage = UnsafeAllocator.checkInstantiable(rawType);
+ final String exceptionMessage = checkInstantiable(rawType);
if (exceptionMessage != null) {
return new ObjectConstructor<T>() {
@Override public T construct() {
@@ -242,14 +261,17 @@ public final class ConstructorConstructor {
@SuppressWarnings("unchecked") // T is the same raw type as is requested
T newInstance = (T) constructor.newInstance();
return newInstance;
- } catch (InstantiationException e) {
- // TODO: JsonParseException ?
- throw new RuntimeException("Failed to invoke " + constructor + " with no args", e);
+ }
+ // Note: InstantiationException should be impossible because check at start of method made sure
+ // that class is not abstract
+ catch (InstantiationException e) {
+ throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with no args", e);
} catch (InvocationTargetException e) {
- // TODO: don't wrap if cause is unchecked!
+ // TODO: don't wrap if cause is unchecked?
// TODO: JsonParseException ?
- throw new RuntimeException("Failed to invoke " + constructor + " with no args",
- e.getTargetException());
+ throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with no args", e.getCause());
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
@@ -342,11 +364,10 @@ public final class ConstructorConstructor {
private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
if (useJdkUnsafe) {
return new ObjectConstructor<T>() {
- private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
@Override public T construct() {
try {
@SuppressWarnings("unchecked")
- T newInstance = (T) unsafeAllocator.newInstance(rawType);
+ T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(("Unable to create instance of " + rawType + ". "
diff --git a/gson/src/main/java/com/google/gson/internal/Excluder.java b/gson/src/main/java/com/google/gson/internal/Excluder.java
index 8d8a25f4..03bd45cb 100644
--- a/gson/src/main/java/com/google/gson/internal/Excluder.java
+++ b/gson/src/main/java/com/google/gson/internal/Excluder.java
@@ -240,9 +240,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
private boolean isValidSince(Since annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
- if (annotationVersion > version) {
- return false;
- }
+ return version >= annotationVersion;
}
return true;
}
@@ -250,9 +248,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
private boolean isValidUntil(Until annotation) {
if (annotation != null) {
double annotationVersion = annotation.value();
- if (annotationVersion <= version) {
- return false;
- }
+ return version < annotationVersion;
}
return true;
}
diff --git a/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java b/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java
index 6138dfff..abc4b2a4 100644
--- a/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java
+++ b/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java
@@ -26,6 +26,7 @@ import java.math.BigDecimal;
*
* @author Inderjeet Singh
*/
+@SuppressWarnings("serial") // ignore warning about missing serialVersionUID
public final class LazilyParsedNumber extends Number {
private final String value;
diff --git a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java
index 40eb8bb1..e47e165d 100644
--- a/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java
+++ b/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java
@@ -38,6 +38,7 @@ import java.util.Set;
*
* <p>This implementation was derived from Android 4.1's TreeMap class.
*/
+@SuppressWarnings("serial") // ignore warning about missing serialVersionUID
public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Serializable {
@SuppressWarnings({ "unchecked", "rawtypes" }) // to avoid Comparable<Comparable<Comparable<...>>>
private static final Comparator<Comparable> NATURAL_ORDER = new Comparator<Comparable>() {
@@ -46,21 +47,33 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
}
};
- Comparator<? super K> comparator;
+ private final Comparator<? super K> comparator;
+ private final boolean allowNullValues;
Node<K, V> root;
int size = 0;
int modCount = 0;
// Used to preserve iteration order
- final Node<K, V> header = new Node<>();
+ final Node<K, V> header;
/**
* Create a natural order, empty tree map whose keys must be mutually
- * comparable and non-null.
+ * comparable and non-null, and whose values can be {@code null}.
*/
@SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
public LinkedTreeMap() {
- this((Comparator<? super K>) NATURAL_ORDER);
+ this((Comparator<? super K>) NATURAL_ORDER, true);
+ }
+
+ /**
+ * Create a natural order, empty tree map whose keys must be mutually
+ * comparable and non-null.
+ *
+ * @param allowNullValues whether {@code null} is allowed as entry value
+ */
+ @SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
+ public LinkedTreeMap(boolean allowNullValues) {
+ this((Comparator<? super K>) NATURAL_ORDER, allowNullValues);
}
/**
@@ -69,12 +82,15 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
*
* @param comparator the comparator to order elements with, or {@code null} to
* use the natural ordering.
+ * @param allowNullValues whether {@code null} is allowed as entry value
*/
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
- public LinkedTreeMap(Comparator<? super K> comparator) {
+ public LinkedTreeMap(Comparator<? super K> comparator, boolean allowNullValues) {
this.comparator = comparator != null
? comparator
: (Comparator) NATURAL_ORDER;
+ this.allowNullValues = allowNullValues;
+ this.header = new Node<>(allowNullValues);
}
@Override public int size() {
@@ -94,6 +110,9 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
if (key == null) {
throw new NullPointerException("key == null");
}
+ if (value == null && !allowNullValues) {
+ throw new NullPointerException("value == null");
+ }
Node<K, V> created = find(key, true);
V result = created.value;
created.value = value;
@@ -166,10 +185,10 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) {
throw new ClassCastException(key.getClass().getName() + " is not Comparable");
}
- created = new Node<>(nearest, key, header, header.prev);
+ created = new Node<>(allowNullValues, nearest, key, header, header.prev);
root = created;
} else {
- created = new Node<>(nearest, key, header, header.prev);
+ created = new Node<>(allowNullValues, nearest, key, header, header.prev);
if (comparison < 0) { // nearest.key is higher
nearest.left = created;
} else { // comparison > 0, nearest.key is lower
@@ -446,19 +465,22 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
Node<K, V> next;
Node<K, V> prev;
final K key;
+ final boolean allowNullValue;
V value;
int height;
/** Create the header entry */
- Node() {
+ Node(boolean allowNullValue) {
key = null;
+ this.allowNullValue = allowNullValue;
next = prev = this;
}
/** Create a regular entry */
- Node(Node<K, V> parent, K key, Node<K, V> next, Node<K, V> prev) {
+ Node(boolean allowNullValue, Node<K, V> parent, K key, Node<K, V> next, Node<K, V> prev) {
this.parent = parent;
this.key = key;
+ this.allowNullValue = allowNullValue;
this.height = 1;
this.next = next;
this.prev = prev;
@@ -475,15 +497,17 @@ public final class LinkedTreeMap<K, V> extends AbstractMap<K, V> implements Seri
}
@Override public V setValue(V value) {
+ if (value == null && !allowNullValue) {
+ throw new NullPointerException("value == null");
+ }
V oldValue = this.value;
this.value = value;
return oldValue;
}
- @SuppressWarnings("rawtypes")
@Override public boolean equals(Object o) {
if (o instanceof Entry) {
- Entry other = (Entry) o;
+ Entry<?, ?> other = (Entry<?, ?>) o;
return (key == null ? other.getKey() == null : key.equals(other.getKey()))
&& (value == null ? other.getValue() == null : value.equals(other.getValue()));
}
diff --git a/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
new file mode 100644
index 00000000..b3017430
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
@@ -0,0 +1,98 @@
+package com.google.gson.internal;
+
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.RandomAccess;
+
+/**
+ * {@link List} which wraps another {@code List} but prevents insertion of
+ * {@code null} elements. Methods which only perform checks with the element
+ * argument (e.g. {@link #contains(Object)}) do not throw exceptions for
+ * {@code null} arguments.
+ */
+public class NonNullElementWrapperList<E> extends AbstractList<E> implements RandomAccess {
+ // Explicitly specify ArrayList as type to guarantee that delegate implements RandomAccess
+ private final ArrayList<E> delegate;
+
+ public NonNullElementWrapperList(ArrayList<E> delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
+ }
+
+ @Override public E get(int index) {
+ return delegate.get(index);
+ }
+
+ @Override public int size() {
+ return delegate.size();
+ }
+
+ private E nonNull(E element) {
+ if (element == null) {
+ throw new NullPointerException("Element must be non-null");
+ }
+ return element;
+ }
+
+ @Override public E set(int index, E element) {
+ return delegate.set(index, nonNull(element));
+ }
+
+ @Override public void add(int index, E element) {
+ delegate.add(index, nonNull(element));
+ }
+
+ @Override public E remove(int index) {
+ return delegate.remove(index);
+ }
+
+ /* The following methods are overridden because their default implementation is inefficient */
+
+ @Override public void clear() {
+ delegate.clear();
+ }
+
+ @Override public boolean remove(Object o) {
+ return delegate.remove(o);
+ }
+
+ @Override public boolean removeAll(Collection<?> c) {
+ return delegate.removeAll(c);
+ }
+
+ @Override public boolean retainAll(Collection<?> c) {
+ return delegate.retainAll(c);
+ }
+
+ @Override public boolean contains(Object o) {
+ return delegate.contains(o);
+ }
+
+ @Override public int indexOf(Object o) {
+ return delegate.indexOf(o);
+ }
+
+ @Override public int lastIndexOf(Object o) {
+ return delegate.lastIndexOf(o);
+ }
+
+ @Override public Object[] toArray() {
+ return delegate.toArray();
+ }
+
+ @Override public <T> T[] toArray(T[] a) {
+ return delegate.toArray(a);
+ }
+
+ @Override public boolean equals(Object o) {
+ return delegate.equals(o);
+ }
+
+ @Override public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ // TODO: Once Gson targets Java 8 also override List.sort
+}
diff --git a/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/src/main/java/com/google/gson/internal/Streams.java
index 0bb73aa1..eafbbbec 100644
--- a/gson/src/main/java/com/google/gson/internal/Streams.java
+++ b/gson/src/main/java/com/google/gson/internal/Streams.java
@@ -28,6 +28,7 @@ import com.google.gson.stream.MalformedJsonException;
import java.io.EOFException;
import java.io.IOException;
import java.io.Writer;
+import java.util.Objects;
/**
* Reads and writes GSON parse trees over streams.
@@ -89,22 +90,48 @@ public final class Streams {
}
@Override public void write(char[] chars, int offset, int length) throws IOException {
- currentWrite.chars = chars;
+ currentWrite.setChars(chars);
appendable.append(currentWrite, offset, offset + length);
}
+ @Override public void flush() {}
+ @Override public void close() {}
+
+ // Override these methods for better performance
+ // They would otherwise unnecessarily create Strings or char arrays
+
@Override public void write(int i) throws IOException {
appendable.append((char) i);
}
- @Override public void flush() {}
- @Override public void close() {}
+ @Override public void write(String str, int off, int len) throws IOException {
+ // Appendable.append turns null -> "null", which is not desired here
+ Objects.requireNonNull(str);
+ appendable.append(str, off, off + len);
+ }
+
+ @Override public Writer append(CharSequence csq) throws IOException {
+ appendable.append(csq);
+ return this;
+ }
+
+ @Override public Writer append(CharSequence csq, int start, int end) throws IOException {
+ appendable.append(csq, start, end);
+ return this;
+ }
/**
* A mutable char sequence pointing at a single char[].
*/
- static class CurrentWrite implements CharSequence {
- char[] chars;
+ private static class CurrentWrite implements CharSequence {
+ private char[] chars;
+ private String cachedString;
+
+ void setChars(char[] chars) {
+ this.chars = chars;
+ this.cachedString = null;
+ }
+
@Override public int length() {
return chars.length;
}
@@ -114,7 +141,14 @@ public final class Streams {
@Override public CharSequence subSequence(int start, int end) {
return new String(chars, start, end - start);
}
+
+ // Must return string representation to satisfy toString() contract
+ @Override public String toString() {
+ if (cachedString == null) {
+ cachedString = new String(chars);
+ }
+ return cachedString;
+ }
}
}
-
}
diff --git a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
index 429bac6b..fae6f802 100644
--- a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
+++ b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
@@ -20,7 +20,6 @@ import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
/**
* Do sneaky things to allocate objects without invoking their constructors.
@@ -32,37 +31,20 @@ public abstract class UnsafeAllocator {
public abstract <T> T newInstance(Class<T> c) throws Exception;
/**
- * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers
- * return an exception message.
- * @param c instance of the class to be checked
- * @return if instantiable {@code null}, else a non-{@code null} exception message
- */
- static String checkInstantiable(Class<?> c) {
- int modifiers = c.getModifiers();
- if (Modifier.isInterface(modifiers)) {
- return "Interfaces can't be instantiated! Register an InstanceCreator "
- + "or a TypeAdapter for this type. Interface name: " + c.getName();
- }
- if (Modifier.isAbstract(modifiers)) {
- return "Abstract classes can't be instantiated! Register an InstanceCreator "
- + "or a TypeAdapter for this type. Class name: " + c.getName();
- }
- return null;
- }
-
- /**
* Asserts that the class is instantiable. This check should have already occurred
* in {@link ConstructorConstructor}; this check here acts as safeguard since trying
* to use Unsafe for non-instantiable classes might crash the JVM on some devices.
*/
private static void assertInstantiable(Class<?> c) {
- String exceptionMessage = checkInstantiable(c);
+ String exceptionMessage = ConstructorConstructor.checkInstantiable(c);
if (exceptionMessage != null) {
throw new AssertionError("UnsafeAllocator is used for non-instantiable type: " + exceptionMessage);
}
}
- public static UnsafeAllocator create() {
+ public static final UnsafeAllocator INSTANCE = create();
+
+ private static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
diff --git a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java
index efaa834f..23648e56 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java
@@ -16,13 +16,6 @@
package com.google.gson.internal.bind;
-import java.io.IOException;
-import java.lang.reflect.Array;
-import java.lang.reflect.GenericArrayType;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.List;
-
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
@@ -31,13 +24,17 @@ import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
/**
* Adapt an array of objects.
*/
public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
- @SuppressWarnings({"unchecked", "rawtypes"})
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Type type = typeToken.getType();
if (!(type instanceof GenericArrayType || type instanceof Class && ((Class<?>) type).isArray())) {
@@ -46,8 +43,11 @@ public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
Type componentType = $Gson$Types.getArrayComponentType(type);
TypeAdapter<?> componentTypeAdapter = gson.getAdapter(TypeToken.get(componentType));
- return new ArrayTypeAdapter(
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ TypeAdapter<T> arrayAdapter = new ArrayTypeAdapter(
gson, componentTypeAdapter, $Gson$Types.getRawType(componentType));
+ return arrayAdapter;
}
};
@@ -66,7 +66,7 @@ public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
return null;
}
- List<E> list = new ArrayList<>();
+ ArrayList<E> list = new ArrayList<>();
in.beginArray();
while (in.hasNext()) {
E instance = componentTypeAdapter.read(in);
@@ -75,14 +75,22 @@ public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
in.endArray();
int size = list.size();
- Object array = Array.newInstance(componentType, size);
- for (int i = 0; i < size; i++) {
- Array.set(array, i, list.get(i));
+ // Have to copy primitives one by one to primitive array
+ if (componentType.isPrimitive()) {
+ Object array = Array.newInstance(componentType, size);
+ for (int i = 0; i < size; i++) {
+ Array.set(array, i, list.get(i));
+ }
+ return array;
+ }
+ // But for Object[] can use ArrayList.toArray
+ else {
+ @SuppressWarnings("unchecked")
+ E[] array = (E[]) Array.newInstance(componentType, size);
+ return list.toArray(array);
}
- return array;
}
- @SuppressWarnings("unchecked")
@Override public void write(JsonWriter out, Object array) throws IOException {
if (array == null) {
out.nullValue();
@@ -91,6 +99,7 @@ public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
out.beginArray();
for (int i = 0, length = Array.getLength(array); i < length; i++) {
+ @SuppressWarnings("unchecked")
E value = (E) Array.get(array, i);
componentTypeAdapter.write(out, value);
}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
index 0b7435f2..4719ea15 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
@@ -16,26 +16,25 @@
package com.google.gson.internal.bind;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.ParsePosition;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
-import com.google.gson.internal.$Gson$Preconditions;
import com.google.gson.internal.JavaVersion;
import com.google.gson.internal.PreJava9DateFormatProvider;
import com.google.gson.internal.bind.util.ISO8601Utils;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
/**
* This type adapter supports subclasses of date by defining a
@@ -93,7 +92,7 @@ public final class DefaultDateTypeAdapter<T extends Date> extends TypeAdapter<T>
private final List<DateFormat> dateFormats = new ArrayList<>();
private DefaultDateTypeAdapter(DateType<T> dateType, String datePattern) {
- this.dateType = $Gson$Preconditions.checkNotNull(dateType);
+ this.dateType = Objects.requireNonNull(dateType);
dateFormats.add(new SimpleDateFormat(datePattern, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
dateFormats.add(new SimpleDateFormat(datePattern));
@@ -101,7 +100,7 @@ public final class DefaultDateTypeAdapter<T extends Date> extends TypeAdapter<T>
}
private DefaultDateTypeAdapter(DateType<T> dateType, int style) {
- this.dateType = $Gson$Preconditions.checkNotNull(dateType);
+ this.dateType = Objects.requireNonNull(dateType);
dateFormats.add(DateFormat.getDateInstance(style, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
dateFormats.add(DateFormat.getDateInstance(style));
@@ -112,7 +111,7 @@ public final class DefaultDateTypeAdapter<T extends Date> extends TypeAdapter<T>
}
private DefaultDateTypeAdapter(DateType<T> dateType, int dateStyle, int timeStyle) {
- this.dateType = $Gson$Preconditions.checkNotNull(dateType);
+ this.dateType = Objects.requireNonNull(dateType);
dateFormats.add(DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US));
if (!Locale.getDefault().equals(Locale.US)) {
dateFormats.add(DateFormat.getDateTimeInstance(dateStyle, timeStyle));
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
index 13a7bb7e..643c5190 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
@@ -38,7 +38,7 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
this.constructorConstructor = constructorConstructor;
}
- @SuppressWarnings("unchecked")
+ @SuppressWarnings("unchecked") // this is not safe; requires that user has specified correct adapter class for @JsonAdapter
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) {
Class<? super T> rawType = targetType.getRawType();
@@ -49,24 +49,29 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
return (TypeAdapter<T>) getTypeAdapter(constructorConstructor, gson, targetType, annotation);
}
- @SuppressWarnings({ "unchecked", "rawtypes" }) // Casts guarded by conditionals.
TypeAdapter<?> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson,
TypeToken<?> type, JsonAdapter annotation) {
Object instance = constructorConstructor.get(TypeToken.get(annotation.value())).construct();
TypeAdapter<?> typeAdapter;
+ boolean nullSafe = annotation.nullSafe();
if (instance instanceof TypeAdapter) {
typeAdapter = (TypeAdapter<?>) instance;
} else if (instance instanceof TypeAdapterFactory) {
typeAdapter = ((TypeAdapterFactory) instance).create(gson, type);
} else if (instance instanceof JsonSerializer || instance instanceof JsonDeserializer) {
JsonSerializer<?> serializer = instance instanceof JsonSerializer
- ? (JsonSerializer) instance
+ ? (JsonSerializer<?>) instance
: null;
JsonDeserializer<?> deserializer = instance instanceof JsonDeserializer
- ? (JsonDeserializer) instance
+ ? (JsonDeserializer<?>) instance
: null;
- typeAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, null);
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ TypeAdapter<?> tempAdapter = new TreeTypeAdapter(serializer, deserializer, gson, type, null, nullSafe);
+ typeAdapter = tempAdapter;
+
+ nullSafe = false;
} else {
throw new IllegalArgumentException("Invalid attempt to bind an instance of "
+ instance.getClass().getName() + " as a @JsonAdapter for " + type.toString()
@@ -74,7 +79,7 @@ public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapte
+ " JsonSerializer or JsonDeserializer.");
}
- if (typeAdapter != null && annotation.nullSafe()) {
+ if (typeAdapter != null && nullSafe) {
typeAdapter = typeAdapter.nullSafe();
}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
index a753402e..81c3363c 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
@@ -23,11 +23,12 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
import java.io.IOException;
import java.io.Reader;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
-import java.util.Arrays;
/**
* This reader walks the elements of a JsonElement as if it was coming from a
@@ -92,6 +93,7 @@ public final class JsonTreeReader extends JsonReader {
@Override public void endObject() throws IOException {
expect(JsonToken.END_OBJECT);
+ pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected
popStack(); // empty iterator
popStack(); // object
if (stackSize > 0) {
@@ -143,7 +145,7 @@ public final class JsonTreeReader extends JsonReader {
} else if (o == SENTINEL_CLOSED) {
throw new IllegalStateException("JsonReader is closed");
} else {
- throw new AssertionError();
+ throw new MalformedJsonException("Custom JsonElement subclass " + o.getClass().getName() + " is not supported");
}
}
@@ -164,16 +166,20 @@ public final class JsonTreeReader extends JsonReader {
}
}
- @Override public String nextName() throws IOException {
+ private String nextName(boolean skipName) throws IOException {
expect(JsonToken.NAME);
Iterator<?> i = (Iterator<?>) peekStack();
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
String result = (String) entry.getKey();
- pathNames[stackSize - 1] = result;
+ pathNames[stackSize - 1] = skipName ? "<skipped>" : result;
push(entry.getValue());
return result;
}
+ @Override public String nextName() throws IOException {
+ return nextName(false);
+ }
+
@Override public String nextString() throws IOException {
JsonToken token = peek();
if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
@@ -212,7 +218,7 @@ public final class JsonTreeReader extends JsonReader {
}
double result = ((JsonPrimitive) peekStack()).getAsDouble();
if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) {
- throw new NumberFormatException("JSON forbids NaN and infinities: " + result);
+ throw new MalformedJsonException("JSON forbids NaN and infinities: " + result);
}
popStack();
if (stackSize > 0) {
@@ -268,17 +274,26 @@ public final class JsonTreeReader extends JsonReader {
}
@Override public void skipValue() throws IOException {
- if (peek() == JsonToken.NAME) {
- nextName();
- pathNames[stackSize - 2] = "null";
- } else {
- popStack();
- if (stackSize > 0) {
- pathNames[stackSize - 1] = "null";
- }
- }
- if (stackSize > 0) {
- pathIndices[stackSize - 1]++;
+ JsonToken peeked = peek();
+ switch (peeked) {
+ case NAME:
+ String unused = nextName(true);
+ break;
+ case END_ARRAY:
+ endArray();
+ break;
+ case END_OBJECT:
+ endObject();
+ break;
+ case END_DOCUMENT:
+ // Do nothing
+ break;
+ default:
+ popStack();
+ if (stackSize > 0) {
+ pathIndices[stackSize - 1]++;
+ }
+ break;
}
}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java
index e28fbfeb..6ff1aa46 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
/**
* This writer creates a JsonElement.
@@ -130,9 +131,7 @@ public final class JsonTreeWriter extends JsonWriter {
}
@Override public JsonWriter name(String name) throws IOException {
- if (name == null) {
- throw new NullPointerException("name == null");
- }
+ Objects.requireNonNull(name, "name == null");
if (stack.isEmpty() || pendingName != null) {
throw new IllegalStateException();
}
@@ -152,6 +151,10 @@ public final class JsonTreeWriter extends JsonWriter {
return this;
}
+ @Override public JsonWriter jsonValue(String value) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
@Override public JsonWriter nullValue() throws IOException {
put(JsonNull.INSTANCE);
return this;
diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
index f7c5a554..68ecffb9 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
@@ -40,7 +40,7 @@ import java.util.Map;
/**
* Adapts maps to either JSON objects or JSON arrays.
*
- * <h3>Maps as JSON objects</h3>
+ * <h2>Maps as JSON objects</h2>
* For primitive keys or when complex map key serialization is not enabled, this
* converts Java {@link Map Maps} to JSON Objects. This requires that map keys
* can be serialized as strings; this is insufficient for some key types. For
@@ -65,7 +65,7 @@ import java.util.Map;
* at com.google.gson.ObjectNavigator.navigateClassFields
* ...</pre>
*
- * <h3>Maps as JSON arrays</h3>
+ * <h2>Maps as JSON arrays</h2>
* An alternative approach taken by this type adapter when it is required and
* complex map key serialization is enabled is to encode maps as arrays of map
* entries. Each map entry is a two element array containing a key and a value.
diff --git a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java
index c5f2ec73..4b409445 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java
@@ -166,13 +166,13 @@ public final class ObjectTypeAdapter extends TypeAdapter<Object> {
}
}
- @SuppressWarnings("unchecked")
@Override public void write(JsonWriter out, Object value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
+ @SuppressWarnings("unchecked")
TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass());
if (typeAdapter instanceof ObjectTypeAdapter) {
out.beginObject();
diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
index 95d01ace..5ddac50e 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
@@ -19,6 +19,7 @@ package com.google.gson.internal.bind;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
+import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
@@ -38,11 +39,18 @@ import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -88,88 +96,137 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
List<String> fieldNames = new ArrayList<>(alternates.length + 1);
fieldNames.add(serializedName);
- for (String alternate : alternates) {
- fieldNames.add(alternate);
- }
+ Collections.addAll(fieldNames, alternates);
return fieldNames;
}
- @Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+ @Override
+ public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
- FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
+ FilterResult filterResult =
+ ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
- throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
- + raw + ". Register a TypeAdapter for this type or adjust the access filter.");
+ throw new JsonIOException(
+ "ReflectionAccessFilter does not permit using reflection for " + raw
+ + ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
+ // If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
+ // on JVMs that do not support records.
+ if (ReflectionHelper.isRecord(raw)) {
+ @SuppressWarnings("unchecked")
+ TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
+ getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
+ return adapter;
+ }
+
ObjectConstructor<T> constructor = constructorConstructor.get(type);
- return new Adapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible));
+ return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}
- private static void checkAccessible(Object object, Field field) {
- if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) {
- throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#"
- + field.getName() + "' is not accessible and ReflectionAccessFilter does not "
- + "permit making it accessible. Register a TypeAdapter for the declaring type "
- + "or adjust the access filter.");
+ private static <M extends AccessibleObject & Member> void checkAccessible(Object object, M member) {
+ if (!ReflectionAccessFilterHelper.canAccess(member, Modifier.isStatic(member.getModifiers()) ? null : object)) {
+ String memberDescription = ReflectionHelper.getAccessibleObjectDescription(member, true);
+ throw new JsonIOException(memberDescription + " is not accessible and ReflectionAccessFilter does not"
+ + " permit making it accessible. Register a TypeAdapter for the declaring type, adjust the"
+ + " access filter or increase the visibility of the element and its declaring type.");
}
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
- final Gson context, final Field field, final String name,
+ final Gson context, final Field field, final Method accessor, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
final boolean blockInaccessible) {
+
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
- // special casing primitives here saves ~5% on Android...
+
+ int modifiers = field.getModifiers();
+ final boolean isStaticFinalField = Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
+
JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
TypeAdapter<?> mapped = null;
if (annotation != null) {
+ // This is not safe; requires that user has specified correct adapter class for @JsonAdapter
mapped = jsonAdapterFactory.getTypeAdapter(
constructorConstructor, context, fieldType, annotation);
}
final boolean jsonAdapterPresent = mapped != null;
if (mapped == null) mapped = context.getAdapter(fieldType);
- final TypeAdapter<?> typeAdapter = mapped;
- return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
- @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
- @Override void write(JsonWriter writer, Object value)
+ @SuppressWarnings("unchecked")
+ final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
+ return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) {
+ @Override void write(JsonWriter writer, Object source)
throws IOException, IllegalAccessException {
if (!serialized) return;
if (blockInaccessible) {
- checkAccessible(value, field);
+ if (accessor == null) {
+ checkAccessible(source, field);
+ } else {
+ // Note: This check might actually be redundant because access check for canonical
+ // constructor should have failed already
+ checkAccessible(source, accessor);
+ }
}
- Object fieldValue = field.get(value);
- if (fieldValue == value) {
+ Object fieldValue;
+ if (accessor != null) {
+ try {
+ fieldValue = accessor.invoke(source);
+ } catch (InvocationTargetException e) {
+ String accessorDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
+ throw new JsonIOException("Accessor " + accessorDescription + " threw exception", e.getCause());
+ }
+ } else {
+ fieldValue = field.get(source);
+ }
+ if (fieldValue == source) {
// avoid direct recursion
return;
}
writer.name(name);
- TypeAdapter t = jsonAdapterPresent ? typeAdapter
- : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType());
+ TypeAdapter<Object> t = jsonAdapterPresent ? typeAdapter
+ : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
- @Override void read(JsonReader reader, Object value)
+
+ @Override
+ void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException {
+ Object fieldValue = typeAdapter.read(reader);
+ if (fieldValue == null && isPrimitive) {
+ throw new JsonParseException("null is not allowed as value for record component '" + fieldName + "'"
+ + " of primitive type; at path " + reader.getPath());
+ }
+ target[index] = fieldValue;
+ }
+
+ @Override
+ void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
- checkAccessible(value, field);
+ checkAccessible(target, field);
+ } else if (isStaticFinalField) {
+ // Reflection does not permit setting value of `static final` field, even after calling `setAccessible`
+ // Handle this here to avoid causing IllegalAccessException when calling `Field.set`
+ String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false);
+ throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription);
}
- field.set(value, fieldValue);
+ field.set(target, fieldValue);
}
}
};
}
- private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) {
+ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,
+ boolean blockInaccessible, boolean isRecord) {
Map<String, BoundField> result = new LinkedHashMap<>();
if (raw.isInterface()) {
return result;
@@ -184,9 +241,9 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (raw != originalRaw && fields.length > 0) {
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
- throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
- + raw + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type "
- + "or adjust the access filter.");
+ throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + raw
+ + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type"
+ + " or adjust the access filter.");
}
blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
}
@@ -197,9 +254,36 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (!serialize && !deserialize) {
continue;
}
+ // The accessor method is only used for records. If the type is a record, we will read out values
+ // via its accessor method instead of via reflection. This way we will bypass the accessible restrictions
+ Method accessor = null;
+ if (isRecord) {
+ // If there is a static field on a record, there will not be an accessor. Instead we will use the default
+ // field serialization logic, but for deserialization the field is excluded for simplicity. Note that Gson
+ // ignores static fields by default, but GsonBuilder.excludeFieldsWithModifiers can overwrite this.
+ if (Modifier.isStatic(field.getModifiers())) {
+ deserialize = false;
+ } else {
+ accessor = ReflectionHelper.getAccessor(raw, field);
+ // If blockInaccessible, skip and perform access check later
+ if (!blockInaccessible) {
+ ReflectionHelper.makeAccessible(accessor);
+ }
+
+ // @SerializedName can be placed on accessor method, but it is not supported there
+ // If field and method have annotation it is not easily possible to determine if accessor method
+ // is implicit and has inherited annotation, or if it is explicitly declared with custom annotation
+ if (accessor.getAnnotation(SerializedName.class) != null
+ && field.getAnnotation(SerializedName.class) == null) {
+ String methodDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
+ throw new JsonIOException("@SerializedName on " + methodDescription + " is not supported");
+ }
+ }
+ }
// If blockInaccessible, skip and perform access check later
- if (!blockInaccessible) {
+ // For Records if the accessor method is used the field does not have to be made accessible
+ if (!blockInaccessible && accessor == null) {
ReflectionHelper.makeAccessible(field);
}
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
@@ -208,7 +292,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
for (int i = 0, size = fieldNames.size(); i < size; ++i) {
String name = fieldNames.get(i);
if (i != 0) serialize = false; // only serialize the default name
- BoundField boundField = createBoundField(context, field, name,
+ BoundField boundField = createBoundField(context, field, accessor, name,
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
BoundField replaced = result.put(name, boundField);
if (previous == null) previous = replaced;
@@ -226,34 +310,75 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
static abstract class BoundField {
final String name;
+ /** Name of the underlying field */
+ final String fieldName;
final boolean serialized;
final boolean deserialized;
- protected BoundField(String name, boolean serialized, boolean deserialized) {
+ protected BoundField(String name, String fieldName, boolean serialized, boolean deserialized) {
this.name = name;
+ this.fieldName = fieldName;
this.serialized = serialized;
this.deserialized = deserialized;
}
- abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
- abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
+
+ /** Read this field value from the source, and append its JSON value to the writer */
+ abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException;
+
+ /** Read the value into the target array, used to provide constructor arguments for records */
+ abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException;
+
+ /** Read the value from the reader, and set it on the corresponding field on target via reflection */
+ abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
}
- public static final class Adapter<T> extends TypeAdapter<T> {
- private final ObjectConstructor<T> constructor;
- private final Map<String, BoundField> boundFields;
+ /**
+ * Base class for Adapters produced by this factory.
+ *
+ * <p>The {@link RecordAdapter} is a special case to handle records for JVMs that support it, for
+ * all other types we use the {@link FieldReflectionAdapter}. This class encapsulates the common
+ * logic for serialization and deserialization. During deserialization, we construct an
+ * accumulator A, which we use to accumulate values from the source JSON. After the object has been read in
+ * full, the {@link #finalize(Object)} method is used to convert the accumulator to an instance
+ * of T.
+ *
+ * @param <T> type of objects that this Adapter creates.
+ * @param <A> type of accumulator used to build the deserialization result.
+ */
+ // This class is public because external projects check for this class with `instanceof` (even though it is internal)
+ public static abstract class Adapter<T, A> extends TypeAdapter<T> {
+ final Map<String, BoundField> boundFields;
- Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
- this.constructor = constructor;
+ Adapter(Map<String, BoundField> boundFields) {
this.boundFields = boundFields;
}
- @Override public T read(JsonReader in) throws IOException {
+ @Override
+ public void write(JsonWriter out, T value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ return;
+ }
+
+ out.beginObject();
+ try {
+ for (BoundField boundField : boundFields.values()) {
+ boundField.write(out, value);
+ }
+ } catch (IllegalAccessException e) {
+ throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+ }
+ out.endObject();
+ }
+
+ @Override
+ public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
- T instance = constructor.construct();
+ A accumulator = createAccumulator();
try {
in.beginObject();
@@ -263,7 +388,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (field == null || !field.deserialized) {
in.skipValue();
} else {
- field.read(in, instance);
+ readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
@@ -272,24 +397,136 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
- return instance;
+ return finalize(accumulator);
}
- @Override public void write(JsonWriter out, T value) throws IOException {
- if (value == null) {
- out.nullValue();
- return;
+ /** Create the Object that will be used to collect each field value */
+ abstract A createAccumulator();
+ /**
+ * Read a single BoundField into the accumulator. The JsonReader will be pointed at the
+ * start of the value for the BoundField to read from.
+ */
+ abstract void readField(A accumulator, JsonReader in, BoundField field)
+ throws IllegalAccessException, IOException;
+ /** Convert the accumulator to a final instance of T. */
+ abstract T finalize(A accumulator);
+ }
+
+ private static final class FieldReflectionAdapter<T> extends Adapter<T, T> {
+ private final ObjectConstructor<T> constructor;
+
+ FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
+ super(boundFields);
+ this.constructor = constructor;
+ }
+
+ @Override
+ T createAccumulator() {
+ return constructor.construct();
+ }
+
+ @Override
+ void readField(T accumulator, JsonReader in, BoundField field)
+ throws IllegalAccessException, IOException {
+ field.readIntoField(in, accumulator);
+ }
+
+ @Override
+ T finalize(T accumulator) {
+ return accumulator;
+ }
+ }
+
+ private static final class RecordAdapter<T> extends Adapter<T, Object[]> {
+ static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = primitiveDefaults();
+
+ // The canonical constructor of the record
+ private final Constructor<T> constructor;
+ // Array of arguments to the constructor, initialized with default values for primitives
+ private final Object[] constructorArgsDefaults;
+ // Map from component names to index into the constructors arguments.
+ private final Map<String, Integer> componentIndices = new HashMap<>();
+
+ RecordAdapter(Class<T> raw, Map<String, BoundField> boundFields, boolean blockInaccessible) {
+ super(boundFields);
+ constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
+
+ if (blockInaccessible) {
+ checkAccessible(null, constructor);
+ } else {
+ // Ensure the constructor is accessible
+ ReflectionHelper.makeAccessible(constructor);
}
- out.beginObject();
+ String[] componentNames = ReflectionHelper.getRecordComponentNames(raw);
+ for (int i = 0; i < componentNames.length; i++) {
+ componentIndices.put(componentNames[i], i);
+ }
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+
+ // We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this,
+ // we create an Object[] where all primitives are initialized to non-null values.
+ constructorArgsDefaults = new Object[parameterTypes.length];
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // This will correctly be null for non-primitive types:
+ constructorArgsDefaults[i] = PRIMITIVE_DEFAULTS.get(parameterTypes[i]);
+ }
+ }
+
+ private static Map<Class<?>, Object> primitiveDefaults() {
+ Map<Class<?>, Object> zeroes = new HashMap<>();
+ zeroes.put(byte.class, (byte) 0);
+ zeroes.put(short.class, (short) 0);
+ zeroes.put(int.class, 0);
+ zeroes.put(long.class, 0L);
+ zeroes.put(float.class, 0F);
+ zeroes.put(double.class, 0D);
+ zeroes.put(char.class, '\0');
+ zeroes.put(boolean.class, false);
+ return zeroes;
+ }
+
+ @Override
+ Object[] createAccumulator() {
+ return constructorArgsDefaults.clone();
+ }
+
+ @Override
+ void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
+ // Obtain the component index from the name of the field backing it
+ Integer componentIndex = componentIndices.get(field.fieldName);
+ if (componentIndex == null) {
+ throw new IllegalStateException(
+ "Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " for field with name '" + field.fieldName + "',"
+ + " unable to determine which argument in the constructor the field corresponds"
+ + " to. This is unexpected behavior, as we expect the RecordComponents to have the"
+ + " same names as the fields in the Java class, and that the order of the"
+ + " RecordComponents is the same as the order of the canonical constructor parameters.");
+ }
+ field.readIntoArray(in, componentIndex, accumulator);
+ }
+
+ @Override
+ T finalize(Object[] accumulator) {
try {
- for (BoundField boundField : boundFields.values()) {
- boundField.write(out, value);
- }
+ return constructor.newInstance(accumulator);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
- out.endObject();
+ // Note: InstantiationException should be impossible because record class is not abstract;
+ // IllegalArgumentException should not be possible unless a bad adapter returns objects of the wrong type
+ catch (InstantiationException | IllegalArgumentException e) {
+ throw new RuntimeException(
+ "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with args " + Arrays.toString(accumulator), e);
+ }
+ catch (InvocationTargetException e) {
+ // TODO: JsonParseException ?
+ throw new RuntimeException(
+ "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with args " + Arrays.toString(accumulator), e.getCause());
+ }
}
}
}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java
new file mode 100644
index 00000000..dad4ff11
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java
@@ -0,0 +1,14 @@
+package com.google.gson.internal.bind;
+
+import com.google.gson.TypeAdapter;
+
+/**
+ * Type adapter which might delegate serialization to another adapter.
+ */
+public abstract class SerializationDelegatingTypeAdapter<T> extends TypeAdapter<T> {
+ /**
+ * Returns the adapter used for serialization, might be {@code this} or another adapter.
+ * That other adapter might itself also be a {@code SerializationDelegatingTypeAdapter}.
+ */
+ public abstract TypeAdapter<T> getSerializationDelegate();
+}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java
index 50f46b5a..560234c0 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java
@@ -38,24 +38,31 @@ import java.lang.reflect.Type;
* tree adapter may be serialization-only or deserialization-only, this class
* has a facility to lookup a delegate type adapter on demand.
*/
-public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
+public final class TreeTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
private final JsonSerializer<T> serializer;
private final JsonDeserializer<T> deserializer;
final Gson gson;
private final TypeToken<T> typeToken;
private final TypeAdapterFactory skipPast;
private final GsonContextImpl context = new GsonContextImpl();
+ private final boolean nullSafe;
/** The delegate is lazily created because it may not be needed, and creating it may fail. */
private volatile TypeAdapter<T> delegate;
public TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer,
- Gson gson, TypeToken<T> typeToken, TypeAdapterFactory skipPast) {
+ Gson gson, TypeToken<T> typeToken, TypeAdapterFactory skipPast, boolean nullSafe) {
this.serializer = serializer;
this.deserializer = deserializer;
this.gson = gson;
this.typeToken = typeToken;
this.skipPast = skipPast;
+ this.nullSafe = nullSafe;
+ }
+
+ public TreeTypeAdapter(JsonSerializer<T> serializer, JsonDeserializer<T> deserializer,
+ Gson gson, TypeToken<T> typeToken, TypeAdapterFactory skipPast) {
+ this(serializer, deserializer, gson, typeToken, skipPast, true);
}
@Override public T read(JsonReader in) throws IOException {
@@ -63,7 +70,7 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
return delegate().read(in);
}
JsonElement value = Streams.parse(in);
- if (value.isJsonNull()) {
+ if (nullSafe && value.isJsonNull()) {
return null;
}
return deserializer.deserialize(value, typeToken.getType(), context);
@@ -74,7 +81,7 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
delegate().write(out, value);
return;
}
- if (value == null) {
+ if (nullSafe && value == null) {
out.nullValue();
return;
}
@@ -91,6 +98,15 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
}
/**
+ * Returns the type adapter which is used for serialization. Returns {@code this}
+ * if this {@code TreeTypeAdapter} has a {@link #serializer}; otherwise returns
+ * the delegate.
+ */
+ @Override public TypeAdapter<T> getSerializationDelegate() {
+ return serializer != null ? this : delegate();
+ }
+
+ /**
* Returns a new factory that will match each type against {@code exactType}.
*/
public static TypeAdapterFactory newFactory(TypeToken<?> exactType, Object typeAdapter) {
@@ -162,5 +178,5 @@ public final class TreeTypeAdapter<T> extends TypeAdapter<T> {
@Override public <R> R deserialize(JsonElement json, Type typeOfT) throws JsonParseException {
return (R) gson.fromJson(json, typeOfT);
}
- };
+ }
}
diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java
index 2bf37ad0..75a991ea 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java
@@ -15,15 +15,14 @@
*/
package com.google.gson.internal.bind;
-import java.io.IOException;
-import java.lang.reflect.Type;
-import java.lang.reflect.TypeVariable;
-
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
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.lang.reflect.TypeVariable;
final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
private final Gson context;
@@ -41,7 +40,6 @@ final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
return delegate.read(in);
}
- @SuppressWarnings({"rawtypes", "unchecked"})
@Override
public void write(JsonWriter out, T value) throws IOException {
// Order of preference for choosing type adapters
@@ -50,14 +48,17 @@ final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
// Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type)
// Fourth preference: reflective type adapter for the declared type
- TypeAdapter chosen = delegate;
+ TypeAdapter<T> chosen = delegate;
Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
if (runtimeType != type) {
- TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType));
+ @SuppressWarnings("unchecked")
+ TypeAdapter<T> runtimeTypeAdapter = (TypeAdapter<T>) context.getAdapter(TypeToken.get(runtimeType));
+ // For backward compatibility only check ReflectiveTypeAdapterFactory.Adapter here but not any other
+ // wrapping adapters, see https://github.com/google/gson/pull/1787#issuecomment-1222175189
if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
// The user registered a type adapter for the runtime type, so we will use that
chosen = runtimeTypeAdapter;
- } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
+ } else if (!isReflective(delegate)) {
// The user registered a type adapter for Base class, so we prefer it over the
// reflective type adapter for the runtime type
chosen = delegate;
@@ -70,11 +71,29 @@ final class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
}
/**
+ * Returns whether the type adapter uses reflection.
+ *
+ * @param typeAdapter the type adapter to check.
+ */
+ private static boolean isReflective(TypeAdapter<?> typeAdapter) {
+ // Run this in loop in case multiple delegating adapters are nested
+ while (typeAdapter instanceof SerializationDelegatingTypeAdapter) {
+ TypeAdapter<?> delegate = ((SerializationDelegatingTypeAdapter<?>) typeAdapter).getSerializationDelegate();
+ // Break if adapter does not delegate serialization
+ if (delegate == typeAdapter) {
+ break;
+ }
+ typeAdapter = delegate;
+ }
+
+ return typeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter;
+ }
+
+ /**
* Finds a compatible runtime type if it is more specific
*/
- private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
- if (value != null
- && (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) {
+ private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
+ if (value != null && (type instanceof Class<?> || type instanceof TypeVariable<?>)) {
type = value.getClass();
}
return type;
diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
index 9ba13637..cb069ae7 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
@@ -194,7 +194,11 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.byteValue());
+ }
}
};
@@ -223,7 +227,11 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.shortValue());
+ }
}
};
@@ -245,7 +253,11 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.intValue());
+ }
}
};
public static final TypeAdapterFactory INTEGER_FACTORY
@@ -323,7 +335,11 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.longValue());
+ }
}
};
@@ -338,7 +354,14 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ // For backward compatibility don't call `JsonWriter.value(float)` because that method has
+ // been newly added and not all custom JsonWriter implementations might override it yet
+ Number floatNumber = value instanceof Float ? value : value.floatValue();
+ out.value(floatNumber);
+ }
}
};
@@ -353,7 +376,11 @@ public final class TypeAdapters {
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
- out.value(value);
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(value.doubleValue());
+ }
}
};
@@ -891,7 +918,6 @@ public final class TypeAdapters {
}
public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() {
- @SuppressWarnings({"rawtypes", "unchecked"})
@Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) {
@@ -900,7 +926,9 @@ public final class TypeAdapters {
if (!rawType.isEnum()) {
rawType = rawType.getSuperclass(); // handle anonymous subclasses
}
- return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ TypeAdapter<T> adapter = (TypeAdapter<T>) new EnumTypeAdapter(rawType);
+ return adapter;
}
};
diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
index 97230ff6..ac061212 100644
--- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
+++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
@@ -2,39 +2,97 @@ package com.google.gson.internal.reflect;
import com.google.gson.JsonIOException;
import com.google.gson.internal.GsonBuildConfig;
+import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
public class ReflectionHelper {
- private ReflectionHelper() { }
+
+ private static final RecordHelper RECORD_HELPER;
+
+ static {
+ RecordHelper instance;
+ try {
+ // Try to construct the RecordSupportedHelper, if this fails, records are not supported on this JVM.
+ instance = new RecordSupportedHelper();
+ } catch (NoSuchMethodException e) {
+ instance = new RecordNotSupportedHelper();
+ }
+ RECORD_HELPER = instance;
+ }
+
+ private ReflectionHelper() {}
/**
- * Tries making the field accessible, wrapping any thrown exception in a
- * {@link JsonIOException} with descriptive message.
+ * Internal implementation of making an {@link AccessibleObject} accessible.
*
- * @param field field to make accessible
- * @throws JsonIOException if making the field accessible fails
+ * @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on.
+ * @throws JsonIOException if making the object accessible fails
*/
- public static void makeAccessible(Field field) throws JsonIOException {
+ public static void makeAccessible(AccessibleObject object) throws JsonIOException {
try {
- field.setAccessible(true);
+ object.setAccessible(true);
} catch (Exception exception) {
- throw new JsonIOException("Failed making field '" + field.getDeclaringClass().getName() + "#"
- + field.getName() + "' accessible; either change its visibility or write a custom "
- + "TypeAdapter for its declaring type", exception);
+ String description = getAccessibleObjectDescription(object, false);
+ throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility"
+ + " or write a custom TypeAdapter for its declaring type.", exception);
+ }
+ }
+
+ /**
+ * Returns a short string describing the {@link AccessibleObject} in a human-readable way.
+ * The result is normally shorter than {@link AccessibleObject#toString()} because it omits
+ * modifiers (e.g. {@code final}) and uses simple names for constructor and method parameter
+ * types.
+ *
+ * @param object object to describe
+ * @param uppercaseFirstLetter whether the first letter of the description should be uppercased
+ */
+ public static String getAccessibleObjectDescription(AccessibleObject object, boolean uppercaseFirstLetter) {
+ String description;
+
+ if (object instanceof Field) {
+ Field field = (Field) object;
+ description = "field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'";
+ } else if (object instanceof Method) {
+ Method method = (Method) object;
+
+ StringBuilder methodSignatureBuilder = new StringBuilder(method.getName());
+ appendExecutableParameters(method, methodSignatureBuilder);
+ String methodSignature = methodSignatureBuilder.toString();
+
+ description = "method '" + method.getDeclaringClass().getName() + "#" + methodSignature + "'";
+ } else if (object instanceof Constructor) {
+ description = "constructor '" + constructorToString((Constructor<?>) object) + "'";
+ } else {
+ description = "<unknown AccessibleObject> " + object.toString();
+ }
+
+ if (uppercaseFirstLetter && Character.isLowerCase(description.charAt(0))) {
+ description = Character.toUpperCase(description.charAt(0)) + description.substring(1);
}
+ return description;
}
/**
* Creates a string representation for a constructor.
- * E.g.: {@code java.lang.String#String(char[], int, int)}
+ * E.g.: {@code java.lang.String(char[], int, int)}
*/
- private static String constructorToString(Constructor<?> constructor) {
- StringBuilder stringBuilder = new StringBuilder(constructor.getDeclaringClass().getName())
- .append('#')
- .append(constructor.getDeclaringClass().getSimpleName())
- .append('(');
- Class<?>[] parameters = constructor.getParameterTypes();
+ public static String constructorToString(Constructor<?> constructor) {
+ StringBuilder stringBuilder = new StringBuilder(constructor.getDeclaringClass().getName());
+ appendExecutableParameters(constructor, stringBuilder);
+
+ return stringBuilder.toString();
+ }
+
+ // Note: Ideally parameter type would be java.lang.reflect.Executable, but that was added in Java 8
+ private static void appendExecutableParameters(AccessibleObject executable, StringBuilder stringBuilder) {
+ stringBuilder.append('(');
+
+ Class<?>[] parameters = (executable instanceof Method)
+ ? ((Method) executable).getParameterTypes()
+ : ((Constructor<?>) executable).getParameterTypes();
for (int i = 0; i < parameters.length; i++) {
if (i > 0) {
stringBuilder.append(", ");
@@ -42,7 +100,7 @@ public class ReflectionHelper {
stringBuilder.append(parameters[i].getSimpleName());
}
- return stringBuilder.append(')').toString();
+ stringBuilder.append(')');
}
/**
@@ -58,17 +116,155 @@ public class ReflectionHelper {
constructor.setAccessible(true);
return null;
} catch (Exception exception) {
- return "Failed making constructor '" + constructorToString(constructor) + "' accessible; "
- + "either change its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: "
+ return "Failed making constructor '" + constructorToString(constructor) + "' accessible;"
+ + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for"
// Include the message since it might contain more detailed information
- + exception.getMessage();
+ + " its declaring type: " + exception.getMessage();
}
}
- public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) {
- throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + "). "
- + "Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using "
- + "ReflectionAccessFilter, report this to the Gson maintainers.",
+ /** If records are supported on the JVM, this is equivalent to a call to Class.isRecord() */
+ public static boolean isRecord(Class<?> raw) {
+ return RECORD_HELPER.isRecord(raw);
+ }
+
+ public static String[] getRecordComponentNames(Class<?> raw) {
+ return RECORD_HELPER.getRecordComponentNames(raw);
+ }
+
+ /** Looks up the record accessor method that corresponds to the given record field */
+ public static Method getAccessor(Class<?> raw, Field field) {
+ return RECORD_HELPER.getAccessor(raw, field);
+ }
+
+ public static <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
+ return RECORD_HELPER.getCanonicalRecordConstructor(raw);
+ }
+
+ public static RuntimeException createExceptionForUnexpectedIllegalAccess(
+ IllegalAccessException exception) {
+ throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + ")."
+ + " Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using"
+ + " ReflectionAccessFilter, report this to the Gson maintainers.",
exception);
}
+
+
+ private static RuntimeException createExceptionForRecordReflectionException(
+ ReflectiveOperationException exception) {
+ throw new RuntimeException("Unexpected ReflectiveOperationException occurred"
+ + " (Gson " + GsonBuildConfig.VERSION + ")."
+ + " To support Java records, reflection is utilized to read out information"
+ + " about records. All these invocations happens after it is established"
+ + " that records exist in the JVM. This exception is unexpected behavior.",
+ exception);
+ }
+
+ /**
+ * Internal abstraction over reflection when Records are supported.
+ */
+ private abstract static class RecordHelper {
+ abstract boolean isRecord(Class<?> clazz);
+
+ abstract String[] getRecordComponentNames(Class<?> clazz);
+
+ abstract <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw);
+
+ public abstract Method getAccessor(Class<?> raw, Field field);
+ }
+
+ private static class RecordSupportedHelper extends RecordHelper {
+ private final Method isRecord;
+ private final Method getRecordComponents;
+ private final Method getName;
+ private final Method getType;
+
+ private RecordSupportedHelper() throws NoSuchMethodException {
+ isRecord = Class.class.getMethod("isRecord");
+ getRecordComponents = Class.class.getMethod("getRecordComponents");
+ // Class java.lang.reflect.RecordComponent
+ Class<?> classRecordComponent = getRecordComponents.getReturnType().getComponentType();
+ getName = classRecordComponent.getMethod("getName");
+ getType = classRecordComponent.getMethod("getType");
+ }
+
+ @Override
+ boolean isRecord(Class<?> raw) {
+ try {
+ return (boolean) isRecord.invoke(raw);
+ } catch (ReflectiveOperationException e) {
+ throw createExceptionForRecordReflectionException(e);
+ }
+ }
+
+ @Override
+ String[] getRecordComponentNames(Class<?> raw) {
+ try {
+ Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw);
+ String[] componentNames = new String[recordComponents.length];
+ for (int i = 0; i < recordComponents.length; i++) {
+ componentNames[i] = (String) getName.invoke(recordComponents[i]);
+ }
+ return componentNames;
+ } catch (ReflectiveOperationException e) {
+ throw createExceptionForRecordReflectionException(e);
+ }
+ }
+
+ @Override
+ public <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
+ try {
+ Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw);
+ Class<?>[] recordComponentTypes = new Class<?>[recordComponents.length];
+ for (int i = 0; i < recordComponents.length; i++) {
+ recordComponentTypes[i] = (Class<?>) getType.invoke(recordComponents[i]);
+ }
+ // Uses getDeclaredConstructor because implicit constructor has same visibility as record and might
+ // therefore not be public
+ return raw.getDeclaredConstructor(recordComponentTypes);
+ } catch (ReflectiveOperationException e) {
+ throw createExceptionForRecordReflectionException(e);
+ }
+ }
+
+ @Override
+ public Method getAccessor(Class<?> raw, Field field) {
+ try {
+ // Records consists of record components, each with a unique name, a corresponding field and accessor method
+ // with the same name. Ref.: https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.10.3
+ return raw.getMethod(field.getName());
+ } catch (ReflectiveOperationException e) {
+ throw createExceptionForRecordReflectionException(e);
+ }
+ }
+ }
+
+ /**
+ * Instance used when records are not supported
+ */
+ private static class RecordNotSupportedHelper extends RecordHelper {
+
+ @Override
+ boolean isRecord(Class<?> clazz) {
+ return false;
+ }
+
+ @Override
+ String[] getRecordComponentNames(Class<?> clazz) {
+ throw new UnsupportedOperationException(
+ "Records are not supported on this JVM, this method should not be called");
+ }
+
+ @Override
+ <T> Constructor<T> getCanonicalRecordConstructor(Class<T> raw) {
+ throw new UnsupportedOperationException(
+ "Records are not supported on this JVM, this method should not be called");
+ }
+
+ @Override
+ public Method getAccessor(Class<?> raw, Field field) {
+ throw new UnsupportedOperationException(
+ "Records are not supported on this JVM, this method should not be called");
+ }
+ }
}
diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java
index b12d201f..39e81f33 100644
--- a/gson/src/main/java/com/google/gson/reflect/TypeToken.java
+++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java
@@ -17,13 +17,13 @@
package com.google.gson.reflect;
import com.google.gson.internal.$Gson$Types;
-import com.google.gson.internal.$Gson$Preconditions;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
/**
* Represents a generic type {@code T}. Java doesn't yet provide a way to
@@ -32,7 +32,7 @@ import java.util.Map;
* runtime.
*
* <p>For example, to create a type literal for {@code List<String>}, you can
- * create an empty anonymous inner class:
+ * create an empty anonymous class:
*
* <p>
* {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};}
@@ -43,6 +43,11 @@ import java.util.Map;
* might expect, which gives a false sense of type-safety at compilation time
* and can lead to an unexpected {@code ClassCastException} at runtime.
*
+ * <p>If the type arguments of the parameterized type are only available at
+ * runtime, for example when you want to create a {@code List<E>} based on
+ * a {@code Class<E>} representing the element type, the method
+ * {@link #getParameterized(Type, Type...)} can be used.
+ *
* @author Bob Lee
* @author Sven Mawson
* @author Jesse Wilson
@@ -72,7 +77,7 @@ public class TypeToken<T> {
*/
@SuppressWarnings("unchecked")
private TypeToken(Type type) {
- this.type = $Gson$Types.canonicalize($Gson$Preconditions.checkNotNull(type));
+ this.type = $Gson$Types.canonicalize(Objects.requireNonNull(type));
this.rawType = (Class<? super T>) $Gson$Types.getRawType(this.type);
this.hashCode = this.type.hashCode();
}
@@ -317,10 +322,57 @@ public class TypeToken<T> {
}
/**
- * Gets type literal for the parameterized type represented by applying {@code typeArguments} to
- * {@code rawType}.
+ * Gets a type literal for the parameterized type represented by applying {@code typeArguments} to
+ * {@code rawType}. This is mainly intended for situations where the type arguments are not
+ * available at compile time. The following example shows how a type token for {@code Map<K, V>}
+ * can be created:
+ * <pre>{@code
+ * Class<K> keyClass = ...;
+ * Class<V> valueClass = ...;
+ * TypeToken<?> mapTypeToken = TypeToken.getParameterized(Map.class, keyClass, valueClass);
+ * }</pre>
+ * As seen here the result is a {@code TypeToken<?>}; this method cannot provide any type safety,
+ * and care must be taken to pass in the correct number of type arguments.
+ *
+ * @throws IllegalArgumentException
+ * If {@code rawType} is not of type {@code Class}, or if the type arguments are invalid for
+ * the raw type
*/
public static TypeToken<?> getParameterized(Type rawType, Type... typeArguments) {
+ Objects.requireNonNull(rawType);
+ Objects.requireNonNull(typeArguments);
+
+ // Perform basic validation here because this is the only public API where users
+ // can create malformed parameterized types
+ if (!(rawType instanceof Class)) {
+ // See also https://bugs.openjdk.org/browse/JDK-8250659
+ throw new IllegalArgumentException("rawType must be of type Class, but was " + rawType);
+ }
+ Class<?> rawClass = (Class<?>) rawType;
+ TypeVariable<?>[] typeVariables = rawClass.getTypeParameters();
+
+ int expectedArgsCount = typeVariables.length;
+ int actualArgsCount = typeArguments.length;
+ if (actualArgsCount != expectedArgsCount) {
+ throw new IllegalArgumentException(rawClass.getName() + " requires " + expectedArgsCount +
+ " type arguments, but got " + actualArgsCount);
+ }
+
+ for (int i = 0; i < expectedArgsCount; i++) {
+ Type typeArgument = typeArguments[i];
+ Class<?> rawTypeArgument = $Gson$Types.getRawType(typeArgument);
+ TypeVariable<?> typeVariable = typeVariables[i];
+
+ for (Type bound : typeVariable.getBounds()) {
+ Class<?> rawBound = $Gson$Types.getRawType(bound);
+
+ if (!rawBound.isAssignableFrom(rawTypeArgument)) {
+ throw new IllegalArgumentException("Type argument " + typeArgument + " does not satisfy bounds "
+ + "for type variable " + typeVariable + " declared by " + rawType);
+ }
+ }
+ }
+
return new TypeToken<>($Gson$Types.newParameterizedTypeWithOwner(null, rawType, typeArguments));
}
diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java
index 6cb820be..ed6bab97 100644
--- a/gson/src/main/java/com/google/gson/stream/JsonReader.java
+++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java
@@ -23,6 +23,7 @@ import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
+import java.util.Objects;
/**
* Reads a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
@@ -32,7 +33,7 @@ import java.util.Arrays;
* depth-first order, the same order that they appear in the JSON document.
* Within JSON objects, name/value pairs are represented by a single token.
*
- * <h3>Parsing JSON</h3>
+ * <h2>Parsing JSON</h2>
* To create a recursive descent parser for your own JSON streams, first create
* an entry point method that creates a {@code JsonReader}.
*
@@ -61,7 +62,7 @@ import java.util.Arrays;
* Null literals can be consumed using either {@link #nextNull()} or {@link
* #skipValue()}.
*
- * <h3>Example</h3>
+ * <h2>Example</h2>
* Suppose we'd like to parse a stream of messages such as the following: <pre> {@code
* [
* {
@@ -160,7 +161,7 @@ import java.util.Arrays;
* return new User(username, followersCount);
* }}</pre>
*
- * <h3>Number Handling</h3>
+ * <h2>Number Handling</h2>
* This reader permits numeric values to be read as strings and string values to
* be read as numbers. For example, both elements of the JSON array {@code
* [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}.
@@ -170,7 +171,7 @@ import java.util.Arrays;
* precision loss, extremely large values should be written and read as strings
* in JSON.
*
- * <h3 id="nonexecuteprefix">Non-Execute Prefix</h3>
+ * <h2 id="nonexecuteprefix">Non-Execute Prefix</h2>
* Web servers that serve private data using JSON may be vulnerable to <a
* href="http://en.wikipedia.org/wiki/JSON#Cross-site_request_forgery">Cross-site
* request forgery</a> attacks. In such an attack, a malicious site gains access
@@ -287,10 +288,7 @@ public class JsonReader implements Closeable {
* Creates a new instance that reads a JSON-encoded stream from {@code in}.
*/
public JsonReader(Reader in) {
- if (in == null) {
- throw new NullPointerException("in == null");
- }
- this.in = in;
+ this.in = Objects.requireNonNull(in, "in == null");
}
/**
@@ -468,6 +466,7 @@ public class JsonReader implements Closeable {
}
}
+ @SuppressWarnings("fallthrough")
int doPeek() throws IOException {
int peekStack = stack[stackSize - 1];
if (peekStack == JsonScope.EMPTY_ARRAY) {
@@ -751,6 +750,7 @@ public class JsonReader implements Closeable {
}
}
+ @SuppressWarnings("fallthrough")
private boolean isLiteral(char c) throws IOException {
switch (c) {
case '/':
@@ -777,10 +777,9 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and
- * consumes it.
+ * Returns the next token, a {@link JsonToken#NAME property name}, and consumes it.
*
- * @throws java.io.IOException if the next token in the stream is not a property
+ * @throws IOException if the next token in the stream is not a property
* name.
*/
public String nextName() throws IOException {
@@ -804,7 +803,7 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the {@link com.google.gson.stream.JsonToken#STRING string} value of the next token,
+ * Returns the {@link JsonToken#STRING string} value of the next token,
* consuming it. If the next token is a number, this method will return its
* string form.
*
@@ -840,7 +839,7 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the {@link com.google.gson.stream.JsonToken#BOOLEAN boolean} value of the next token,
+ * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token,
* consuming it.
*
* @throws IllegalStateException if the next token is not a boolean or if
@@ -884,13 +883,15 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the {@link com.google.gson.stream.JsonToken#NUMBER double} value of the next token,
+ * Returns the {@link JsonToken#NUMBER double} value of the next token,
* consuming it. If the next token is a string, this method will attempt to
* parse it as a double using {@link Double#parseDouble(String)}.
*
* @throws IllegalStateException if the next token is not a literal value.
* @throws NumberFormatException if the next literal value cannot be parsed
- * as a double, or is non-finite.
+ * as a double.
+ * @throws MalformedJsonException if the next literal value is NaN or Infinity
+ * and this reader is not {@link #setLenient(boolean) lenient}.
*/
public double nextDouble() throws IOException {
int p = peeked;
@@ -928,7 +929,7 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the {@link com.google.gson.stream.JsonToken#NUMBER long} value of the next token,
+ * Returns the {@link JsonToken#NUMBER long} value of the next token,
* consuming it. If the next token is a string, this method will attempt to
* parse it as a long. If the next token's numeric value cannot be exactly
* represented by a Java {@code long}, this method throws.
@@ -1129,6 +1130,7 @@ public class JsonReader implements Closeable {
throw syntaxError("Unterminated string");
}
+ @SuppressWarnings("fallthrough")
private void skipUnquotedValue() throws IOException {
do {
int i = 0;
@@ -1160,7 +1162,7 @@ public class JsonReader implements Closeable {
}
/**
- * Returns the {@link com.google.gson.stream.JsonToken#NUMBER int} value of the next token,
+ * Returns the {@link JsonToken#NUMBER int} value of the next token,
* consuming it. If the next token is a string, this method will attempt to
* parse it as an int. If the next token's numeric value cannot be exactly
* represented by a Java {@code int}, this method throws.
@@ -1220,7 +1222,7 @@ public class JsonReader implements Closeable {
}
/**
- * Closes this JSON reader and the underlying {@link java.io.Reader}.
+ * Closes this JSON reader and the underlying {@link Reader}.
*/
@Override public void close() throws IOException {
peeked = PEEKED_NONE;
@@ -1230,9 +1232,19 @@ public class JsonReader implements Closeable {
}
/**
- * Skips the next value recursively. If it is an object or array, all nested
- * elements are skipped. This method is intended for use when the JSON token
- * stream contains unrecognized or unhandled values.
+ * Skips the next value recursively. This method is intended for use when
+ * the JSON token stream contains unrecognized or unhandled values.
+ *
+ * <p>The behavior depends on the type of the next JSON token:
+ * <ul>
+ * <li>Start of a JSON array or object: It and all of its nested values are skipped.</li>
+ * <li>Primitive value (for example a JSON number): The primitive value is skipped.</li>
+ * <li>Property name: Only the name but not the value of the property is skipped.
+ * {@code skipValue()} has to be called again to skip the property value as well.</li>
+ * <li>End of a JSON array or object: Only this end token is skipped.</li>
+ * <li>End of JSON document: Skipping has no effect, the next token continues to be the
+ * end of the document.</li>
+ * </ul>
*/
public void skipValue() throws IOException {
int count = 0;
@@ -1242,32 +1254,69 @@ public class JsonReader implements Closeable {
p = doPeek();
}
- if (p == PEEKED_BEGIN_ARRAY) {
- push(JsonScope.EMPTY_ARRAY);
- count++;
- } else if (p == PEEKED_BEGIN_OBJECT) {
- push(JsonScope.EMPTY_OBJECT);
- count++;
- } else if (p == PEEKED_END_ARRAY) {
- stackSize--;
- count--;
- } else if (p == PEEKED_END_OBJECT) {
- stackSize--;
- count--;
- } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
- skipUnquotedValue();
- } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
- skipQuotedValue('\'');
- } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
- skipQuotedValue('"');
- } else if (p == PEEKED_NUMBER) {
- pos += peekedNumberLength;
+ switch (p) {
+ case PEEKED_BEGIN_ARRAY:
+ push(JsonScope.EMPTY_ARRAY);
+ count++;
+ break;
+ case PEEKED_BEGIN_OBJECT:
+ push(JsonScope.EMPTY_OBJECT);
+ count++;
+ break;
+ case PEEKED_END_ARRAY:
+ stackSize--;
+ count--;
+ break;
+ case PEEKED_END_OBJECT:
+ // Only update when object end is explicitly skipped, otherwise stack is not updated anyways
+ if (count == 0) {
+ pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected
+ }
+ stackSize--;
+ count--;
+ break;
+ case PEEKED_UNQUOTED:
+ skipUnquotedValue();
+ break;
+ case PEEKED_SINGLE_QUOTED:
+ skipQuotedValue('\'');
+ break;
+ case PEEKED_DOUBLE_QUOTED:
+ skipQuotedValue('"');
+ break;
+ case PEEKED_UNQUOTED_NAME:
+ skipUnquotedValue();
+ // Only update when name is explicitly skipped, otherwise stack is not updated anyways
+ if (count == 0) {
+ pathNames[stackSize - 1] = "<skipped>";
+ }
+ break;
+ case PEEKED_SINGLE_QUOTED_NAME:
+ skipQuotedValue('\'');
+ // Only update when name is explicitly skipped, otherwise stack is not updated anyways
+ if (count == 0) {
+ pathNames[stackSize - 1] = "<skipped>";
+ }
+ break;
+ case PEEKED_DOUBLE_QUOTED_NAME:
+ skipQuotedValue('"');
+ // Only update when name is explicitly skipped, otherwise stack is not updated anyways
+ if (count == 0) {
+ pathNames[stackSize - 1] = "<skipped>";
+ }
+ break;
+ case PEEKED_NUMBER:
+ pos += peekedNumberLength;
+ break;
+ case PEEKED_EOF:
+ // Do nothing
+ return;
+ // For all other tokens there is nothing to do; token has already been consumed from underlying reader
}
peeked = PEEKED_NONE;
- } while (count != 0);
+ } while (count > 0);
pathIndices[stackSize - 1]++;
- pathNames[stackSize - 1] = "null";
}
private void push(int newTop) {
@@ -1502,7 +1551,7 @@ public class JsonReader implements Closeable {
* <li>For JSON arrays the path points to the index of the previous element.<br>
* If no element has been consumed yet it uses the index 0 (even if there are no elements).</li>
* <li>For JSON objects the path points to the last property, or to the current
- * property if its value has not been consumed yet.</li>
+ * property if its name has already been consumed.</li>
* </ul>
*
* <p>This method can be useful to add additional context to exception messages
@@ -1519,7 +1568,7 @@ public class JsonReader implements Closeable {
* <li>For JSON arrays the path points to the index of the next element (even
* if there are no further elements).</li>
* <li>For JSON objects the path points to the last property, or to the current
- * property if its value has not been consumed yet.</li>
+ * property if its name has already been consumed.</li>
* </ul>
*
* <p>This method can be useful to add additional context to exception messages
@@ -1539,6 +1588,7 @@ public class JsonReader implements Closeable {
* @throws NumberFormatException if any unicode escape sequences are
* malformed.
*/
+ @SuppressWarnings("fallthrough")
private char readEscapeCharacter() throws IOException {
if (pos == limit && !fillBuffer(1)) {
throw syntaxError("Unterminated escape sequence");
diff --git a/gson/src/main/java/com/google/gson/stream/JsonScope.java b/gson/src/main/java/com/google/gson/stream/JsonScope.java
index da691372..9ec0bcf1 100644
--- a/gson/src/main/java/com/google/gson/stream/JsonScope.java
+++ b/gson/src/main/java/com/google/gson/stream/JsonScope.java
@@ -31,7 +31,7 @@ final class JsonScope {
static final int EMPTY_ARRAY = 1;
/**
- * A array with at least one value requires a comma and newline before
+ * An array with at least one value requires a comma and newline before
* the next element.
*/
static final int NONEMPTY_ARRAY = 2;
diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java
index c281009c..90e3529c 100644
--- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java
+++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java
@@ -16,6 +16,14 @@
package com.google.gson.stream;
+import static com.google.gson.stream.JsonScope.DANGLING_NAME;
+import static com.google.gson.stream.JsonScope.EMPTY_ARRAY;
+import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT;
+import static com.google.gson.stream.JsonScope.EMPTY_OBJECT;
+import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY;
+import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT;
+import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT;
+
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
@@ -23,29 +31,21 @@ import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;
-import static com.google.gson.stream.JsonScope.DANGLING_NAME;
-import static com.google.gson.stream.JsonScope.EMPTY_ARRAY;
-import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT;
-import static com.google.gson.stream.JsonScope.EMPTY_OBJECT;
-import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY;
-import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT;
-import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT;
-
/**
* Writes a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
* encoded value to a stream, one token at a time. The stream includes both
* literal values (strings, numbers, booleans and nulls) as well as the begin
* and end delimiters of objects and arrays.
*
- * <h3>Encoding JSON</h3>
- * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON
- * document must contain one top-level array or object. Call methods on the
- * writer as you walk the structure's contents, nesting arrays and objects as
- * necessary:
+ * <h2>Encoding JSON</h2>
+ * To encode your data as JSON, create a new {@code JsonWriter}. Call methods
+ * on the writer as you walk the structure's contents, nesting arrays and objects
+ * as necessary:
* <ul>
* <li>To write <strong>arrays</strong>, first call {@link #beginArray()}.
* Write each of the array's elements with the appropriate {@link #value}
@@ -58,7 +58,7 @@ import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT;
* Finally close the object using {@link #endObject()}.
* </ul>
*
- * <h3>Example</h3>
+ * <h2>Example</h2>
* Suppose we'd like to encode a stream of messages such as the following: <pre> {@code
* [
* {
@@ -153,7 +153,7 @@ public class JsonWriter implements Closeable, Flushable {
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
- REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
+ REPLACEMENT_CHARS[i] = String.format("\\u%04x", i);
}
REPLACEMENT_CHARS['"'] = "\\\"";
REPLACEMENT_CHARS['\\'] = "\\\\";
@@ -170,7 +170,7 @@ public class JsonWriter implements Closeable, Flushable {
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
- /** The output data, containing at most one top-level array or object. */
+ /** The JSON output destination */
private final Writer out;
private int[] stack = new int[32];
@@ -204,10 +204,7 @@ public class JsonWriter implements Closeable, Flushable {
* {@link java.io.BufferedWriter BufferedWriter} if necessary.
*/
public JsonWriter(Writer out) {
- if (out == null) {
- throw new NullPointerException("out == null");
- }
- this.out = out;
+ this.out = Objects.requireNonNull(out, "out == null");
}
/**
@@ -234,8 +231,6 @@ public class JsonWriter implements Closeable, Flushable {
* href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>. Setting the writer
* to lenient permits the following:
* <ul>
- * <li>Top-level values of any type. With strict writing, the top-level
- * value must be an object or an array.
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
* Double#isInfinite() infinities}.
* </ul>
@@ -390,9 +385,7 @@ public class JsonWriter implements Closeable, Flushable {
* @return this writer.
*/
public JsonWriter name(String name) throws IOException {
- if (name == null) {
- throw new NullPointerException("name == null");
- }
+ Objects.requireNonNull(name, "name == null");
if (deferredName != null) {
throw new IllegalStateException();
}
@@ -429,10 +422,14 @@ public class JsonWriter implements Closeable, Flushable {
/**
* Writes {@code value} directly to the writer without quoting or
- * escaping.
+ * escaping. This might not be supported by all implementations, if
+ * not supported an {@code UnsupportedOperationException} is thrown.
*
* @param value the literal string value, or null to encode a null literal.
* @return this writer.
+ * @throws UnsupportedOperationException if this writer does not support
+ * writing raw JSON values.
+ * @since 2.4
*/
public JsonWriter jsonValue(String value) throws IOException {
if (value == null) {
@@ -479,6 +476,7 @@ public class JsonWriter implements Closeable, Flushable {
* Encodes {@code value}.
*
* @return this writer.
+ * @since 2.7
*/
public JsonWriter value(Boolean value) throws IOException {
if (value == null) {
@@ -499,6 +497,7 @@ public class JsonWriter implements Closeable, Flushable {
* @return this writer.
* @throws IllegalArgumentException if the value is NaN or Infinity and this writer is not {@link
* #setLenient(boolean) lenient}.
+ * @since 2.9.1
*/
public JsonWriter value(float value) throws IOException {
writeDeferredName();
diff --git a/gson/src/test/java/com/google/gson/GsonBuilderTest.java b/gson/src/test/java/com/google/gson/GsonBuilderTest.java
index d1fd0d4f..e1a013b5 100644
--- a/gson/src/test/java/com/google/gson/GsonBuilderTest.java
+++ b/gson/src/test/java/com/google/gson/GsonBuilderTest.java
@@ -16,20 +16,25 @@
package com.google.gson;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.Type;
-
-import junit.framework.TestCase;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.fail;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import org.junit.Test;
/**
* Unit tests for {@link GsonBuilder}.
*
* @author Inderjeet Singh
*/
-public class GsonBuilderTest extends TestCase {
+public class GsonBuilderTest {
private static final TypeAdapter<Object> NULL_TYPE_ADAPTER = new TypeAdapter<Object>() {
@Override public void write(JsonWriter out, Object value) {
throw new AssertionError();
@@ -39,12 +44,106 @@ public class GsonBuilderTest extends TestCase {
}
};
+ @Test
public void testCreatingMoreThanOnce() {
GsonBuilder builder = new GsonBuilder();
- builder.create();
- builder.create();
+ Gson gson = builder.create();
+ assertNotNull(gson);
+ assertNotNull(builder.create());
+
+ builder.setFieldNamingStrategy(new FieldNamingStrategy() {
+ @Override public String translateName(Field f) {
+ return "test";
+ }
+ });
+
+ Gson otherGson = builder.create();
+ assertNotNull(otherGson);
+ // Should be different instances because builder has been modified in the meantime
+ assertNotSame(gson, otherGson);
+ }
+
+ /**
+ * Gson instances should not be affected by subsequent modification of GsonBuilder
+ * which created them.
+ */
+ @Test
+ public void testModificationAfterCreate() {
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ Gson gson = gsonBuilder.create();
+
+ // Modifications of `gsonBuilder` should not affect `gson` object
+ gsonBuilder.registerTypeAdapter(CustomClass1.class, new TypeAdapter<CustomClass1>() {
+ @Override public CustomClass1 read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override public void write(JsonWriter out, CustomClass1 value) throws IOException {
+ out.value("custom-adapter");
+ }
+ });
+ gsonBuilder.registerTypeHierarchyAdapter(CustomClass2.class, new JsonSerializer<CustomClass2>() {
+ @Override public JsonElement serialize(CustomClass2 src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom-hierarchy-adapter");
+ }
+ });
+ gsonBuilder.registerTypeAdapter(CustomClass3.class, new InstanceCreator<CustomClass3>() {
+ @Override public CustomClass3 createInstance(Type type) {
+ return new CustomClass3("custom-instance");
+ }
+ });
+
+ assertDefaultGson(gson);
+ // New GsonBuilder created from `gson` should not have been affected by changes
+ // to `gsonBuilder` either
+ assertDefaultGson(gson.newBuilder().create());
+
+ // New Gson instance from modified GsonBuilder should be affected by changes
+ assertCustomGson(gsonBuilder.create());
+ }
+
+ private static void assertDefaultGson(Gson gson) {
+ // Should use default reflective adapter
+ String json1 = gson.toJson(new CustomClass1());
+ assertEquals("{}", json1);
+
+ // Should use default reflective adapter
+ String json2 = gson.toJson(new CustomClass2());
+ assertEquals("{}", json2);
+
+ // Should use default instance creator
+ CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+ assertEquals(CustomClass3.NO_ARG_CONSTRUCTOR_VALUE, customClass3.s);
+ }
+
+ private static void assertCustomGson(Gson gson) {
+ String json1 = gson.toJson(new CustomClass1());
+ assertEquals("\"custom-adapter\"", json1);
+
+ String json2 = gson.toJson(new CustomClass2());
+ assertEquals("\"custom-hierarchy-adapter\"", json2);
+
+ CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+ assertEquals("custom-instance", customClass3.s);
+ }
+
+ static class CustomClass1 { }
+ static class CustomClass2 { }
+ static class CustomClass3 {
+ static final String NO_ARG_CONSTRUCTOR_VALUE = "default instance";
+
+ final String s;
+
+ public CustomClass3(String s) {
+ this.s = s;
+ }
+
+ public CustomClass3() {
+ this(NO_ARG_CONSTRUCTOR_VALUE);
+ }
}
+ @Test
public void testExcludeFieldsWithModifiers() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
@@ -52,20 +151,6 @@ public class GsonBuilderTest extends TestCase {
assertEquals("{\"d\":\"d\"}", gson.toJson(new HasModifiers()));
}
- public void testRegisterTypeAdapterForCoreType() {
- Type[] types = {
- byte.class,
- int.class,
- double.class,
- Short.class,
- Long.class,
- String.class,
- };
- for (Type type : types) {
- new GsonBuilder().registerTypeAdapter(type, NULL_TYPE_ADAPTER);
- }
- }
-
@SuppressWarnings("unused")
static class HasModifiers {
private String a = "a";
@@ -74,6 +159,7 @@ public class GsonBuilderTest extends TestCase {
String d = "d";
}
+ @Test
public void testTransientFieldExclusion() {
Gson gson = new GsonBuilder()
.excludeFieldsWithModifiers()
@@ -85,6 +171,22 @@ public class GsonBuilderTest extends TestCase {
transient String a = "a";
}
+ @Test
+ public void testRegisterTypeAdapterForCoreType() {
+ Type[] types = {
+ byte.class,
+ int.class,
+ double.class,
+ Short.class,
+ Long.class,
+ String.class,
+ };
+ for (Type type : types) {
+ new GsonBuilder().registerTypeAdapter(type, NULL_TYPE_ADAPTER);
+ }
+ }
+
+ @Test
public void testDisableJdkUnsafe() {
Gson gson = new GsonBuilder()
.disableJdkUnsafe()
@@ -107,4 +209,22 @@ public class GsonBuilderTest extends TestCase {
public ClassWithoutNoArgsConstructor(String s) {
}
}
+
+ @Test
+ public void testSetVersionInvalid() {
+ GsonBuilder builder = new GsonBuilder();
+ try {
+ builder.setVersion(Double.NaN);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Invalid version: NaN", e.getMessage());
+ }
+
+ try {
+ builder.setVersion(-0.1);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Invalid version: -0.1", e.getMessage());
+ }
+ }
}
diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java
index abb0de21..4274d26a 100644
--- a/gson/src/test/java/com/google/gson/GsonTest.java
+++ b/gson/src/test/java/com/google/gson/GsonTest.java
@@ -17,6 +17,7 @@
package com.google.gson;
import com.google.gson.internal.Excluder;
+import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
@@ -29,6 +30,8 @@ import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import junit.framework.TestCase;
/**
@@ -89,6 +92,69 @@ public final class GsonTest extends TestCase {
@Override public Object read(JsonReader in) throws IOException { return null; }
}
+ public void testGetAdapter_Null() {
+ Gson gson = new Gson();
+ try {
+ gson.getAdapter((TypeToken<?>) null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("type must not be null", e.getMessage());
+ }
+ }
+
+ public void testGetAdapter_Concurrency() {
+ class DummyAdapter<T> extends TypeAdapter<T> {
+ @Override public void write(JsonWriter out, T value) throws IOException {
+ throw new AssertionError("not needed for test");
+ }
+
+ @Override public T read(JsonReader in) throws IOException {
+ throw new AssertionError("not needed for test");
+ }
+ }
+
+ final AtomicInteger adapterInstancesCreated = new AtomicInteger(0);
+ final AtomicReference<TypeAdapter<?>> threadAdapter = new AtomicReference<>();
+ final Class<?> requestedType = Number.class;
+
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapterFactory(new TypeAdapterFactory() {
+ private volatile boolean isFirstCall = true;
+
+ @Override public <T> TypeAdapter<T> create(final Gson gson, TypeToken<T> type) {
+ if (isFirstCall) {
+ isFirstCall = false;
+
+ // Create a separate thread which requests an adapter for the same type
+ // This will cause this factory to return a different adapter instance than
+ // the one it is currently creating
+ Thread thread = new Thread() {
+ @Override public void run() {
+ threadAdapter.set(gson.getAdapter(requestedType));
+ }
+ };
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // Create a new dummy adapter instance
+ adapterInstancesCreated.incrementAndGet();
+ return new DummyAdapter<>();
+ }
+ })
+ .create();
+
+ TypeAdapter<?> adapter = gson.getAdapter(requestedType);
+ assertTrue(adapter instanceof DummyAdapter);
+ assertEquals(2, adapterInstancesCreated.get());
+ // Should be the same adapter instance the concurrent thread received
+ assertSame(threadAdapter.get(), adapter);
+ }
+
public void testNewJsonWriter_Default() throws IOException {
StringWriter writer = new StringWriter();
JsonWriter jsonWriter = new Gson().newJsonWriter(writer);
@@ -155,4 +221,151 @@ public final class GsonTest extends TestCase {
assertEquals("test", jsonReader.nextString());
jsonReader.close();
}
+
+ /**
+ * Modifying a GsonBuilder obtained from {@link Gson#newBuilder()} of a
+ * {@code new Gson()} should not affect the Gson instance it came from.
+ */
+ public void testDefaultGsonNewBuilderModification() {
+ Gson gson = new Gson();
+ GsonBuilder gsonBuilder = gson.newBuilder();
+
+ // Modifications of `gsonBuilder` should not affect `gson` object
+ gsonBuilder.registerTypeAdapter(CustomClass1.class, new TypeAdapter<CustomClass1>() {
+ @Override public CustomClass1 read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override public void write(JsonWriter out, CustomClass1 value) throws IOException {
+ out.value("custom-adapter");
+ }
+ });
+ gsonBuilder.registerTypeHierarchyAdapter(CustomClass2.class, new JsonSerializer<CustomClass2>() {
+ @Override public JsonElement serialize(CustomClass2 src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom-hierarchy-adapter");
+ }
+ });
+ gsonBuilder.registerTypeAdapter(CustomClass3.class, new InstanceCreator<CustomClass3>() {
+ @Override public CustomClass3 createInstance(Type type) {
+ return new CustomClass3("custom-instance");
+ }
+ });
+
+ assertDefaultGson(gson);
+ // New GsonBuilder created from `gson` should not have been affected by changes either
+ assertDefaultGson(gson.newBuilder().create());
+
+ // But new Gson instance from `gsonBuilder` should use custom adapters
+ assertCustomGson(gsonBuilder.create());
+ }
+
+ private static void assertDefaultGson(Gson gson) {
+ // Should use default reflective adapter
+ String json1 = gson.toJson(new CustomClass1());
+ assertEquals("{}", json1);
+
+ // Should use default reflective adapter
+ String json2 = gson.toJson(new CustomClass2());
+ assertEquals("{}", json2);
+
+ // Should use default instance creator
+ CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+ assertEquals(CustomClass3.NO_ARG_CONSTRUCTOR_VALUE, customClass3.s);
+ }
+
+ /**
+ * Modifying a GsonBuilder obtained from {@link Gson#newBuilder()} of a custom
+ * Gson instance (created using a GsonBuilder) should not affect the Gson instance
+ * it came from.
+ */
+ public void testNewBuilderModification() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(CustomClass1.class, new TypeAdapter<CustomClass1>() {
+ @Override public CustomClass1 read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override public void write(JsonWriter out, CustomClass1 value) throws IOException {
+ out.value("custom-adapter");
+ }
+ })
+ .registerTypeHierarchyAdapter(CustomClass2.class, new JsonSerializer<CustomClass2>() {
+ @Override public JsonElement serialize(CustomClass2 src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom-hierarchy-adapter");
+ }
+ })
+ .registerTypeAdapter(CustomClass3.class, new InstanceCreator<CustomClass3>() {
+ @Override public CustomClass3 createInstance(Type type) {
+ return new CustomClass3("custom-instance");
+ }
+ })
+ .create();
+
+ assertCustomGson(gson);
+
+ // Modify `gson.newBuilder()`
+ GsonBuilder gsonBuilder = gson.newBuilder();
+ gsonBuilder.registerTypeAdapter(CustomClass1.class, new TypeAdapter<CustomClass1>() {
+ @Override public CustomClass1 read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override public void write(JsonWriter out, CustomClass1 value) throws IOException {
+ out.value("overwritten custom-adapter");
+ }
+ });
+ gsonBuilder.registerTypeHierarchyAdapter(CustomClass2.class, new JsonSerializer<CustomClass2>() {
+ @Override public JsonElement serialize(CustomClass2 src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("overwritten custom-hierarchy-adapter");
+ }
+ });
+ gsonBuilder.registerTypeAdapter(CustomClass3.class, new InstanceCreator<CustomClass3>() {
+ @Override public CustomClass3 createInstance(Type type) {
+ return new CustomClass3("overwritten custom-instance");
+ }
+ });
+
+ // `gson` object should not have been affected by changes to new GsonBuilder
+ assertCustomGson(gson);
+ // New GsonBuilder based on `gson` should not have been affected either
+ assertCustomGson(gson.newBuilder().create());
+
+ // But new Gson instance from `gsonBuilder` should be affected by changes
+ Gson otherGson = gsonBuilder.create();
+ String json1 = otherGson.toJson(new CustomClass1());
+ assertEquals("\"overwritten custom-adapter\"", json1);
+
+ String json2 = otherGson.toJson(new CustomClass2());
+ assertEquals("\"overwritten custom-hierarchy-adapter\"", json2);
+
+ CustomClass3 customClass3 = otherGson.fromJson("{}", CustomClass3.class);
+ assertEquals("overwritten custom-instance", customClass3.s);
+ }
+
+ private static void assertCustomGson(Gson gson) {
+ String json1 = gson.toJson(new CustomClass1());
+ assertEquals("\"custom-adapter\"", json1);
+
+ String json2 = gson.toJson(new CustomClass2());
+ assertEquals("\"custom-hierarchy-adapter\"", json2);
+
+ CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+ assertEquals("custom-instance", customClass3.s);
+ }
+
+ static class CustomClass1 { }
+ static class CustomClass2 { }
+ static class CustomClass3 {
+ static final String NO_ARG_CONSTRUCTOR_VALUE = "default instance";
+
+ final String s;
+
+ public CustomClass3(String s) {
+ this.s = s;
+ }
+
+ public CustomClass3() {
+ this(NO_ARG_CONSTRUCTOR_VALUE);
+ }
+ }
}
diff --git a/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java b/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java
index 2e00dc9b..d92994fa 100644
--- a/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java
+++ b/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java
@@ -53,10 +53,16 @@ public class GsonTypeAdapterTest extends TestCase {
fail("Type Adapter should have thrown an exception");
} catch (IllegalStateException expected) { }
+ // Verify that serializer is made null-safe, i.e. it is not called for null
+ assertEquals("null", gson.toJson(null, AtomicLong.class));
+
try {
gson.fromJson("123", AtomicLong.class);
fail("Type Adapter should have thrown an exception");
} catch (JsonParseException expected) { }
+
+ // Verify that deserializer is made null-safe, i.e. it is not called for null
+ assertNull(gson.fromJson(JsonNull.INSTANCE, AtomicLong.class));
}
public void testTypeAdapterProperlyConvertsTypes() throws Exception {
diff --git a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
new file mode 100644
index 00000000..a1786ce6
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
@@ -0,0 +1,285 @@
+package com.google.gson;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gson.common.MoreAsserts;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+/**
+ * Tests for {@link JsonArray#asList()}.
+ */
+public class JsonArrayAsListTest {
+ @Test
+ public void testGet() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertEquals(new JsonPrimitive(1), list.get(0));
+
+ try {
+ list.get(-1);
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ try {
+ list.get(2);
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ a.add((JsonElement) null);
+ assertEquals(JsonNull.INSTANCE, list.get(1));
+ }
+
+ @Test
+ public void testSize() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertEquals(1, list.size());
+ list.add(new JsonPrimitive(2));
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void testSet() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ JsonElement old = list.set(0, new JsonPrimitive(2));
+ assertEquals(new JsonPrimitive(1), old);
+ assertEquals(new JsonPrimitive(2), list.get(0));
+ assertEquals(new JsonPrimitive(2), a.get(0));
+
+ try {
+ list.set(-1, new JsonPrimitive(1));
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ try {
+ list.set(2, new JsonPrimitive(1));
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ try {
+ list.set(0, null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("Element must be non-null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testAdd() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ list.add(0, new JsonPrimitive(2));
+ list.add(1, new JsonPrimitive(3));
+ assertTrue(list.add(new JsonPrimitive(4)));
+ assertTrue(list.add(JsonNull.INSTANCE));
+
+ List<JsonElement> expectedList = Arrays.<JsonElement>asList(
+ new JsonPrimitive(2),
+ new JsonPrimitive(3),
+ new JsonPrimitive(1),
+ new JsonPrimitive(4),
+ JsonNull.INSTANCE
+ );
+ assertEquals(expectedList, list);
+
+ try {
+ list.set(-1, new JsonPrimitive(1));
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ try {
+ list.set(list.size(), new JsonPrimitive(1));
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+
+ try {
+ list.add(0, null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("Element must be non-null", e.getMessage());
+ }
+ try {
+ list.add(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("Element must be non-null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testAddAll() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ list.addAll(Arrays.asList(new JsonPrimitive(2), new JsonPrimitive(3)));
+
+ List<JsonElement> expectedList = Arrays.<JsonElement>asList(
+ new JsonPrimitive(1),
+ new JsonPrimitive(2),
+ new JsonPrimitive(3)
+ );
+ assertEquals(expectedList, list);
+
+ try {
+ list.addAll(0, Collections.<JsonElement>singletonList(null));
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("Element must be non-null", e.getMessage());
+ }
+ try {
+ list.addAll(Collections.<JsonElement>singletonList(null));
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("Element must be non-null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testRemoveIndex() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertEquals(new JsonPrimitive(1), list.remove(0));
+ assertEquals(0, list.size());
+ assertEquals(0, a.size());
+
+ try {
+ list.remove(0);
+ fail();
+ } catch (IndexOutOfBoundsException e) {
+ }
+ }
+
+ @Test
+ public void testRemoveElement() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertTrue(list.remove(new JsonPrimitive(1)));
+ assertEquals(0, list.size());
+ assertEquals(0, a.size());
+
+ assertFalse(list.remove(new JsonPrimitive(1)));
+ assertFalse(list.remove(null));
+ }
+
+ @Test
+ public void testClear() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ list.clear();
+ assertEquals(0, list.size());
+ assertEquals(0, a.size());
+ }
+
+ @Test
+ public void testContains() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertTrue(list.contains(new JsonPrimitive(1)));
+ assertFalse(list.contains(new JsonPrimitive(2)));
+ assertFalse(list.contains(null));
+
+ @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+ boolean containsInt = list.contains(1); // should only contain JsonPrimitive(1)
+ assertFalse(containsInt);
+ }
+
+ @Test
+ public void testIndexOf() {
+ JsonArray a = new JsonArray();
+ // Add the same value twice to test indexOf vs. lastIndexOf
+ a.add(1);
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertEquals(0, list.indexOf(new JsonPrimitive(1)));
+ assertEquals(-1, list.indexOf(new JsonPrimitive(2)));
+ assertEquals(-1, list.indexOf(null));
+
+ @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+ int indexOfInt = list.indexOf(1); // should only contain JsonPrimitive(1)
+ assertEquals(-1, indexOfInt);
+
+ assertEquals(1, list.lastIndexOf(new JsonPrimitive(1)));
+ assertEquals(-1, list.lastIndexOf(new JsonPrimitive(2)));
+ assertEquals(-1, list.lastIndexOf(null));
+ }
+
+ @Test
+ public void testToArray() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ assertArrayEquals(new Object[] {new JsonPrimitive(1)}, list.toArray());
+
+ JsonElement[] array = list.toArray(new JsonElement[0]);
+ assertArrayEquals(new Object[] {new JsonPrimitive(1)}, array);
+
+ array = new JsonElement[1];
+ assertSame(array, list.toArray(array));
+ assertArrayEquals(new Object[] {new JsonPrimitive(1)}, array);
+
+ array = new JsonElement[] {null, new JsonPrimitive(2)};
+ assertSame(array, list.toArray(array));
+ // Should have set existing array element to null
+ assertArrayEquals(new Object[] {new JsonPrimitive(1), null}, array);
+ }
+
+ @Test
+ public void testEqualsHashCode() {
+ JsonArray a = new JsonArray();
+ a.add(1);
+
+ List<JsonElement> list = a.asList();
+ MoreAsserts.assertEqualsAndHashCode(list, Collections.singletonList(new JsonPrimitive(1)));
+ assertFalse(list.equals(Collections.emptyList()));
+ assertFalse(list.equals(Collections.singletonList(new JsonPrimitive(2))));
+ }
+
+ /** Verify that {@code JsonArray} updates are visible to view and vice versa */
+ @Test
+ public void testViewUpdates() {
+ JsonArray a = new JsonArray();
+ List<JsonElement> list = a.asList();
+
+ a.add(1);
+ assertEquals(1, list.size());
+ assertEquals(new JsonPrimitive(1), list.get(0));
+
+ list.add(new JsonPrimitive(2));
+ assertEquals(2, a.size());
+ assertEquals(new JsonPrimitive(2), a.get(1));
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/src/test/java/com/google/gson/JsonArrayTest.java
index 3975ce2c..45070e3f 100644
--- a/gson/src/test/java/com/google/gson/JsonArrayTest.java
+++ b/gson/src/test/java/com/google/gson/JsonArrayTest.java
@@ -16,18 +16,26 @@
package com.google.gson;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
import com.google.gson.common.MoreAsserts;
-import junit.framework.TestCase;
+import java.math.BigInteger;
+import org.junit.Test;
/**
* @author Jesse Wilson
*/
-public final class JsonArrayTest extends TestCase {
+public final class JsonArrayTest {
+ @Test
public void testEqualsOnEmptyArray() {
MoreAsserts.assertEqualsAndHashCode(new JsonArray(), new JsonArray());
}
+ @Test
public void testEqualsNonEmptyArray() {
JsonArray a = new JsonArray();
JsonArray b = new JsonArray();
@@ -50,6 +58,7 @@ public final class JsonArrayTest extends TestCase {
assertFalse(b.equals(a));
}
+ @Test
public void testRemove() {
JsonArray array = new JsonArray();
try {
@@ -67,6 +76,7 @@ public final class JsonArrayTest extends TestCase {
assertTrue(array.contains(a));
}
+ @Test
public void testSet() {
JsonArray array = new JsonArray();
try {
@@ -75,15 +85,23 @@ public final class JsonArrayTest extends TestCase {
} catch (IndexOutOfBoundsException expected) {}
JsonPrimitive a = new JsonPrimitive("a");
array.add(a);
- array.set(0, new JsonPrimitive("b"));
+
+ JsonPrimitive b = new JsonPrimitive("b");
+ JsonElement oldValue = array.set(0, b);
+ assertEquals(a, oldValue);
assertEquals("b", array.get(0).getAsString());
- array.set(0, null);
- assertNull(array.get(0));
- array.set(0, new JsonPrimitive("c"));
+
+ oldValue = array.set(0, null);
+ assertEquals(b, oldValue);
+ assertEquals(JsonNull.INSTANCE, array.get(0));
+
+ oldValue = array.set(0, new JsonPrimitive("c"));
+ assertEquals(JsonNull.INSTANCE, oldValue);
assertEquals("c", array.get(0).getAsString());
assertEquals(1, array.size());
}
+ @Test
public void testDeepCopy() {
JsonArray original = new JsonArray();
JsonArray firstEntry = new JsonArray();
@@ -99,6 +117,20 @@ public final class JsonArrayTest extends TestCase {
assertEquals(0, copy.get(0).getAsJsonArray().size());
}
+ @Test
+ public void testIsEmpty() {
+ JsonArray array = new JsonArray();
+ assertTrue(array.isEmpty());
+
+ JsonPrimitive a = new JsonPrimitive("a");
+ array.add(a);
+ assertFalse(array.isEmpty());
+
+ array.remove(0);
+ assertTrue(array.isEmpty());
+ }
+
+ @Test
public void testFailedGetArrayValues() {
JsonArray jsonArray = new JsonArray();
jsonArray.add(JsonParser.parseString("{" + "\"key1\":\"value1\"," + "\"key2\":\"value2\"," + "\"key3\":\"value3\"," + "\"key4\":\"value4\"" + "}"));
@@ -162,4 +194,180 @@ public final class JsonArrayTest extends TestCase {
"For input string: \"hello\"", e.getMessage());
}
}
+
+ @Test
+ public void testGetAs_WrongArraySize() {
+ JsonArray jsonArray = new JsonArray();
+ try {
+ jsonArray.getAsByte();
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Array must have size 1, but has size 0", e.getMessage());
+ }
+
+ jsonArray.add(true);
+ jsonArray.add(false);
+ try {
+ jsonArray.getAsByte();
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Array must have size 1, but has size 2", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testStringPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add("Hello");
+ jsonArray.add("Goodbye");
+ jsonArray.add("Thank you");
+ jsonArray.add((String) null);
+ jsonArray.add("Yes");
+
+ assertEquals("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]", jsonArray.toString());
+ }
+
+ @Test
+ public void testIntegerPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ int x = 1;
+ jsonArray.add(x);
+
+ x = 2;
+ jsonArray.add(x);
+
+ x = -3;
+ jsonArray.add(x);
+
+ jsonArray.add((Integer) null);
+
+ x = 4;
+ jsonArray.add(x);
+
+ x = 0;
+ jsonArray.add(x);
+
+ assertEquals("[1,2,-3,null,4,0]", jsonArray.toString());
+ }
+
+ @Test
+ public void testDoublePrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ double x = 1.0;
+ jsonArray.add(x);
+
+ x = 2.13232;
+ jsonArray.add(x);
+
+ x = 0.121;
+ jsonArray.add(x);
+
+ jsonArray.add((Double) null);
+
+ x = -0.00234;
+ jsonArray.add(x);
+
+ jsonArray.add((Double) null);
+
+ assertEquals("[1.0,2.13232,0.121,null,-0.00234,null]", jsonArray.toString());
+ }
+
+ @Test
+ public void testBooleanPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add(true);
+ jsonArray.add(true);
+ jsonArray.add(false);
+ jsonArray.add(false);
+ jsonArray.add((Boolean) null);
+ jsonArray.add(true);
+
+ assertEquals("[true,true,false,false,null,true]", jsonArray.toString());
+ }
+
+ @Test
+ public void testCharPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add('a');
+ jsonArray.add('e');
+ jsonArray.add('i');
+ jsonArray.add((char) 111);
+ jsonArray.add((Character) null);
+ jsonArray.add('u');
+ jsonArray.add("and sometimes Y");
+
+ assertEquals("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]", jsonArray.toString());
+ }
+
+ @Test
+ public void testMixedPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add('a');
+ jsonArray.add("apple");
+ jsonArray.add(12121);
+ jsonArray.add((char) 111);
+
+ jsonArray.add((Boolean) null);
+ assertEquals(JsonNull.INSTANCE, jsonArray.get(jsonArray.size() - 1));
+
+ jsonArray.add((Character) null);
+ assertEquals(JsonNull.INSTANCE, jsonArray.get(jsonArray.size() - 1));
+
+ jsonArray.add(12.232);
+ jsonArray.add(BigInteger.valueOf(2323));
+
+ assertEquals("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]", jsonArray.toString());
+ }
+
+ @Test
+ public void testNullPrimitiveAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add((Character) null);
+ jsonArray.add((Boolean) null);
+ jsonArray.add((Integer) null);
+ jsonArray.add((Double) null);
+ jsonArray.add((Float) null);
+ jsonArray.add((BigInteger) null);
+ jsonArray.add((String) null);
+ jsonArray.add((Boolean) null);
+ jsonArray.add((Number) null);
+
+ assertEquals("[null,null,null,null,null,null,null,null,null]", jsonArray.toString());
+ for (int i = 0; i < jsonArray.size(); i++) {
+ // Verify that they are actually a JsonNull and not a Java null
+ assertEquals(JsonNull.INSTANCE, jsonArray.get(i));
+ }
+ }
+
+ @Test
+ public void testNullJsonElementAddition() {
+ JsonArray jsonArray = new JsonArray();
+ jsonArray.add((JsonElement) null);
+ assertEquals(JsonNull.INSTANCE, jsonArray.get(0));
+ }
+
+ @Test
+ public void testSameAddition() {
+ JsonArray jsonArray = new JsonArray();
+
+ jsonArray.add('a');
+ jsonArray.add('a');
+ jsonArray.add(true);
+ jsonArray.add(true);
+ jsonArray.add(1212);
+ jsonArray.add(1212);
+ jsonArray.add(34.34);
+ jsonArray.add(34.34);
+ jsonArray.add((Boolean) null);
+ jsonArray.add((Boolean) null);
+
+ assertEquals("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]", jsonArray.toString());
+ }
}
diff --git a/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
new file mode 100644
index 00000000..00a89a6f
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
@@ -0,0 +1,287 @@
+package com.google.gson;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gson.common.MoreAsserts;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.junit.Test;
+
+/**
+ * Tests for {@link JsonObject#asMap()}.
+ */
+public class JsonObjectAsMapTest {
+ @Test
+ public void testSize() {
+ JsonObject o = new JsonObject();
+ assertEquals(0, o.asMap().size());
+
+ o.addProperty("a", 1);
+ Map<String, JsonElement> map = o.asMap();
+ assertEquals(1, map.size());
+
+ map.clear();
+ assertEquals(0, map.size());
+ assertEquals(0, o.size());
+ }
+
+ @Test
+ public void testContainsKey() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ assertTrue(map.containsKey("a"));
+ assertFalse(map.containsKey("b"));
+ assertFalse(map.containsKey(null));
+ }
+
+ @Test
+ public void testContainsValue() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+ o.add("b", JsonNull.INSTANCE);
+
+ Map<String, JsonElement> map = o.asMap();
+ assertTrue(map.containsValue(new JsonPrimitive(1)));
+ assertFalse(map.containsValue(new JsonPrimitive(2)));
+ assertFalse(map.containsValue(null));
+
+ @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+ boolean containsInt = map.containsValue(1); // should only contain JsonPrimitive(1)
+ assertFalse(containsInt);
+ }
+
+ @Test
+ public void testGet() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ assertEquals(new JsonPrimitive(1), map.get("a"));
+ assertNull(map.get("b"));
+ assertNull(map.get(null));
+ }
+
+ @Test
+ public void testPut() {
+ JsonObject o = new JsonObject();
+ Map<String, JsonElement> map = o.asMap();
+
+ assertNull(map.put("a", new JsonPrimitive(1)));
+ assertEquals(1, map.size());
+ assertEquals(new JsonPrimitive(1), map.get("a"));
+
+ JsonElement old = map.put("a", new JsonPrimitive(2));
+ assertEquals(new JsonPrimitive(1), old);
+ assertEquals(1, map.size());
+ assertEquals(new JsonPrimitive(2), map.get("a"));
+ assertEquals(new JsonPrimitive(2), o.get("a"));
+
+ assertNull(map.put("b", JsonNull.INSTANCE));
+ assertEquals(JsonNull.INSTANCE, map.get("b"));
+
+ try {
+ map.put(null, new JsonPrimitive(1));
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("key == null", e.getMessage());
+ }
+
+ try {
+ map.put("a", null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ assertNull(map.remove("b"));
+ assertEquals(1, map.size());
+
+ JsonElement old = map.remove("a");
+ assertEquals(new JsonPrimitive(1), old);
+ assertEquals(0, map.size());
+
+ assertNull(map.remove("a"));
+ assertEquals(0, map.size());
+ assertEquals(0, o.size());
+
+ assertNull(map.remove(null));
+ }
+
+ @Test
+ public void testPutAll() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> otherMap = new HashMap<>();
+ otherMap.put("a", new JsonPrimitive(2));
+ otherMap.put("b", new JsonPrimitive(3));
+
+ Map<String, JsonElement> map = o.asMap();
+ map.putAll(otherMap);
+ assertEquals(2, map.size());
+ assertEquals(new JsonPrimitive(2), map.get("a"));
+ assertEquals(new JsonPrimitive(3), map.get("b"));
+
+ try {
+ map.putAll(Collections.<String, JsonElement>singletonMap(null, new JsonPrimitive(1)));
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("key == null", e.getMessage());
+ }
+
+ try {
+ map.putAll(Collections.<String, JsonElement>singletonMap("a", null));
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testClear() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ map.clear();
+ assertEquals(0, map.size());
+ assertEquals(0, o.size());
+ }
+
+ @Test
+ public void testKeySet() {
+ JsonObject o = new JsonObject();
+ o.addProperty("b", 1);
+ o.addProperty("a", 2);
+
+ Map<String, JsonElement> map = o.asMap();
+ Set<String> keySet = map.keySet();
+ // Should contain keys in same order
+ assertEquals(Arrays.asList("b", "a"), new ArrayList<>(keySet));
+
+ // Key set doesn't support insertions
+ try {
+ keySet.add("c");
+ fail();
+ } catch (UnsupportedOperationException e) {
+ }
+
+ assertTrue(keySet.remove("a"));
+ assertEquals(Collections.singleton("b"), map.keySet());
+ assertEquals(Collections.singleton("b"), o.keySet());
+ }
+
+ @Test
+ public void testValues() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 2);
+ o.addProperty("b", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ Collection<JsonElement> values = map.values();
+ // Should contain values in same order
+ assertEquals(Arrays.asList(new JsonPrimitive(2), new JsonPrimitive(1)), new ArrayList<>(values));
+
+ // Values collection doesn't support insertions
+ try {
+ values.add(new JsonPrimitive(3));
+ fail();
+ } catch (UnsupportedOperationException e) {
+ }
+
+ assertTrue(values.remove(new JsonPrimitive(2)));
+ assertEquals(Collections.singletonList(new JsonPrimitive(1)), new ArrayList<>(map.values()));
+ assertEquals(1, o.size());
+ assertEquals(new JsonPrimitive(1), o.get("b"));
+ }
+
+ @Test
+ public void testEntrySet() {
+ JsonObject o = new JsonObject();
+ o.addProperty("b", 2);
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ Set<Entry<String, JsonElement>> entrySet = map.entrySet();
+
+ List<Entry<?, ?>> expectedEntrySet = Arrays.<Entry<?, ?>>asList(
+ new SimpleEntry<>("b", new JsonPrimitive(2)),
+ new SimpleEntry<>("a", new JsonPrimitive(1))
+ );
+ // Should contain entries in same order
+ assertEquals(expectedEntrySet, new ArrayList<>(entrySet));
+
+ try {
+ entrySet.add(new SimpleEntry<String, JsonElement>("c", new JsonPrimitive(3)));
+ fail();
+ } catch (UnsupportedOperationException e) {
+ }
+
+ assertTrue(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1))));
+ assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))), map.entrySet());
+ assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))), o.entrySet());
+
+ // Should return false because entry has already been removed
+ assertFalse(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1))));
+
+ Entry<String, JsonElement> entry = entrySet.iterator().next();
+ JsonElement old = entry.setValue(new JsonPrimitive(3));
+ assertEquals(new JsonPrimitive(2), old);
+ assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))), map.entrySet());
+ assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))), o.entrySet());
+
+ try {
+ entry.setValue(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testEqualsHashCode() {
+ JsonObject o = new JsonObject();
+ o.addProperty("a", 1);
+
+ Map<String, JsonElement> map = o.asMap();
+ MoreAsserts.assertEqualsAndHashCode(map, Collections.singletonMap("a", new JsonPrimitive(1)));
+ assertFalse(map.equals(Collections.emptyMap()));
+ assertFalse(map.equals(Collections.singletonMap("a", new JsonPrimitive(2))));
+ }
+
+ /** Verify that {@code JsonObject} updates are visible to view and vice versa */
+ @Test
+ public void testViewUpdates() {
+ JsonObject o = new JsonObject();
+ Map<String, JsonElement> map = o.asMap();
+
+ o.addProperty("a", 1);
+ assertEquals(1, map.size());
+ assertEquals(new JsonPrimitive(1), map.get("a"));
+
+ map.put("b", new JsonPrimitive(2));
+ assertEquals(2, o.size());
+ assertEquals(new JsonPrimitive(2), o.get("b"));
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/JsonObjectTest.java b/gson/src/test/java/com/google/gson/JsonObjectTest.java
index 6f5274fc..a0109ba8 100644
--- a/gson/src/test/java/com/google/gson/JsonObjectTest.java
+++ b/gson/src/test/java/com/google/gson/JsonObjectTest.java
@@ -16,17 +16,34 @@
package com.google.gson;
-import com.google.gson.common.MoreAsserts;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
-import junit.framework.TestCase;
+import com.google.gson.common.MoreAsserts;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.junit.Test;
/**
* Unit test for the {@link JsonObject} class.
*
* @author Joel Leitch
*/
-public class JsonObjectTest extends TestCase {
+public class JsonObjectTest {
+ @Test
public void testAddingAndRemovingObjectProperties() throws Exception {
JsonObject jsonObj = new JsonObject();
String propertyName = "property";
@@ -41,8 +58,11 @@ public class JsonObjectTest extends TestCase {
assertEquals(value, removedElement);
assertFalse(jsonObj.has(propertyName));
assertNull(jsonObj.get(propertyName));
+
+ assertNull(jsonObj.remove(propertyName));
}
+ @Test
public void testAddingNullPropertyValue() throws Exception {
String propertyName = "property";
JsonObject jsonObj = new JsonObject();
@@ -55,6 +75,7 @@ public class JsonObjectTest extends TestCase {
assertTrue(jsonElement.isJsonNull());
}
+ @Test
public void testAddingNullOrEmptyPropertyName() throws Exception {
JsonObject jsonObj = new JsonObject();
try {
@@ -66,6 +87,7 @@ public class JsonObjectTest extends TestCase {
jsonObj.add(" \t", JsonNull.INSTANCE);
}
+ @Test
public void testAddingBooleanProperties() throws Exception {
String propertyName = "property";
JsonObject jsonObj = new JsonObject();
@@ -78,6 +100,7 @@ public class JsonObjectTest extends TestCase {
assertTrue(jsonElement.getAsBoolean());
}
+ @Test
public void testAddingStringProperties() throws Exception {
String propertyName = "property";
String value = "blah";
@@ -92,6 +115,7 @@ public class JsonObjectTest extends TestCase {
assertEquals(value, jsonElement.getAsString());
}
+ @Test
public void testAddingCharacterProperties() throws Exception {
String propertyName = "property";
char value = 'a';
@@ -113,6 +137,7 @@ public class JsonObjectTest extends TestCase {
/**
* From bug report http://code.google.com/p/google-gson/issues/detail?id=182
*/
+ @Test
public void testPropertyWithQuotes() {
JsonObject jsonObj = new JsonObject();
jsonObj.add("a\"b", new JsonPrimitive("c\"d"));
@@ -123,6 +148,7 @@ public class JsonObjectTest extends TestCase {
/**
* From issue 227.
*/
+ @Test
public void testWritePropertyWithEmptyStringName() {
JsonObject jsonObj = new JsonObject();
jsonObj.add("", new JsonPrimitive(true));
@@ -130,15 +156,18 @@ public class JsonObjectTest extends TestCase {
}
+ @Test
public void testReadPropertyWithEmptyStringName() {
JsonObject jsonObj = JsonParser.parseString("{\"\":true}").getAsJsonObject();
assertEquals(true, jsonObj.get("").getAsBoolean());
}
+ @Test
public void testEqualsOnEmptyObject() {
MoreAsserts.assertEqualsAndHashCode(new JsonObject(), new JsonObject());
}
+ @Test
public void testEqualsNonEmptyObject() {
JsonObject a = new JsonObject();
JsonObject b = new JsonObject();
@@ -161,6 +190,24 @@ public class JsonObjectTest extends TestCase {
assertFalse(b.equals(a));
}
+ @Test
+ public void testEqualsHashCodeIgnoringOrder() {
+ JsonObject a = new JsonObject();
+ JsonObject b = new JsonObject();
+
+ a.addProperty("1", true);
+ b.addProperty("2", false);
+
+ a.addProperty("2", false);
+ b.addProperty("1", true);
+
+ assertEquals(Arrays.asList("1", "2"), new ArrayList<>(a.keySet()));
+ assertEquals(Arrays.asList("2", "1"), new ArrayList<>(b.keySet()));
+
+ MoreAsserts.assertEqualsAndHashCode(a, b);
+ }
+
+ @Test
public void testSize() {
JsonObject o = new JsonObject();
assertEquals(0, o.size());
@@ -175,6 +222,7 @@ public class JsonObjectTest extends TestCase {
assertEquals(1, o.size());
}
+ @Test
public void testDeepCopy() {
JsonObject original = new JsonObject();
JsonArray firstEntry = new JsonArray();
@@ -190,8 +238,10 @@ public class JsonObjectTest extends TestCase {
/**
* From issue 941
*/
+ @Test
public void testKeySet() {
JsonObject a = new JsonObject();
+ assertEquals(0, a.keySet().size());
a.add("foo", new JsonArray());
a.add("bar", new JsonObject());
@@ -200,5 +250,95 @@ public class JsonObjectTest extends TestCase {
assertEquals(2, a.keySet().size());
assertTrue(a.keySet().contains("foo"));
assertTrue(a.keySet().contains("bar"));
+
+ a.addProperty("1", true);
+ a.addProperty("2", false);
+
+ // Insertion order should be preserved by keySet()
+ Deque<String> expectedKeys = new ArrayDeque<>(Arrays.asList("foo", "bar", "1", "2"));
+ // Note: Must wrap in ArrayList because Deque implementations do not implement `equals`
+ assertEquals(new ArrayList<>(expectedKeys), new ArrayList<>(a.keySet()));
+ Iterator<String> iterator = a.keySet().iterator();
+
+ // Remove keys one by one
+ for (int i = a.size(); i >= 1; i--) {
+ assertTrue(iterator.hasNext());
+ assertEquals(expectedKeys.getFirst(), iterator.next());
+ iterator.remove();
+ expectedKeys.removeFirst();
+
+ assertEquals(i - 1, a.size());
+ assertEquals(new ArrayList<>(expectedKeys), new ArrayList<>(a.keySet()));
+ }
+ }
+
+ @Test
+ public void testEntrySet() {
+ JsonObject o = new JsonObject();
+ assertEquals(0, o.entrySet().size());
+
+ o.addProperty("b", true);
+ Set<?> expectedEntries = Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(true)));
+ assertEquals(expectedEntries, o.entrySet());
+ assertEquals(1, o.entrySet().size());
+
+ o.addProperty("a", false);
+ // Insertion order should be preserved by entrySet()
+ List<?> expectedEntriesList = Arrays.asList(
+ new SimpleEntry<>("b", new JsonPrimitive(true)),
+ new SimpleEntry<>("a", new JsonPrimitive(false))
+ );
+ assertEquals(expectedEntriesList, new ArrayList<>(o.entrySet()));
+
+ Iterator<Entry<String, JsonElement>> iterator = o.entrySet().iterator();
+ // Test behavior of Entry.setValue
+ for (int i = 0; i < o.size(); i++) {
+ Entry<String, JsonElement> entry = iterator.next();
+ entry.setValue(new JsonPrimitive(i));
+
+ assertEquals(new JsonPrimitive(i), entry.getValue());
+ }
+
+ expectedEntriesList = Arrays.asList(
+ new SimpleEntry<>("b", new JsonPrimitive(0)),
+ new SimpleEntry<>("a", new JsonPrimitive(1))
+ );
+ assertEquals(expectedEntriesList, new ArrayList<>(o.entrySet()));
+
+ Entry<String, JsonElement> entry = o.entrySet().iterator().next();
+ try {
+ // null value is not permitted, only JsonNull is supported
+ // This intentionally deviates from the behavior of the other JsonObject methods which
+ // implicitly convert null -> JsonNull, to match more closely the contract of Map.Entry
+ entry.setValue(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ assertNotNull(entry.getValue());
+
+ o.addProperty("key1", 1);
+ o.addProperty("key2", 2);
+
+ Deque<?> expectedEntriesQueue = new ArrayDeque<>(Arrays.asList(
+ new SimpleEntry<>("b", new JsonPrimitive(0)),
+ new SimpleEntry<>("a", new JsonPrimitive(1)),
+ new SimpleEntry<>("key1", new JsonPrimitive(1)),
+ new SimpleEntry<>("key2", new JsonPrimitive(2))
+ ));
+ // Note: Must wrap in ArrayList because Deque implementations do not implement `equals`
+ assertEquals(new ArrayList<>(expectedEntriesQueue), new ArrayList<>(o.entrySet()));
+ iterator = o.entrySet().iterator();
+
+ // Remove entries one by one
+ for (int i = o.size(); i >= 1; i--) {
+ assertTrue(iterator.hasNext());
+ assertEquals(expectedEntriesQueue.getFirst(), iterator.next());
+ iterator.remove();
+ expectedEntriesQueue.removeFirst();
+
+ assertEquals(i - 1, o.size());
+ assertEquals(new ArrayList<>(expectedEntriesQueue), new ArrayList<>(o.entrySet()));
+ }
}
}
diff --git a/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java b/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java
index cdd4fdb6..ae2e0f2a 100644
--- a/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java
+++ b/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java
@@ -17,11 +17,9 @@
package com.google.gson;
import com.google.gson.common.MoreAsserts;
-
-import junit.framework.TestCase;
-
import java.math.BigDecimal;
import java.math.BigInteger;
+import junit.framework.TestCase;
/**
* Unit test for the {@link JsonPrimitive} class.
@@ -98,6 +96,17 @@ public class JsonPrimitiveTest extends TestCase {
assertEquals(new BigDecimal("1"), json.getAsBigDecimal());
}
+ public void testAsNumber_Boolean() {
+ JsonPrimitive json = new JsonPrimitive(true);
+ try {
+ json.getAsNumber();
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Primitive is neither a number nor a string", e.getMessage());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
public void testStringsAndChar() throws Exception {
JsonPrimitive json = new JsonPrimitive("abc");
assertTrue(json.isString());
@@ -111,6 +120,15 @@ public class JsonPrimitiveTest extends TestCase {
json = new JsonPrimitive(true);
assertEquals("true", json.getAsString());
+
+ json = new JsonPrimitive("");
+ assertEquals("", json.getAsString());
+ try {
+ json.getAsCharacter();
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("String value is empty", e.getMessage());
+ }
}
public void testExponential() throws Exception {
@@ -256,7 +274,7 @@ public class JsonPrimitiveTest extends TestCase {
public void testEqualsIntegerAndBigInteger() {
JsonPrimitive a = new JsonPrimitive(5L);
JsonPrimitive b = new JsonPrimitive(new BigInteger("18446744073709551621")); // 2^64 + 5
- // Ideally, the following assertion should have failed but the price is too much to pay
+ // Ideally, the following assertion should have failed but the price is too much to pay
// assertFalse(a + " equals " + b, a.equals(b));
assertTrue(a + " equals " + b, a.equals(b));
}
diff --git a/gson/src/test/java/com/google/gson/MixedStreamTest.java b/gson/src/test/java/com/google/gson/MixedStreamTest.java
index 00eb4bc8..fa16659f 100644
--- a/gson/src/test/java/com/google/gson/MixedStreamTest.java
+++ b/gson/src/test/java/com/google/gson/MixedStreamTest.java
@@ -174,7 +174,7 @@ public final class MixedStreamTest extends TestCase {
} catch (NullPointerException expected) {
}
try {
- gson.fromJson(new JsonReader(new StringReader("true")), null);
+ gson.fromJson(new JsonReader(new StringReader("true")), (Type) null);
fail();
} catch (NullPointerException expected) {
}
diff --git a/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/src/test/java/com/google/gson/TypeAdapterTest.java
index ab446373..725ecdae 100644
--- a/gson/src/test/java/com/google/gson/TypeAdapterTest.java
+++ b/gson/src/test/java/com/google/gson/TypeAdapterTest.java
@@ -2,6 +2,7 @@ package com.google.gson;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -26,6 +27,38 @@ public class TypeAdapterTest {
assertNull(adapter.fromJson("null"));
}
+ /**
+ * Tests behavior when {@link TypeAdapter#write(JsonWriter, Object)} manually throws
+ * {@link IOException} which is not caused by writer usage.
+ */
+ @Test
+ public void testToJson_ThrowingIOException() {
+ final IOException exception = new IOException("test");
+ TypeAdapter<Integer> adapter = new TypeAdapter<Integer>() {
+ @Override public void write(JsonWriter out, Integer value) throws IOException {
+ throw exception;
+ }
+
+ @Override public Integer read(JsonReader in) throws IOException {
+ throw new AssertionError("not needed by this test");
+ }
+ };
+
+ try {
+ adapter.toJson(1);
+ fail();
+ } catch (JsonIOException e) {
+ assertEquals(exception, e.getCause());
+ }
+
+ try {
+ adapter.toJsonTree(1);
+ fail();
+ } catch (JsonIOException e) {
+ assertEquals(exception, e.getCause());
+ }
+ }
+
private static final TypeAdapter<String> adapter = new TypeAdapter<String>() {
@Override public void write(JsonWriter out, String value) throws IOException {
out.value(value);
diff --git a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
index d878850e..2b3fbafa 100644
--- a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
+++ b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
@@ -16,40 +16,82 @@
package com.google.gson;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
import com.google.gson.annotations.Since;
+import com.google.gson.annotations.Until;
import com.google.gson.internal.Excluder;
-import junit.framework.TestCase;
+import org.junit.Test;
/**
* Unit tests for the {@link Excluder} class.
*
* @author Joel Leitch
*/
-public class VersionExclusionStrategyTest extends TestCase {
+public class VersionExclusionStrategyTest {
private static final double VERSION = 5.0D;
- public void testClassAndFieldAreAtSameVersion() throws Exception {
+ @Test
+ public void testSameVersion() throws Exception {
Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
- assertFalse(excluder.excludeClass(MockObject.class, true));
- assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
+ assertFalse(excluder.excludeClass(MockClassSince.class, true));
+ assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
+
+ // Until version is exclusive
+ assertTrue(excluder.excludeClass(MockClassUntil.class, true));
+ assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
+
+ assertFalse(excluder.excludeClass(MockClassBoth.class, true));
+ assertFalse(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
}
- public void testClassAndFieldAreBehindInVersion() throws Exception {
- Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1);
- assertFalse(excluder.excludeClass(MockObject.class, true));
- assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
+ @Test
+ public void testNewerVersion() throws Exception {
+ Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
+ assertFalse(excluder.excludeClass(MockClassSince.class, true));
+ assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
+
+ assertTrue(excluder.excludeClass(MockClassUntil.class, true));
+ assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
+
+ assertTrue(excluder.excludeClass(MockClassBoth.class, true));
+ assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
+ }
+
+ @Test
+ public void testOlderVersion() throws Exception {
+ Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
+ assertTrue(excluder.excludeClass(MockClassSince.class, true));
+ assertTrue(excluder.excludeField(MockClassSince.class.getField("someField"), true));
+
+ assertFalse(excluder.excludeClass(MockClassUntil.class, true));
+ assertFalse(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
+
+ assertTrue(excluder.excludeClass(MockClassBoth.class, true));
+ assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
}
- public void testClassAndFieldAreAheadInVersion() throws Exception {
- Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1);
- assertTrue(excluder.excludeClass(MockObject.class, true));
- assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true));
+ @Since(VERSION)
+ private static class MockClassSince {
+
+ @Since(VERSION)
+ public final int someField = 0;
+ }
+
+ @Until(VERSION)
+ private static class MockClassUntil {
+
+ @Until(VERSION)
+ public final int someField = 0;
}
@Since(VERSION)
- private static class MockObject {
+ @Until(VERSION + 2)
+ private static class MockClassBoth {
@Since(VERSION)
+ @Until(VERSION + 2)
public final int someField = 0;
}
}
diff --git a/gson/src/test/java/com/google/gson/common/MoreAsserts.java b/gson/src/test/java/com/google/gson/common/MoreAsserts.java
index f409480d..f69802cb 100644
--- a/gson/src/test/java/com/google/gson/common/MoreAsserts.java
+++ b/gson/src/test/java/com/google/gson/common/MoreAsserts.java
@@ -16,9 +16,13 @@
package com.google.gson.common;
-import org.junit.Assert;
-
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.Assert;
/**
* Handy asserts that we wish were present in {@link Assert}
@@ -49,4 +53,53 @@ public class MoreAsserts {
Assert.assertFalse(a.equals(null));
Assert.assertFalse(a.equals(new Object()));
}
+
+ private static boolean isProtectedOrPublic(Method method) {
+ int modifiers = method.getModifiers();
+ return Modifier.isProtected(modifiers) || Modifier.isPublic(modifiers);
+ }
+
+ private static String getMethodSignature(Method method) {
+ StringBuilder builder = new StringBuilder(method.getName());
+ builder.append('(');
+
+ String sep = "";
+ for (Class<?> paramType : method.getParameterTypes()) {
+ builder.append(sep).append(paramType.getName());
+ sep = ",";
+ }
+
+ builder.append(')');
+ return builder.toString();
+ }
+
+ /**
+ * Asserts that {@code subClass} overrides all protected and public methods declared by
+ * {@code baseClass} except for the ones whose signatures are in {@code ignoredMethods}.
+ */
+ public static void assertOverridesMethods(Class<?> baseClass, Class<?> subClass, List<String> ignoredMethods) {
+ Set<String> requiredOverriddenMethods = new LinkedHashSet<>();
+ for (Method method : baseClass.getDeclaredMethods()) {
+ // Note: Do not filter out `final` methods; maybe they should not be `final` and subclass needs
+ // to override them
+ if (isProtectedOrPublic(method)) {
+ requiredOverriddenMethods.add(getMethodSignature(method));
+ }
+ }
+
+ for (Method method : subClass.getDeclaredMethods()) {
+ requiredOverriddenMethods.remove(getMethodSignature(method));
+ }
+
+ for (String ignoredMethod : ignoredMethods) {
+ boolean foundIgnored = requiredOverriddenMethods.remove(ignoredMethod);
+ if (!foundIgnored) {
+ throw new IllegalArgumentException("Method '" + ignoredMethod + "' does not exist or is already overridden");
+ }
+ }
+
+ if (!requiredOverriddenMethods.isEmpty()) {
+ Assert.fail(subClass.getSimpleName() + " must override these methods: " + requiredOverriddenMethods);
+ }
+ }
}
diff --git a/gson/src/test/java/com/google/gson/common/TestTypes.java b/gson/src/test/java/com/google/gson/common/TestTypes.java
index 11d3d0ab..13807637 100644
--- a/gson/src/test/java/com/google/gson/common/TestTypes.java
+++ b/gson/src/test/java/com/google/gson/common/TestTypes.java
@@ -16,9 +16,6 @@
package com.google.gson.common;
-import java.lang.reflect.Type;
-import java.util.Collection;
-
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
@@ -28,6 +25,8 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
+import java.lang.reflect.Type;
+import java.util.Collection;
/**
* Types used for testing JSON serialization and deserialization
@@ -36,7 +35,7 @@ import com.google.gson.annotations.SerializedName;
* @author Joel Leitch
*/
public class TestTypes {
-
+
public static class Base {
public static final String BASE_NAME = Base.class.getSimpleName();
public static final String BASE_FIELD_KEY = "baseName";
@@ -76,7 +75,7 @@ public class TestTypes {
}
public static class BaseSerializer implements JsonSerializer<Base> {
- public static final String NAME = BaseSerializer.class.getSimpleName();
+ public static final String NAME = BaseSerializer.class.getSimpleName();
@Override
public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
@@ -85,13 +84,13 @@ public class TestTypes {
}
}
public static class SubSerializer implements JsonSerializer<Sub> {
- public static final String NAME = SubSerializer.class.getSimpleName();
+ public static final String NAME = SubSerializer.class.getSimpleName();
@Override
public JsonElement serialize(Sub src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty(Base.SERIALIZER_KEY, NAME);
return obj;
- }
+ }
}
public static class StringWrapper {
@@ -228,6 +227,7 @@ public class TestTypes {
}
}
+ @SuppressWarnings("overrides") // for missing hashCode() override
public static class ClassWithNoFields {
// Nothing here..
@Override
@@ -271,7 +271,7 @@ public class TestTypes {
}
public static class ClassWithTransientFields<T> {
- public transient T transientT;
+ public transient T transientT;
public final transient long transientLongValue;
private final long[] longValue;
diff --git a/gson/src/test/java/com/google/gson/functional/ArrayTest.java b/gson/src/test/java/com/google/gson/functional/ArrayTest.java
index da8be85b..9d0f89ad 100644
--- a/gson/src/test/java/com/google/gson/functional/ArrayTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ArrayTest.java
@@ -16,20 +16,19 @@
package com.google.gson.functional;
+import static org.junit.Assert.assertArrayEquals;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.common.TestTypes.BagOfPrimitives;
import com.google.gson.common.TestTypes.ClassWithObjects;
import com.google.gson.reflect.TypeToken;
-
-import junit.framework.TestCase;
-import static org.junit.Assert.assertArrayEquals;
-
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
+import junit.framework.TestCase;
/**
* Functional tests for Json serialization and deserialization of arrays.
*
@@ -51,7 +50,7 @@ public class ArrayTest extends TestCase {
}
public void testTopLevelArrayOfIntsDeserialization() {
- int[] expected = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+ int[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] actual = gson.fromJson("[1,2,3,4,5,6,7,8,9]", int[].class);
assertArrayEquals(expected, actual);
}
@@ -142,12 +141,12 @@ public class ArrayTest extends TestCase {
assertEquals("hello", arrayType[0]);
}
- @SuppressWarnings("unchecked")
public void testArrayOfCollectionSerialization() throws Exception {
StringBuilder sb = new StringBuilder("[");
int arraySize = 3;
Type typeToSerialize = new TypeToken<Collection<Integer>[]>() {}.getType();
+ @SuppressWarnings({"rawtypes", "unchecked"})
Collection<Integer>[] arrayOfCollection = new ArrayList[arraySize];
for (int i = 0; i < arraySize; ++i) {
int startValue = (3 * i) + 1;
@@ -173,8 +172,8 @@ public class ArrayTest extends TestCase {
Collection<Integer>[] target = gson.fromJson(json, type);
assertEquals(2, target.length);
- assertArrayEquals(new Integer[] { 1, 2 }, target[0].toArray(new Integer[0]));
- assertArrayEquals(new Integer[] { 3, 4 }, target[1].toArray(new Integer[0]));
+ assertArrayEquals(new Integer[] {1, 2}, target[0].toArray(new Integer[0]));
+ assertArrayEquals(new Integer[] {3, 4}, target[1].toArray(new Integer[0]));
}
public void testArrayOfPrimitivesAsObjectsSerialization() throws Exception {
@@ -201,7 +200,7 @@ public class ArrayTest extends TestCase {
String classWithObjectsJson = gson.toJson(classWithObjects);
String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives);
- Object[] objects = new Object[] { classWithObjects, bagOfPrimitives };
+ Object[] objects = {classWithObjects, bagOfPrimitives};
String json = gson.toJson(objects);
assertTrue(json.contains(classWithObjectsJson));
@@ -209,7 +208,7 @@ public class ArrayTest extends TestCase {
}
public void testArrayOfNullSerialization() {
- Object[] array = new Object[] {null};
+ Object[] array = {null};
String json = gson.toJson(array);
assertEquals("[null]", json);
}
@@ -222,8 +221,8 @@ public class ArrayTest extends TestCase {
/**
* Regression tests for Issue 272
*/
- public void testMultidimenstionalArraysSerialization() {
- String[][] items = new String[][]{
+ public void testMultidimensionalArraysSerialization() {
+ String[][] items = {
{"3m Co", "71.72", "0.02", "0.03", "4/2 12:00am", "Manufacturing"},
{"Alcoa Inc", "29.01", "0.42", "1.47", "4/1 12:00am", "Manufacturing"}
};
@@ -232,23 +231,28 @@ public class ArrayTest extends TestCase {
assertTrue(json.contains("Manufacturing\"]]"));
}
- public void testMultiDimenstionalObjectArraysSerialization() {
- Object[][] array = new Object[][] { new Object[] { 1, 2 } };
+ public void testMultidimensionalObjectArraysSerialization() {
+ Object[][] array = {new Object[] { 1, 2 }};
assertEquals("[[1,2]]", gson.toJson(array));
}
+ public void testMultidimensionalPrimitiveArraysSerialization() {
+ int[][] array = {{1, 2}, {3, 4}};
+ assertEquals("[[1,2],[3,4]]", gson.toJson(array));
+ }
+
/**
* Regression test for Issue 205
*/
public void testMixingTypesInObjectArraySerialization() {
- Object[] array = new Object[] { 1, 2, new Object[] { "one", "two", 3 } };
+ Object[] array = {1, 2, new Object[] {"one", "two", 3}};
assertEquals("[1,2,[\"one\",\"two\",3]]", gson.toJson(array));
}
/**
* Regression tests for Issue 272
*/
- public void testMultidimenstionalArraysDeserialization() {
+ public void testMultidimensionalArraysDeserialization() {
String json = "[['3m Co','71.72','0.02','0.03','4/2 12:00am','Manufacturing'],"
+ "['Alcoa Inc','29.01','0.42','1.47','4/1 12:00am','Manufacturing']]";
String[][] items = gson.fromJson(json, String[][].class);
@@ -256,6 +260,12 @@ public class ArrayTest extends TestCase {
assertEquals("Manufacturing", items[1][5]);
}
+ public void testMultidimensionalPrimitiveArraysDeserialization() {
+ String json = "[[1,2],[3,4]]";
+ int[][] expected = {{1, 2}, {3, 4}};
+ assertArrayEquals(expected, gson.fromJson(json, int[][].class));
+ }
+
/** http://code.google.com/p/google-gson/issues/detail?id=342 */
public void testArrayElementsAreArrays() {
Object[] stringArrays = {
diff --git a/gson/src/test/java/com/google/gson/functional/CollectionTest.java b/gson/src/test/java/com/google/gson/functional/CollectionTest.java
index f113b850..44a655c8 100644
--- a/gson/src/test/java/com/google/gson/functional/CollectionTest.java
+++ b/gson/src/test/java/com/google/gson/functional/CollectionTest.java
@@ -16,6 +16,16 @@
package com.google.gson.functional;
+import static org.junit.Assert.assertArrayEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
@@ -30,18 +40,7 @@ import java.util.Queue;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonPrimitive;
-import com.google.gson.JsonSerializationContext;
-import com.google.gson.JsonSerializer;
-import com.google.gson.common.TestTypes.BagOfPrimitives;
-import com.google.gson.reflect.TypeToken;
-
import junit.framework.TestCase;
-import static org.junit.Assert.assertArrayEquals;
/**
* Functional tests for Json serialization and deserialization of collections.
@@ -241,35 +240,33 @@ public class CollectionTest extends TestCase {
assertEquals("[1,2,3,4,5,6,7,8,9]", gson.toJson(target));
}
- @SuppressWarnings("rawtypes")
- public void testRawCollectionSerialization() {
+ public void testObjectCollectionSerialization() {
BagOfPrimitives bag1 = new BagOfPrimitives();
- Collection target = Arrays.asList(bag1, bag1);
+ Collection<?> target = Arrays.asList(bag1, bag1, "test");
String json = gson.toJson(target);
assertTrue(json.contains(bag1.getExpectedJson()));
}
- @SuppressWarnings("rawtypes")
public void testRawCollectionDeserializationNotAlllowed() {
String json = "[0,1,2,3,4,5,6,7,8,9]";
- Collection integers = gson.fromJson(json, Collection.class);
+ Collection<?> integers = gson.fromJson(json, Collection.class);
// JsonReader converts numbers to double by default so we need a floating point comparison
assertEquals(Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0), integers);
json = "[\"Hello\", \"World\"]";
- Collection strings = gson.fromJson(json, Collection.class);
+ Collection<?> strings = gson.fromJson(json, Collection.class);
assertTrue(strings.contains("Hello"));
assertTrue(strings.contains("World"));
}
- @SuppressWarnings({"rawtypes", "unchecked"})
public void testRawCollectionOfBagOfPrimitivesNotAllowed() {
BagOfPrimitives bag = new BagOfPrimitives(10, 20, false, "stringValue");
String json = '[' + bag.getExpectedJson() + ',' + bag.getExpectedJson() + ']';
- Collection target = gson.fromJson(json, Collection.class);
+ Collection<?> target = gson.fromJson(json, Collection.class);
assertEquals(2, target.size());
for (Object bag1 : target) {
// Gson 2.0 converts raw objects into maps
+ @SuppressWarnings("unchecked")
Map<String, Object> values = (Map<String, Object>) bag1;
assertTrue(values.containsValue(10.0));
assertTrue(values.containsValue(20.0));
@@ -324,7 +321,7 @@ public class CollectionTest extends TestCase {
HasArrayListField copy = gson.fromJson("{\"longs\":[1,3]}", HasArrayListField.class);
assertEquals(Arrays.asList(1L, 3L), copy.longs);
}
-
+
public void testUserCollectionTypeAdapter() {
Type listOfString = new TypeToken<List<String>>() {}.getType();
Object stringListSerializer = new JsonSerializer<List<String>>() {
@@ -343,11 +340,10 @@ public class CollectionTest extends TestCase {
ArrayList<Long> longs = new ArrayList<>();
}
- @SuppressWarnings("rawtypes")
- private static int[] toIntArray(Collection collection) {
+ private static int[] toIntArray(Collection<?> collection) {
int[] ints = new int[collection.size()];
int i = 0;
- for (Iterator iterator = collection.iterator(); iterator.hasNext(); ++i) {
+ for (Iterator<?> iterator = collection.iterator(); iterator.hasNext(); ++i) {
Object obj = iterator.next();
if (obj instanceof Integer) {
ints[i] = ((Integer)obj).intValue();
diff --git a/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java
index b14ed52e..1c38e6ca 100644
--- a/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java
+++ b/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java
@@ -29,15 +29,13 @@ import com.google.gson.JsonSerializer;
import com.google.gson.common.TestTypes.BagOfPrimitives;
import com.google.gson.common.TestTypes.ClassWithCustomTypeConverter;
import com.google.gson.reflect.TypeToken;
-
-import java.util.Date;
-import junit.framework.TestCase;
-
import java.lang.reflect.Type;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import junit.framework.TestCase;
/**
* Functional tests for the support of custom serializer and deserializers.
@@ -220,12 +218,11 @@ public class CustomTypeAdaptersTest extends TestCase {
assertEquals("true", gson.toJson(true, Boolean.class));
}
- @SuppressWarnings("rawtypes")
public void testCustomDeserializerInvokedForPrimitives() {
Gson gson = new GsonBuilder()
- .registerTypeAdapter(boolean.class, new JsonDeserializer() {
+ .registerTypeAdapter(boolean.class, new JsonDeserializer<Boolean>() {
@Override
- public Object deserialize(JsonElement json, Type t, JsonDeserializationContext context) {
+ public Boolean deserialize(JsonElement json, Type t, JsonDeserializationContext context) {
return json.getAsInt() != 0;
}
})
diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
index 91a4639e..218c97ab 100644
--- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
+++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
@@ -54,7 +54,6 @@ import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;
-
import junit.framework.TestCase;
/**
@@ -654,14 +653,13 @@ public class DefaultTypeAdaptersTest extends TestCase {
assertEquals("abc", sb.toString());
}
- @SuppressWarnings("rawtypes")
- private static class MyClassTypeAdapter extends TypeAdapter<Class> {
+ private static class MyClassTypeAdapter extends TypeAdapter<Class<?>> {
@Override
- public void write(JsonWriter out, Class value) throws IOException {
+ public void write(JsonWriter out, Class<?> value) throws IOException {
out.value(value.getName());
}
@Override
- public Class read(JsonReader in) throws IOException {
+ public Class<?> read(JsonReader in) throws IOException {
String className = in.nextString();
try {
return Class.forName(className);
diff --git a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
index aa6f4ccb..daa7aa48 100644
--- a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
+++ b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
@@ -35,7 +35,7 @@ import junit.framework.TestCase;
* @author Inderjeet Singh
*/
public class GsonVersionDiagnosticsTest extends TestCase {
- private static final Pattern GSON_VERSION_PATTERN = Pattern.compile("(\\(GSON \\d\\.\\d\\.\\d)(?:[-.][A-Z]+)?\\)$");
+ private static final Pattern GSON_VERSION_PATTERN = Pattern.compile("(\\(GSON \\d\\.\\d+(\\.\\d)?)(?:[-.][A-Z]+)?\\)$");
private Gson gson;
diff --git a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java
index 95e3e3ef..3ed60327 100644
--- a/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java
+++ b/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java
@@ -22,15 +22,13 @@ import com.google.gson.InstanceCreator;
import com.google.gson.common.TestTypes.Base;
import com.google.gson.common.TestTypes.ClassWithBaseField;
import com.google.gson.common.TestTypes.Sub;
-
import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
-import junit.framework.TestCase;
-
-import java.lang.reflect.Type;
import java.util.SortedSet;
import java.util.TreeSet;
+import junit.framework.TestCase;
/**
* Functional Test exercising custom serialization only. When test applies to both
@@ -102,13 +100,13 @@ public class InstanceCreatorTest extends TestCase {
assertEquals(SubArrayList.class, list.getClass());
}
- @SuppressWarnings({ "unchecked", "rawtypes" })
+ @SuppressWarnings("unchecked")
public void testInstanceCreatorForParametrizedType() throws Exception {
@SuppressWarnings("serial")
class SubTreeSet<T> extends TreeSet<T> {}
- InstanceCreator<SortedSet> sortedSetCreator = new InstanceCreator<SortedSet>() {
- @Override public SortedSet createInstance(Type type) {
- return new SubTreeSet();
+ InstanceCreator<SortedSet<?>> sortedSetCreator = new InstanceCreator<SortedSet<?>>() {
+ @Override public SortedSet<?> createInstance(Type type) {
+ return new SubTreeSet<>();
}
};
Gson gson = new GsonBuilder()
diff --git a/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java b/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java
index 169c37a5..bdf6ea6e 100644
--- a/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java
+++ b/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java
@@ -17,7 +17,6 @@
package com.google.gson.functional;
import com.google.gson.Gson;
-
import junit.framework.TestCase;
/**
@@ -34,32 +33,16 @@ public class InternationalizationTest extends TestCase {
gson = new Gson();
}
- /*
- public void testStringsWithRawChineseCharactersSerialization() throws Exception {
- String target = "好好好";
- String json = gson.toJson(target);
- String expected = "\"\\u597d\\u597d\\u597d\"";
- assertEquals(expected, json);
- }
- */
-
- public void testStringsWithRawChineseCharactersDeserialization() throws Exception {
- String expected = "好好好";
- String json = "\"" + expected + "\"";
- String actual = gson.fromJson(json, String.class);
- assertEquals(expected, actual);
- }
-
public void testStringsWithUnicodeChineseCharactersSerialization() throws Exception {
String target = "\u597d\u597d\u597d";
String json = gson.toJson(target);
- String expected = "\"\u597d\u597d\u597d\"";
+ String expected = '"' + target + '"';
assertEquals(expected, json);
}
public void testStringsWithUnicodeChineseCharactersDeserialization() throws Exception {
String expected = "\u597d\u597d\u597d";
- String json = "\"" + expected + "\"";
+ String json = '"' + expected + '"';
String actual = gson.fromJson(json, String.class);
assertEquals(expected, actual);
}
@@ -68,4 +51,25 @@ public class InternationalizationTest extends TestCase {
String actual = gson.fromJson("'\\u597d\\u597d\\u597d'", String.class);
assertEquals("\u597d\u597d\u597d", actual);
}
+
+ public void testSupplementaryUnicodeSerialization() throws Exception {
+ // Supplementary code point U+1F60A
+ String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+ String json = gson.toJson(supplementaryCodePoint);
+ assertEquals('"' + supplementaryCodePoint + '"', json);
+ }
+
+ public void testSupplementaryUnicodeDeserialization() throws Exception {
+ // Supplementary code point U+1F60A
+ String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+ String actual = gson.fromJson('"' + supplementaryCodePoint + '"', String.class);
+ assertEquals(supplementaryCodePoint, actual);
+ }
+
+ public void testSupplementaryUnicodeEscapedDeserialization() throws Exception {
+ // Supplementary code point U+1F60A
+ String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+ String actual = gson.fromJson("\"\\uD83D\\uDE0A\"", String.class);
+ assertEquals(supplementaryCodePoint, actual);
+ }
}
diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
new file mode 100644
index 00000000..a172f5a4
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2022 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.functional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.ReflectionAccessFilter.FilterResult;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.Expose;
+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 java.lang.reflect.Type;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class Java17RecordTest {
+ private final Gson gson = new Gson();
+
+ @Test
+ public void testFirstNameIsChosenForSerialization() {
+ RecordWithCustomNames target = new RecordWithCustomNames("v1", "v2");
+ // Ensure name1 occurs exactly once, and name2 and name3 don't appear
+ assertEquals("{\"name\":\"v1\",\"name1\":\"v2\"}", gson.toJson(target));
+ }
+
+ @Test
+ public void testMultipleNamesDeserializedCorrectly() {
+ assertEquals("v1", gson.fromJson("{'name':'v1'}", RecordWithCustomNames.class).a);
+
+ // Both name1 and name2 gets deserialized to b
+ assertEquals("v11", gson.fromJson("{'name': 'v1', 'name1':'v11'}", RecordWithCustomNames.class).b);
+ assertEquals("v2", gson.fromJson("{'name': 'v1', 'name2':'v2'}", RecordWithCustomNames.class).b);
+ assertEquals("v3", gson.fromJson("{'name': 'v1', 'name3':'v3'}", RecordWithCustomNames.class).b);
+ }
+
+ @Test
+ public void testMultipleNamesInTheSameString() {
+ // The last value takes precedence
+ assertEquals("v3",
+ gson.fromJson("{'name': 'foo', 'name1':'v1','name2':'v2','name3':'v3'}", RecordWithCustomNames.class).b);
+ }
+
+ private record RecordWithCustomNames(
+ @SerializedName("name") String a,
+ @SerializedName(value = "name1", alternate = {"name2", "name3"}) String b) {}
+
+ @Test
+ public void testSerializedNameOnAccessor() {
+ record LocalRecord(int i) {
+ @SerializedName("a")
+ @Override
+ public int i() {
+ return i;
+ }
+ }
+
+ var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
+ assertEquals("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testFieldNamingStrategy() {
+ record LocalRecord(int i) {}
+
+ Gson gson = new GsonBuilder()
+ .setFieldNamingStrategy(f -> f.getName() + "-custom")
+ .create();
+
+ assertEquals("{\"i-custom\":1}", gson.toJson(new LocalRecord(1)));
+ assertEquals(new LocalRecord(2), gson.fromJson("{\"i-custom\":2}", LocalRecord.class));
+ }
+
+ @Test
+ public void testUnknownJsonProperty() {
+ record LocalRecord(int i) {}
+
+ // Unknown property 'x' should be ignored
+ assertEquals(new LocalRecord(1), gson.fromJson("{\"i\":1,\"x\":2}", LocalRecord.class));
+ }
+
+ @Test
+ public void testDuplicateJsonProperties() {
+ record LocalRecord(Integer a, Integer b) {}
+
+ String json = "{\"a\":null,\"a\":2,\"b\":1,\"b\":null}";
+ // Should use value of last occurrence
+ assertEquals(new LocalRecord(2, null), gson.fromJson(json, LocalRecord.class));
+ }
+
+ @Test
+ public void testConstructorRuns() {
+ record LocalRecord(String s) {
+ LocalRecord {
+ s = "custom-" + s;
+ }
+ }
+
+ LocalRecord deserialized = gson.fromJson("{\"s\": null}", LocalRecord.class);
+ assertEquals(new LocalRecord(null), deserialized);
+ assertEquals("custom-null", deserialized.s());
+ }
+
+ /** Tests behavior when the canonical constructor throws an exception */
+ @Test
+ public void testThrowingConstructor() {
+ record LocalRecord(String s) {
+ static final RuntimeException thrownException = new RuntimeException("Custom exception");
+
+ @SuppressWarnings("unused")
+ LocalRecord {
+ throw thrownException;
+ }
+ }
+
+ try {
+ gson.fromJson("{\"s\":\"value\"}", LocalRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once Gson throws more specific exception type
+ catch (RuntimeException e) {
+ assertEquals("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]",
+ e.getMessage());
+ assertSame(LocalRecord.thrownException, e.getCause());
+ }
+ }
+
+ @Test
+ public void testAccessorIsCalled() {
+ record LocalRecord(String s) {
+ @Override
+ public String s() {
+ return "accessor-value";
+ }
+ }
+
+ assertEquals("{\"s\":\"accessor-value\"}", gson.toJson(new LocalRecord(null)));
+ }
+
+ /** Tests behavior when a record accessor method throws an exception */
+ @Test
+ public void testThrowingAccessor() {
+ record LocalRecord(String s) {
+ static final RuntimeException thrownException = new RuntimeException("Custom exception");
+
+ @Override
+ public String s() {
+ throw thrownException;
+ }
+ }
+
+ try {
+ gson.toJson(new LocalRecord("a"));
+ fail();
+ } catch (JsonIOException e) {
+ assertEquals("Accessor method '" + LocalRecord.class.getName() + "#s()' threw exception",
+ e.getMessage());
+ assertSame(LocalRecord.thrownException, e.getCause());
+ }
+ }
+
+ /** Tests behavior for a record without components */
+ @Test
+ public void testEmptyRecord() {
+ record EmptyRecord() {}
+
+ assertEquals("{}", gson.toJson(new EmptyRecord()));
+ assertEquals(new EmptyRecord(), gson.fromJson("{}", EmptyRecord.class));
+ }
+
+ /**
+ * Tests behavior when {@code null} is serialized / deserialized as record value;
+ * basically makes sure the adapter is 'null-safe'
+ */
+ @Test
+ public void testRecordNull() throws IOException {
+ record LocalRecord(int i) {}
+
+ TypeAdapter<LocalRecord> adapter = gson.getAdapter(LocalRecord.class);
+ assertEquals("null", adapter.toJson(null));
+ assertNull(adapter.fromJson("null"));
+ }
+
+ @Test
+ public void testPrimitiveDefaultValues() {
+ RecordWithPrimitives expected = new RecordWithPrimitives("s", (byte) 0, (short) 0, 0, 0, 0, 0, '\0', false);
+ assertEquals(expected, gson.fromJson("{'aString': 's'}", RecordWithPrimitives.class));
+ }
+
+ @Test
+ public void testPrimitiveJsonNullValue() {
+ String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
+ var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
+ assertEquals("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte",
+ e.getMessage());
+ }
+
+ /**
+ * Tests behavior when JSON contains non-null value, but custom adapter returns null
+ * for primitive component
+ */
+ @Test
+ public void testPrimitiveAdapterNullValue() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(byte.class, new TypeAdapter<Byte>() {
+ @Override public Byte read(JsonReader in) throws IOException {
+ in.skipValue();
+ // Always return null
+ return null;
+ }
+
+ @Override public void write(JsonWriter out, Byte value) {
+ throw new AssertionError("not needed for test");
+ }
+ })
+ .create();
+
+ String s = "{'aString': 's', 'aByte': 0}";
+ var exception = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
+ assertEquals("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte",
+ exception.getMessage());
+ }
+
+ private record RecordWithPrimitives(
+ String aString, byte aByte, short aShort, int anInt, long aLong, float aFloat, double aDouble, char aChar, boolean aBoolean) {}
+
+ /** Tests behavior when value of Object component is missing; should default to null */
+ @Test
+ public void testObjectDefaultValue() {
+ record LocalRecord(String s, int i) {}
+
+ assertEquals(new LocalRecord(null, 1), gson.fromJson("{\"i\":1}", LocalRecord.class));
+ }
+
+ /**
+ * Tests serialization of a record with {@code static} field.
+ *
+ * <p>Important: It is not documented that this is officially supported; this
+ * test just checks the current behavior.
+ */
+ @Test
+ public void testStaticFieldSerialization() {
+ // By default Gson should ignore static fields
+ assertEquals("{}", gson.toJson(new RecordWithStaticField()));
+
+ Gson gson = new GsonBuilder()
+ // Include static fields
+ .excludeFieldsWithModifiers(0)
+ .create();
+
+ String json = gson.toJson(new RecordWithStaticField());
+ assertEquals("{\"s\":\"initial\"}", json);
+ }
+
+ /**
+ * Tests deserialization of a record with {@code static} field.
+ *
+ * <p>Important: It is not documented that this is officially supported; this
+ * test just checks the current behavior.
+ */
+ @Test
+ public void testStaticFieldDeserialization() {
+ // By default Gson should ignore static fields
+ gson.fromJson("{\"s\":\"custom\"}", RecordWithStaticField.class);
+ assertEquals("initial", RecordWithStaticField.s);
+
+ Gson gson = new GsonBuilder()
+ // Include static fields
+ .excludeFieldsWithModifiers(0)
+ .create();
+
+ String oldValue = RecordWithStaticField.s;
+ try {
+ RecordWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", RecordWithStaticField.class);
+ assertNotNull(obj);
+ // Currently record deserialization always ignores static fields
+ assertEquals("initial", RecordWithStaticField.s);
+ } finally {
+ RecordWithStaticField.s = oldValue;
+ }
+ }
+
+ private record RecordWithStaticField() {
+ static String s = "initial";
+ }
+
+ @Test
+ public void testExposeAnnotation() {
+ record RecordWithExpose(
+ @Expose int a,
+ int b
+ ) {}
+
+ Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+ String json = gson.toJson(new RecordWithExpose(1, 2));
+ assertEquals("{\"a\":1}", json);
+ }
+
+ @Test
+ public void testFieldExclusionStrategy() {
+ record LocalRecord(int a, int b, double c) {}
+
+ Gson gson = new GsonBuilder()
+ .setExclusionStrategies(new ExclusionStrategy() {
+ @Override public boolean shouldSkipField(FieldAttributes f) {
+ return f.getName().equals("a");
+ }
+
+ @Override public boolean shouldSkipClass(Class<?> clazz) {
+ return clazz == double.class;
+ }
+ })
+ .create();
+
+ assertEquals("{\"b\":2}", gson.toJson(new LocalRecord(1, 2, 3.0)));
+ }
+
+ @Test
+ public void testJsonAdapterAnnotation() {
+ record Adapter() implements JsonSerializer<String>, JsonDeserializer<String> {
+ @Override public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ return "deserializer-" + json.getAsString();
+ }
+
+ @Override public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("serializer-" + src);
+ }
+ }
+ record LocalRecord(
+ @JsonAdapter(Adapter.class) String s
+ ) {}
+
+ assertEquals("{\"s\":\"serializer-a\"}", gson.toJson(new LocalRecord("a")));
+ assertEquals(new LocalRecord("deserializer-a"), gson.fromJson("{\"s\":\"a\"}", LocalRecord.class));
+ }
+
+ @Test
+ public void testClassReflectionFilter() {
+ record Allowed(int a) {}
+ record Blocked(int b) {}
+
+ Gson gson = new GsonBuilder()
+ .addReflectionAccessFilter(c -> c == Allowed.class ? FilterResult.ALLOW : FilterResult.BLOCK_ALL)
+ .create();
+
+ String json = gson.toJson(new Allowed(1));
+ assertEquals("{\"a\":1}", json);
+
+ var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
+ assertEquals("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() +
+ ". Register a TypeAdapter for this type or adjust the access filter.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void testReflectionFilterBlockInaccessible() {
+ Gson gson = new GsonBuilder()
+ .addReflectionAccessFilter(c -> FilterResult.BLOCK_INACCESSIBLE)
+ .create();
+
+ var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
+ assertEquals("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ + " type, adjust the access filter or increase the visibility of the element and its declaring type.",
+ exception.getMessage());
+
+ exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
+ assertEquals("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and"
+ + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring"
+ + " type, adjust the access filter or increase the visibility of the element and its declaring type.",
+ exception.getMessage());
+
+ assertEquals("{\"i\":1}", gson.toJson(new PublicRecord(1)));
+ assertEquals(new PublicRecord(2), gson.fromJson("{\"i\":2}", PublicRecord.class));
+ }
+
+ private record PrivateRecord(int i) {}
+ public record PublicRecord(int i) {}
+
+ /**
+ * Tests behavior when {@code java.lang.Record} is used as type for serialization
+ * and deserialization.
+ */
+ @Test
+ public void testRecordBaseClass() {
+ record LocalRecord(int i) {}
+
+ assertEquals("{}", gson.toJson(new LocalRecord(1), Record.class));
+
+ var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
+ assertEquals("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+ + " this type. Class name: java.lang.Record",
+ exception.getMessage());
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java b/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java
index e6cb6dc8..f5398843 100644
--- a/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java
+++ b/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java
@@ -161,4 +161,22 @@ public final class JsonAdapterSerializerDeserializerTest extends TestCase {
return new JsonPrimitive("BaseIntegerAdapter");
}
}
+
+ public void testJsonAdapterNullSafe() {
+ Gson gson = new Gson();
+ String json = gson.toJson(new Computer3(null, null));
+ assertEquals("{\"user1\":\"UserSerializerDeserializer\"}", json);
+ Computer3 computer3 = gson.fromJson("{\"user1\":null, \"user2\":null}", Computer3.class);
+ assertEquals("UserSerializerDeserializer", computer3.user1.name);
+ assertNull(computer3.user2);
+ }
+
+ private static final class Computer3 {
+ @JsonAdapter(value = UserSerializerDeserializer.class, nullSafe = false) final User user1;
+ @JsonAdapter(value = UserSerializerDeserializer.class) final User user2;
+ Computer3(User user1, User user2) {
+ this.user1 = user1;
+ this.user2 = user2;
+ }
+ }
}
diff --git a/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java b/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java
deleted file mode 100644
index 22a479b8..00000000
--- a/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright (C) 2008 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.functional;
-
-import com.google.gson.JsonArray;
-import junit.framework.TestCase;
-
-import java.math.BigInteger;
-
-/**
- * Functional tests for adding primitives to a JsonArray.
- *
- * @author Dillon Dixon
- */
-public class JsonArrayTest extends TestCase {
-
- public void testStringPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add("Hello");
- jsonArray.add("Goodbye");
- jsonArray.add("Thank you");
- jsonArray.add((String) null);
- jsonArray.add("Yes");
-
- assertEquals("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]", jsonArray.toString());
- }
-
- public void testIntegerPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- int x = 1;
- jsonArray.add(x);
-
- x = 2;
- jsonArray.add(x);
-
- x = -3;
- jsonArray.add(x);
-
- jsonArray.add((Integer) null);
-
- x = 4;
- jsonArray.add(x);
-
- x = 0;
- jsonArray.add(x);
-
- assertEquals("[1,2,-3,null,4,0]", jsonArray.toString());
- }
-
- public void testDoublePrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- double x = 1.0;
- jsonArray.add(x);
-
- x = 2.13232;
- jsonArray.add(x);
-
- x = 0.121;
- jsonArray.add(x);
-
- jsonArray.add((Double) null);
-
- x = -0.00234;
- jsonArray.add(x);
-
- jsonArray.add((Double) null);
-
- assertEquals("[1.0,2.13232,0.121,null,-0.00234,null]", jsonArray.toString());
- }
-
- public void testBooleanPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add(true);
- jsonArray.add(true);
- jsonArray.add(false);
- jsonArray.add(false);
- jsonArray.add((Boolean) null);
- jsonArray.add(true);
-
- assertEquals("[true,true,false,false,null,true]", jsonArray.toString());
- }
-
- public void testCharPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add('a');
- jsonArray.add('e');
- jsonArray.add('i');
- jsonArray.add((char) 111);
- jsonArray.add((Character) null);
- jsonArray.add('u');
- jsonArray.add("and sometimes Y");
-
- assertEquals("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]", jsonArray.toString());
- }
-
- public void testMixedPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add('a');
- jsonArray.add("apple");
- jsonArray.add(12121);
- jsonArray.add((char) 111);
- jsonArray.add((Boolean) null);
- jsonArray.add((Character) null);
- jsonArray.add(12.232);
- jsonArray.add(BigInteger.valueOf(2323));
-
- assertEquals("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]", jsonArray.toString());
- }
-
- public void testNullPrimitiveAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add((Character) null);
- jsonArray.add((Boolean) null);
- jsonArray.add((Integer) null);
- jsonArray.add((Double) null);
- jsonArray.add((Float) null);
- jsonArray.add((BigInteger) null);
- jsonArray.add((String) null);
- jsonArray.add((Boolean) null);
- jsonArray.add((Number) null);
-
- assertEquals("[null,null,null,null,null,null,null,null,null]", jsonArray.toString());
- }
-
- public void testSameAddition() {
- JsonArray jsonArray = new JsonArray();
-
- jsonArray.add('a');
- jsonArray.add('a');
- jsonArray.add(true);
- jsonArray.add(true);
- jsonArray.add(1212);
- jsonArray.add(1212);
- jsonArray.add(34.34);
- jsonArray.add(34.34);
- jsonArray.add((Boolean) null);
- jsonArray.add((Boolean) null);
-
- assertEquals("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]", jsonArray.toString());
- }
-}
diff --git a/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/src/test/java/com/google/gson/functional/MapTest.java
index ef9eae2b..c5344a76 100644
--- a/gson/src/test/java/com/google/gson/functional/MapTest.java
+++ b/gson/src/test/java/com/google/gson/functional/MapTest.java
@@ -16,18 +16,6 @@
package com.google.gson.functional;
-import java.lang.reflect.Type;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentNavigableMap;
-import java.util.concurrent.ConcurrentSkipListMap;
-
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
@@ -42,7 +30,17 @@ import com.google.gson.JsonSyntaxException;
import com.google.gson.common.TestTypes;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.reflect.TypeToken;
-
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
import junit.framework.TestCase;
/**
@@ -78,9 +76,8 @@ public class MapTest extends TestCase {
assertEquals(2, target.get("b").intValue());
}
- @SuppressWarnings({"unchecked", "rawtypes"})
- public void testRawMapSerialization() {
- Map map = new LinkedHashMap();
+ public void testObjectMapSerialization() {
+ Map<String, Object> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", "string");
String json = gson.toJson(map);
@@ -647,7 +644,6 @@ public class MapTest extends TestCase {
}
static final class MapWithGeneralMapParameters {
- @SuppressWarnings({"rawtypes", "unchecked"})
- final Map<String, Object> map = new LinkedHashMap();
+ final Map<String, Object> map = new LinkedHashMap<>();
}
}
diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java
index e9aa15b2..bed5b598 100644
--- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java
@@ -20,6 +20,7 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
@@ -44,7 +45,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
-
import junit.framework.TestCase;
/**
@@ -121,18 +121,16 @@ public class ObjectTest extends TestCase {
assertEquals(target.getExpectedJson(), gson.toJson(target));
}
- @SuppressWarnings("rawtypes")
public void testClassWithTransientFieldsDeserialization() throws Exception {
String json = "{\"longValue\":[1]}";
- ClassWithTransientFields target = gson.fromJson(json, ClassWithTransientFields.class);
+ ClassWithTransientFields<?> target = gson.fromJson(json, ClassWithTransientFields.class);
assertEquals(json, target.getExpectedJson());
}
- @SuppressWarnings("rawtypes")
public void testClassWithTransientFieldsDeserializationTransientFieldsPassedInJsonAreIgnored()
throws Exception {
String json = "{\"transientLongValue\":1,\"longValue\":[1]}";
- ClassWithTransientFields target = gson.fromJson(json, ClassWithTransientFields.class);
+ ClassWithTransientFields<?> target = gson.fromJson(json, ClassWithTransientFields.class);
assertFalse(target.transientLongValue != 1);
}
@@ -485,6 +483,16 @@ public class ObjectTest extends TestCase {
gson.fromJson(gson.toJson(product), Product.class);
}
+ static final class Department {
+ public String name = "abc";
+ public String code = "123";
+ }
+
+ static final class Product {
+ private List<String> attributes = new ArrayList<>();
+ private List<Department> departments = new ArrayList<>();
+ }
+
// http://code.google.com/p/google-gson/issues/detail?id=270
public void testDateAsMapObjectField() {
HasObjectMap a = new HasObjectMap();
@@ -496,17 +504,92 @@ public class ObjectTest extends TestCase {
}
}
- public class HasObjectMap {
+ static class HasObjectMap {
Map<String, Object> map = new HashMap<>();
}
- static final class Department {
- public String name = "abc";
- public String code = "123";
+ /**
+ * Tests serialization of a class with {@code static} field.
+ *
+ * <p>Important: It is not documented that this is officially supported; this
+ * test just checks the current behavior.
+ */
+ public void testStaticFieldSerialization() {
+ // By default Gson should ignore static fields
+ assertEquals("{}", gson.toJson(new ClassWithStaticField()));
+
+ Gson gson = new GsonBuilder()
+ // Include static fields
+ .excludeFieldsWithModifiers(0)
+ .create();
+
+ String json = gson.toJson(new ClassWithStaticField());
+ assertEquals("{\"s\":\"initial\"}", json);
+
+ json = gson.toJson(new ClassWithStaticFinalField());
+ assertEquals("{\"s\":\"initial\"}", json);
}
- static final class Product {
- private List<String> attributes = new ArrayList<>();
- private List<Department> departments = new ArrayList<>();
+ /**
+ * Tests deserialization of a class with {@code static} field.
+ *
+ * <p>Important: It is not documented that this is officially supported; this
+ * test just checks the current behavior.
+ */
+ public void testStaticFieldDeserialization() {
+ // By default Gson should ignore static fields
+ gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class);
+ assertEquals("initial", ClassWithStaticField.s);
+
+ Gson gson = new GsonBuilder()
+ // Include static fields
+ .excludeFieldsWithModifiers(0)
+ .create();
+
+ String oldValue = ClassWithStaticField.s;
+ try {
+ ClassWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class);
+ assertNotNull(obj);
+ assertEquals("custom", ClassWithStaticField.s);
+ } finally {
+ ClassWithStaticField.s = oldValue;
+ }
+
+ try {
+ gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticFinalField.class);
+ fail();
+ } catch (JsonIOException e) {
+ assertEquals("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'",
+ e.getMessage());
+ }
+ }
+
+ static class ClassWithStaticField {
+ static String s = "initial";
+ }
+
+ static class ClassWithStaticFinalField {
+ static final String s = "initial";
+ }
+
+ public void testThrowingDefaultConstructor() {
+ try {
+ gson.fromJson("{}", ClassWithThrowingConstructor.class);
+ fail();
+ }
+ // TODO: Adjust this once Gson throws more specific exception type
+ catch (RuntimeException e) {
+ assertEquals("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args",
+ e.getMessage());
+ assertSame(ClassWithThrowingConstructor.thrownException, e.getCause());
+ }
+ }
+
+ static class ClassWithThrowingConstructor {
+ static final RuntimeException thrownException = new RuntimeException("Custom exception");
+
+ public ClassWithThrowingConstructor() {
+ throw thrownException;
+ }
}
}
diff --git a/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java
index 8decc640..e6168746 100644
--- a/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java
@@ -16,16 +16,19 @@
package com.google.gson.functional;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
import com.google.gson.ParameterizedTypeFixtures.MyParameterizedType;
import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeAdapter;
import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeInstanceCreator;
import com.google.gson.common.TestTypes.BagOfPrimitives;
import com.google.gson.reflect.TypeToken;
-
-import junit.framework.TestCase;
-
+import com.google.gson.stream.JsonReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
@@ -35,6 +38,8 @@ import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
/**
* Functional tests for the serialization and deserialization of parameterized types in Gson.
@@ -42,15 +47,15 @@ import java.util.List;
* @author Inderjeet Singh
* @author Joel Leitch
*/
-public class ParameterizedTypesTest extends TestCase {
+public class ParameterizedTypesTest {
private Gson gson;
- @Override
- protected void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() {
gson = new Gson();
}
+ @Test
public void testParameterizedTypesSerialization() throws Exception {
MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
Type typeOfSrc = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
@@ -58,6 +63,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(src.getExpectedJson(), json);
}
+ @Test
public void testParameterizedTypeDeserialization() throws Exception {
BagOfPrimitives bag = new BagOfPrimitives();
MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<>(bag);
@@ -72,6 +78,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(expected, actual);
}
+ @Test
public void testTypesWithMultipleParametersSerialization() throws Exception {
MultiParameters<Integer, Float, Double, String, BagOfPrimitives> src =
new MultiParameters<>(10, 1.0F, 2.1D, "abc", new BagOfPrimitives());
@@ -83,6 +90,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(expected, json);
}
+ @Test
public void testTypesWithMultipleParametersDeserialization() throws Exception {
Type typeOfTarget = new TypeToken<MultiParameters<Integer, Float, Double, String,
BagOfPrimitives>>() {}.getType();
@@ -95,6 +103,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(expected, target);
}
+ @Test
public void testParameterizedTypeWithCustomSerializer() {
Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType();
@@ -111,6 +120,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(MyParameterizedTypeAdapter.<String>getExpectedJson(stringTarget), json);
}
+ @Test
public void testParameterizedTypesWithCustomDeserializer() {
Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType();
@@ -132,6 +142,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals("abc", stringTarget.value);
}
+ @Test
public void testParameterizedTypesWithWriterSerialization() throws Exception {
Writer writer = new StringWriter();
MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
@@ -140,6 +151,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(src.getExpectedJson(), writer.toString());
}
+ @Test
public void testParameterizedTypeWithReaderDeserialization() throws Exception {
BagOfPrimitives bag = new BagOfPrimitives();
MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<>(bag);
@@ -154,14 +166,20 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(expected, actual);
}
- @SuppressWarnings("unchecked")
+ @SuppressWarnings("varargs")
+ @SafeVarargs
+ private static <T> T[] arrayOf(T... args) {
+ return args;
+ }
+
+ @Test
public void testVariableTypeFieldsAndGenericArraysSerialization() throws Exception {
Integer obj = 0;
Integer[] array = { 1, 2, 3 };
List<Integer> list = new ArrayList<>();
list.add(4);
list.add(5);
- List<Integer>[] arrayOfLists = new List[] { list, list };
+ List<Integer>[] arrayOfLists = arrayOf(list, list);
Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
ObjectWithTypeVariables<Integer> objToSerialize =
@@ -171,14 +189,14 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(objToSerialize.getExpectedJson(), json);
}
- @SuppressWarnings("unchecked")
+ @Test
public void testVariableTypeFieldsAndGenericArraysDeserialization() throws Exception {
Integer obj = 0;
Integer[] array = { 1, 2, 3 };
List<Integer> list = new ArrayList<>();
list.add(4);
list.add(5);
- List<Integer>[] arrayOfLists = new List[] { list, list };
+ List<Integer>[] arrayOfLists = arrayOf(list, list);
Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
ObjectWithTypeVariables<Integer> objToSerialize =
@@ -189,6 +207,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(objAfterDeserialization.getExpectedJson(), json);
}
+ @Test
public void testVariableTypeDeserialization() throws Exception {
Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
ObjectWithTypeVariables<Integer> objToSerialize =
@@ -199,6 +218,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(objAfterDeserialization.getExpectedJson(), json);
}
+ @Test
public void testVariableTypeArrayDeserialization() throws Exception {
Integer[] array = { 1, 2, 3 };
@@ -211,6 +231,7 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(objAfterDeserialization.getExpectedJson(), json);
}
+ @Test
public void testParameterizedTypeWithVariableTypeDeserialization() throws Exception {
List<Integer> list = new ArrayList<>();
list.add(4);
@@ -225,12 +246,12 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(objAfterDeserialization.getExpectedJson(), json);
}
- @SuppressWarnings("unchecked")
+ @Test
public void testParameterizedTypeGenericArraysSerialization() throws Exception {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
- List<Integer>[] arrayOfLists = new List[] { list, list };
+ List<Integer>[] arrayOfLists = arrayOf(list, list);
Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
ObjectWithTypeVariables<Integer> objToSerialize =
@@ -239,12 +260,12 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals("{\"arrayOfListOfTypeParameters\":[[1,2],[1,2]]}", json);
}
- @SuppressWarnings("unchecked")
+ @Test
public void testParameterizedTypeGenericArraysDeserialization() throws Exception {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
- List<Integer>[] arrayOfLists = new List[] { list, list };
+ List<Integer>[] arrayOfLists = arrayOf(list, list);
Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
ObjectWithTypeVariables<Integer> objToSerialize =
@@ -459,7 +480,7 @@ public class ParameterizedTypesTest extends TestCase {
return true;
}
}
-
+
// Begin: tests to reproduce issue 103
private static class Quantity {
@SuppressWarnings("unused")
@@ -475,21 +496,23 @@ public class ParameterizedTypesTest extends TestCase {
}
private interface Immutable {
}
-
- public static final class Amount<Q extends Quantity>
+
+ public static final class Amount<Q extends Quantity>
implements Measurable<Q>, Field<Amount<?>>, Serializable, Immutable {
private static final long serialVersionUID = -7560491093120970437L;
int value = 30;
}
-
+
+ @Test
public void testDeepParameterizedTypeSerialization() {
Amount<MyQuantity> amount = new Amount<>();
String json = gson.toJson(amount);
assertTrue(json.contains("value"));
assertTrue(json.contains("30"));
}
-
+
+ @Test
public void testDeepParameterizedTypeDeserialization() {
String json = "{value:30}";
Type type = new TypeToken<Amount<MyQuantity>>() {}.getType();
@@ -497,4 +520,47 @@ public class ParameterizedTypesTest extends TestCase {
assertEquals(30, amount.value);
}
// End: tests to reproduce issue 103
+
+ private static void assertCorrectlyDeserialized(Object object) {
+ @SuppressWarnings("unchecked")
+ List<Quantity> list = (List<Quantity>) object;
+ assertEquals(1, list.size());
+ assertEquals(4, list.get(0).q);
+ }
+
+ @Test
+ public void testGsonFromJsonTypeToken() {
+ TypeToken<List<Quantity>> typeToken = new TypeToken<List<Quantity>>() {};
+ Type type = typeToken.getType();
+
+ {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("q", 4);
+ JsonArray jsonArray = new JsonArray();
+ jsonArray.add(jsonObject);
+
+ assertCorrectlyDeserialized(gson.fromJson(jsonArray, typeToken));
+ assertCorrectlyDeserialized(gson.fromJson(jsonArray, type));
+ }
+
+ String json = "[{\"q\":4}]";
+
+ {
+ assertCorrectlyDeserialized(gson.fromJson(json, typeToken));
+ assertCorrectlyDeserialized(gson.fromJson(json, type));
+ }
+
+ {
+ assertCorrectlyDeserialized(gson.fromJson(new StringReader(json), typeToken));
+ assertCorrectlyDeserialized(gson.fromJson(new StringReader(json), type));
+ }
+
+ {
+ JsonReader reader = new JsonReader(new StringReader(json));
+ assertCorrectlyDeserialized(gson.fromJson(reader, typeToken));
+
+ reader = new JsonReader(new StringReader(json));
+ assertCorrectlyDeserialized(gson.fromJson(reader, type));
+ }
+ }
}
diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
index 6d74cc29..c4c25f00 100644
--- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
+++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
@@ -64,6 +64,11 @@ public class PrimitiveTest extends TestCase {
public void testByteSerialization() {
assertEquals("1", gson.toJson(1, byte.class));
assertEquals("1", gson.toJson(1, Byte.class));
+ assertEquals(Byte.toString(Byte.MIN_VALUE), gson.toJson(Byte.MIN_VALUE, Byte.class));
+ assertEquals(Byte.toString(Byte.MAX_VALUE), gson.toJson(Byte.MAX_VALUE, Byte.class));
+ // Should perform narrowing conversion
+ assertEquals("-128", gson.toJson(128, Byte.class));
+ assertEquals("1", gson.toJson(1.5, Byte.class));
}
public void testByteDeserialization() {
@@ -102,6 +107,13 @@ public class PrimitiveTest extends TestCase {
public void testShortSerialization() {
assertEquals("1", gson.toJson(1, short.class));
assertEquals("1", gson.toJson(1, Short.class));
+ assertEquals(Short.toString(Short.MIN_VALUE), gson.toJson(Short.MIN_VALUE, Short.class));
+ assertEquals(Short.toString(Short.MAX_VALUE), gson.toJson(Short.MAX_VALUE, Short.class));
+ // Should perform widening conversion
+ assertEquals("1", gson.toJson((byte) 1, Short.class));
+ // Should perform narrowing conversion
+ assertEquals("-32768", gson.toJson(32768, Short.class));
+ assertEquals("1", gson.toJson(1.5, Short.class));
}
public void testShortDeserialization() {
@@ -137,6 +149,54 @@ public class PrimitiveTest extends TestCase {
}
}
+ public void testIntSerialization() {
+ assertEquals("1", gson.toJson(1, int.class));
+ assertEquals("1", gson.toJson(1, Integer.class));
+ assertEquals(Integer.toString(Integer.MIN_VALUE), gson.toJson(Integer.MIN_VALUE, Integer.class));
+ assertEquals(Integer.toString(Integer.MAX_VALUE), gson.toJson(Integer.MAX_VALUE, Integer.class));
+ // Should perform widening conversion
+ assertEquals("1", gson.toJson((byte) 1, Integer.class));
+ // Should perform narrowing conversion
+ assertEquals("-2147483648", gson.toJson(2147483648L, Integer.class));
+ assertEquals("1", gson.toJson(1.5, Integer.class));
+ }
+
+ public void testLongSerialization() {
+ assertEquals("1", gson.toJson(1L, long.class));
+ assertEquals("1", gson.toJson(1L, Long.class));
+ assertEquals(Long.toString(Long.MIN_VALUE), gson.toJson(Long.MIN_VALUE, Long.class));
+ assertEquals(Long.toString(Long.MAX_VALUE), gson.toJson(Long.MAX_VALUE, Long.class));
+ // Should perform widening conversion
+ assertEquals("1", gson.toJson((byte) 1, Long.class));
+ // Should perform narrowing conversion
+ assertEquals("1", gson.toJson(1.5, Long.class));
+ }
+
+ public void testFloatSerialization() {
+ assertEquals("1.5", gson.toJson(1.5f, float.class));
+ assertEquals("1.5", gson.toJson(1.5f, Float.class));
+ assertEquals(Float.toString(Float.MIN_VALUE), gson.toJson(Float.MIN_VALUE, Float.class));
+ assertEquals(Float.toString(Float.MAX_VALUE), gson.toJson(Float.MAX_VALUE, Float.class));
+ // Should perform widening conversion
+ assertEquals("1.0", gson.toJson((byte) 1, Float.class));
+ // (This widening conversion is actually lossy)
+ assertEquals(Float.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Float.class));
+ // Should perform narrowing conversion
+ gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+ assertEquals("Infinity", gson.toJson(Double.MAX_VALUE, Float.class));
+ }
+
+ public void testDoubleSerialization() {
+ assertEquals("1.5", gson.toJson(1.5, double.class));
+ assertEquals("1.5", gson.toJson(1.5, Double.class));
+ assertEquals(Double.toString(Double.MIN_VALUE), gson.toJson(Double.MIN_VALUE, Double.class));
+ assertEquals(Double.toString(Double.MAX_VALUE), gson.toJson(Double.MAX_VALUE, Double.class));
+ // Should perform widening conversion
+ assertEquals("1.0", gson.toJson((byte) 1, Double.class));
+ // (This widening conversion is actually lossy)
+ assertEquals(Double.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Double.class));
+ }
+
public void testPrimitiveIntegerAutoboxedInASingleElementArraySerialization() {
int target[] = {-9332};
assertEquals("[-9332]", gson.toJson(target));
diff --git a/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java b/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java
index 7dcbc23c..6801ba00 100644
--- a/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java
+++ b/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java
@@ -23,11 +23,9 @@ import com.google.gson.common.TestTypes.BagOfPrimitives;
import com.google.gson.common.TestTypes.ClassWithTransientFields;
import com.google.gson.common.TestTypes.Nested;
import com.google.gson.common.TestTypes.PrimitiveArray;
-
-import junit.framework.TestCase;
-
import java.util.ArrayList;
import java.util.List;
+import junit.framework.TestCase;
/**
* Functional tests for print formatting.
@@ -45,13 +43,12 @@ public class PrintFormattingTest extends TestCase {
gson = new Gson();
}
- @SuppressWarnings({"unchecked", "rawtypes"})
public void testCompactFormattingLeavesNoWhiteSpace() {
- List list = new ArrayList();
+ List<Object> list = new ArrayList<>();
list.add(new BagOfPrimitives());
list.add(new Nested());
list.add(new PrimitiveArray());
- list.add(new ClassWithTransientFields());
+ list.add(new ClassWithTransientFields<>());
String json = gson.toJson(list);
assertContainsNoWhiteSpace(json);
diff --git a/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java b/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java
index e21fb903..a04723b5 100644
--- a/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java
@@ -20,11 +20,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonStreamParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.common.TestTypes.BagOfPrimitives;
-
import com.google.gson.reflect.TypeToken;
-import java.util.Map;
-import junit.framework.TestCase;
-
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.IOException;
@@ -32,6 +28,9 @@ import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
+import java.util.Arrays;
+import java.util.Map;
+import junit.framework.TestCase;
/**
* Functional tests for the support of {@link Reader}s and {@link Writer}s.
@@ -89,8 +88,8 @@ public class ReadersWritersTest extends TestCase {
}
public void testReadWriteTwoStrings() throws IOException {
- Gson gson= new Gson();
- CharArrayWriter writer= new CharArrayWriter();
+ Gson gson = new Gson();
+ CharArrayWriter writer = new CharArrayWriter();
writer.write(gson.toJson("one").toCharArray());
writer.write(gson.toJson("two").toCharArray());
CharArrayReader reader = new CharArrayReader(writer.toCharArray());
@@ -102,8 +101,8 @@ public class ReadersWritersTest extends TestCase {
}
public void testReadWriteTwoObjects() throws IOException {
- Gson gson= new Gson();
- CharArrayWriter writer= new CharArrayWriter();
+ Gson gson = new Gson();
+ CharArrayWriter writer = new CharArrayWriter();
BagOfPrimitives expectedOne = new BagOfPrimitives(1, 1, true, "one");
writer.write(gson.toJson(expectedOne).toCharArray());
BagOfPrimitives expectedTwo = new BagOfPrimitives(2, 2, false, "two");
@@ -132,4 +131,50 @@ public class ReadersWritersTest extends TestCase {
} catch (JsonSyntaxException expected) {
}
}
+
+ /**
+ * Verifies that passing an {@link Appendable} which is not an instance of {@link Writer}
+ * to {@code Gson.toJson} works correctly.
+ */
+ public void testToJsonAppendable() {
+ class CustomAppendable implements Appendable {
+ final StringBuilder stringBuilder = new StringBuilder();
+ int toStringCallCount = 0;
+
+ @Override
+ public Appendable append(char c) throws IOException {
+ stringBuilder.append(c);
+ return this;
+ }
+
+ @Override
+ public Appendable append(CharSequence csq) throws IOException {
+ if (csq == null) {
+ csq = "null"; // Requirement by Writer.append
+ }
+ append(csq, 0, csq.length());
+ return this;
+ }
+
+ @Override
+ public Appendable append(CharSequence csq, int start, int end) throws IOException {
+ if (csq == null) {
+ csq = "null"; // Requirement by Writer.append
+ }
+
+ // According to doc, toString() must return string representation
+ String s = csq.toString();
+ toStringCallCount++;
+ stringBuilder.append(s, start, end);
+ return this;
+ }
+ }
+
+ CustomAppendable appendable = new CustomAppendable();
+ gson.toJson(Arrays.asList("test", 123, true), appendable);
+ // Make sure CharSequence.toString() was called at least two times to verify that
+ // CurrentWrite.cachedString is properly overwritten when char array changes
+ assertTrue(appendable.toStringCallCount >= 2);
+ assertEquals("[\"test\",123,true]", appendable.stringBuilder.toString());
+ }
}
diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
index 775baf9f..6c9ab449 100644
--- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
@@ -2,6 +2,7 @@ package com.google.gson.functional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNotNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -17,10 +18,10 @@ import com.google.gson.TypeAdapter;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
-import java.awt.Point;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.LinkedList;
import java.util.List;
@@ -40,7 +41,7 @@ public class ReflectionAccessFilterTest {
}
@Test
- public void testBlockInaccessibleJava() {
+ public void testBlockInaccessibleJava() throws ReflectiveOperationException {
Gson gson = new GsonBuilder()
.addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
.create();
@@ -52,14 +53,25 @@ public class ReflectionAccessFilterTest {
} catch (JsonIOException expected) {
// Note: This test is rather brittle and depends on the JDK implementation
assertEquals(
- "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit "
- + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.",
+ "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit"
+ + " making it accessible. Register a TypeAdapter for the declaring type, adjust the access"
+ + " filter or increase the visibility of the element and its declaring type.",
expected.getMessage()
);
}
- // But serialization should succeed for classes with only public fields
- String json = gson.toJson(new Point(1, 2));
+
+ // But serialization should succeed for classes with only public fields.
+ // Not many JDK classes have mutable public fields, thank goodness, but java.awt.Point does.
+ Class<?> pointClass = null;
+ try {
+ pointClass = Class.forName("java.awt.Point");
+ } catch (ClassNotFoundException e) {
+ }
+ assumeNotNull(pointClass);
+ Constructor<?> pointConstructor = pointClass.getConstructor(int.class, int.class);
+ Object point = pointConstructor.newInstance(1, 2);
+ String json = gson.toJson(point);
assertEquals("{\"x\":1,\"y\":2}", json);
}
@@ -74,8 +86,9 @@ public class ReflectionAccessFilterTest {
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
- "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit "
- + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.",
+ "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit"
+ + " making it accessible. Register a TypeAdapter for the declaring type, adjust the access"
+ + " filter or increase the visibility of the element and its declaring type.",
expected.getMessage()
);
}
@@ -93,8 +106,8 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread. "
- + "Register a TypeAdapter for this type or adjust the access filter.",
+ "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread."
+ + " Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
@@ -111,9 +124,9 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "ReflectionAccessFilter does not permit using reflection for class java.io.Reader "
- + "(supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass). "
- + "Register a TypeAdapter for this type or adjust the access filter.",
+ "ReflectionAccessFilter does not permit using reflection for class java.io.Reader"
+ + " (supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass)."
+ + " Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
@@ -141,9 +154,10 @@ public class ReflectionAccessFilterTest {
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
- "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i' "
- + "is not accessible and ReflectionAccessFilter does not permit making it accessible. "
- + "Register a TypeAdapter for the declaring type or adjust the access filter.",
+ "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i'"
+ + " is not accessible and ReflectionAccessFilter does not permit making it accessible."
+ + " Register a TypeAdapter for the declaring type, adjust the access filter or increase"
+ + " the visibility of the element and its declaring type.",
expected.getMessage()
);
}
@@ -183,9 +197,9 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "ReflectionAccessFilter does not permit using reflection for class "
- + "com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass. "
- + "Register a TypeAdapter for this type or adjust the access filter.",
+ "ReflectionAccessFilter does not permit using reflection for class"
+ + " com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass."
+ + " Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
@@ -222,9 +236,10 @@ public class ReflectionAccessFilterTest {
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
- "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i' "
- + "is not accessible and ReflectionAccessFilter does not permit making it accessible. "
- + "Register a TypeAdapter for the declaring type or adjust the access filter.",
+ "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i'"
+ + " is not accessible and ReflectionAccessFilter does not permit making it accessible."
+ + " Register a TypeAdapter for the declaring type, adjust the access filter or increase"
+ + " the visibility of the element and its declaring type.",
expected.getMessage()
);
}
@@ -263,9 +278,9 @@ public class ReflectionAccessFilterTest {
fail("Expected exception; test needs to be run with Java >= 9");
} catch (JsonIOException expected) {
assertEquals(
- "Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor; "
- + "constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an "
- + "InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.",
+ "Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor;"
+ + " constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an"
+ + " InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.",
expected.getMessage()
);
}
@@ -295,9 +310,9 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor; "
- + "ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator "
- + "or a TypeAdapter for this type or adjust the access filter to allow using reflection.",
+ "Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor;"
+ + " ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator"
+ + " or a TypeAdapter for this type or adjust the access filter to allow using reflection.",
expected.getMessage()
);
}
@@ -311,7 +326,7 @@ public class ReflectionAccessFilterTest {
}
@Override public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) throws IOException {
throw new AssertionError("Not needed for test");
- };
+ }
})
.create();
ClassWithoutNoArgsConstructor deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
@@ -357,8 +372,8 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass. "
- + "Register a TypeAdapter for this type or adjust the access filter.",
+ "ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass."
+ + " Register a TypeAdapter for this type or adjust the access filter.",
expected.getMessage()
);
}
@@ -417,8 +432,8 @@ public class ReflectionAccessFilterTest {
fail();
} catch (JsonIOException expected) {
assertEquals(
- "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for "
- + "this type. Interface name: java.lang.Runnable",
+ "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+ + " this type. Interface name: java.lang.Runnable",
expected.getMessage()
);
}
diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
index ece35124..02649c5f 100644
--- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
@@ -8,6 +8,7 @@ import static org.junit.Assert.fail;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonIOException;
+import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
@@ -36,6 +37,7 @@ public class ReflectionAccessTest {
}
@Test
+ @SuppressWarnings("removal") // java.lang.SecurityManager deprecation in Java 17
public void testRestrictiveSecurityManager() throws Exception {
// Must use separate class loader, otherwise permission is not checked, see Class.getDeclaredFields()
Class<?> clazz = loadClassWithDifferentClassLoader(ClassWithPrivateMembers.class);
@@ -111,12 +113,14 @@ public class ReflectionAccessTest {
// But deserialization should fail
Class<?> internalClass = Collections.emptyList().getClass();
try {
- gson.fromJson("{}", internalClass);
+ gson.fromJson("[]", internalClass);
fail("Missing exception; test has to be run with `--illegal-access=deny`");
+ } catch (JsonSyntaxException e) {
+ fail("Unexpected exception; test has to be run with `--illegal-access=deny`");
} catch (JsonIOException expected) {
assertTrue(expected.getMessage().startsWith(
- "Failed making constructor 'java.util.Collections$EmptyList#EmptyList()' accessible; "
- + "either change its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type"
+ "Failed making constructor 'java.util.Collections$EmptyList()' accessible;"
+ + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: "
));
}
}
diff --git a/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java
index f82d92e4..006c6ebd 100644
--- a/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java
+++ b/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java
@@ -16,14 +16,6 @@
package com.google.gson.functional;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import junit.framework.TestCase;
-
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
@@ -34,6 +26,12 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import junit.framework.TestCase;
/**
* Collection of functional tests for DOM tree based type adapters.
@@ -44,7 +42,7 @@ public class TreeTypeAdaptersTest extends TestCase {
private static final Student STUDENT1 = new Student(STUDENT1_ID, "first");
private static final Student STUDENT2 = new Student(STUDENT2_ID, "second");
private static final Type TYPE_COURSE_HISTORY =
- new TypeToken<Course<HistoryCourse>>(){}.getType();
+ new TypeToken<Course<HistoryCourse>>(){}.getType();
private static final Id<Course<HistoryCourse>> COURSE_ID =
new Id<>("10", TYPE_COURSE_HISTORY);
@@ -93,7 +91,6 @@ public class TreeTypeAdaptersTest extends TestCase {
private static final class IdTreeTypeAdapter implements JsonSerializer<Id<?>>,
JsonDeserializer<Id<?>> {
- @SuppressWarnings("rawtypes")
@Override
public Id<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
@@ -104,7 +101,7 @@ public class TreeTypeAdaptersTest extends TestCase {
// Since Id takes only one TypeVariable, the actual type corresponding to the first
// TypeVariable is the Type we are looking for
Type typeOfId = parameterizedType.getActualTypeArguments()[0];
- return new Id(json.getAsString(), typeOfId);
+ return new Id<>(json.getAsString(), typeOfId);
}
@Override
diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java
new file mode 100644
index 00000000..73a01012
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java
@@ -0,0 +1,193 @@
+package com.google.gson.functional;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+public class TypeAdapterRuntimeTypeWrapperTest {
+ private static class Base {
+ }
+ private static class Subclass extends Base {
+ @SuppressWarnings("unused")
+ String f = "test";
+ }
+ private static class Container {
+ @SuppressWarnings("unused")
+ Base b = new Subclass();
+ }
+ private static class Deserializer implements JsonDeserializer<Base> {
+ @Override
+ public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ throw new AssertionError("not needed for this test");
+ }
+ }
+
+ /**
+ * When custom {@link JsonSerializer} is registered for Base should
+ * prefer that over reflective adapter for Subclass for serialization.
+ */
+ @Test
+ public void testJsonSerializer() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(Base.class, new JsonSerializer<Base>() {
+ @Override
+ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("serializer");
+ }
+ })
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":\"serializer\"}", json);
+ }
+
+ /**
+ * When only {@link JsonDeserializer} is registered for Base, then on
+ * serialization should prefer reflective adapter for Subclass since
+ * Base would use reflective adapter as delegate.
+ */
+ @Test
+ public void testJsonDeserializer_ReflectiveSerializerDelegate() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(Base.class, new Deserializer())
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":{\"f\":\"test\"}}", json);
+ }
+
+ /**
+ * When {@link JsonDeserializer} with custom adapter as delegate is
+ * registered for Base, then on serialization should prefer custom adapter
+ * delegate for Base over reflective adapter for Subclass.
+ */
+ @Test
+ public void testJsonDeserializer_CustomSerializerDelegate() {
+ Gson gson = new GsonBuilder()
+ // Register custom delegate
+ .registerTypeAdapter(Base.class, new TypeAdapter<Base>() {
+ @Override
+ public Base read(JsonReader in) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+ @Override
+ public void write(JsonWriter out, Base value) throws IOException {
+ out.value("custom delegate");
+ }
+ })
+ .registerTypeAdapter(Base.class, new Deserializer())
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":\"custom delegate\"}", json);
+ }
+
+ /**
+ * When two (or more) {@link JsonDeserializer}s are registered for Base
+ * which eventually fall back to reflective adapter as delegate, then on
+ * serialization should prefer reflective adapter for Subclass.
+ */
+ @Test
+ public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() {
+ Gson gson = new GsonBuilder()
+ // Register delegate which itself falls back to reflective serialization
+ .registerTypeAdapter(Base.class, new Deserializer())
+ .registerTypeAdapter(Base.class, new Deserializer())
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":{\"f\":\"test\"}}", json);
+ }
+
+ /**
+ * When {@link JsonDeserializer} with {@link JsonSerializer} as delegate
+ * is registered for Base, then on serialization should prefer
+ * {@code JsonSerializer} over reflective adapter for Subclass.
+ */
+ @Test
+ public void testJsonDeserializer_JsonSerializerDelegate() {
+ Gson gson = new GsonBuilder()
+ // Register JsonSerializer as delegate
+ .registerTypeAdapter(Base.class, new JsonSerializer<Base>() {
+ @Override
+ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("custom delegate");
+ }
+ })
+ .registerTypeAdapter(Base.class, new Deserializer())
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":\"custom delegate\"}", json);
+ }
+
+ /**
+ * When a {@link JsonDeserializer} is registered for Subclass, and a custom
+ * {@link JsonSerializer} is registered for Base, then Gson should prefer
+ * the reflective adapter for Subclass for backward compatibility (see
+ * https://github.com/google/gson/pull/1787#issuecomment-1222175189) even
+ * though normally TypeAdapterRuntimeTypeWrapper should prefer the custom
+ * serializer for Base.
+ */
+ @Test
+ public void testJsonDeserializer_SubclassBackwardCompatibility() {
+ Gson gson = new GsonBuilder()
+ .registerTypeAdapter(Subclass.class, new JsonDeserializer<Subclass>() {
+ @Override
+ public Subclass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+ throw new AssertionError("not needed for this test");
+ }
+ })
+ .registerTypeAdapter(Base.class, new JsonSerializer<Base>() {
+ @Override
+ public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive("base");
+ }
+ })
+ .create();
+
+ String json = gson.toJson(new Container());
+ assertEquals("{\"b\":{\"f\":\"test\"}}", json);
+ }
+
+ private static class CyclicBase {
+ @SuppressWarnings("unused")
+ CyclicBase f;
+ }
+
+ private static class CyclicSub extends CyclicBase {
+ @SuppressWarnings("unused")
+ int i;
+
+ public CyclicSub(int i) {
+ this.i = i;
+ }
+ }
+
+ /**
+ * Tests behavior when the type of a field refers to a type whose adapter is
+ * currently in the process of being created. For these cases {@link Gson}
+ * uses a future adapter for the type. That adapter later uses the actual
+ * adapter as delegate.
+ */
+ @Test
+ public void testGsonFutureAdapter() {
+ CyclicBase b = new CyclicBase();
+ b.f = new CyclicSub(2);
+ String json = new Gson().toJson(b);
+ assertEquals("{\"f\":{\"i\":2}}", json);
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java b/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java
index e5a4d8ba..f9ef46b3 100644
--- a/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java
+++ b/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java
@@ -16,16 +16,14 @@
package com.google.gson.functional;
import com.google.gson.Gson;
-
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
-import java.util.Arrays;
-import junit.framework.TestCase;
-
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import junit.framework.TestCase;
/**
* Functional test for Gson serialization and deserialization of
@@ -70,6 +68,7 @@ public class TypeVariableTest extends TestCase {
assertEquals(blue1, blue2);
}
+ @SuppressWarnings("overrides") // for missing hashCode() override
public static class Blue extends Red<Boolean> {
public Blue() {
super(false);
@@ -79,7 +78,6 @@ public class TypeVariableTest extends TestCase {
super(value);
}
- // Technically, we should implement hashcode too
@Override
public boolean equals(Object o) {
if (!(o instanceof Blue)) {
@@ -100,6 +98,7 @@ public class TypeVariableTest extends TestCase {
}
}
+ @SuppressWarnings("overrides") // for missing hashCode() override
public static class Foo<S, T> extends Red<Boolean> {
private S someSField;
private T someTField;
@@ -113,7 +112,6 @@ public class TypeVariableTest extends TestCase {
this.someTField = tValue;
}
- // Technically, we should implement hashcode too
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object o) {
diff --git a/gson/src/test/java/com/google/gson/functional/VersioningTest.java b/gson/src/test/java/com/google/gson/functional/VersioningTest.java
index 2416fc06..49dabcab 100644
--- a/gson/src/test/java/com/google/gson/functional/VersioningTest.java
+++ b/gson/src/test/java/com/google/gson/functional/VersioningTest.java
@@ -15,13 +15,17 @@
*/
package com.google.gson.functional;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;
import com.google.gson.common.TestTypes.BagOfPrimitives;
-
-import junit.framework.TestCase;
+import org.junit.Test;
/**
* Functional tests for versioning support in Gson.
@@ -29,47 +33,60 @@ import junit.framework.TestCase;
* @author Inderjeet Singh
* @author Joel Leitch
*/
-public class VersioningTest extends TestCase {
+public class VersioningTest {
private static final int A = 0;
private static final int B = 1;
private static final int C = 2;
private static final int D = 3;
- private GsonBuilder builder;
-
- @Override
- protected void setUp() throws Exception {
- super.setUp();
- builder = new GsonBuilder();
+ private static Gson gsonWithVersion(double version) {
+ return new GsonBuilder().setVersion(version).create();
}
+ @Test
public void testVersionedUntilSerialization() {
Version1 target = new Version1();
- Gson gson = builder.setVersion(1.29).create();
+ Gson gson = gsonWithVersion(1.29);
String json = gson.toJson(target);
assertTrue(json.contains("\"a\":" + A));
- gson = builder.setVersion(1.3).create();
+ gson = gsonWithVersion(1.3);
+ json = gson.toJson(target);
+ assertFalse(json.contains("\"a\":" + A));
+
+ gson = gsonWithVersion(1.31);
json = gson.toJson(target);
assertFalse(json.contains("\"a\":" + A));
}
+ @Test
public void testVersionedUntilDeserialization() {
- Gson gson = builder.setVersion(1.3).create();
String json = "{\"a\":3,\"b\":4,\"c\":5}";
+
+ Gson gson = gsonWithVersion(1.29);
Version1 version1 = gson.fromJson(json, Version1.class);
+ assertEquals(3, version1.a);
+
+ gson = gsonWithVersion(1.3);
+ version1 = gson.fromJson(json, Version1.class);
+ assertEquals(A, version1.a);
+
+ gson = gsonWithVersion(1.31);
+ version1 = gson.fromJson(json, Version1.class);
assertEquals(A, version1.a);
}
+ @Test
public void testVersionedClassesSerialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
String json1 = gson.toJson(new Version1());
String json2 = gson.toJson(new Version1_1());
assertEquals(json1, json2);
}
+ @Test
public void testVersionedClassesDeserialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
String json = "{\"a\":3,\"b\":4,\"c\":5}";
Version1 version1 = gson.fromJson(json, Version1.class);
assertEquals(3, version1.a);
@@ -80,13 +97,15 @@ public class VersioningTest extends TestCase {
assertEquals(C, version1_1.c);
}
+ @Test
public void testIgnoreLaterVersionClassSerialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
assertEquals("null", gson.toJson(new Version1_2()));
}
+ @Test
public void testIgnoreLaterVersionClassDeserialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}";
Version1_2 version1_2 = gson.fromJson(json, Version1_2.class);
// Since the class is versioned to be after 1.0, we expect null
@@ -94,14 +113,16 @@ public class VersioningTest extends TestCase {
assertNull(version1_2);
}
+ @Test
public void testVersionedGsonWithUnversionedClassesSerialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue");
assertEquals(target.getExpectedJson(), gson.toJson(target));
}
+ @Test
public void testVersionedGsonWithUnversionedClassesDeserialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}";
BagOfPrimitives expected = new BagOfPrimitives();
@@ -112,34 +133,45 @@ public class VersioningTest extends TestCase {
assertEquals(expected, actual);
}
+ @Test
public void testVersionedGsonMixingSinceAndUntilSerialization() {
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
SinceUntilMixing target = new SinceUntilMixing();
String json = gson.toJson(target);
assertFalse(json.contains("\"b\":" + B));
- gson = builder.setVersion(1.2).create();
+ gson = gsonWithVersion(1.2);
json = gson.toJson(target);
assertTrue(json.contains("\"b\":" + B));
- gson = builder.setVersion(1.3).create();
+ gson = gsonWithVersion(1.3);
+ json = gson.toJson(target);
+ assertFalse(json.contains("\"b\":" + B));
+
+ gson = gsonWithVersion(1.4);
json = gson.toJson(target);
assertFalse(json.contains("\"b\":" + B));
}
+ @Test
public void testVersionedGsonMixingSinceAndUntilDeserialization() {
String json = "{\"a\":5,\"b\":6}";
- Gson gson = builder.setVersion(1.0).create();
+ Gson gson = gsonWithVersion(1.0);
SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a);
assertEquals(B, result.b);
- gson = builder.setVersion(1.2).create();
+ gson = gsonWithVersion(1.2);
result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a);
assertEquals(6, result.b);
- gson = builder.setVersion(1.3).create();
+ gson = gsonWithVersion(1.3);
+ result = gson.fromJson(json, SinceUntilMixing.class);
+ assertEquals(5, result.a);
+ assertEquals(B, result.b);
+
+ gson = gsonWithVersion(1.4);
result = gson.fromJson(json, SinceUntilMixing.class);
assertEquals(5, result.a);
assertEquals(B, result.b);
diff --git a/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java b/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java
index 54b62862..582d683a 100644
--- a/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java
+++ b/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java
@@ -15,7 +15,8 @@
*/
package com.google.gson.internal;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import org.junit.Test;
@@ -29,7 +30,7 @@ public class JavaVersionTest {
@Test
public void testGetMajorJavaVersion() {
- JavaVersion.getMajorJavaVersion();
+ assertTrue(JavaVersion.getMajorJavaVersion() >= 7); // Gson currently requires at least Java 7
}
@Test
diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java
index ee1bb102..0b08d32e 100644
--- a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java
+++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java
@@ -16,6 +16,7 @@
package com.google.gson.internal;
+import com.google.gson.common.MoreAsserts;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -26,12 +27,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Random;
-
import junit.framework.TestCase;
-import com.google.gson.common.MoreAsserts;
-
public final class LinkedTreeMapTest extends TestCase {
public void testIterationOrder() {
@@ -73,6 +72,59 @@ public final class LinkedTreeMapTest extends TestCase {
} catch (ClassCastException expected) {}
}
+ public void testPutNullValue() {
+ LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+ map.put("a", null);
+ assertEquals(1, map.size());
+ assertTrue(map.containsKey("a"));
+ assertTrue(map.containsValue(null));
+ assertNull(map.get("a"));
+ }
+
+ public void testPutNullValue_Forbidden() {
+ LinkedTreeMap<String, String> map = new LinkedTreeMap<>(false);
+ try {
+ map.put("a", null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ assertEquals(0, map.size());
+ assertFalse(map.containsKey("a"));
+ assertFalse(map.containsValue(null));
+ }
+
+ public void testEntrySetValueNull() {
+ LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+ map.put("a", "1");
+ assertEquals("1", map.get("a"));
+ Entry<String, String> entry = map.entrySet().iterator().next();
+ assertEquals("a", entry.getKey());
+ assertEquals("1", entry.getValue());
+ entry.setValue(null);
+ assertNull(entry.getValue());
+
+ assertTrue(map.containsKey("a"));
+ assertTrue(map.containsValue(null));
+ assertNull(map.get("a"));
+ }
+
+
+ public void testEntrySetValueNull_Forbidden() {
+ LinkedTreeMap<String, String> map = new LinkedTreeMap<>(false);
+ map.put("a", "1");
+ Entry<String, String> entry = map.entrySet().iterator().next();
+ try {
+ entry.setValue(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("value == null", e.getMessage());
+ }
+ assertEquals("1", entry.getValue());
+ assertEquals("1", map.get("a"));
+ assertFalse(map.containsValue(null));
+ }
+
public void testContainsNonComparableKeyReturnsFalse() {
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
map.put("a", "android");
@@ -81,6 +133,7 @@ public final class LinkedTreeMapTest extends TestCase {
public void testContainsNullKeyIsAlwaysFalse() {
LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+ assertFalse(map.containsKey(null));
map.put("a", "android");
assertFalse(map.containsKey(null));
}
@@ -160,6 +213,7 @@ public final class LinkedTreeMapTest extends TestCase {
assertEquals(Collections.singletonMap("a", 1), deserialized);
}
+ @SuppressWarnings("varargs")
@SafeVarargs
private final <T> void assertIterationOrder(Iterable<T> actual, T... expected) {
ArrayList<T> actualList = new ArrayList<>();
diff --git a/gson/src/test/java/com/google/gson/internal/StreamsTest.java b/gson/src/test/java/com/google/gson/internal/StreamsTest.java
new file mode 100644
index 00000000..d0cb90aa
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/internal/StreamsTest.java
@@ -0,0 +1,68 @@
+package com.google.gson.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.Writer;
+import org.junit.Test;
+
+public class StreamsTest {
+ @Test
+ public void testWriterForAppendable() throws IOException {
+ StringBuilder stringBuilder = new StringBuilder();
+ Writer writer = Streams.writerForAppendable(stringBuilder);
+
+ writer.append('a');
+ writer.append('\u1234');
+ writer.append("test");
+ writer.append(null); // test custom null handling mandated by `append`
+ writer.append("abcdef", 2, 4);
+ writer.append(null, 1, 3); // test custom null handling mandated by `append`
+ writer.append(',');
+
+ writer.write('a');
+ writer.write('\u1234');
+ // Should only consider the 16 low-order bits
+ writer.write(0x4321_1234);
+ writer.append(',');
+
+ writer.write("chars".toCharArray());
+ try {
+ writer.write((char[]) null);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ writer.write("chars".toCharArray(), 1, 2);
+ try {
+ writer.write((char[]) null, 1, 2);
+ fail();
+ } catch (NullPointerException e) {
+ }
+ writer.append(',');
+
+ writer.write("string");
+ try {
+ writer.write((String) null);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ writer.write("string", 1, 2);
+ try {
+ writer.write((String) null, 1, 2);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ String actualOutput = stringBuilder.toString();
+ assertEquals("a\u1234testnullcdul,a\u1234\u1234,charsha,stringtr", actualOutput);
+
+ writer.flush();
+ writer.close();
+
+ // flush() and close() calls should have had no effect
+ assertEquals(actualOutput, stringBuilder.toString());
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java
index e3ce147e..54d0a506 100644
--- a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java
+++ b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java
@@ -37,9 +37,8 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase {
* to instantiate an interface
*/
public void testInterfaceInstantiation() throws Exception {
- UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
try {
- unsafeAllocator.newInstance(Interface.class);
+ UnsafeAllocator.INSTANCE.newInstance(Interface.class);
fail();
} catch (AssertionError e) {
assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type"));
@@ -51,9 +50,8 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase {
* to instantiate an abstract class
*/
public void testAbstractClassInstantiation() throws Exception {
- UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
try {
- unsafeAllocator.newInstance(AbstractClass.class);
+ UnsafeAllocator.INSTANCE.newInstance(AbstractClass.class);
fail();
} catch (AssertionError e) {
assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type"));
@@ -64,8 +62,7 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase {
* Ensure that no exception is thrown when trying to instantiate a concrete class
*/
public void testConcreteClassInstantiation() throws Exception {
- UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
- ConcreteClass instance = unsafeAllocator.newInstance(ConcreteClass.class);
+ ConcreteClass instance = UnsafeAllocator.INSTANCE.newInstance(ConcreteClass.class);
assertNotNull(instance);
}
}
diff --git a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
index 3d1ec7f7..c20a3683 100644
--- a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
+++ b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
@@ -22,14 +22,12 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
-
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.JavaVersion;
import com.google.gson.internal.bind.DefaultDateTypeAdapter.DateType;
import com.google.gson.reflect.TypeToken;
-
import junit.framework.TestCase;
/**
@@ -76,6 +74,10 @@ public class DefaultDateTypeAdapterTest extends TestCase {
}
public void testParsingDatesFormattedWithSystemLocale() throws Exception {
+ // TODO(eamonnmcmanus): fix this test, which fails on JDK 8 and 17
+ if (JavaVersion.getMajorJavaVersion() != 11) {
+ return;
+ }
TimeZone defaultTimeZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
Locale defaultLocale = Locale.getDefault();
diff --git a/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java
new file mode 100644
index 00000000..18984c7b
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java
@@ -0,0 +1,81 @@
+package com.google.gson.internal.bind;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.reflect.Java17ReflectionHelperTest;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.security.Principal;
+import org.junit.Before;
+import org.junit.Test;
+
+public class Java17ReflectiveTypeAdapterFactoryTest {
+
+ // The class jdk.net.UnixDomainPrincipal is one of the few Record types that are included in the JDK.
+ // We use this to test serialization and deserialization of Record classes, so we do not need to
+ // have record support at the language level for these tests. This class was added in JDK 16.
+ Class<?> unixDomainPrincipalClass;
+
+ @Before
+ public void setUp() throws Exception {
+ unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
+ }
+
+ // Class for which the normal reflection based adapter is used
+ private static class DummyClass {
+ @SuppressWarnings("unused")
+ public String s;
+ }
+
+ @Test
+ public void testCustomAdapterForRecords() {
+ Gson gson = new Gson();
+ TypeAdapter<?> recordAdapter = gson.getAdapter(unixDomainPrincipalClass);
+ TypeAdapter<?> defaultReflectionAdapter = gson.getAdapter(DummyClass.class);
+ assertNotEquals(recordAdapter.getClass(), defaultReflectionAdapter.getClass());
+ }
+
+ @Test
+ public void testSerializeRecords() throws ReflectiveOperationException {
+ Gson gson =
+ new GsonBuilder()
+ .registerTypeAdapter(UserPrincipal.class, new PrincipalTypeAdapter<>())
+ .registerTypeAdapter(GroupPrincipal.class, new PrincipalTypeAdapter<>())
+ .create();
+
+ UserPrincipal userPrincipal = gson.fromJson("\"user\"", UserPrincipal.class);
+ GroupPrincipal groupPrincipal = gson.fromJson("\"group\"", GroupPrincipal.class);
+ Object recordInstance =
+ unixDomainPrincipalClass
+ .getDeclaredConstructor(UserPrincipal.class, GroupPrincipal.class)
+ .newInstance(userPrincipal, groupPrincipal);
+ String serialized = gson.toJson(recordInstance);
+ Object deserializedRecordInstance = gson.fromJson(serialized, unixDomainPrincipalClass);
+
+ assertEquals(recordInstance, deserializedRecordInstance);
+ assertEquals("{\"user\":\"user\",\"group\":\"group\"}", serialized);
+ }
+
+ private static class PrincipalTypeAdapter<T extends Principal> extends TypeAdapter<T> {
+ @Override
+ public void write(JsonWriter out, T principal) throws IOException {
+ out.value(principal.getName());
+ }
+
+ @Override
+ public T read(JsonReader in) throws IOException {
+ final String name = in.nextString();
+ // This type adapter is only used for Group and User Principal, both of which are implemented by PrincipalImpl.
+ @SuppressWarnings("unchecked")
+ T principal = (T) new Java17ReflectionHelperTest.PrincipalImpl(name);
+ return principal;
+ }
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java
index 204fb3c3..b8e5f623 100644
--- a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java
+++ b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java
@@ -20,6 +20,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
import java.io.IOException;
import junit.framework.TestCase;
@@ -55,19 +56,22 @@ public final class JsonElementReaderTest extends TestCase {
try {
reader.nextDouble();
fail();
- } catch (NumberFormatException e) {
+ } catch (MalformedJsonException e) {
+ assertEquals("JSON forbids NaN and infinities: NaN", e.getMessage());
}
assertEquals("NaN", reader.nextString());
try {
reader.nextDouble();
fail();
- } catch (NumberFormatException e) {
+ } catch (MalformedJsonException e) {
+ assertEquals("JSON forbids NaN and infinities: -Infinity", e.getMessage());
}
assertEquals("-Infinity", reader.nextString());
try {
reader.nextDouble();
fail();
- } catch (NumberFormatException e) {
+ } catch (MalformedJsonException e) {
+ assertEquals("JSON forbids NaN and infinities: Infinity", e.getMessage());
}
assertEquals("Infinity", reader.nextString());
reader.endArray();
diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java
index 1166381b..767d63bd 100644
--- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java
+++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java
@@ -16,10 +16,17 @@
package com.google.gson.internal.bind;
import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
+import com.google.gson.common.MoreAsserts;
+import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.List;
import junit.framework.TestCase;
@SuppressWarnings("resource")
@@ -28,6 +35,7 @@ public class JsonTreeReaderTest extends TestCase {
JsonTreeReader in = new JsonTreeReader(new JsonObject());
in.skipValue();
assertEquals(JsonToken.END_DOCUMENT, in.peek());
+ assertEquals("$", in.getPath());
}
public void testSkipValue_filledJsonObject() throws IOException {
@@ -46,6 +54,46 @@ public class JsonTreeReaderTest extends TestCase {
JsonTreeReader in = new JsonTreeReader(jsonObject);
in.skipValue();
assertEquals(JsonToken.END_DOCUMENT, in.peek());
+ assertEquals("$", in.getPath());
+ }
+
+ public void testSkipValue_name() throws IOException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("a", "value");
+ JsonTreeReader in = new JsonTreeReader(jsonObject);
+ in.beginObject();
+ in.skipValue();
+ assertEquals(JsonToken.STRING, in.peek());
+ assertEquals("$.<skipped>", in.getPath());
+ assertEquals("value", in.nextString());
+ }
+
+ public void testSkipValue_afterEndOfDocument() throws IOException {
+ JsonTreeReader reader = new JsonTreeReader(new JsonObject());
+ reader.beginObject();
+ reader.endObject();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+
+ assertEquals("$", reader.getPath());
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
+ }
+
+ public void testSkipValue_atArrayEnd() throws IOException {
+ JsonTreeReader reader = new JsonTreeReader(new JsonArray());
+ reader.beginArray();
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
+ }
+
+ public void testSkipValue_atObjectEnd() throws IOException {
+ JsonTreeReader reader = new JsonTreeReader(new JsonObject());
+ reader.beginObject();
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
}
public void testHasNext_endOfDocument() throws IOException {
@@ -54,4 +102,38 @@ public class JsonTreeReaderTest extends TestCase {
reader.endObject();
assertFalse(reader.hasNext());
}
+
+ public void testCustomJsonElementSubclass() throws IOException {
+ @SuppressWarnings("deprecation") // superclass constructor
+ class CustomSubclass extends JsonElement {
+ @Override
+ public JsonElement deepCopy() {
+ return this;
+ }
+ }
+
+ JsonArray array = new JsonArray();
+ array.add(new CustomSubclass());
+
+ JsonTreeReader reader = new JsonTreeReader(array);
+ reader.beginArray();
+ try {
+ // Should fail due to custom JsonElement subclass
+ reader.peek();
+ fail();
+ } catch (MalformedJsonException expected) {
+ assertEquals("Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported",
+ expected.getMessage());
+ }
+ }
+
+ /**
+ * {@link JsonTreeReader} effectively replaces the complete reading logic of {@link JsonReader} to
+ * read from a {@link JsonElement} instead of a {@link Reader}. Therefore all relevant methods of
+ * {@code JsonReader} must be overridden.
+ */
+ public void testOverrides() {
+ List<String> ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()");
+ MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods);
+ }
}
diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java
index 3167be13..ce91664e 100644
--- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java
+++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java
@@ -16,8 +16,14 @@
package com.google.gson.internal.bind;
+import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
+import com.google.gson.common.MoreAsserts;
+import com.google.gson.stream.JsonWriter;
import java.io.IOException;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.List;
import junit.framework.TestCase;
@SuppressWarnings("resource")
@@ -233,4 +239,25 @@ public final class JsonTreeWriterTest extends TestCase {
} catch (IllegalArgumentException expected) {
}
}
+
+ public void testJsonValue() throws IOException {
+ JsonTreeWriter writer = new JsonTreeWriter();
+ writer.beginArray();
+ try {
+ writer.jsonValue("test");
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+
+ /**
+ * {@link JsonTreeWriter} effectively replaces the complete writing logic of {@link JsonWriter} to
+ * create a {@link JsonElement} tree instead of writing to a {@link Writer}. Therefore all relevant
+ * methods of {@code JsonWriter} must be overridden.
+ */
+ public void testOverrides() {
+ List<String> ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()", "setIndent(java.lang.String)",
+ "setHtmlSafe(boolean)", "isHtmlSafe()", "setSerializeNulls(boolean)", "getSerializeNulls()");
+ MoreAsserts.assertOverridesMethods(JsonWriter.class, JsonTreeWriter.class, ignoredMethods);
+ }
}
diff --git a/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java
new file mode 100644
index 00000000..4d4089e8
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java
@@ -0,0 +1,83 @@
+package com.google.gson.internal.reflect;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.Objects;
+import org.junit.Test;
+
+public class Java17ReflectionHelperTest {
+ @Test
+ public void testJava17Record() throws ClassNotFoundException {
+ Class<?> unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
+ // UnixDomainPrincipal is a record
+ assertTrue(ReflectionHelper.isRecord(unixDomainPrincipalClass));
+ // with 2 components
+ assertArrayEquals(
+ new String[] {"user", "group"},
+ ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass));
+ // Check canonical constructor
+ Constructor<?> constructor =
+ ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass);
+ assertNotNull(constructor);
+ assertArrayEquals(
+ new Class<?>[] {UserPrincipal.class, GroupPrincipal.class},
+ constructor.getParameterTypes());
+ }
+
+ @Test
+ public void testJava17RecordAccessors() throws ReflectiveOperationException {
+ // Create an instance of UnixDomainPrincipal, using our custom implementation of UserPrincipal,
+ // and GroupPrincipal. Then attempt to access each component of the record using our accessor
+ // methods.
+ Class<?> unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal");
+ Object unixDomainPrincipal =
+ ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass)
+ .newInstance(new PrincipalImpl("user"), new PrincipalImpl("group"));
+
+ String[] componentNames = ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass);
+ assertTrue(componentNames.length > 0);
+
+ for (String componentName : componentNames) {
+ Field componentField = unixDomainPrincipalClass.getDeclaredField(componentName);
+ Method accessor = ReflectionHelper.getAccessor(unixDomainPrincipalClass, componentField);
+ Object principal = accessor.invoke(unixDomainPrincipal);
+
+ assertEquals(new PrincipalImpl(componentName), principal);
+ }
+ }
+
+ /** Implementation of {@link UserPrincipal} and {@link GroupPrincipal} just for record tests. */
+ public static class PrincipalImpl implements UserPrincipal, GroupPrincipal {
+ private final String name;
+
+ public PrincipalImpl(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof PrincipalImpl) {
+ return Objects.equals(name, ((PrincipalImpl) o).name);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java
index 1cdd7361..55c2e823 100644
--- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java
+++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java
@@ -16,6 +16,7 @@
package com.google.gson.reflect;
+import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
@@ -91,6 +92,12 @@ public final class TypeTokenTest extends TestCase {
TypeToken<?> expectedListOfStringArray = new TypeToken<List<String>[]>() {};
Type listOfString = new TypeToken<List<String>>() {}.getType();
assertEquals(expectedListOfStringArray, TypeToken.getArray(listOfString));
+
+ try {
+ TypeToken.getArray(null);
+ fail();
+ } catch (NullPointerException e) {
+ }
}
public void testParameterizedFactory() {
@@ -104,6 +111,97 @@ public final class TypeTokenTest extends TestCase {
Type listOfString = TypeToken.getParameterized(List.class, String.class).getType();
Type listOfListOfString = TypeToken.getParameterized(List.class, listOfString).getType();
assertEquals(expectedListOfListOfListOfString, TypeToken.getParameterized(List.class, listOfListOfString));
+
+ TypeToken<?> expectedWithExactArg = new TypeToken<GenericWithBound<Number>>() {};
+ assertEquals(expectedWithExactArg, TypeToken.getParameterized(GenericWithBound.class, Number.class));
+
+ TypeToken<?> expectedWithSubclassArg = new TypeToken<GenericWithBound<Integer>>() {};
+ assertEquals(expectedWithSubclassArg, TypeToken.getParameterized(GenericWithBound.class, Integer.class));
+
+ TypeToken<?> expectedSatisfyingTwoBounds = new TypeToken<GenericWithMultiBound<ClassSatisfyingBounds>>() {};
+ assertEquals(expectedSatisfyingTwoBounds, TypeToken.getParameterized(GenericWithMultiBound.class, ClassSatisfyingBounds.class));
+ }
+
+ public void testParameterizedFactory_Invalid() {
+ try {
+ TypeToken.getParameterized(null, new Type[0]);
+ fail();
+ } catch (NullPointerException e) {
+ }
+
+ GenericArrayType arrayType = (GenericArrayType) TypeToken.getArray(String.class).getType();
+ try {
+ TypeToken.getParameterized(arrayType, new Type[0]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("rawType must be of type Class, but was java.lang.String[]", e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(String.class, String.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("java.lang.String requires 0 type arguments, but got 1", e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(List.class, new Type[0]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("java.util.List requires 1 type arguments, but got 0", e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(List.class, String.class, String.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("java.util.List requires 1 type arguments, but got 2", e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(GenericWithBound.class, String.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Type argument class java.lang.String does not satisfy bounds "
+ + "for type variable T declared by " + GenericWithBound.class,
+ e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(GenericWithBound.class, Object.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Type argument class java.lang.Object does not satisfy bounds "
+ + "for type variable T declared by " + GenericWithBound.class,
+ e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(GenericWithMultiBound.class, Number.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Type argument class java.lang.Number does not satisfy bounds "
+ + "for type variable T declared by " + GenericWithMultiBound.class,
+ e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(GenericWithMultiBound.class, CharSequence.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Type argument interface java.lang.CharSequence does not satisfy bounds "
+ + "for type variable T declared by " + GenericWithMultiBound.class,
+ e.getMessage());
+ }
+
+ try {
+ TypeToken.getParameterized(GenericWithMultiBound.class, Object.class);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Type argument class java.lang.Object does not satisfy bounds "
+ + "for type variable T declared by " + GenericWithMultiBound.class,
+ e.getMessage());
+ }
}
private static class CustomTypeToken extends TypeToken<String> {
@@ -158,3 +256,13 @@ public final class TypeTokenTest extends TestCase {
}
}
}
+
+// Have to declare these classes here as top-level classes because otherwise tests for
+// TypeToken.getParameterized fail due to owner type mismatch
+class GenericWithBound<T extends Number> {
+}
+class GenericWithMultiBound<T extends Number & CharSequence> {
+}
+@SuppressWarnings("serial")
+abstract class ClassSatisfyingBounds extends Number implements CharSequence {
+}
diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java
index ab802be1..a755bd83 100644
--- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java
+++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java
@@ -16,6 +16,9 @@
package com.google.gson.stream;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
import com.google.gson.JsonElement;
import com.google.gson.internal.Streams;
import com.google.gson.internal.bind.JsonTreeReader;
@@ -27,9 +30,6 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assume.assumeTrue;
-
@SuppressWarnings("resource")
@RunWith(Parameterized.class)
public class JsonReaderPathTest {
@@ -221,12 +221,27 @@ public class JsonReaderPathTest {
assertEquals("$[2]", reader.getPath());
}
+ @Test public void skipArrayEnd() throws IOException {
+ JsonReader reader = factory.create("[[],1]");
+ reader.beginArray();
+ reader.beginArray();
+ assertEquals("$[0][0]", reader.getPreviousPath());
+ assertEquals("$[0][0]", reader.getPath());
+ reader.skipValue(); // skip end of array
+ assertEquals("$[0]", reader.getPreviousPath());
+ assertEquals("$[1]", reader.getPath());
+ }
+
@Test public void skipObjectNames() throws IOException {
- JsonReader reader = factory.create("{\"a\":1}");
+ JsonReader reader = factory.create("{\"a\":[]}");
reader.beginObject();
reader.skipValue();
- assertEquals("$.null", reader.getPreviousPath());
- assertEquals("$.null", reader.getPath());
+ assertEquals("$.<skipped>", reader.getPreviousPath());
+ assertEquals("$.<skipped>", reader.getPath());
+
+ reader.beginArray();
+ assertEquals("$.<skipped>[0]", reader.getPreviousPath());
+ assertEquals("$.<skipped>[0]", reader.getPath());
}
@Test public void skipObjectValues() throws IOException {
@@ -236,13 +251,25 @@ public class JsonReaderPathTest {
assertEquals("$.", reader.getPath());
reader.nextName();
reader.skipValue();
- assertEquals("$.null", reader.getPreviousPath());
- assertEquals("$.null", reader.getPath());
+ assertEquals("$.a", reader.getPreviousPath());
+ assertEquals("$.a", reader.getPath());
reader.nextName();
assertEquals("$.b", reader.getPreviousPath());
assertEquals("$.b", reader.getPath());
}
+ @Test public void skipObjectEnd() throws IOException {
+ JsonReader reader = factory.create("{\"a\":{},\"b\":2}");
+ reader.beginObject();
+ reader.nextName();
+ reader.beginObject();
+ assertEquals("$.a.", reader.getPreviousPath());
+ assertEquals("$.a.", reader.getPath());
+ reader.skipValue(); // skip end of object
+ assertEquals("$.a", reader.getPreviousPath());
+ assertEquals("$.a", reader.getPath());
+ }
+
@Test public void skipNestedStructures() throws IOException {
JsonReader reader = factory.create("[[1,2,3],4]");
reader.beginArray();
@@ -251,6 +278,20 @@ public class JsonReaderPathTest {
assertEquals("$[1]", reader.getPath());
}
+ @Test public void skipEndOfDocument() throws IOException {
+ JsonReader reader = factory.create("[]");
+ reader.beginArray();
+ reader.endArray();
+ assertEquals("$", reader.getPreviousPath());
+ assertEquals("$", reader.getPath());
+ reader.skipValue();
+ assertEquals("$", reader.getPreviousPath());
+ assertEquals("$", reader.getPath());
+ reader.skipValue();
+ assertEquals("$", reader.getPreviousPath());
+ assertEquals("$", reader.getPath());
+ }
+
@Test public void arrayOfObjects() throws IOException {
JsonReader reader = factory.create("[{},{},{}]");
reader.beginArray();
@@ -307,6 +348,52 @@ public class JsonReaderPathTest {
assertEquals("$", reader.getPath());
}
+ @Test public void objectOfObjects() throws IOException {
+ JsonReader reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}");
+ reader.beginObject();
+ assertEquals("$.", reader.getPreviousPath());
+ assertEquals("$.", reader.getPath());
+ reader.nextName();
+ assertEquals("$.a", reader.getPreviousPath());
+ assertEquals("$.a", reader.getPath());
+ reader.beginObject();
+ assertEquals("$.a.", reader.getPreviousPath());
+ assertEquals("$.a.", reader.getPath());
+ reader.nextName();
+ assertEquals("$.a.a1", reader.getPreviousPath());
+ assertEquals("$.a.a1", reader.getPath());
+ reader.nextInt();
+ assertEquals("$.a.a1", reader.getPreviousPath());
+ assertEquals("$.a.a1", reader.getPath());
+ reader.nextName();
+ assertEquals("$.a.a2", reader.getPreviousPath());
+ assertEquals("$.a.a2", reader.getPath());
+ reader.nextInt();
+ assertEquals("$.a.a2", reader.getPreviousPath());
+ assertEquals("$.a.a2", reader.getPath());
+ reader.endObject();
+ assertEquals("$.a", reader.getPreviousPath());
+ assertEquals("$.a", reader.getPath());
+ reader.nextName();
+ assertEquals("$.b", reader.getPreviousPath());
+ assertEquals("$.b", reader.getPath());
+ reader.beginObject();
+ assertEquals("$.b.", reader.getPreviousPath());
+ assertEquals("$.b.", reader.getPath());
+ reader.nextName();
+ assertEquals("$.b.b1", reader.getPreviousPath());
+ assertEquals("$.b.b1", reader.getPath());
+ reader.nextInt();
+ assertEquals("$.b.b1", reader.getPreviousPath());
+ assertEquals("$.b.b1", reader.getPath());
+ reader.endObject();
+ assertEquals("$.b", reader.getPreviousPath());
+ assertEquals("$.b", reader.getPath());
+ reader.endObject();
+ assertEquals("$", reader.getPreviousPath());
+ assertEquals("$", reader.getPath());
+ }
+
public enum Factory {
STRING_READER {
@Override public JsonReader create(String data) {
diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
index 7ec5e462..faaa87a2 100644
--- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
+++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
@@ -16,13 +16,6 @@
package com.google.gson.stream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.util.Arrays;
-import junit.framework.TestCase;
-
import static com.google.gson.stream.JsonToken.BEGIN_ARRAY;
import static com.google.gson.stream.JsonToken.BEGIN_OBJECT;
import static com.google.gson.stream.JsonToken.BOOLEAN;
@@ -33,6 +26,13 @@ import static com.google.gson.stream.JsonToken.NULL;
import static com.google.gson.stream.JsonToken.NUMBER;
import static com.google.gson.stream.JsonToken.STRING;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Arrays;
+import junit.framework.TestCase;
+
@SuppressWarnings("resource")
public final class JsonReaderTest extends TestCase {
public void testReadArray() throws IOException {
@@ -140,6 +140,35 @@ public final class JsonReaderTest extends TestCase {
assertEquals(JsonToken.END_DOCUMENT, reader.peek());
}
+ public void testSkipObjectName() throws IOException {
+ JsonReader reader = new JsonReader(reader("{\"a\": 1}"));
+ reader.beginObject();
+ reader.skipValue();
+ assertEquals(JsonToken.NUMBER, reader.peek());
+ assertEquals("$.<skipped>", reader.getPath());
+ assertEquals(1, reader.nextInt());
+ }
+
+ public void testSkipObjectNameSingleQuoted() throws IOException {
+ JsonReader reader = new JsonReader(reader("{'a': 1}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ reader.skipValue();
+ assertEquals(JsonToken.NUMBER, reader.peek());
+ assertEquals("$.<skipped>", reader.getPath());
+ assertEquals(1, reader.nextInt());
+ }
+
+ public void testSkipObjectNameUnquoted() throws IOException {
+ JsonReader reader = new JsonReader(reader("{a: 1}"));
+ reader.setLenient(true);
+ reader.beginObject();
+ reader.skipValue();
+ assertEquals(JsonToken.NUMBER, reader.peek());
+ assertEquals("$.<skipped>", reader.getPath());
+ assertEquals(1, reader.nextInt());
+ }
+
public void testSkipInteger() throws IOException {
JsonReader reader = new JsonReader(reader(
"{\"a\":123456789,\"b\":-123456789}"));
@@ -164,6 +193,34 @@ public final class JsonReaderTest extends TestCase {
assertEquals(JsonToken.END_DOCUMENT, reader.peek());
}
+ public void testSkipValueAfterEndOfDocument() throws IOException {
+ JsonReader reader = new JsonReader(reader("{}"));
+ reader.beginObject();
+ reader.endObject();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+
+ assertEquals("$", reader.getPath());
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
+ }
+
+ public void testSkipValueAtArrayEnd() throws IOException {
+ JsonReader reader = new JsonReader(reader("[]"));
+ reader.beginArray();
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
+ }
+
+ public void testSkipValueAtObjectEnd() throws IOException {
+ JsonReader reader = new JsonReader(reader("{}"));
+ reader.beginObject();
+ reader.skipValue();
+ assertEquals(JsonToken.END_DOCUMENT, reader.peek());
+ assertEquals("$", reader.getPath());
+ }
+
public void testHelloWorld() throws IOException {
String json = "{\n" +
" \"hello\": true,\n" +
diff --git a/metrics/pom.xml b/metrics/pom.xml
index 9073e96f..ca904248 100644
--- a/metrics/pom.xml
+++ b/metrics/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
- <version>2.9.1</version>
+ <version>2.10</version>
</parent>
<artifactId>gson-metrics</artifactId>
@@ -32,7 +32,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
- <version>2.13.3</version>
+ <version>2.13.4.2</version>
</dependency>
<dependency>
<groupId>com.google.caliper</groupId>
@@ -45,9 +45,17 @@
<pluginManagement>
<plugins>
<plugin>
+ <groupId>com.github.siom79.japicmp</groupId>
+ <artifactId>japicmp-maven-plugin</artifactId>
+ <version>0.16.0</version>
+ <configuration>
+ <!-- This module is not supposed to be consumed as library, so no need to check API -->
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
- <version>3.0.0</version>
<configuration>
<!-- Not deployed -->
<skip>true</skip>
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 dad0d99a..738b5ae4 100644
--- a/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java
+++ b/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java
@@ -33,14 +33,15 @@ import java.util.List;
*/
public class CollectionsDeserializationBenchmark {
- private static final Type LIST_TYPE = new TypeToken<List<BagOfPrimitives>>(){}.getType();
+ private static final TypeToken<List<BagOfPrimitives>> LIST_TYPE_TOKEN = new TypeToken<List<BagOfPrimitives>>(){};
+ private static final Type LIST_TYPE = LIST_TYPE_TOKEN.getType();
private Gson gson;
private String json;
public static void main(String[] args) {
NonUploadingCaliperRunner.run(CollectionsDeserializationBenchmark.class, args);
}
-
+
@BeforeExperiment
void setUp() throws Exception {
this.gson = new Gson();
@@ -51,12 +52,12 @@ public class CollectionsDeserializationBenchmark {
this.json = gson.toJson(bags, LIST_TYPE);
}
- /**
+ /**
* Benchmark to measure Gson performance for deserializing an object
*/
public void timeCollectionsDefault(int reps) {
for (int i=0; i<reps; ++i) {
- gson.fromJson(json, LIST_TYPE);
+ gson.fromJson(json, LIST_TYPE_TOKEN);
}
}
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 c00dbdcf..e6a45302 100644
--- a/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java
+++ b/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java
@@ -63,11 +63,11 @@ public final class ParseBenchmark {
READER_SHORT(new TypeToken<Feed>() {}, new TypeReference<Feed>() {}),
READER_LONG(new TypeToken<Feed>() {}, new TypeReference<Feed>() {});
- private final Type gsonType;
+ private final TypeToken<?> gsonType;
private final TypeReference<?> jacksonType;
private Document(TypeToken<?> typeToken, TypeReference<?> typeReference) {
- this.gsonType = typeToken.getType();
+ this.gsonType = typeToken;
this.jacksonType = typeReference;
}
}
diff --git a/pom.xml b/pom.xml
index 5d21fb93..155d98dd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,15 +3,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.sonatype.oss</groupId>
- <artifactId>oss-parent</artifactId>
- <version>9</version>
- </parent>
-
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
- <version>2.9.1</version>
+ <version>2.10</version>
<packaging>pom</packaging>
<name>Gson Parent</name>
@@ -20,23 +14,30 @@
<modules>
<module>gson</module>
- <module>extras</module>
+ <module>extras</module>
<module>metrics</module>
<module>proto</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <javaVersion>7</javaVersion>
+ <maven.compiler.release>7</maven.compiler.release>
</properties>
<scm>
<url>https://github.com/google/gson/</url>
<connection>scm:git:https://github.com/google/gson.git</connection>
<developerConnection>scm:git:git@github.com:google/gson.git</developerConnection>
- <tag>gson-parent-2.9.1</tag>
+ <tag>gson-parent-2.10</tag>
</scm>
+ <developers>
+ <developer>
+ <organization>Google</organization>
+ <organizationUrl>http://www.google.com</organizationUrl>
+ </developer>
+ </developers>
+
<issueManagement>
<system>GitHub Issues</system>
<url>https://github.com/google/gson/issues</url>
@@ -49,6 +50,14 @@
</license>
</licenses>
+ <distributionManagement>
+ <repository>
+ <id>sonatype-nexus-staging</id>
+ <name>Nexus Release Repository</name>
+ <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+ </repository>
+ </distributionManagement>
+
<dependencyManagement>
<dependencies>
<dependency>
@@ -68,7 +77,14 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
- <release>${javaVersion}</release>
+ <showWarnings>true</showWarnings>
+ <showDeprecation>true</showDeprecation>
+ <failOnWarning>true</failOnWarning>
+ <compilerArgs>
+ <!-- Enable all warnings, except for ones which cause issues when building with newer JDKs, see also
+ https://docs.oracle.com/en/java/javase/11/tools/javac.html -->
+ <compilerArg>-Xlint:all,-options</compilerArg>
+ </compilerArgs>
<jdkToolchain>
<version>[11,)</version>
</jdkToolchain>
@@ -77,11 +93,14 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
- <version>3.4.0</version>
+ <version>3.4.1</version>
<configuration>
<jdkToolchain>
<version>[11,)</version>
</jdkToolchain>
+ <!-- Specify newer JDK as target to allow linking to newer Java API, and to generate
+ module overview in Javadoc for Gson's module descriptor -->
+ <release>11</release>
<!-- Exclude `missing` group because some tags have been omitted when they are redundant -->
<doclint>all,-missing</doclint>
<!-- Link against newer Java API Javadoc because most users likely
@@ -96,36 +115,188 @@
the project URL (= Gson GitHub repo) which is incorrect because it is not
hosting the Javadoc (3) It might fail due to https://bugs.openjdk.java.net/browse/JDK-8212233 -->
<detectOfflineLinks>false</detectOfflineLinks>
+ <!-- Only show warnings and errors -->
+ <quiet>true</quiet>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
- <version>3.2.2</version>
+ <version>3.3.0</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <version>3.2.1</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <version>3.0.1</version>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-release-plugin</artifactId>
+ <version>3.0.0-M6</version>
+ <configuration>
+ <autoVersionSubmodules>true</autoVersionSubmodules>
+ <!-- Disable Maven Super POM release profile and instead use own one -->
+ <useReleaseProfile>false</useReleaseProfile>
+ <releaseProfiles>release</releaseProfiles>
+ <!-- Run custom goals to replace version references, see plugin configuration below -->
+ <!-- Also run `package`; otherwise goals fail for modules depending on each; possibly
+ same issue as https://issues.apache.org/jira/browse/MRELEASE-271 -->
+ <preparationGoals>
+ package -DskipTests
+ antrun:run@replace-version-placeholders
+ antrun:run@replace-old-version-references
+ antrun:run@git-add-changed
+ </preparationGoals>
+ </configuration>
+ </plugin>
+ <plugin>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>3.1.0</version>
+ <executions>
+ <!-- Replaces version placeholders with the current version; this is mainly useful for
+ Javadoc where this allows writing `@since $next-version$` -->
+ <execution>
+ <id>replace-version-placeholders</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <replace token="$next-version$" value="${project.version}" encoding="${project.build.sourceEncoding}">
+ <!-- erroronmissingdir=false for gson-parent which does not have source directory -->
+ <fileset dir="${project.build.sourceDirectory}" includes="**" erroronmissingdir="false" />
+ </replace>
+ </target>
+ </configuration>
+ </execution>
+ <!-- Replaces references to the old version in the documentation -->
+ <execution>
+ <id>replace-old-version-references</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <!-- Replace Maven and Gradle version references; uses regex lookbehind and lookahead -->
+ <replaceregexp match="(?&lt;=&lt;version&gt;).*(?=&lt;/version&gt;)|(?&lt;='com\.google\.code\.gson:gson:).*(?=')" flags="g" replace="${project.version}" encoding="${project.build.sourceEncoding}">
+ <fileset dir="${project.basedir}">
+ <include name="README.md" />
+ <include name="UserGuide.md" />
+ </fileset>
+ </replaceregexp>
+ </target>
+ </configuration>
+ <!-- Only has to be executed for parent project; don't inherit this to modules -->
+ <!-- This might be a bit hacky; execution with this ID seems to be missing for modules and Maven just executes default
+ configuration which does not have any targets configured. (not sure if this behavior is guaranteed) -->
+ <inherited>false</inherited>
+ </execution>
+ <!-- Adds changed files to the Git index; workaround because Maven Release Plugin does not support committing
+ additional files yet (https://issues.apache.org/jira/browse/MRELEASE-798), and for workarounds with
+ Maven SCM Plugin it is apparently necessary to know modified files in advance -->
+ <!-- Maven Release Plugin then just happens to include these changed files in its Git commit;
+ not sure if this behavior is guaranteed or if this relies on implementation details -->
+ <execution>
+ <id>git-add-changed</id>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <exec executable="git" dir="${project.basedir}" failonerror="true">
+ <arg value="add" />
+ <arg value="." />
+ </exec>
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <!-- Plugin for checking source and binary compatibility; used by GitHub workflow -->
+ <plugin>
+ <groupId>com.github.siom79.japicmp</groupId>
+ <artifactId>japicmp-maven-plugin</artifactId>
+ <version>0.16.0</version>
+ <configuration>
+ <oldVersion>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>${project.artifactId}</artifactId>
+ <!-- This is set by the GitHub workflow -->
+ <version>JAPICMP-OLD</version>
+ </dependency>
+ </oldVersion>
+ <newVersion>
+ <file>
+ <path>${project.build.directory}/${project.build.finalName}.${project.packaging}</path>
+ </file>
+ </newVersion>
+ <parameter>
+ <breakBuildOnSourceIncompatibleModifications>true</breakBuildOnSourceIncompatibleModifications>
+ <breakBuildOnBinaryIncompatibleModifications>true</breakBuildOnBinaryIncompatibleModifications>
+ <excludes>
+ <exclude>com.google.gson.internal</exclude>
+ </excludes>
+ <onlyModified>true</onlyModified>
+ <skipXmlReport>true</skipXmlReport>
+ <reportOnlyFilename>true</reportOnlyFilename>
+ </parameter>
+ </configuration>
</plugin>
</plugins>
</pluginManagement>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-release-plugin</artifactId>
- <version>2.5.3</version>
- <dependencies>
- <dependency>
- <groupId>org.apache.maven.scm</groupId>
- <artifactId>maven-scm-api</artifactId>
- <version>1.13.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.maven.scm</groupId>
- <artifactId>maven-scm-provider-gitexe</artifactId>
- <version>1.13.0</version>
- </dependency>
- </dependencies>
- <configuration>
- <autoVersionSubmodules>true</autoVersionSubmodules>
- </configuration>
- </plugin>
- </plugins>
</build>
+
+ <profiles>
+ <!-- Profile defining additional plugins to be executed for release -->
+ <profile>
+ <id>release</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-gpg-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>sign-artifacts</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>sign</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/proto/pom.xml b/proto/pom.xml
index 5bd3ecf1..ff920604 100644
--- a/proto/pom.xml
+++ b/proto/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.google.code.gson</groupId>
<artifactId>gson-parent</artifactId>
- <version>2.9.1</version>
+ <version>2.10</version>
</parent>
<artifactId>proto</artifactId>
@@ -87,7 +87,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
- <version>3.0.0</version>
<configuration>
<!-- Not deployed -->
<skip>true</skip>
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 4b38c7c4..9aa166fc 100644
--- a/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java
+++ b/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java
@@ -16,7 +16,7 @@
package com.google.gson.protobuf;
-import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Objects.requireNonNull;
import com.google.common.base.CaseFormat;
import com.google.common.collect.MapMaker;
@@ -64,7 +64,6 @@ import java.util.concurrent.ConcurrentMap;
* string os_build_id = 1 [(serialized_name) = "osBuildID"];
* }
* </pre>
- * <p>
*
* @author Inderjeet Singh
* @author Emmanuel Cron
@@ -104,7 +103,7 @@ public class ProtoTypeAdapter
}
public Builder setEnumSerialization(EnumSerialization enumSerialization) {
- this.enumSerialization = checkNotNull(enumSerialization);
+ this.enumSerialization = requireNonNull(enumSerialization);
return this;
}
@@ -144,7 +143,7 @@ public class ProtoTypeAdapter
*/
public Builder addSerializedNameExtension(
Extension<FieldOptions, String> serializedNameExtension) {
- serializedNameExtensions.add(checkNotNull(serializedNameExtension));
+ serializedNameExtensions.add(requireNonNull(serializedNameExtension));
return this;
}
@@ -169,7 +168,7 @@ public class ProtoTypeAdapter
*/
public Builder addSerializedEnumValueExtension(
Extension<EnumValueOptions, String> serializedEnumValueExtension) {
- serializedEnumValueExtensions.add(checkNotNull(serializedEnumValueExtension));
+ serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension));
return this;
}