Upgrade gson to gson-parent-2.10

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

Test: TreeHugger
Change-Id: I15d05032a727c597a2527358a9a06a7c0f7d0c05
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index b8a47ca..0000000
--- 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 0008892..ef1b23d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,16 +4,20 @@
 
 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 0000000..a446576
--- /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 17ed734..01d95bd 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -25,7 +25,7 @@
 
     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 @@
     # 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/CHANGELOG.md b/CHANGELOG.md
index 374faf3..b0790fc 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 71e9085..ce36b88 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 @@
     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 b3c5b55..dddaf95 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
 Gradle:
 ```gradle
 dependencies {
-  implementation 'com.google.code.gson:gson:2.9.0'
+  implementation 'com.google.code.gson:gson:2.10'
 }
 ```
 
@@ -28,7 +28,7 @@
 <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 4b85f4d..eaa0e7c 100644
--- a/ReleaseProcess.md
+++ b/ReleaseProcess.md
@@ -6,19 +6,23 @@
 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 @@
 
 ## 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 3764b82..b82bd72 100644
--- a/UserGuide.md
+++ b/UserGuide.md
@@ -76,7 +76,7 @@
 
 ```gradle
 dependencies {
-    implementation 'com.google.code.gson:gson:2.9.0'
+    implementation 'com.google.code.gson:gson:2.10'
 }
 ```
 
@@ -90,7 +90,7 @@
     <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 @@
   * 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 @@
 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 @@
 
 ```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 da752f1..533f78f 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 e6a07f1..c48c3cd 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 @@
  * 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 @@
     }
   }
 
-  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 @@
      * <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 @@
      * 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 @@
       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 502ad4e..87b522f 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 @@
  * 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 @@
  *       .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 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 @@
         } 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 @@
               + " 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 e3574bb..4ade154 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 @@
         assertEquals(sandwiches, sandwichesFromJson);
     }
 
+    @SuppressWarnings("overrides") // for missing hashCode() override
     static class Sandwich {
         public String bread;
         public String cheese;
@@ -89,6 +87,7 @@
         }
     }
 
+    @SuppressWarnings("overrides") // for missing hashCode() override
     static class MultipleSandwiches {
         public List<Sandwich> sandwiches;
 
diff --git a/gson/pom.xml b/gson/pom.xml
index 04af473..277ab59 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,93 +31,9 @@
 
   <build>
     <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-compiler-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>default-compile</id>
-            <configuration>
-              <excludes>
-                <!-- module-info.java is compiled using ModiTect -->
-                <exclude>module-info.java</exclude>
-              </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>
-            <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>
-      <plugin>
-        <groupId>biz.aQute.bnd</groupId>
-        <artifactId>bnd-maven-plugin</artifactId>
-        <version>6.3.1</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>bnd-process</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
-      <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>
+      <!--
+        Plugins for source generation and compilation
+      -->
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>templating-maven-plugin</artifactId>
@@ -132,6 +52,64 @@
         </executions>
       </plugin>
       <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>default-compile</id>
+            <configuration>
+              <excludes>
+                <!-- module-info.java is compiled using ModiTect -->
+                <exclude>module-info.java</exclude>
+              </excludes>
+            </configuration>
+          </execution>
+          <execution>
+            <id>default-testCompile</id>
+            <phase>test-compile</phase>
+            <goals>
+              <goal>testCompile</goal>
+            </goals>
+            <configuration>
+              <testExcludes>
+                <exclude>${excludeTestCompilation}</exclude>
+              </testExcludes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>biz.aQute.bnd</groupId>
+        <artifactId>bnd-maven-plugin</artifactId>
+        <version>6.3.1</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>bnd-process</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!--
+        Plugins for test execution
+      -->
+      <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>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 bc0dc74..557eddb 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 @@
  *
  * <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 9fb93f7..4b9e16a 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 @@
    * @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 a4fa7c2..cd42f42 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 @@
    *   <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 @@
    * 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 @@
    * 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 666e5f8..262cd17 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.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.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 @@
  * <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 @@
  *       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 @@
   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 @@
   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 @@
    *   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 @@
    * instance.
    *
    * @return a GsonBuilder instance.
+   * @since 2.8.3
    */
   public GsonBuilder newBuilder() {
     return new GsonBuilder(this);
@@ -398,7 +407,7 @@
         }
         double doubleValue = value.doubleValue();
         checkValidFloatingPoint(doubleValue);
-        out.value(value);
+        out.value(doubleValue);
       }
     };
   }
@@ -422,7 +431,10 @@
         }
         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 @@
    * @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 @@
     }
 
     // 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 @@
       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 @@
    * {@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 @@
    * </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 @@
   }
 
   /**
-   * 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 @@
 
   /**
    * 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 @@
    * <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 @@
   }
 
   /**
-   * 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 @@
 
   /**
    * 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 @@
    * <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 @@
    *
    * @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 @@
     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 @@
    * 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 @@
   }
 
   /**
-   * 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 @@
    * 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. 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>
+   * @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 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 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 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 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 the type typeOfT
+   *
+   * @see #fromJson(Reader, TypeToken)
+   * @see #fromJson(String, Class)
+   * @since 2.10
+   */
+  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 @@
     }
   }
 
+  // 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 @@
    * 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 @@
   }
 
   /**
-   * 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
+   * @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 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 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
-   * @since 1.3
+   *
+   * @see #fromJson(Reader, TypeToken)
+   * @see #fromJson(JsonElement, Class)
+   * @since 2.10
    */
-  @SuppressWarnings("unchecked")
-  public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException {
+  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 @@
       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 4c540ac..8b04430 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_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 @@
   }
 
   /**
-   * 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.
    *
-   * @param ignoreVersionsAfter any field or type marked with a version higher than this value
-   * are ignored during serialization or deserialization.
+   * <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until}
+   * has no effect.
+   *
+   * @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 @@
    * @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 @@
   }
 
   /**
-   * 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 @@
    * 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 @@
    *   }
    * }</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 @@
   }
 
   /**
-   * 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 @@
    * @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 @@
    * @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 @@
    * @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 @@
    * 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 @@
    * 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 @@
    * 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 @@
    * 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 @@
    * 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 @@
    * @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 @@
       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 @@
    * 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 @@
    * 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 fe8b686..370b323 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.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 @@
    * 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 @@
    * 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 @@
    * 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 @@
    * 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 @@
 
   /**
    * 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 @@
    * 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 @@
 
   /**
    * 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 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 @@
   }
 
   /**
-   * 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 @@
     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 0589eb2..6462d45 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 @@
 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 @@
  * </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 fb6a5f1..640f881 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.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 @@
  */
 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 67cb932..b14fd3f 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 @@
  */

 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 @@
   }

 

   /**

-   * 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 @@
   }

 

   /**

-   * 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 @@
   }

 

   /**

-   * 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 285a842..60dac41 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 @@
  * 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 a deep copy of this element and all its children
+   * Creates an empty JsonObject.
+   */
+  @SuppressWarnings("deprecation") // superclass constructor
+  public JsonObject() {
+  }
+
+  /**
+   * Creates a deep copy of this element and all its children.
+   *
    * @since 2.8.2
    */
   @Override
@@ -47,7 +59,7 @@
 
   /**
    * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
 
   /**
    * 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 @@
 
   /**
    * 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 @@
    * 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 @@
    * 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 d3508c1..5b80042 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 @@
    * @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 @@
    * @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 @@
    *

    * @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 5e95d5a..92a8df1 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 @@
    *
    * @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 @@
    *
    * @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 @@
    *
    * @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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
     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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
  /**
-  * 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 @@
     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 19eaf17..2bdb8fb 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 @@
 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 @@
  * </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 b787ae8..254d2e5 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 @@
  * 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 bd70213..8689298 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 @@
  * {@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 3cd84fa..ae74221 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 @@
  * @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 ba79853..98e1668 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 @@
 /**
  * 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 @@
  */
 // 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 @@
 //
 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 @@
    * 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 @@
    * 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 @@
     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 @@
    *
    * @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 @@
 
   /**
    * 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 @@
    *
    * @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 c12429e..75fdddb 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 @@
  * 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 @@
  * 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 a01d77a..d168575 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 @@
  * </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 @@
  *
  * <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 e23b6ec..a7e51fc 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 @@
 
 /**
  * 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 @@
  *
  * @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 7c61d10..a5fcabd 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 @@
 
 /**
  * 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 @@
  * 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 @@
 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 f0e7d3f..f1056ef 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 @@
     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 9891154..965e010 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.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 @@
     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 @@
       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 @@
     private final Type componentType;
 
     public GenericArrayTypeImpl(Type componentType) {
+      requireNonNull(componentType);
       this.componentType = canonicalize(componentType);
     }
 
@@ -590,14 +592,14 @@
       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 68b2bd6..115a2a0 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 @@
     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 @@
 
     // 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 @@
           @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 @@
   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 8d8a25f..03bd45c 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 @@
   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 @@
   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 6138dff..abc4b2a 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 @@
  *
  * @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 40eb8bb..e47e165 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 @@
  *
  * <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 @@
     }
   };
 
-  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, and whose values can be {@code null}.
+   */
+  @SuppressWarnings("unchecked") // unsafe! this assumes K is comparable
+  public LinkedTreeMap() {
+    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() {
-    this((Comparator<? super K>) NATURAL_ORDER);
+  public LinkedTreeMap(boolean allowNullValues) {
+    this((Comparator<? super K>) NATURAL_ORDER, allowNullValues);
   }
 
   /**
@@ -69,12 +82,15 @@
    *
    * @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 @@
     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 @@
       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 @@
     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 @@
     }
 
     @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 0000000..b301743
--- /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 0bb73aa..eafbbbe 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 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 @@
     }
 
     @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 write(int i) throws IOException {
-      appendable.append((char) i);
-    }
-
     @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 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 @@
       @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 429bac6..fae6f80 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.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 <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 efaa834..23648e5 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.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 @@
 
       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 @@
       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 @@
     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;
     }
-    return array;
+    // But for Object[] can use ArrayList.toArray
+    else {
+      @SuppressWarnings("unchecked")
+      E[] array = (E[]) Array.newInstance(componentType, size);
+      return list.toArray(array);
+    }
   }
 
-  @SuppressWarnings("unchecked")
   @Override public void write(JsonWriter out, Object array) throws IOException {
     if (array == null) {
       out.nullValue();
@@ -91,6 +99,7 @@
 
     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 0b7435f..4719ea1 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,6 +16,15 @@
 
 package com.google.gson.internal.bind;
 
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+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;
@@ -25,17 +34,7 @@
 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.util.Objects;
 
 /**
  * This type adapter supports subclasses of date by defining a
@@ -93,7 +92,7 @@
   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 @@
   }
 
   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 @@
   }
 
   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 13a7bb7..643c519 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 @@
     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 @@
     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 @@
           + " 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 a753402..81c3363 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.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 @@
 
   @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 @@
     } 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 @@
     }
   }
 
-  @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 @@
     }
     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 @@
   }
 
   @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 e28fbfe..6ff1aa4 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.Writer;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * This writer creates a JsonElement.
@@ -130,9 +131,7 @@
   }
 
   @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 @@
     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 f7c5a55..68ecffb 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 @@
 /**
  * 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 @@
  *   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 c5f2ec7..4b40944 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 @@
     }
   }
 
-  @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 95d01ac..5ddac50 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 @@
 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.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 @@
 
     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 @@
       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 @@
         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 @@
         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,56 +310,51 @@
 
   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 {
-      if (in.peek() == JsonToken.NULL) {
-        in.nextNull();
-        return null;
-      }
-
-      T instance = constructor.construct();
-
-      try {
-        in.beginObject();
-        while (in.hasNext()) {
-          String name = in.nextName();
-          BoundField field = boundFields.get(name);
-          if (field == null || !field.deserialized) {
-            in.skipValue();
-          } else {
-            field.read(in, instance);
-          }
-        }
-      } catch (IllegalStateException e) {
-        throw new JsonSyntaxException(e);
-      } catch (IllegalAccessException e) {
-        throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
-      }
-      in.endObject();
-      return instance;
-    }
-
-    @Override public void write(JsonWriter out, T value) throws IOException {
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
       if (value == null) {
         out.nullValue();
         return;
@@ -291,5 +370,163 @@
       }
       out.endObject();
     }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+
+      A accumulator = createAccumulator();
+
+      try {
+        in.beginObject();
+        while (in.hasNext()) {
+          String name = in.nextName();
+          BoundField field = boundFields.get(name);
+          if (field == null || !field.deserialized) {
+            in.skipValue();
+          } else {
+            readField(accumulator, in, field);
+          }
+        }
+      } catch (IllegalStateException e) {
+        throw new JsonSyntaxException(e);
+      } catch (IllegalAccessException e) {
+        throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+      }
+      in.endObject();
+      return finalize(accumulator);
+    }
+
+    /** 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);
+      }
+
+      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 {
+        return constructor.newInstance(accumulator);
+      } catch (IllegalAccessException e) {
+        throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+      }
+      // 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 0000000..dad4ff1
--- /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 50f46b5..560234c 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 @@
  * 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 @@
       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 @@
       delegate().write(out, value);
       return;
     }
-    if (value == null) {
+    if (nullSafe && value == null) {
       out.nullValue();
       return;
     }
@@ -91,6 +98,15 @@
   }
 
   /**
+   * 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 @@
     @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 2bf37ad..75a991e 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 @@
     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 @@
     // 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 @@
   }

 

   /**

+   * 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 9ba1363..cb069ae 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 @@
     }
     @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 @@
     }
     @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 @@
     }
     @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 @@
     }
     @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 @@
     }
     @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 @@
     }
     @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 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 @@
       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 97230ff..ac06121 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 @@
 
 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);
     }
   }
 
   /**
-   * Creates a string representation for a constructor.
-   * E.g.: {@code java.lang.String#String(char[], int, int)}
+   * 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
    */
-  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 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(char[], int, int)}
+   */
+  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 @@
       stringBuilder.append(parameters[i].getSimpleName());
     }
 
-    return stringBuilder.append(')').toString();
+    stringBuilder.append(')');
   }
 
   /**
@@ -58,17 +116,155 @@
       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 b12d201..39e81f3 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 @@
  * 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 @@
  * 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 @@
    */
   @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 @@
   }
 
   /**
-   * 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 6cb820b..ed6bab9 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.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 @@
  * 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 @@
  * 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 @@
  *     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 @@
  * 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 @@
    * 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 @@
     }
   }
 
+  @SuppressWarnings("fallthrough")
   int doPeek() throws IOException {
     int peekStack = stack[stackSize - 1];
     if (peekStack == JsonScope.EMPTY_ARRAY) {
@@ -751,6 +750,7 @@
     }
   }
 
+  @SuppressWarnings("fallthrough")
   private boolean isLiteral(char c) throws IOException {
     switch (c) {
     case '/':
@@ -777,10 +777,9 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
     throw syntaxError("Unterminated string");
   }
 
+  @SuppressWarnings("fallthrough")
   private void skipUnquotedValue() throws IOException {
     do {
       int i = 0;
@@ -1160,7 +1162,7 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
   }
 
   /**
-   * 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 @@
         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 @@
    *   <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 @@
    *   <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 @@
    * @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 da69137..9ec0bcf 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 @@
     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 c281009..90e3529 100644
--- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java
+++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java
@@ -16,17 +16,6 @@
 
 package com.google.gson.stream;
 
-import java.io.Closeable;
-import java.io.Flushable;
-import java.io.IOException;
-import java.io.Writer;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.Arrays;
-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;
@@ -35,17 +24,28 @@
 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;
+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;
+
 /**
  * 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 @@
  *       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 @@
   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 @@
     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 @@
    * {@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 @@
    * 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 @@
    * @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 @@
 
   /**
    * 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 @@
    * Encodes {@code value}.
    *
    * @return this writer.
+   * @since 2.7
    */
   public JsonWriter value(Boolean value) throws IOException {
     if (value == null) {
@@ -499,6 +497,7 @@
    * @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 d1fd0d4..e1a013b 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 @@
     }

   };

 

+  @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,6 +151,27 @@
     assertEquals("{\"d\":\"d\"}", gson.toJson(new HasModifiers()));

   }

 

+  @SuppressWarnings("unused")

+  static class HasModifiers {

+    private String a = "a";

+    volatile String b = "b";

+    private volatile String c = "c";

+    String d = "d";

+  }

+

+  @Test

+  public void testTransientFieldExclusion() {

+    Gson gson = new GsonBuilder()

+        .excludeFieldsWithModifiers()

+        .create();

+    assertEquals("{\"a\":\"a\"}", gson.toJson(new HasTransients()));

+  }

+

+  static class HasTransients {

+    transient String a = "a";

+  }

+

+  @Test

   public void testRegisterTypeAdapterForCoreType() {

     Type[] types = {

         byte.class,

@@ -66,25 +186,7 @@
     }

   }

 

-  @SuppressWarnings("unused")

-  static class HasModifiers {

-    private String a = "a";

-    volatile String b = "b";

-    private volatile String c = "c";

-    String d = "d";

-  }

-

-  public void testTransientFieldExclusion() {

-    Gson gson = new GsonBuilder()

-        .excludeFieldsWithModifiers()

-        .create();

-    assertEquals("{\"a\":\"a\"}", gson.toJson(new HasTransients()));

-  }

-

-  static class HasTransients {

-    transient String a = "a";

-  }

-

+  @Test

   public void testDisableJdkUnsafe() {

     Gson gson = new GsonBuilder()

         .disableJdkUnsafe()

@@ -107,4 +209,22 @@
     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 abb0de2..4274d26 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.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 @@
     @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 @@
     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 2e00dc9..d92994f 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 @@
       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 0000000..a1786ce
--- /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 3975ce2..45070e3 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 @@
     assertFalse(b.equals(a));
   }
 
+  @Test
   public void testRemove() {
     JsonArray array = new JsonArray();
     try {
@@ -67,6 +76,7 @@
     assertTrue(array.contains(a));
   }
 
+  @Test
   public void testSet() {
     JsonArray array = new JsonArray();
     try {
@@ -75,15 +85,23 @@
     } 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 @@
     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 @@
               "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 0000000..00a89a6
--- /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 6f5274f..a0109ba 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 @@
     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 @@
     assertTrue(jsonElement.isJsonNull());
   }
 
+  @Test
   public void testAddingNullOrEmptyPropertyName() throws Exception {
     JsonObject jsonObj = new JsonObject();
     try {
@@ -66,6 +87,7 @@
     jsonObj.add("   \t", JsonNull.INSTANCE);
   }
 
+  @Test
   public void testAddingBooleanProperties() throws Exception {
     String propertyName = "property";
     JsonObject jsonObj = new JsonObject();
@@ -78,6 +100,7 @@
     assertTrue(jsonElement.getAsBoolean());
   }
 
+  @Test
   public void testAddingStringProperties() throws Exception {
     String propertyName = "property";
     String value = "blah";
@@ -92,6 +115,7 @@
     assertEquals(value, jsonElement.getAsString());
   }
 
+  @Test
   public void testAddingCharacterProperties() throws Exception {
     String propertyName = "property";
     char value = 'a';
@@ -113,6 +137,7 @@
   /**
    * 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 @@
   /**
    * From issue 227.
    */
+  @Test
   public void testWritePropertyWithEmptyStringName() {
     JsonObject jsonObj = new JsonObject();
     jsonObj.add("", new JsonPrimitive(true));
@@ -130,15 +156,18 @@
 
   }
 
+  @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 @@
     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 @@
     assertEquals(1, o.size());
   }
 
+  @Test
   public void testDeepCopy() {
     JsonObject original = new JsonObject();
     JsonArray firstEntry = new JsonArray();
@@ -190,8 +238,10 @@
   /**
    * 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 @@
     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 cdd4fdb..ae2e0f2 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 @@
     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 @@
 
     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 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 00eb4bc..fa16659 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 @@
     } 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 ab44637..725ecda 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 @@
 
 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 @@
     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 d878850..2b3fbaf 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));
   }
 
-  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));
+  @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));
   }
 
   @Since(VERSION)
-  private static class MockObject {
+  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)
+  @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 f409480..f69802c 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 @@
     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 11d3d0a..1380763 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.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 @@
  * @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 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 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 @@
     }
   }
 
+  @SuppressWarnings("overrides") // for missing hashCode() override
   public static class ClassWithNoFields {
     // Nothing here..
     @Override
@@ -271,7 +271,7 @@
   }
 
   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 da8be85..9d0f89a 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 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 @@
     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 @@
     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 @@
     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 void testArrayOfNullSerialization() {
-    Object[] array = new Object[] {null};
+    Object[] array = {null};
     String json = gson.toJson(array);
     assertEquals("[null]", json);
   }
@@ -222,8 +221,8 @@
   /**
    * 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 @@
     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 @@
     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 f113b85..44a655c 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.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 @@
     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 @@
     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 @@
     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 b14ed52..1c38e6c 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.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 @@
     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 91a4639..218c97a 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.TimeZone;
 import java.util.TreeSet;
 import java.util.UUID;
-
 import junit.framework.TestCase;
 
 /**
@@ -654,14 +653,13 @@
     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 aa6f4cc..daa7aa4 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 @@
  * @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 95e3e3e..3ed6032 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.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 @@
     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 169c37a..bdf6ea6 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 @@
     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 @@
     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 0000000..a172f5a
--- /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 e6cb6dc..f539884 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 @@
       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 22a479b..0000000
--- 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 ef9eae2..c5344a7 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.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 @@
     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 @@
   }
 
   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 e9aa15b..bed5b59 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.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.Locale;
 import java.util.Map;
 import java.util.TimeZone;
-
 import junit.framework.TestCase;
 
 /**
@@ -121,18 +121,16 @@
     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 @@
     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 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 8decc64..e616874 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.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 @@
  * @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 @@
     assertEquals(src.getExpectedJson(), json);
   }
 
+  @Test
   public void testParameterizedTypeDeserialization() throws Exception {
     BagOfPrimitives bag = new BagOfPrimitives();
     MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<>(bag);
@@ -72,6 +78,7 @@
     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 @@
     assertEquals(expected, json);
   }
 
+  @Test
   public void testTypesWithMultipleParametersDeserialization() throws Exception {
     Type typeOfTarget = new TypeToken<MultiParameters<Integer, Float, Double, String,
         BagOfPrimitives>>() {}.getType();
@@ -95,6 +103,7 @@
     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 @@
     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 @@
     assertEquals("abc", stringTarget.value);
   }
 
+  @Test
   public void testParameterizedTypesWithWriterSerialization() throws Exception {
     Writer writer = new StringWriter();
     MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
@@ -140,6 +151,7 @@
     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 @@
     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 @@
     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 @@
     assertEquals(objAfterDeserialization.getExpectedJson(), json);
   }
 
+  @Test
   public void testVariableTypeDeserialization() throws Exception {
     Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
     ObjectWithTypeVariables<Integer> objToSerialize =
@@ -199,6 +218,7 @@
     assertEquals(objAfterDeserialization.getExpectedJson(), json);
   }
 
+  @Test
   public void testVariableTypeArrayDeserialization() throws Exception {
     Integer[] array = { 1, 2, 3 };
 
@@ -211,6 +231,7 @@
     assertEquals(objAfterDeserialization.getExpectedJson(), json);
   }
 
+  @Test
   public void testParameterizedTypeWithVariableTypeDeserialization() throws Exception {
     List<Integer> list = new ArrayList<>();
     list.add(4);
@@ -225,12 +246,12 @@
     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 @@
     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 @@
       return true;
     }
   }
-  
+
   // Begin: tests to reproduce issue 103
   private static class Quantity {
     @SuppressWarnings("unused")
@@ -475,21 +496,23 @@
   }
   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 @@
     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 6d74cc2..c4c25f0 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 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 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 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 7dcbc23..6801ba0 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.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 @@
     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 e21fb90..a04723b 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.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.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 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 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 @@
     } 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 775baf9..6c9ab44 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 @@
 
 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.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 @@
   }
 
   @Test
-  public void testBlockInaccessibleJava() {
+  public void testBlockInaccessibleJava() throws ReflectiveOperationException {
     Gson gson = new GsonBuilder()
       .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
       .create();
@@ -52,14 +53,25 @@
     } 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 @@
       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 @@
       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 @@
       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 @@
         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 @@
       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 @@
       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 @@
       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 @@
       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 @@
         }
         @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 @@
       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 @@
       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 ece3512..02649c5 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 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 @@
   }
 
   @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 @@
     // 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 f82d92e..006c6eb 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.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 @@
   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 @@
   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 @@
       // 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 0000000..73a0101
--- /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 e5a4d8b..f9ef46b 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 @@
     assertEquals(blue1, blue2);
   }
 
+  @SuppressWarnings("overrides") // for missing hashCode() override
   public static class Blue extends Red<Boolean> {
     public Blue() {
       super(false);
@@ -79,7 +78,6 @@
       super(value);
     }
 
-    // Technically, we should implement hashcode too
     @Override
     public boolean equals(Object o) {
       if (!(o instanceof Blue)) {
@@ -100,6 +98,7 @@
     }
   }
 
+  @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 @@
       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 2416fc0..49dabca 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 @@
  * @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 @@
     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 @@
     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 @@
     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 54b6286..582d683 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 @@
 
   @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 ee1bb10..0b08d32 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.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 @@
     } 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 void testContainsNullKeyIsAlwaysFalse() {
     LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    assertFalse(map.containsKey(null));
     map.put("a", "android");
     assertFalse(map.containsKey(null));
   }
@@ -160,6 +213,7 @@
     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 0000000..d0cb90a
--- /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 e3ce147..54d0a50 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 @@
    * 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 @@
    * 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 @@
    * 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 3d1ec7f..c20a368 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.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 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 0000000..18984c7
--- /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 204fb3c..b8e5f62 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.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 @@
     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 1166381..767d63b 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 @@
     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 @@
     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 @@
     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 3167be1..ce91664 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 @@
     } 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 0000000..4d4089e
--- /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 1cdd736..55c2e82 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 @@
     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 @@
     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 @@
     }
   }
 }
+
+// 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 ab802be..a755bd8 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.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 @@
     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 @@
     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 @@
     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 @@
     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 7ec5e46..faaa87a 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.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 @@
     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 @@
     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 9073e96..ca90424 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 dad0d99..738b5ae 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 @@
  */
 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 @@
     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 c00dbdc..e6a4530 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 @@
     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 5d21fb9..155d98d 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 5bd3ecf..ff92060 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 4b38c7c..9aa166f 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 @@
  *   string os_build_id = 1 [(serialized_name) = "osBuildID"];
  * }
  * </pre>
- * <p>
  *
  * @author Inderjeet Singh
  * @author Emmanuel Cron
@@ -104,7 +103,7 @@
     }
 
     public Builder setEnumSerialization(EnumSerialization enumSerialization) {
-      this.enumSerialization = checkNotNull(enumSerialization);
+      this.enumSerialization = requireNonNull(enumSerialization);
       return this;
     }
 
@@ -144,7 +143,7 @@
      */
     public Builder addSerializedNameExtension(
         Extension<FieldOptions, String> serializedNameExtension) {
-      serializedNameExtensions.add(checkNotNull(serializedNameExtension));
+      serializedNameExtensions.add(requireNonNull(serializedNameExtension));
       return this;
     }
 
@@ -169,7 +168,7 @@
      */
     public Builder addSerializedEnumValueExtension(
         Extension<EnumValueOptions, String> serializedEnumValueExtension) {
-      serializedEnumValueExtensions.add(checkNotNull(serializedEnumValueExtension));
+      serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension));
       return this;
     }