diff --git a/gson/.git-blame-ignore-revs b/gson/.git-blame-ignore-revs
new file mode 100644
index 0000000000000000000000000000000000000000..6d87ad4b55efa9b0cfcec0272ecab689b8b27052
--- /dev/null
+++ b/gson/.git-blame-ignore-revs
@@ -0,0 +1,5 @@
+# Ignore commit which reformatted code
+2c94c757a6a9426cc2fe47bc1c63f69e7c73b7b4
+
+# Ignore commit which changed line endings consistently to LF
+c2a0e4634a2100494159add78db2ee06f5eb9be6
diff --git a/gson/.github/ISSUE_TEMPLATE/bug_report.md b/gson/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000000000000000000000000000000000..7776465ccc8175c6286f9f014f0ba3a9596b4402
--- /dev/null
+++ b/gson/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,49 @@
+---
+name: Bug report
+about: Report a Gson bug. Please have a look at the troubleshooting guide (Troubleshooting.md) first.
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+# Gson version
+<!-- Gson version you are using, for example '2.8.8' -->
+
+
+# Java / Android version
+<!-- Version of the Java or Android platform on which the bug occurred -->
+
+
+# Used tools
+<!-- List relevant build tools and plugins with version number here which might affect Gson -->
+- [ ] Maven; version: 
+- [ ] Gradle; version: 
+- [ ] ProGuard (attach the configuration file please); version: 
+- [ ] ...
+
+# Description
+<!-- Describe the bug you experienced -->
+
+
+## Expected behavior
+<!-- What behavior did you expect? -->
+
+
+## Actual behavior
+<!-- What happened instead? -->
+
+
+# Reproduction steps
+<!-- Provide exact reproduction steps for reproducing the bug -->
+<!-- Provide a short code snippet or link to a demo project -->
+
+1. ...
+2. ...
+
+# Exception stack trace
+<!-- In case an exception occurred, paste the COMPLETE exception stack trace in the code block below or attach it as file -->
+
+```
+
+```
diff --git a/gson/.github/ISSUE_TEMPLATE/config.yml b/gson/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b798788d02e2538b963890b5cda231c76eb1292c
--- /dev/null
+++ b/gson/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,4 @@
+contact_links:
+  - name: Usage question
+    url: https://stackoverflow.com/questions/tagged/gson
+    about: Ask usage questions on StackOverflow.
diff --git a/gson/.github/ISSUE_TEMPLATE/feature_request.md b/gson/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000000000000000000000000000000000..176fa7a1509e0e26d2cc70d99cdd23c463b6b8fb
--- /dev/null
+++ b/gson/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Request a feature. ⚠️ Gson is in maintenance mode; large feature requests might be rejected.
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+# Problem solved by the feature
+<!-- Describe which problem the requested feature solves -->
+
+
+# Feature description
+<!-- Describe the feature -->
+
+
+# Alternatives / workarounds
+<!-- Describe alternatives or workarounds in case you are aware of any -->
+
diff --git a/gson/.github/dependabot.yml b/gson/.github/dependabot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f98244232ef50b3fffc5862083761c64e19cd0d5
--- /dev/null
+++ b/gson/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+  - package-ecosystem: "maven"
+    directory: "/"
+    schedule:
+      interval: "daily"
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/gson/.github/pull_request_template.md b/gson/.github/pull_request_template.md
new file mode 100644
index 0000000000000000000000000000000000000000..4021d48a922f2fd540bce530b3a7febed53b71ac
--- /dev/null
+++ b/gson/.github/pull_request_template.md
@@ -0,0 +1,33 @@
+<!--
+    Thank you for your contribution!
+    Please see the contributing guide: https://github.com/google/.github/blob/master/CONTRIBUTING.md
+
+    Keep in mind that Gson is in maintenance mode. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback.
+-->
+
+### Purpose
+<!-- Describe the purpose of this pull request, for example which new feature it adds or which bug it fixes -->
+<!-- If this pull request closes a GitHub issue, please write "Closes #<issue>", for example "Closes #123" -->
+
+
+### Description
+<!-- If necessary provide more information, for example relevant implementation details or corner cases which are not covered yet -->
+<!-- If there are related issues or pull requests, link to them by referencing their number, for example "pull request #123" -->
+
+
+
+### Checklist
+<!-- The following checklist is mainly intended for yourself to verify that you did not miss anything -->
+
+- [ ] New code follows the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)\
+  This is automatically checked by `mvn verify`, but can also be checked on its own using `mvn spotless:check`.\
+  Style violations can be fixed using `mvn spotless:apply`; this can be done in a separate commit to verify that it did not cause undesired changes.
+- [ ] If necessary, new public API validates arguments, for example rejects `null`
+- [ ] New public API has Javadoc
+    - [ ] Javadoc uses `@since $next-version$`  
+      (`$next-version$` is a special placeholder which is automatically replaced during release)
+- [ ] If necessary, new unit tests have been added  
+  - [ ] Assertions in unit tests use [Truth](https://truth.dev/), see existing tests
+  - [ ] No JUnit 3 features are used (such as extending class `TestCase`)
+  - [ ] If this pull request fixes a bug, a new test was added for a situation which failed previously and is now fixed
+- [ ] `mvn clean verify javadoc:jar` passes without errors
diff --git a/gson/.github/workflows/build.yml b/gson/.github/workflows/build.yml
new file mode 100644
index 0000000000000000000000000000000000000000..12ebe5dc2089973f54091b732a54cff9c2d07d9d
--- /dev/null
+++ b/gson/.github/workflows/build.yml
@@ -0,0 +1,75 @@
+name: Build
+
+on: [push, pull_request]
+
+permissions:
+  contents: read #  to fetch code (actions/checkout)
+
+jobs:
+  build:
+    name: "Build on JDK ${{ matrix.java }}"
+    strategy:
+      matrix:
+        java: [ 11, 17 ]
+        # Custom JDK 21 configuration
+        include:
+          - java: 21
+            # Disable Enforcer check which (intentionally) prevents using JDK 21 for building
+            extra-mvn-args: -Denforcer.fail=false
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+      - name: "Set up JDK ${{ matrix.java }}"
+        uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93  # v4.0.0
+        with:
+          distribution: 'temurin'
+          java-version: ${{ matrix.java }}
+          cache: 'maven'
+      - name: Build with Maven
+        # This also runs javadoc:jar to detect any issues with the Javadoc generated during release
+        run: mvn --batch-mode --no-transfer-progress verify javadoc:jar ${{ matrix.extra-mvn-args || '' }}
+
+  native-image-test:
+    name: "GraalVM Native Image test"
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+      - name: "Set up GraalVM"
+        uses: graalvm/setup-graalvm@b8dc5fccfbc65b21dd26e8341e7b21c86547f61b  # v1.1.5.1
+        with:
+          java-version: '17'
+          distribution: 'graalvm'
+          # According to documentation in graalvm/setup-graalvm this is used to avoid rate-limiting issues
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          cache: 'maven'
+      - name: Build and run tests
+        # Only run tests in `graal-native-image-test` (and implicitly build and run tests in `gson`),
+        # everything else is covered already by regular build job above
+        run: mvn test --batch-mode --no-transfer-progress --activate-profiles native-image-test --projects graal-native-image-test --also-make
+
+  verify-reproducible-build:
+    name: "Verify reproducible build"
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+      - name: "Set up JDK 17"
+        uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93  # v4.0.0
+        with:
+          distribution: 'temurin'
+          java-version: 17
+          cache: 'maven'
+
+      - name: "Verify no plugin issues"
+        run: mvn artifact:check-buildplan --batch-mode --no-transfer-progress
+
+      - name: "Verify reproducible build"
+        # See https://maven.apache.org/guides/mini/guide-reproducible-builds.html#how-to-test-my-maven-build-reproducibility
+        run: |
+          mvn clean install --batch-mode --no-transfer-progress -Dproguard.skip -DskipTests
+          # Run with `-Dbuildinfo.attach=false`; otherwise `artifact:compare` fails because it creates a `.buildinfo` file which
+          # erroneously references the existing `.buildinfo` file (respectively because it is overwriting it, a file with size 0)
+          # See https://issues.apache.org/jira/browse/MARTIFACT-57
+          mvn clean verify artifact:compare --batch-mode --no-transfer-progress -Dproguard.skip -DskipTests -Dbuildinfo.attach=false
diff --git a/gson/.github/workflows/check-android-compatibility.yml b/gson/.github/workflows/check-android-compatibility.yml
new file mode 100644
index 0000000000000000000000000000000000000000..72afb932b080d847d2222317e8fdac1918719526
--- /dev/null
+++ b/gson/.github/workflows/check-android-compatibility.yml
@@ -0,0 +1,29 @@
+# For security reasons this is a separate GitHub workflow, see https://github.com/google/gson/issues/2429#issuecomment-1622522842
+# Once https://github.com/mojohaus/animal-sniffer/issues/252 or https://github.com/mojohaus/animal-sniffer/pull/253
+# are resolved, can consider adjusting pom.xml to include this as part of normal Maven build
+
+name: Check Android compatibility
+
+on: [push, pull_request]
+
+permissions:
+  contents: read #  to fetch code (actions/checkout)
+
+jobs:
+  check-android-compatibility:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+
+      - name: Set up JDK 11
+        uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93  # v4.0.0
+        with:
+          distribution: 'temurin'
+          java-version: '11'
+          cache: 'maven'
+
+      - name: Check Android compatibility
+        run: |
+          # Run 'test' phase because plugin normally expects to be executed after tests have been compiled
+          mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests
diff --git a/gson/.github/workflows/check-api-compatibility.yml b/gson/.github/workflows/check-api-compatibility.yml
new file mode 100644
index 0000000000000000000000000000000000000000..33aa14d283019b2b6b7c2deac473d3e0b94dc09a
--- /dev/null
+++ b/gson/.github/workflows/check-api-compatibility.yml
@@ -0,0 +1,52 @@
+# This workflow makes sure that a pull request does not make any incompatible changes
+# to the public API of Gson
+name: Check API compatibility
+
+on: pull_request
+
+jobs:
+  check-api-compatibility:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout old version
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+        with:
+          ref: ${{ github.event.pull_request.base.sha }}
+          path: 'gson-old-japicmp'
+
+      - name: Set up JDK 11
+        uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93  # v4.0.0
+        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@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+
+      - 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@26f96dfa697d77e81fd5907df203aa23a56210a8  # v4.3.0
+        # 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/gson/.github/workflows/cifuzz.yml b/gson/.github/workflows/cifuzz.yml
new file mode 100644
index 0000000000000000000000000000000000000000..168b934be97a53ac7491027056b0b70ca50ccb15
--- /dev/null
+++ b/gson/.github/workflows/cifuzz.yml
@@ -0,0 +1,25 @@
+name: CIFuzz
+on: [pull_request]
+jobs:
+  Fuzzing:
+    runs-on: ubuntu-latest
+    steps:
+    - name: Build Fuzzers
+      id: build
+      uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
+      with:
+        oss-fuzz-project-name: 'gson'
+        dry-run: false
+        language: jvm
+    - name: Run Fuzzers
+      uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
+      with:
+        oss-fuzz-project-name: 'gson'
+        fuzz-seconds: 600
+        dry-run: false
+    - name: Upload Crash
+      uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8  # v4.3.0
+      if: failure() && steps.build.outcome == 'success'
+      with:
+        name: artifacts
+        path: ./out/artifacts
diff --git a/gson/.github/workflows/codeql-analysis.yml b/gson/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2395c738c7fd7701a7f2fd1fcf982d6dee2224d9
--- /dev/null
+++ b/gson/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,53 @@
+# Based on default config generated by GitHub, see also https://github.com/github/codeql-action
+
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+  schedule:
+    # Run every Monday at 16:10
+    - cron: '10 16 * * 1'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'java' ]
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
+
+    - name: Set up JDK 17
+      uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93  # v4.0.0
+      with:
+        distribution: 'temurin'
+        java-version: '17'
+        cache: 'maven'
+
+    # Initializes the CodeQL tools for scanning
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@b7bf0a3ed3ecfa44160715d7c442788f65f0f923  # v3.23.2
+      with:
+        languages: ${{ matrix.language }}
+        # Run all security queries and maintainability and reliability queries
+        queries: +security-and-quality
+
+    # Only compile main sources, but ignore test sources because findings for them might not
+    # be that relevant (though GitHub security view also allows filtering by source type)
+    # Can replace this with github/codeql-action/autobuild action to run complete build
+    - name: Compile sources
+      run: |
+        mvn compile --batch-mode --no-transfer-progress
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@b7bf0a3ed3ecfa44160715d7c442788f65f0f923  # v3.23.2
diff --git a/gson/.gitignore b/gson/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2fc591bd194cde00f58ff7fddc46c9b03531b4db
--- /dev/null
+++ b/gson/.gitignore
@@ -0,0 +1,22 @@
+.classpath
+.project
+.settings
+eclipsebin
+target
+*/target
+pom.xml.*
+release.properties
+
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+
+.gradle
+local.properties
+build
+
+.DS_Store
+
+examples/android-proguard-example/gen
diff --git a/gson/.mvn/jvm.config b/gson/.mvn/jvm.config
new file mode 100644
index 0000000000000000000000000000000000000000..32599cefea51ed6d960b8b5070c7721f31013a14
--- /dev/null
+++ b/gson/.mvn/jvm.config
@@ -0,0 +1,10 @@
+--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
diff --git a/gson/CHANGELOG.md b/gson/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..7efc0da9de8ffebd9d796f60e7707258168c1b85
--- /dev/null
+++ b/gson/CHANGELOG.md
@@ -0,0 +1,488 @@
+Change Log
+==========
+
+The change log for versions newer than 2.10 is available only on the [GitHub Releases page](https://github.com/google/gson/releases).
+
+## Version 2.10
+
+* Support for serializing and deserializing Java records, on Java ≥ 16. (https://github.com/google/gson/pull/2201)
+* Add `JsonArray.asList` and `JsonObject.asMap` view methods (https://github.com/google/gson/pull/2225)
+* Fix `TypeAdapterRuntimeTypeWrapper` not detecting reflective `TreeTypeAdapter` and `FutureTypeAdapter` (https://github.com/google/gson/pull/1787)
+* Improve `JsonReader.skipValue()` (https://github.com/google/gson/pull/2062)
+* Perform numeric conversion for primitive numeric type adapters (https://github.com/google/gson/pull/2158)
+* Add `Gson.fromJson(..., TypeToken)` overloads (https://github.com/google/gson/pull/1700)
+* Fix changes to `GsonBuilder` affecting existing `Gson` instances (https://github.com/google/gson/pull/1815)
+* Make `JsonElement` conversion methods more consistent and fix javadoc (https://github.com/google/gson/pull/2178)
+* Throw `UnsupportedOperationException` when `JsonWriter.jsonValue` is not supported (https://github.com/google/gson/pull/1651)
+* Disallow `JsonObject` `Entry.setValue(null)` (https://github.com/google/gson/pull/2167)
+* Fix `TypeAdapter.toJson` throwing AssertionError for custom IOException (https://github.com/google/gson/pull/2172)
+* Convert null to JsonNull for `JsonArray.set` (https://github.com/google/gson/pull/2170)
+* Fixed nullSafe usage. (https://github.com/google/gson/pull/1555)
+* Validate `TypeToken.getParameterized` arguments (https://github.com/google/gson/pull/2166)
+* Fix #1702: Gson.toJson creates CharSequence which does not implement toString (https://github.com/google/gson/pull/1703)
+* Prefer existing adapter for concurrent `Gson.getAdapter` calls (https://github.com/google/gson/pull/2153)
+* Improve `ArrayTypeAdapter` for `Object[]` (https://github.com/google/gson/pull/1716)
+* Improve `AppendableWriter` performance (https://github.com/google/gson/pull/1706)
+
+## Version 2.9.1
+
+* Make `Object` and `JsonElement` deserialization iterative rather than
+  recursive (https://github.com/google/gson/pull/1912)
+* Added parsing support for enum that has overridden toString() method (https://github.com/google/gson/pull/1950)
+* Removed support for building Gson with Gradle (https://github.com/google/gson/pull/2081)
+* Removed obsolete `codegen` hierarchy (https://github.com/google/gson/pull/2099)
+* Add support for reflection access filter (https://github.com/google/gson/pull/1905)
+* Improve `TypeToken` creation validation (https://github.com/google/gson/pull/2072)
+* Add explicit support for `float` in `JsonWriter` (https://github.com/google/gson/pull/2130, https://github.com/google/gson/pull/2132)
+* Fail when parsing invalid local date (https://github.com/google/gson/pull/2134)
+
+Also many small improvements to javadoc.
+
+## Version 2.9.0
+
+**The minimum supported Java version changes from 6 to 7.**
+
+* Change target Java version to 7 (https://github.com/google/gson/pull/2043)
+* Put `module-info.class` into Multi-Release JAR folder (https://github.com/google/gson/pull/2013)
+* Improve error message when abstract class cannot be constructed (https://github.com/google/gson/pull/1814)
+* Support EnumMap deserialization (https://github.com/google/gson/pull/2071)
+* Add LazilyParsedNumber default adapter (https://github.com/google/gson/pull/2060)
+* Fix JsonReader.hasNext() returning true at end of document (https://github.com/google/gson/pull/2061)
+* Remove Gradle build support. Build script was outdated and not actively
+  maintained anymore (https://github.com/google/gson/pull/2063)
+* Add `GsonBuilder.disableJdkUnsafe()` (https://github.com/google/gson/pull/1904)
+* Add `UPPER_CASE_WITH_UNDERSCORES` in FieldNamingPolicy (https://github.com/google/gson/pull/2024)
+* Fix failing to serialize Collection or Map with inaccessible constructor (https://github.com/google/gson/pull/1902)
+* Improve TreeTypeAdapter thread-safety (https://github.com/google/gson/pull/1976)
+* Fix `Gson.newJsonWriter` ignoring lenient and HTML-safe setting (https://github.com/google/gson/pull/1989)
+* Delete unused LinkedHashTreeMap (https://github.com/google/gson/pull/1992)
+* Make default adapters stricter; improve exception messages (https://github.com/google/gson/pull/2000)
+* Fix `FieldNamingPolicy.upperCaseFirstLetter` uppercasing non-letter (https://github.com/google/gson/pull/2004)
+
+## Version 2.8.9
+
+* Make OSGi bundle's dependency on `sun.misc` optional (https://github.com/google/gson/pull/1993).
+* Deprecate `Gson.excluder()` exposing internal `Excluder` class (https://github.com/google/gson/pull/1986).
+* Prevent Java deserialization of internal classes (https://github.com/google/gson/pull/1991).
+* Improve number strategy implementation (https://github.com/google/gson/pull/1987).
+* Fix LongSerializationPolicy null handling being inconsistent with Gson (https://github.com/google/gson/pull/1990).
+* Support arbitrary Number implementation for Object and Number deserialization (https://github.com/google/gson/pull/1290).
+* Bump proguard-maven-plugin from 2.4.0 to 2.5.1 (https://github.com/google/gson/pull/1980).
+* Don't exclude static local classes (https://github.com/google/gson/pull/1969).
+* Fix `RuntimeTypeAdapterFactory` depending on internal `Streams` class (https://github.com/google/gson/pull/1959).
+* Improve Maven build (https://github.com/google/gson/pull/1964).
+* Make dependency on `java.sql` optional (https://github.com/google/gson/pull/1707).
+
+## Version 2.8.8
+
+* Fixed issue with recursive types (https://github.com/google/gson/issues/1390).
+* Better behaviour with Java 9+ and `Unsafe` if there is a security manager (https://github.com/google/gson/pull/1712).
+* `EnumTypeAdapter` now works better when ProGuard has obfuscated enum fields (https://github.com/google/gson/pull/1495).
+
+## Version 2.8.7
+
+* Fixed `ISO8601UtilsTest` failing on systems with UTC+X.
+* Improved javadoc for `JsonStreamParser`.
+* Updated proguard.cfg (https://github.com/google/gson/pull/1693).
+* Fixed `IllegalStateException` in `JsonTreeWriter` (https://github.com/google/gson/issues/1592).
+* Added `JsonArray.isEmpty()` (https://github.com/google/gson/pull/1640).
+* Added new test cases (https://github.com/google/gson/pull/1638).
+* Fixed OSGi metadata generation to work on JavaSE < 9 (https://github.com/google/gson/pull/1603).
+
+## Version 2.8.6
+_2019-10-04_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.5...gson-parent-2.8.6)
+ * Added static methods `JsonParser.parseString` and `JsonParser.parseReader` and deprecated instance method `JsonParser.parse`
+ * Java 9 module-info support
+
+## Version 2.8.5
+_2018-05-21_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.4...gson-parent-2.8.5)
+ * Print Gson version while throwing AssertionError and IllegalArgumentException
+ * Moved `utils.VersionUtils` class to `internal.JavaVersion`. This is a potential backward incompatible change from 2.8.4
+ * Fixed issue https://github.com/google/gson/issues/1310 by supporting Debian Java 9
+
+## Version 2.8.4
+_2018-05-01_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.3...gson-parent-2.8.4)
+ * Added a new FieldNamingPolicy, `LOWER_CASE_WITH_DOTS` that mapps JSON name `someFieldName` to `some.field.name`
+ * Fixed issue https://github.com/google/gson/issues/1305 by removing compile/runtime dependency on `sun.misc.Unsafe`
+
+## Version 2.8.3
+_2018-04-27_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.2...gson-parent-2.8.3)
+ * Added a new API, `GsonBuilder.newBuilder()` that clones the current builder
+ * Preserving DateFormatter behavior on JDK 9
+ * Numerous other bugfixes
+
+## Version 2.8.2
+_2017-09-19_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.1...gson-parent-2.8.2)
+ * Introduced a new API, `JsonElement.deepCopy()`
+ * Numerous other bugfixes
+
+## Version 2.8.1
+_2017-05-30_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.8.0...gson-parent-2.8.1)
+ * New: `JsonObject.keySet()`
+ * `@JsonAdapter` annotation can now use `JsonSerializer` and `JsonDeserializer` as well.
+
+## Version 2.8
+_2016-10-26_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.7...gson-parent-2.8.0)
+ * New: `TypeToken.getParameterized()` and `TypeToken.getArray()` make it easier to
+   register or look up a `TypeAdapter`.
+ * New: `@JsonAdapter(nullSafe=true)` to specify that a custom type adapter handles null.
+
+## Version 2.7
+_2016-06-14_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.6.2...gson-parent-2.7)
+ * Added support for JsonSerializer/JsonDeserializer in @JsonAdapter annotation
+ * Exposing Gson properties excluder(), fieldNamingStrategy(), serializeNulls(), htmlSafe()
+ * Added JsonObject.size() method
+ * Added JsonWriter.value(Boolean value) method
+ * Using ArrayDeque, ConcurrentHashMap, and other JDK 1.6 features
+ * Better error reporting
+ * Plenty of other bug fixes
+
+## Version 2.6.2
+_2016-02-26_  [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.6.1...gson-parent-2.6.2)
+ * Fixed an NPE bug with @JsonAdapter annotation
+ * Added back OSGI manifest
+ * Some documentation typo fixes
+
+## Version 2.6.1
+
+_2016-02-11_ [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.6...gson-parent-2.6.1)
+
+ * Fix: The 2.6 release targeted Java 1.7, but we intend to target Java 1.6. The
+   2.6.1 release is identical to 2.6, but it targets Java 1.6.
+
+
+## Version 2.6
+
+_2016-02-11_ [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.5...gson-parent-2.6)
+
+ * Permit timezones without minutes in the default date adapter.
+ * Update reader and writer for RFC 7159. This means that strings, numbers,
+   booleans and null may be top-level values in JSON documents, even if the
+   reader is strict.
+ * New `setLenient()` method on `GsonBuilder`. This setting impacts the new
+   factory method `Gson.newJsonReader()`.
+ * Adapters discovered with `@JsonAdapter` are now null safe by default.
+
+
+## Version 2.5
+
+_2015-11-24_ [GitHub Diff](https://github.com/google/gson/compare/gson-parent-2.4...gson-parent-2.5)
+
+ * Updated minimum JDK version to 1.6
+ * Improved Date Deserialization by accepting many date formats
+ * Added support for `java.util.Currency`, `AtomicLong`, `AtomicLongArray`, `AtomicInteger`, `AtomicIntegerArray`, `AtomicBoolean`. This change is backward-incompatible because the earlier version of Gson used the default serialization which wasn't intuitive. We hope that these classes are not used enough to actually cause problems in the field.
+ * Improved debugging information when some exceptions are thrown
+
+
+## Version 2.4
+
+_2015-10-04_
+
+ * **Drop `IOException` from `TypeAdapter.toJson()`.** This is a binary-compatible change, but may
+   cause compiler errors where `IOExceptions` are being caught but no longer thrown. The correct fix
+   for this problem is to remove the unnecessary `catch` clause.
+ * New: `Gson.newJsonWriter` method returns configured `JsonWriter` instances.
+ * New: `@SerializedName` now works with [AutoValue’s][autovalue] abstract property methods.
+ * New: `@SerializedName` permits alternate names when deserializing.
+ * New: `JsonWriter#jsonValue` writes raw JSON values.
+ * New: APIs to add primitives directly to `JsonArray` instances.
+ * New: ISO 8601 date type adapter. Find this in _extras_.
+ * Fix: `FieldNamingPolicy` now works properly when running on a device with a Turkish locale.
+  [autovalue]: https://github.com/google/auto/tree/main/value
+
+
+## Version 2.3.1
+
+_2014-11-20_
+
+ * Added support to serialize objects with self-referential fields. The self-referential field is set to null in JSON. Previous version of Gson threw a StackOverflowException on encountering any self-referential fields.
+   * The most visible impact of this is that Gson can now serialize Throwable (Exception and Error)
+ * Added support for @JsonAdapter annotation on enums which are user defined types
+ * Fixed bug in getPath() with array of objects and arrays of arrays
+ * Other smaller bug fixes
+
+
+## Version 2.3
+
+_2014-08-11_
+
+ * The new @JsonAdapter annotation to specify a Json TypeAdapter for a class field
+ * JsonPath support: JsonReader.getPath() method returns the JsonPath expression
+ * New public methods in JsonArray (similar to the java.util.List): `contains(JsonElement), remove(JsonElement), remove(int index), set(int index, JsonElement element)`
+ * Many other smaller bug fixes
+
+
+## Version 2.2.4
+
+_2013-05-13_
+
+ * Fix internal map (LinkedHashTreeMap) hashing bug.
+ * Bug fix (Issue 511)
+
+
+## Version 2.2.3
+
+_2013-04-12_
+
+ * Fixes for possible DoS attack due to poor String hashing
+
+
+## Version 2.2.2
+
+_2012-07-02_
+
+ * Gson now allows a user to override default type adapters for Primitives and Strings. This behavior was allowed in earlier versions of Gson but was prohibited started Gson 2.0. We decided to allow it again: This enables a user to parse 1/0 as boolean values for compatibility with iOS JSON libraries.
+ * (Incompatible behavior change in `JsonParser`): In the past, if `JsonParser` encountered a stream that terminated prematurely, it returned `JsonNull`. This behavior wasn't correct because the stream had invalid JSON, not a null. `JsonParser` is now changed to throw `JsonSyntaxException` in this case. Note that if JsonParser (or Gson) encounter an empty stream, they still return `JsonNull`.
+
+
+## Version 2.2.1
+
+_2012-05-05_
+
+ * Very minor fixes
+
+
+## Version 2.2
+
+_2012-05-05_
+
+ * Added getDelegateAdapter in Gson class
+ * Fixed a security bug related to denial of service attack with Java HashMap String collisions.
+
+
+## Version 2.1
+
+_2011-12-30_ (Targeted Dec 31, 2011)
+
+ * Support for user-defined streaming type adapters
+ * continued performance enhancements
+ * Dropped support for type hierarchy instance creators. We don't expect this to be a problem. We'll also detect fewer errors where multiple type adapters can serialize the same type. With APIs like getNextTypeAdapter, this might actually be an improvement!
+
+
+## Version 2.0
+
+_2011-11-13_
+
+#### Faster
+
+ * Previous versions first parsed complete document into a DOM-style model (JsonObject or JsonArray) and then bound data against that. Gson 2 does data binding directly from the stream parser.
+
+#### More Predictable
+
+ * Objects are serialized and deserialized in the same way, regardless of where they occur in the object graph.
+
+#### Changes to watch out for
+
+  * Gson 1.7 would serialize top-level nulls as "". 2.0 serializes them as "null".
+    ```
+    String json = gson.toJson(null, Foo.class);
+    1.7: json == ""
+    2.0: json == "null"
+    ```
+
+  * Gson 1.7 permitted duplicate map keys. 2.0 forbids them.
+    ```
+    String json = "{'a':1,'a':2}";
+    Map<String, Integer> map = gson.fromJson(json, mapType);
+    1.7: map == {a=2}
+    2.0: JsonSyntaxException thrown
+    ```
+
+  * Gson 1.7 won’t serialize subclass fields in collection elements. 2.0 adds this extra information.
+    ```
+    List<Point2d> points = new ArrayList<Point2d>();
+    points.add(new Point3d(1, 2, 3));
+    String json = gson.toJson(points,
+        new TypeToken<List<Point2d>>() {}.getType());
+    1.7: json == "[{'x':1,'y':2}]"
+    2.0: json == "[{'x':1,'y':2,'z':3}]"
+    ```
+
+  * Gson 1.7 binds single-element arrays as their contents. 2.0 doesn’t.
+    ```
+    Integer i = gson.fromJson("[42]", Integer.class);
+    1.7: i == 42
+    2.0: JsonSyntaxException thrown
+    ```
+
+#### Other changes to be aware of
+ * Gson 2.0 doesn’t support type adapters for primitive types.
+ * Gson 1.7 uses arbitrary precision for primitive type conversion (so -122.08e-2132 != 0). Gson 2.0 uses double precision (so -122.08e-2132 == 0).
+ * Gson 1.7 sets subclass fields when an InstanceCreator returns a subclass when the value is a field of another object. Gson 2.0 sets fields of the requested type only.
+ * Gson 1.7 versioning never skips the top-level object. Gson 2.0 versioning applies to all objects.
+ * Gson 1.7 truncates oversized large integers. Gson 2.0 fails on them.
+ * Gson 2.0 permits integers to have .0 fractions like "1.0".
+ * Gson 1.7 throws IllegalStateException on circular references. Gson 2.0 lets the runtime throw a StackOverflowError.
+
+
+## Version 1.7.2
+
+_2011-09-30_ (Unplanned release)
+ * Fixed a threading issue in FieldAttributes (Issue 354)
+
+
+## Version 1.7.1
+
+_2011-04-13_ (Unplanned release)
+
+ * Fixed Gson jars in Maven Central repository
+ * Removed assembly-descriptor.xml and maven pom.xml/pom.properties files from Gson binary jar. This also ensures that jarjar can be run correctly on Gson.
+
+
+## Version 1.7
+
+_2011-04-12_ (Targeted: Jan 2011)
+
+ * No need to define no-args constructors for classes serialized with Gson
+ * Ability to register a hierarchical type adapter
+ * Support for serialization and deserialization of maps with complex keys
+ * Serialization and deserialization specific exclusion strategies
+ * Allow concrete data structure fields without type adapters
+ * Fixes "type" management (i.e. Wildcards, etc.)
+ * Major performance enhancements by reducing the need for Java reflection
+See detailed announcement at this thread in the Gson Google Group.
+
+
+## Version 1.6
+
+_2010-11-24_ (Targeted: Oct, 2010)
+
+ * New stream parser APIs
+ * New parser that improves parsing performance significantly
+
+
+## Version 1.5
+
+_2010-08-19_ (Target Date: Aug 18, 2010)
+
+ * Added `UPPER_CAMEL_CASE_WITH_SPACES` naming policy
+ * Added SQL date and time support
+ * A number of performance improvements: Using caching of field annotations for speeding up reflection, replacing recursive calls in the parser with a for loop.
+
+
+## Version 1.4 BETA
+
+_2009_10_09_
+
+ * JsonStreamParser: A streaming parser API class to deserialize multiple JSON objects on a stream (such as a pipelined HTTP response)
+ * Raised the deserialization limit for byte and object arrays and collection to over 11MB from 80KB. See issue 96.
+ * While serializing, Gson now uses the actual type of a field. This allows serialization of base-class references holding sub-classes to the JSON for the sub-class. It also allows serialization of raw collections. See Issue 155, 156.
+ * Added a `Gson.toJsonTree()` method that serializes a Java object to a tree of JsonElements. See issue 110.
+ * Added a `Gson.fromJson(JsonElement)` method that deserializes from a Json parse tree.
+ * Updated `Expose` annotation to contain parameters serialize and deserialize to control whether a field gets serialized or deserialized. See issue 146.
+ * Added a new naming policy `LOWER_CASE_WITH_DASHES`
+ * Default date type adapter is now thread-safe. See Issue 162.
+ * `JsonElement.toString()` now outputs valid JSON after escaping characters properly. See issue 154.
+ * `JsonPrimitive.equals()` now returns true for two numbers if their values are equal. All integral types (long, int, short, byte, BigDecimal, Long, Integer, Short, Byte) are treated equivalent for comparison. Similarly, floating point types (double, float, BigDecimal, Double, Float) are treated equivalent as well. See issue 147.
+ * Fixed bugs in pretty printing. See issue 153.
+ * If a field causes circular reference error, Gson lists the field name instead of the object value. See issue 118.
+ * Gson now serializes a list with null elements correctly. See issue 117.
+ * Fixed issue 121, 123, 126.
+ * Support user defined exclusion strategies (Feature Request 138).
+
+
+## Version 1.3
+
+_2009-04-01_
+
+ * Fix security token to remove the `<data>` element.
+ * Changed JsonParser.parse method to be non-static
+ * Throw JsonParseExceptions instead of ClassCastExceptions and UnsupportedOperationExceptions
+
+
+## Version 1.3 beta3
+
+_2009-03-17_
+
+ * Supported custom mapping of field names by making `FieldNamingStrategy` public and allowing `FieldNamingStrategy` to be set in GsonBuilder. See issue 104.
+ * Added a new GsonBuilder setting `generateNonExecutableJson()` that prefixes the generated JSON with some text to make the output non-executable Javascript. Gson now recognizes this text from input while deserializing and filters it out. This feature is meant to prevent script sourcing attacks. See Issue 42.
+ * Supported deserialization of sets with elements that do not implement Comparable. See Issue 100
+ * Supported deserialization of floating point numbers without a sign after E. See Issue 94
+
+
+## Version 1.3 beta2
+
+_2009-02-05_
+
+ * Added a new Parser API. See issue 65
+ * Supported deserialization of java.util.Properties. See Issue 87
+ * Fixed the pretty printing of maps. See Issue 93
+ * Supported automatic conversion of strings into numeric and boolean types if possible. See Issue 89
+ * Supported deserialization of longs into strings. See Issue 82
+
+
+## Version 1.3 beta1
+
+_2009_01_ (Target Date Friday, Dec 15, 2008)
+
+ * Made JSON parser lenient by allowing unquoted member names while parsing. See Issue 41
+ * Better precision handling for floating points. See Issue 71, 72
+ * Support for deserialization of special double values: NaN, infinity and negative infinity. See Issue 81
+ * Backward compatibility issue found with serialization of `Collection<Object>` type.  See Issue 73 and 83.
+ * Able to serialize null keys and/or values within a Map.  See Issue 77
+ * Deserializing non-String value keys for Maps.  See Issue 85.
+
+ * Support for clashing field name.  See Issue 76.
+ * Removed the need to invoke instance creator if a deserializer is registered. See issues 37 and 69.
+ * Added default support for java.util.UUID. See Issue 79
+ * Changed `Gson.toJson()` methods to use `Appendable` instead of `Writer`. Issue 52. This requires that clients recompile their source code that uses Gson.
+
+
+## Version 1.2.3
+
+_2008-11-15_ (Target Date Friday, Oct 31, 2008)
+
+ * Added support to serialize raw maps. See issue 45
+ * Made Gson thread-safe by fixing Issue 63
+ * Fixed Issue 68 to allow default type adapters for primitive types to be replaced by custom type adapters.
+ * Relaxed the JSON parser to accept escaped slash (\/) as a valid character in the string. See Issue 66
+
+
+## Version 1.2.2
+
+_2008-10-14_ (Target Date: None, Unplanned)
+
+ * This version was released to fix Issue 58 which caused a regression bug in version 1.2.1. It includes the contents from the release 1.2.1
+
+
+## Version 1.2.1
+
+_2008-10-13_ (Target Date Friday, Oct 7, 2008)
+
+**Note:** This release was abandoned since it caused a regression (Issue 58) bug.
+
+ * Includes updated parser for JSON that supports much larger strings. For example, Gson 1.2 failed at parsing a 100k string, Gson 1.2.1 has successfully parsed strings of size 15-20MB. The parser also is faster and consumes less memory since it uses a token match instead of a recursion-based Grammar production match. See Issue 47.
+ * Gson now supports field names with single quotes ' in addition to double quotes ". See Issue 55.
+ * Includes bug fixes for issue 46, 49, 51, 53, 54, and 56.
+
+
+## Version 1.2
+
+_2008-08-29_ (Target Date Tuesday Aug 26, 2008)
+
+ * Includes support for feature requests 21, 24, 29
+ * Includes bug fixes for Issue 22, Issue 23, Issue 25, Issue 26, Issue 32 , Issue 34, Issue 35, Issue 36, Issue 37, Issue 38, Issue 39
+ * Performance enhancements (see r137)
+ * Documentation updates
+
+
+## Version 1.1.1
+
+_2008-07-18_ (Target Date Friday, Aug 1, 2008)
+
+ * Includes fixes for Issue 19, Partial fix for Issue 20
+
+
+## Version 1.1
+
+_2008-07-01_ (Target Date Thursday, July 3, 2008)
+
+ * Includes fixes for Issue 9, Issue 16, Issue 18
+
+
+## Version 1.0.1
+
+_2008-06-17_ (Target Date Friday,  Jun 13, 2008)
+
+ * Includes fixes for Issue 15, Issue 14, Issue 3, Issue 8
+ * Javadoc improvements
diff --git a/gson/GsonDesignDocument.md b/gson/GsonDesignDocument.md
new file mode 100644
index 0000000000000000000000000000000000000000..a80a81e6274bb587b955e009872ec4b651b999ed
--- /dev/null
+++ b/gson/GsonDesignDocument.md
@@ -0,0 +1,59 @@
+# Gson Design Document
+
+This document presents issues that we faced while designing Gson. It is meant for advanced users or developers working on Gson. If you are interested in learning how to use Gson, see its user guide.
+
+Some information in this document is outdated and does not reflect the current state of Gson. This information can however still be relevant for understanding the history of Gson.
+
+## Navigating the Json tree or the target Type Tree while deserializing
+
+When you are deserializing a Json string into an object of desired type, you can either navigate the tree of the input, or the type tree of the desired type. Gson uses the latter approach of navigating the type of the target object. This keeps you in tight control of instantiating only the type of objects that you are expecting (essentially validating the input against the expected "schema"). By doing this, you also ignore any extra fields that the Json input has but were not expected.
+
+As part of Gson, we wrote a general purpose ObjectNavigator that can take any object and navigate through its fields calling a visitor of your choice.
+
+## Supporting richer serialization semantics than deserialization semantics
+
+Gson supports serialization of arbitrary collections, but can only deserialize genericized collections. this means that Gson can, in some cases, fail to deserialize Json that it wrote. This is primarily a limitation of the Java type system since when you encounter a Json array of arbitrary types there is no way to detect the types of individual elements. We could have chosen to restrict the serialization to support only generic collections, but chose not to. This is because often the user of the library are concerned with either serialization or deserialization, but not both. In such cases, there is no need to artificially restrict the serialization capabilities.
+
+## Supporting serialization and deserialization of classes that are not under your control and hence can not be modified
+
+Some Json libraries use annotations on fields or methods to indicate which fields should be used for Json serialization. That approach essentially precludes the use of classes from JDK or third-party libraries. We solved this problem by defining the notion of custom serializers and deserializers. This approach is not new, and was used by the JAX-RPC technology to solve essentially the same problem.
+
+## Using Checked vs Unchecked exceptions to indicate a parsing error
+
+We chose to use unchecked exceptions to indicate a parsing failure. This is primarily done because usually the client can not recover from bad input, and hence forcing them to catch a checked exception results in sloppy code in the `catch()` block.
+
+## Creating class instances for deserialization
+
+Gson needs to create a dummy class instance before it can deserialize Json data into its fields. We could have used Guice to get such an instance, but that would have resulted in a dependency on Guice. Moreover, it probably would have done the wrong thing since Guice is expected to return a valid instance, whereas we need to create a dummy one. Worse, Gson would overwrite the fields of that instance with the incoming data thereby modifying the instance for all subsequent Guice injections. This is clearly not a desired behavior. Hence, we create class instances by invoking the parameterless constructor. We also handle the primitive types, enums, collections, sets, maps and trees as a special case.
+
+To solve the problem of supporting unmodifiable types, we use custom instance creators. So, if you want to use a library type that does not define a default constructor (for example, `Money` class), then you can register an instance creator that returns a dummy instance when asked.
+
+## Using fields vs getters to indicate Json elements
+
+Some Json libraries use the getters of a type to deduce the Json elements. We chose to use all fields (up the inheritance hierarchy) that are not transient, static, or synthetic. We did this because not all classes are written with suitably named getters. Moreover, `getXXX` or `isXXX` might be semantic rather than indicating properties.
+
+However, there are good arguments to support properties as well. We intend to enhance Gson in a later version to support properties as an alternate mapping for indicating Json fields. For now, Gson is fields-based.
+
+## Why are most classes in Gson marked as final?
+
+While Gson provides a fairly extensible architecture by providing pluggable serializers and deserializers, Gson classes were not specifically designed to be extensible. Providing non-final classes would have allowed a user to legitimately extend Gson classes, and then expect that behavior to work in all subsequent revisions. We chose to limit such use-cases by marking classes as final, and waiting until a good use-case emerges to allow extensibility. Marking a class final also has a minor benefit of providing additional optimization opportunities to Java compiler and virtual machine.
+
+## Why are inner interfaces and classes used heavily in Gson?
+
+Gson uses inner classes substantially. Many of the public interfaces are inner interfaces too (see `JsonSerializer.Context` or `JsonDeserializer.Context` as an example). These are primarily done as a matter of style. For example, we could have moved `JsonSerializer.Context` to be a top-level class `JsonSerializerContext`, but chose not to do so. However, if you can give us good reasons to rename it alternately, we are open to changing this philosophy.
+
+## Why do you provide two ways of constructing Gson?
+
+Gson can be constructed in two ways: by invoking `new Gson()` or by using a `GsonBuilder`. We chose to provide a simple no-args constructor to handle simple use-cases for Gson where you want to use default options, and quickly want to get going with writing code. For all other situations, where you need to configure Gson with options such as formatters, version controls etc., we use a builder pattern. The builder pattern allows a user to specify multiple optional settings for what essentially become constructor parameters for Gson.
+
+## Comparing Gson with alternate approaches
+
+Note that these comparisons were done while developing Gson so these date back to mid to late 2007.
+
+### Comparing Gson with org.json library
+
+org.json is a much lower-level library that can be used to write a `toJson()` method in a class. If you can not use Gson directly (maybe because of platform restrictions regarding reflection), you could use org.json to hand-code a `toJson` method in each object.
+
+### Comparing Gson with org.json.simple library
+
+org.json.simple library is very similar to org.json library and hence fairly low level. The key issue with this library is that it does not handle exceptions very well. In some cases it appeared to just eat the exception while in other cases it throws an "Error" rather than an exception.
diff --git a/gson/LICENSE b/gson/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/gson/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/gson/README.md b/gson/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..cd06c0a421aca091a3bc84e2a2634a4f67a06abd
--- /dev/null
+++ b/gson/README.md
@@ -0,0 +1,117 @@
+# Gson
+
+Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object.
+Gson can work with arbitrary Java objects including pre-existing objects that you do not have source-code of.
+
+There are a few open-source projects that can convert Java objects to JSON. However, most of them require that you place Java annotations in your classes; something that you can not do if you do not have access to the source-code. Most also do not fully support the use of Java Generics. Gson considers both of these as very important design goals.
+
+:information_source: Gson is currently in maintenance mode; existing bugs will be fixed, but large new features will likely not be added. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback.
+
+### Goals
+  * Provide simple `toJson()` and `fromJson()` methods to convert Java objects to JSON and vice-versa
+  * Allow pre-existing unmodifiable objects to be converted to and from JSON
+  * Extensive support of Java Generics
+  * Allow custom representations for objects
+  * Support arbitrarily complex objects (with deep inheritance hierarchies and extensive use of generic types)
+
+### Download
+
+Gradle:
+```gradle
+dependencies {
+  implementation 'com.google.code.gson:gson:2.10.1'
+}
+```
+
+Maven:
+```xml
+<dependency>
+  <groupId>com.google.code.gson</groupId>
+  <artifactId>gson</artifactId>
+  <version>2.10.1</version>
+</dependency>
+```
+
+[Gson jar downloads](https://maven-badges.herokuapp.com/maven-central/com.google.code.gson/gson) are available from Maven Central.
+
+![Build Status](https://github.com/google/gson/actions/workflows/build.yml/badge.svg)
+
+### Requirements
+#### Minimum Java version
+- Gson 2.9.0 and newer: Java 7
+- Gson 2.8.9 and older: Java 6
+
+Despite supporting older Java versions, Gson also provides a JPMS module descriptor (module name `com.google.gson`) for users of Java 9 or newer.
+
+#### JPMS dependencies (Java 9+)
+These are the optional Java Platform Module System (JPMS) JDK modules which Gson depends on.
+This only applies when running Java 9 or newer.
+
+- `java.sql` (optional since Gson 2.8.9)\
+When this module is present, Gson provides default adapters for some SQL date and time classes.
+
+- `jdk.unsupported`, respectively class `sun.misc.Unsafe` (optional)\
+When this module is present, Gson can use the `Unsafe` class to create instances of classes without no-args constructor.
+However, care should be taken when relying on this. `Unsafe` is not available in all environments and its usage has some pitfalls,
+see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()).
+
+#### Minimum Android API level
+
+- Gson 2.11.0 and newer: API level 21
+- Gson 2.10.1 and older: API level 19
+
+Older Gson versions may also support lower API levels, however this has not been verified.
+
+### Documentation
+  * [API Javadoc](https://www.javadoc.io/doc/com.google.code.gson/gson): Documentation for the current release
+  * [User guide](UserGuide.md): This guide contains examples on how to use Gson in your code
+  * [Troubleshooting guide](Troubleshooting.md): Describes how to solve common issues when using Gson
+  * [Releases and change log](https://github.com/google/gson/releases): Latest releases and changes in these versions; for older releases see [`CHANGELOG.md`](CHANGELOG.md)
+  * [Design document](GsonDesignDocument.md): This document discusses issues we faced while designing Gson. It also includes a comparison of Gson with other Java libraries that can be used for Json conversion
+
+Please use the ['gson' tag on StackOverflow](https://stackoverflow.com/questions/tagged/gson) or the [google-gson Google group](https://groups.google.com/group/google-gson) to discuss Gson or to post questions.
+
+### Related Content Created by Third Parties
+  * [Gson Tutorial](https://www.studytrails.com/java/json/java-google-json-introduction/) by `StudyTrails`
+  * [Gson Tutorial Series](https://futurestud.io/tutorials/gson-getting-started-with-java-json-serialization-deserialization) by `Future Studio`
+  * [Gson API Report](https://abi-laboratory.pro/java/tracker/timeline/gson/)
+
+### Building
+
+Gson uses Maven to build the project:
+```
+mvn clean verify
+```
+
+JDK 11 or newer is required for building, JDK 17 is recommended. Newer JDKs are currently not supported for building (but are supported when _using_ Gson).
+
+### Contributing
+
+See the [contributing guide](https://github.com/google/.github/blob/master/CONTRIBUTING.md).\
+Please perform a quick search to check if there are already existing issues or pull requests related to your contribution.
+
+Keep in mind that Gson is in maintenance mode. If you want to add a new feature, please first search for existing GitHub issues, or create a new one to discuss the feature and get feedback.
+
+### License
+
+Gson is released under the [Apache 2.0 license](LICENSE).
+
+```
+Copyright 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.
+```
+
+### Disclaimer
+
+This is not an officially supported Google product.
diff --git a/gson/ReleaseProcess.md b/gson/ReleaseProcess.md
new file mode 100644
index 0000000000000000000000000000000000000000..7f2b816b35225a41afd53552b24d1ea8ab1d8c8b
--- /dev/null
+++ b/gson/ReleaseProcess.md
@@ -0,0 +1,126 @@
+# Gson Release Process
+
+The following is a step-by-step procedure for releasing a new version of Google-Gson.
+
+1. Go through all open bugs and identify which will be fixed in this release. Mark all others with an appropriate release tag. Identify duplicates, and close the bugs that will never be fixed. Fix all bugs for the release, and mark them fixed.
+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. Start the release: `mvn release:prepare`
+    - 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://central.sonatype.org/publish/release/).
+1. Create a [GitHub release](https://github.com/google/gson/releases) for the new version. You can let GitHub [automatically generate the description for the release](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes), but you should edit it manually to point out the most important changes and potentially incompatible changes.
+1. Update version references in (version might be referenced multiple times):
+    - [`README.md`](README.md)
+    - [`UserGuide.md`](UserGuide.md)
+
+    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
+
+This section was borrowed heavily from [Doclava release process](https://code.google.com/archive/p/doclava/wikis/ProcessRelease.wiki).
+
+1. Install/Configure GPG following this [guide](https://blog.sonatype.com/2010/01/how-to-generate-pgp-signatures-with-maven/).
+1. [Create encrypted passwords](https://maven.apache.org/guides/mini/guide-encryption.html).
+1. Create `~/.m2/settings.xml` similar to as described in [Doclava release process](https://code.google.com/p/doclava/wiki/ProcessRelease).
+1. Now for deploying a snapshot repository, use `mvn deploy`.
+
+## Getting Maven Publishing Privileges
+
+See [OSSRH Publish Guide](https://central.sonatype.org/publish/publish-guide/).
+
+## Testing Maven release workflow locally
+
+The following describes how to perform the steps of the release locally to verify that they work as desired.
+
+**Warning:** Be careful with this, these steps might be outdated or incomplete. Doublecheck that you are working on a copy of your local Gson Git repository and make sure you have followed all steps. To be safe you can also temporarily turn off your internet connection to avoid accidentally pushing changes to the real remote Git or Maven repository.\
+As an alternative to the steps described below you can instead [perform a dry run](https://maven.apache.org/maven-release/maven-release-plugin/usage.html#do-a-dry-run), though this might not behave identical to a real release.
+
+1. Make a copy of your local Gson Git repository and only work with that copy
+2. Make sure you are on the `main` branch
+3. Create a temp directory outside the Gson directory\
+   In the following steps this will be called `#gson-remote-temp#`; replace this with the actual absolute file path of the directory, using only forward slashes. For example under Windows `C:\my-dir` becomes `C:/my-dir`.
+4. Create the directory `#gson-remote-temp#/git-repo`
+5. In that directory run
+
+    ```sh
+    git init --bare --initial-branch=main .
+    ```
+
+6. Create the directory `#gson-remote-temp#/maven-repo`
+7. Edit the root `pom.xml` of Gson
+    1. Change the `<developerConnection>` to
+
+       ```txt
+       scm:git:file:///#gson-remote-temp#/git-repo
+       ```
+
+    2. Change the `<url>` of the `<distributionManagement>` to
+
+       ```txt
+       file:///#gson-remote-temp#/maven-repo
+       ```
+
+    3. If you don't want to use GPG, remove the `maven-gpg-plugin` entry from the 'release' profile.\
+       There is also an entry under `<pluginManagement>`; you can remove that as well.
+8. Commit the changes using Git
+9. Change the remote repository of the Git project
+
+    <!-- Uses `txt` instead of `sh` to avoid the `#` being highlighted in some way -->
+    ```txt
+    git remote set-url origin file:///#gson-remote-temp#/git-repo
+    ```
+
+10. Push the changes
+
+    ```sh
+    git push origin main
+    ```
+
+Now you can perform the steps of the release:
+
+1. ```sh
+   mvn release:clean
+   ```
+
+2. ```sh
+   mvn release:prepare
+   ```
+
+3. ```sh
+   mvn release:perform
+   ```
+
+4. Verify that `#gson-remote-temp#/git-repo` and `#gson-remote-temp#/maven-repo` contain all the desired changes
+5. Afterwards delete all Gson files under `${user.home}/.m2/repository/com/google/code/gson` which have been installed in your local Maven repository during the release.\
+   Otherwise Maven might not download the real Gson artifacts with these version numbers, once they are released.
+
+## Running Benchmarks or Tests on Android
+
+* Download vogar
+* Put `adb` on your `$PATH` and run:
+
+  ```bash
+  vogar --benchmark --classpath gson.jar path/to/Benchmark.java
+  ```
+
+For example, here is how to run the [CollectionsDeserializationBenchmark](gson/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java):
+
+```bash
+export ANDROID_HOME=~/apps/android-sdk-mac_x86
+export PATH=$PATH:$ANDROID_HOME/platform-tools/:$ANDROID_HOME/android-sdk-mac_x86/tools/
+$VOGAR_HOME/bin/vogar \
+    --benchmark \
+    --sourcepath ../gson/src/main/java/ \
+    src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java \
+    -- \
+    --vm "app_process -Xgc:noconcurrent,app_process"
+```
diff --git a/gson/Troubleshooting.md b/gson/Troubleshooting.md
new file mode 100644
index 0000000000000000000000000000000000000000..d79457c452e165e2c4b997226c773ce0c747e6eb
--- /dev/null
+++ b/gson/Troubleshooting.md
@@ -0,0 +1,357 @@
+# Troubleshooting Guide
+
+This guide describes how to troubleshoot common issues when using Gson.
+
+<!-- The '<a id="..."></a>' anchors below are used to create stable links; don't remove or rename them -->
+<!-- Use only lowercase IDs, GitHub seems to not support uppercase IDs, see also https://github.com/orgs/community/discussions/50962 -->
+
+## <a id="class-cast-exception"></a> `ClassCastException` when using deserialized object
+
+**Symptom:** `ClassCastException` is thrown when accessing an object deserialized by Gson
+
+**Reason:** Your code is most likely not type-safe
+
+**Solution:** Make sure your code adheres to the following:
+
+- Avoid raw types: Instead of calling `fromJson(..., List.class)`, create for example a `TypeToken<List<MyClass>>`.
+  See the [user guide](UserGuide.md#collections-examples) for more information.
+- When using `TypeToken` prefer the `Gson.fromJson` overloads with `TypeToken` parameter such as [`fromJson(Reader, TypeToken)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#fromJson(java.io.Reader,com.google.gson.reflect.TypeToken)).
+  The overloads with `Type` parameter do not provide any type-safety guarantees.
+- When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken<List<T>>()` (where `T` is a type variable). Due to Java [type erasure](https://dev.java/learn/generics/type-erasure/) the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately.
+
+## <a id="reflection-inaccessible"></a> `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module'
+
+**Symptom:** An exception with a message in the form 'module ... does not "opens ..." to unnamed module' is thrown
+
+**Reason:** You use Gson by accident to access internal fields of third-party classes
+
+**Solution:** Write custom Gson [`TypeAdapter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) implementations for the affected classes or change the type of your data. If this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization).
+
+**Explanation:**
+
+When no built-in adapter for a type exists and no custom adapter has been registered, Gson falls back to using reflection to access the fields of a class (including `private` ones). Most likely you are seeing this error because you (by accident) rely on the reflection-based adapter for third-party classes. That should be avoided because you make yourself dependent on the implementation details of these classes which could change at any point. For the JDK it is also not possible anymore to access internal fields using reflection starting with JDK 17, see [JEP 403](https://openjdk.org/jeps/403).
+
+If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`.
+
+## <a id="reflection-inaccessible-to-module-gson"></a> `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson'
+
+**Symptom:** An exception with a message in the form 'module ... does not "opens ..." to module com.google.gson' is thrown
+
+**Reason:**
+
+- If the reported package is your own package then you have not configured the module declaration of your project to allow Gson to use reflection on your classes.
+- If the reported package is from a third party library or the JDK see [this troubleshooting point](#inaccessibleobjectexception-module--does-not-opens--to-unnamed-module).
+
+**Solution:** Make sure the `module-info.java` file of your project allows Gson to use reflection on your classes, for example:
+
+```java
+module mymodule {
+    requires com.google.gson;
+
+    opens mypackage to com.google.gson;
+}
+```
+
+Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization).
+
+## <a id="android-app-random-names"></a> Android app not working in Release mode; random property names
+
+**Symptom:** Your Android app is working fine in Debug mode but fails in Release mode and the JSON properties have seemingly random names such as `a`, `b`, ...
+
+**Reason:** You probably have not configured ProGuard / R8 correctly
+
+**Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information.
+
+## <a id="android-app-broken-after-app-update"></a> Android app unable to parse JSON after app update
+
+**Symptom:** You released a new version of your Android app and it fails to parse JSON data created by the previous version of your app
+
+**Reason:** You probably have not configured ProGuard / R8 correctly; probably the field names are being obfuscated and their naming changed between the versions of your app
+
+**Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information.
+
+If you want to preserve backward compatibility for you app you can use [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) on the fields to specify the obfuscated name as alternate, for example: `@SerializedName(value = "myprop", alternate = "a")`
+
+Normally ProGuard and R8 produce a mapping file, this makes it easier to find out the obfuscated field names instead of having to find them out through trial and error or other means. See the [Android Studio user guide](https://developer.android.com/studio/build/shrink-code.html#retracing) for more information.
+
+## <a id="default-field-values-missing"></a> Default field values not present after deserialization
+
+**Symptom:** You have assign default values to fields but after deserialization the fields have their standard value (such as `null` or `0`)
+
+**Reason:** Gson cannot invoke the constructor of your class and falls back to JDK `Unsafe` (or similar means)
+
+**Solution:** Make sure that the class:
+
+- is `static` (explicitly or implicitly when it is a top-level class)
+- has a no-args constructor
+
+Otherwise Gson will by default try to use JDK `Unsafe` or similar means to create an instance of your class without invoking the constructor and without running any initializers. You can also disable that behavior through [`GsonBuilder.disableJdkUnsafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) to notice such issues early on.
+
+## <a id="anonymous-local-null"></a> `null` values for anonymous and local classes
+
+**Symptom:** Objects of a class are always serialized as JSON `null` / always deserialized as Java `null`
+
+**Reason:** The class you are serializing or deserializing is an anonymous or a local class (or you have specified a custom `ExclusionStrategy`)
+
+**Solution:** Convert the class to a `static` nested class. If the class is already `static` make sure you have not specified a Gson `ExclusionStrategy` which might exclude the class.
+
+Notes:
+
+- "double brace-initialization" also creates anonymous classes
+- Local record classes (feature added in Java 16) are supported by Gson and are not affected by this
+
+## <a id="map-key-wrong-json"></a> Map keys having unexpected format in JSON
+
+**Symptom:** JSON output for `Map` keys is unexpected / cannot be deserialized again
+
+**Reason:** The `Map` key type is 'complex' and you have not configured the `GsonBuilder` properly
+
+**Solution:** Use [`GsonBuilder.enableComplexMapKeySerialization()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#enableComplexMapKeySerialization()). See also the [user guide](UserGuide.md#maps-examples) for more information.
+
+## <a id="malformed-json"></a> Parsing JSON fails with `MalformedJsonException`
+
+**Symptom:** JSON parsing fails with `MalformedJsonException`
+
+**Reason:** The JSON data is actually malformed
+
+**Solution:** During debugging, log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/).
+
+For example, let's assume you want to deserialize the following JSON data:
+
+```json
+{
+  "languages": [
+    "English",
+    "French",
+  ]
+}
+```
+
+This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 5 column 4 path $.languages[2]`\
+The problem here is the trailing comma (`,`) after `"French"`, trailing commas are not allowed by the JSON specification. The location information "line 5 column 4" points to the `]` in the JSON data (with some slight inaccuracies) because Gson expected another value after `,` instead of the closing `]`. The JSONPath `$.languages[2]` in the exception message also points there: `$.` refers to the root object, `languages` refers to its member of that name and `[2]` refers to the (missing) third value in the JSON array value of that member (numbering starts at 0, so it is `[2]` instead of `[3]`).\
+The proper solution here is to fix the malformed JSON data.
+
+To spot syntax errors in the JSON data easily you can open it in an editor with support for JSON, for example Visual Studio Code. It will highlight within the JSON data the error location and show why the JSON data is considered invalid.
+
+## <a id="number-parsed-as-double"></a> Integral JSON number is parsed as `double`
+
+**Symptom:** JSON data contains an integral number such as `45` but Gson returns it as `double`
+
+**Reason:** When parsing a JSON number as `Object`, Gson will by default always return a `double`
+
+**Solution:** Use [`GsonBuilder.setObjectToNumberStrategy`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setObjectToNumberStrategy(com.google.gson.ToNumberStrategy)) to specify what type of number should be returned
+
+## <a id="default-lenient"></a> Malformed JSON not rejected
+
+**Symptom:** Gson parses malformed JSON without throwing any exceptions
+
+**Reason:** Due to legacy reasons Gson performs parsing by default in lenient mode
+
+**Solution:** If you are using Gson 2.11.0 or newer, call [`GsonBuilder.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setStrictness(com.google.gson.Strictness)),
+[`JsonReader.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setStrictness(com.google.gson.Strictness))
+and [`JsonWriter.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonWriter.html#setStrictness(com.google.gson.Strictness))
+with `Strictness.STRICT` to overwrite the default lenient behavior of `Gson` and make these classes strictly adhere to the JSON specification.
+Otherwise if you are using an older Gson version, see the [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient)
+section "JSON Strictness handling" for alternative solutions.
+
+## <a id="unexpected-json-structure"></a> `IllegalStateException`: "Expected ... but was ..."
+
+**Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was ..." is thrown
+
+**Reason:** The JSON data does not have the correct format
+
+**Solution:** Make sure that your classes correctly model the JSON data. Also during debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Read the location information of the exception message, it indicates where exactly in the document the error occurred, including the [JSONPath](https://goessner.net/articles/JsonPath/).
+
+For example, let's assume you have the following Java class:
+
+```java
+class WebPage {
+    String languages;
+}
+```
+
+And you want to deserialize the following JSON data:
+
+```json
+{
+  "languages": ["English", "French"]
+}
+```
+
+This will fail with an exception similar to this one: `IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 17 path $.languages`\
+This means Gson expected a JSON string value but found the beginning of a JSON array (`[`). The location information "line 2 column 17" points to the `[` in the JSON data (with some slight inaccuracies), so does the JSONPath `$.languages` in the exception message. It refers to the `languages` member of the root object (`$.`).\
+The solution here is to change in the `WebPage` class the field `String languages` to `List<String> languages`.
+
+## <a id="adapter-not-null-safe"></a> `IllegalStateException`: "Expected ... but was NULL"
+
+**Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was NULL" is thrown
+
+**Reason:**
+
+- A built-in adapter does not support JSON null values
+- You have written a custom `TypeAdapter` which does not properly handle JSON null values
+
+**Solution:** If this occurs for a custom adapter you wrote, add code similar to the following at the beginning of its `read` method:
+
+```java
+@Override
+public MyClass read(JsonReader in) throws IOException {
+    if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+    }
+
+    ...
+}
+```
+
+Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html#nullSafe()) on the adapter instance you created.
+
+## <a id="serialize-nulls"></a> Properties missing in JSON
+
+**Symptom:** Properties are missing in the JSON output
+
+**Reason:** Gson by default omits JSON null from the output (or: ProGuard / R8 is not configured correctly and removed unused fields)
+
+**Solution:** Use [`GsonBuilder.serializeNulls()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#serializeNulls())
+
+Note: Gson does not support anonymous and local classes and will serialize them as JSON null, see the [related troubleshooting point](#null-values-for-anonymous-and-local-classes).
+
+## <a id="android-internal-fields"></a> JSON output changes for newer Android versions
+
+**Symptom:** The JSON output differs when running on newer Android versions
+
+**Reason:** You use Gson by accident to access internal fields of Android classes
+
+**Solution:** Write custom Gson [`TypeAdapter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) implementations for the affected classes or change the type of your data
+
+**Explanation:**
+
+When no built-in adapter for a type exists and no custom adapter has been registered, Gson falls back to using reflection to access the fields of a class (including `private` ones). Most likely you are experiencing this issue because you (by accident) rely on the reflection-based adapter for Android classes. That should be avoided because you make yourself dependent on the implementation details of these classes which could change at any point.
+
+If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`.
+
+## <a id="json-static-fields"></a> JSON output contains values of `static` fields
+
+**Symptom:** The JSON output contains values of `static` fields
+
+**Reason:** You used `GsonBuilder.excludeFieldsWithModifiers` to overwrite the default excluded modifiers
+
+**Solution:** When calling `GsonBuilder.excludeFieldsWithModifiers` you overwrite the default excluded modifiers. Therefore, you have to explicitly exclude `static` fields if desired. This can be done by adding `Modifier.STATIC` as additional argument.
+
+## <a id="no-such-method-error"></a> `NoSuchMethodError` when calling Gson methods
+
+**Symptom:** A `java.lang.NoSuchMethodError` is thrown when trying to call certain Gson methods
+
+**Reason:**
+
+- You have multiple versions of Gson on your classpath
+- Or, the Gson version you compiled against is different from the one on your classpath
+- Or, you are using a code shrinking tool such as ProGuard or R8 which removed methods from Gson
+
+**Solution:** First disable any code shrinking tools such as ProGuard or R8 and check if the issue persists. If not, you have to tweak the configuration of that tool to not modify Gson classes. Otherwise verify that the Gson JAR on your classpath is the same you are compiling against, and that there is only one Gson JAR on your classpath. See [this Stack Overflow question](https://stackoverflow.com/q/227486) to find out where a class is loaded from. For example, for debugging you could include the following code:
+
+```java
+System.out.println(Gson.class.getProtectionDomain().getCodeSource().getLocation());
+```
+
+If that fails with a `NullPointerException` you have to try one of the other ways to find out where a class is loaded from.
+
+## <a id="duplicate-fields"></a> `IllegalArgumentException`: 'Class ... declares multiple JSON fields named '...''
+
+**Symptom:** An exception with the message 'Class ... declares multiple JSON fields named '...'' is thrown
+
+**Reason:**
+
+- The name you have specified with a [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation for a field collides with the name of another field
+- The [`FieldNamingStrategy`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/FieldNamingStrategy.html) you have specified produces conflicting field names
+- A field of your class has the same name as the field of a superclass
+
+Gson prevents multiple fields with the same name because during deserialization it would be ambiguous for which field the JSON data should be deserialized. For serialization it would cause the same field to appear multiple times in JSON. While the JSON specification permits this, it is likely that the application parsing the JSON data will not handle it correctly.
+
+**Solution:** First identify the fields with conflicting names based on the exception message. Then decide if you want to rename one of them using the [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation, or if you want to [exclude](UserGuide.md#excluding-fields-from-serialization-and-deserialization) one of them. When excluding one of the fields you have to include it for both serialization and deserialization (even if your application only performs one of these actions) because the duplicate field check cannot differentiate between these actions.
+
+## <a id="java-lang-class-unsupported"></a> `UnsupportedOperationException` when serializing or deserializing `java.lang.Class`
+
+**Symptom:** An `UnsupportedOperationException` is thrown when trying to serialize or deserialize `java.lang.Class`
+
+**Reason:** Gson intentionally does not permit serializing and deserializing `java.lang.Class` for security reasons. Otherwise a malicious user could make your application load an arbitrary class from the classpath and, depending on what your application does with the `Class`, in the worst case perform a remote code execution attack.
+
+**Solution:** First check if you really need to serialize or deserialize a `Class`. Often it is possible to use string aliases and then map them to the known `Class`; you could write a custom [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) to do this. If the `Class` values are not known in advance, try to introduce a common base class or interface for all these classes and then verify that the deserialized class is a subclass. For example assuming the base class is called `MyBaseClass`, your custom `TypeAdapter` should load the class like this:
+
+```java
+Class.forName(jsonString, false, getClass().getClassLoader()).asSubclass(MyBaseClass.class)
+```
+
+This will not initialize arbitrary classes, and it will throw a `ClassCastException` if the loaded class is not the same as or a subclass of `MyBaseClass`.
+
+## <a id="type-token-raw"></a> `IllegalStateException`: 'TypeToken must be created with a type argument' <br> `RuntimeException`: 'Missing type parameter'
+
+**Symptom:** An `IllegalStateException` with the message 'TypeToken must be created with a type argument' is thrown.\
+For older Gson versions a `RuntimeException` with message 'Missing type parameter' is thrown.
+
+**Reason:**
+
+- You created a `TypeToken` without type argument, for example `new TypeToken() {}` (note the missing `<...>`). You always have to provide the type argument, for example like this: `new TypeToken<List<String>>() {}`. Normally the compiler will also emit a 'raw types' warning when you forget the `<...>`.
+- You are using a code shrinking tool such as ProGuard or R8 (Android app builds normally have this enabled by default) but have not configured it correctly for usage with Gson.
+
+**Solution:** When you are using a code shrinking tool such as ProGuard or R8 you have to adjust your configuration to include the following rules:
+
+```
+# Keep generic signatures; needed for correct type resolution
+-keepattributes Signature
+
+# Keep class TypeToken (respectively its generic signature)
+-keep class com.google.gson.reflect.TypeToken { *; }
+
+# Keep any (anonymous) classes extending TypeToken
+-keep class * extends com.google.gson.reflect.TypeToken
+```
+
+See also the [Android example](examples/android-proguard-example/README.md) for more information.
+
+Note: For newer Gson versions these rules might be applied automatically; make sure you are using the latest Gson version and the latest version of the code shrinking tool.
+
+## <a id="r8-abstract-class"></a> `JsonIOException`: 'Abstract classes can't be instantiated!' (R8)
+
+**Symptom:** A `JsonIOException` with the message 'Abstract classes can't be instantiated!' is thrown; the class mentioned in the exception message is not actually `abstract` in your source code, and you are using the code shrinking tool R8 (Android app builds normally have this configured by default).
+
+Note: If the class which you are trying to deserialize is actually abstract, then this exception is probably unrelated to R8 and you will have to implement a custom [`InstanceCreator`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/InstanceCreator.html) or [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) which creates an instance of a non-abstract subclass of the class.
+
+**Reason:** The code shrinking tool R8 performs optimizations where it removes the no-args constructor from a class and makes the class `abstract`. Due to this Gson cannot create an instance of the class.
+
+**Solution:** Make sure the class has a no-args constructor, then adjust your R8 configuration file to keep the constructor of the class. For example:
+
+```
+# Keep the no-args constructor of the deserialized class
+-keepclassmembers class com.example.MyClass {
+  <init>();
+}
+```
+
+You can also use `<init>(...);` to keep all constructors of that class, but then you might actually rely on `sun.misc.Unsafe` on both JDK and Android to create classes without no-args constructor, see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) for more information.
+
+For Android you can add this rule to the `proguard-rules.pro` file, see also the [Android documentation](https://developer.android.com/build/shrink-code#keep-code). In case the class name in the exception message is obfuscated, see the Android documentation about [retracing](https://developer.android.com/build/shrink-code#retracing).
+
+For Android you can alternatively use the [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep) on the class or constructor you want to keep. That might be easier than having to maintain a custom R8 configuration.
+
+Note that the latest Gson versions (> 2.10.1) specify a default R8 configuration. If your class is a top-level class or is `static`, has a no-args constructor and its fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional R8 configuration.
+
+## <a id="typetoken-type-variable"></a> `IllegalArgumentException`: 'TypeToken type argument must not contain a type variable'
+
+**Symptom:** An exception with the message 'TypeToken type argument must not contain a type variable' is thrown
+
+**Reason:** This exception is thrown when you create an anonymous `TypeToken` subclass which captures a type variable, for example `new TypeToken<List<T>>() {}` (where `T` is a type variable). At compile time such code looks safe and you can use the type `List<T>` without any warnings. However, this code is not actually type-safe because at runtime due to [type erasure](https://dev.java/learn/generics/type-erasure/) only the upper bound of the type variable is available. For the previous example that would be `List<Object>`. When using such a `TypeToken` with any Gson methods performing deserialization this would lead to confusing and difficult to debug `ClassCastException`s. For serialization it can in some cases also lead to undesired results.
+
+Note: Earlier version of Gson unfortunately did not prevent capturing type variables, which caused many users to unwittingly write type-unsafe code.
+
+**Solution:**
+
+- Use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately.
+- For Kotlin users: Use [`reified` type parameters](https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters), that means change `<T>` to `<reified T>`, if possible. If you have a chain of functions with type parameters you will probably have to make all of them `reified`.
+- If you don't actually use Gson's `TypeToken` for any Gson method, use a general purpose 'type token' implementation provided by a different library instead, for example Guava's [`com.google.common.reflect.TypeToken`](https://javadoc.io/doc/com.google.guava/guava/latest/com/google/common/reflect/TypeToken.html).
+
+For backward compatibility it is possible to restore Gson's old behavior of allowing `TypeToken` to capture type variables by setting the [system property](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/System.html#setProperty(java.lang.String,java.lang.String)) `gson.allowCapturingTypeVariables` to `"true"`, **however**:
+
+- This does not solve any of the type-safety problems mentioned above; in the long term you should prefer one of the other solutions listed above. This system property might be removed in future Gson versions.
+- You should only ever set the property to `"true"`, but never to any other value or manually clear it. Otherwise this might counteract any libraries you are using which might have deliberately set the system property because they rely on its behavior.
diff --git a/gson/UserGuide.md b/gson/UserGuide.md
new file mode 100644
index 0000000000000000000000000000000000000000..aa0b831c84695ca766535da1d724282a3f4e6903
--- /dev/null
+++ b/gson/UserGuide.md
@@ -0,0 +1,750 @@
+# Gson User Guide
+
+1. [Overview](#overview)
+2. [Goals for Gson](#goals-for-gson)
+3. [Gson Performance and Scalability](#gson-performance-and-scalability)
+4. [Gson Users](#gson-users)
+5. [Using Gson](#using-gson)
+   * [Using Gson with Gradle/Android](#using-gson-with-gradleandroid)
+   * [Using Gson with Maven](#using-gson-with-maven)
+   * [Primitives Examples](#primitives-examples)
+   * [Object Examples](#object-examples)
+   * [Finer Points with Objects](#finer-points-with-objects)
+   * [Nested Classes (including Inner Classes)](#nested-classes-including-inner-classes)
+   * [Array Examples](#array-examples)
+   * [Collections Examples](#collections-examples)
+     * [Collections Limitations](#collections-limitations)
+   * [Maps Examples](#maps-examples)
+   * [Serializing and Deserializing Generic Types](#serializing-and-deserializing-generic-types)
+   * [Serializing and Deserializing Collection with Objects of Arbitrary Types](#serializing-and-deserializing-collection-with-objects-of-arbitrary-types)
+   * [Built-in Serializers and Deserializers](#built-in-serializers-and-deserializers)
+   * [Custom Serialization and Deserialization](#custom-serialization-and-deserialization)
+     * [Writing a Serializer](#writing-a-serializer)
+     * [Writing a Deserializer](#writing-a-deserializer)
+   * [Writing an Instance Creator](#writing-an-instance-creator)
+     * [InstanceCreator for a Parameterized Type](#instancecreator-for-a-parameterized-type)
+   * [Compact Vs. Pretty Printing for JSON Output Format](#compact-vs-pretty-printing-for-json-output-format)
+   * [Null Object Support](#null-object-support)
+   * [Versioning Support](#versioning-support)
+   * [Excluding Fields From Serialization and Deserialization](#excluding-fields-from-serialization-and-deserialization)
+     * [Java Modifier Exclusion](#java-modifier-exclusion)
+     * [Gson's `@Expose`](#gsons-expose)
+     * [User Defined Exclusion Strategies](#user-defined-exclusion-strategies)
+   * [JSON Field Naming Support](#json-field-naming-support)
+   * [Sharing State Across Custom Serializers and Deserializers](#sharing-state-across-custom-serializers-and-deserializers)
+   * [Streaming](#streaming)
+6. [Issues in Designing Gson](#issues-in-designing-gson)
+7. [Future Enhancements to Gson](#future-enhancements-to-gson)
+
+## Overview
+
+Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object.
+
+Gson can work with arbitrary Java objects including pre-existing objects that you do not have source code of.
+
+## Goals for Gson
+
+* Provide easy to use mechanisms like `toString()` and constructor (factory method) to convert Java to JSON and vice-versa
+* Allow pre-existing unmodifiable objects to be converted to and from JSON
+* Allow custom representations for objects
+* Support arbitrarily complex objects
+* Generate compact and readable JSON output
+
+## Gson Performance and Scalability
+
+Here are some metrics that we obtained on a desktop (dual opteron, 8GB RAM, 64-bit Ubuntu) running lots of other things along-with the tests. You can rerun these tests by using the class [`PerformanceTest`](gson/src/test/java/com/google/gson/metrics/PerformanceTest.java).
+
+* Strings: Deserialized strings of over 25MB without any problems (see `disabled_testStringDeserializationPerformance` method in `PerformanceTest`)
+* Large collections:
+  * Serialized a collection of 1.4 million objects (see `disabled_testLargeCollectionSerialization` method in `PerformanceTest`)
+  * Deserialized a collection of 87,000 objects (see `disabled_testLargeCollectionDeserialization` in `PerformanceTest`)
+* Gson 1.4 raised the deserialization limit for byte arrays and collection to over 11MB from 80KB.
+
+Note: Delete the `disabled_` prefix to run these tests. We use this prefix to prevent running these tests every time we run JUnit tests.
+
+## Gson Users
+
+Gson was originally created for use inside Google where it is currently used in a number of projects. It is now used by a number of public projects and companies.
+
+## Using Gson
+
+The primary class to use is [`Gson`](gson/src/main/java/com/google/gson/Gson.java) which you can just create by calling `new Gson()`. There is also a class [`GsonBuilder`](gson/src/main/java/com/google/gson/GsonBuilder.java) available that can be used to create a Gson instance with various settings like version control and so on.
+
+The Gson instance does not maintain any state while invoking JSON operations. So, you are free to reuse the same object for multiple JSON serialization and deserialization operations.
+
+## Using Gson with Gradle/Android
+
+```gradle
+dependencies {
+    implementation 'com.google.code.gson:gson:2.10.1'
+}
+```
+
+## Using Gson with Maven
+
+To use Gson with Maven2/3, you can use the Gson version available in Maven Central by adding the following dependency:
+
+```xml
+<dependencies>
+    <!--  Gson: Java to JSON conversion -->
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.10.1</version>
+      <scope>compile</scope>
+    </dependency>
+</dependencies>
+```
+
+That is it, now your Maven project is Gson enabled.
+
+### Primitives Examples
+
+```java
+// Serialization
+Gson gson = new Gson();
+gson.toJson(1);            // ==> 1
+gson.toJson("abcd");       // ==> "abcd"
+gson.toJson(new Long(10)); // ==> 10
+int[] values = { 1 };
+gson.toJson(values);       // ==> [1]
+
+// Deserialization
+int i = gson.fromJson("1", int.class);
+Integer intObj = gson.fromJson("1", Integer.class);
+Long longObj = gson.fromJson("1", Long.class);
+Boolean boolObj = gson.fromJson("false", Boolean.class);
+String str = gson.fromJson("\"abc\"", String.class);
+String[] strArray = gson.fromJson("[\"abc\"]", String[].class);
+```
+
+### Object Examples
+
+```java
+class BagOfPrimitives {
+  private int value1 = 1;
+  private String value2 = "abc";
+  private transient int value3 = 3;
+  BagOfPrimitives() {
+    // no-args constructor
+  }
+}
+
+// Serialization
+BagOfPrimitives obj = new BagOfPrimitives();
+Gson gson = new Gson();
+String json = gson.toJson(obj);
+
+// ==> {"value1":1,"value2":"abc"}
+```
+
+Note that you can not serialize objects with circular references since that will result in infinite recursion.
+
+```java
+// Deserialization
+BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);
+// ==> obj2 is just like obj
+```
+
+#### **Finer Points with Objects**
+
+* It is perfectly fine (and recommended) to use private fields.
+* There is no need to use any annotations to indicate a field is to be included for serialization and deserialization. All fields in the current class (and from all super classes) are included by default.
+* If a field is marked transient, (by default) it is ignored and not included in the JSON serialization or deserialization.
+* This implementation handles nulls correctly.
+  * 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 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.
+
+### Nested Classes (including Inner Classes)
+
+Gson can serialize static nested classes quite easily.
+
+Gson can also deserialize static nested classes. However, Gson can **not** automatically deserialize the **pure inner classes since their no-args constructor also need a reference to the containing Object** which is not available at the time of deserialization. You can address this problem by either making the inner class static or by providing a custom InstanceCreator for it. Here is an example:
+
+```java
+public class A {
+  public String a;
+
+  class B {
+
+    public String b;
+
+    public B() {
+      // No args constructor for B
+    }
+  }
+}
+```
+
+**NOTE**: The above class B can not (by default) be serialized with Gson.
+
+Gson can not deserialize `{"b":"abc"}` into an instance of B since the class B is an inner class. If it was defined as static class B then Gson would have been able to deserialize the string. Another solution is to write a custom instance creator for B.
+
+```java
+public class InstanceCreatorForB implements InstanceCreator<A.B> {
+  private final A a;
+  public InstanceCreatorForB(A a)  {
+    this.a = a;
+  }
+  public A.B createInstance(Type type) {
+    return a.new B();
+  }
+}
+```
+
+The above is possible, but not recommended.
+
+### Array Examples
+
+```java
+Gson gson = new Gson();
+int[] ints = {1, 2, 3, 4, 5};
+String[] strings = {"abc", "def", "ghi"};
+
+// Serialization
+gson.toJson(ints);     // ==> [1,2,3,4,5]
+gson.toJson(strings);  // ==> ["abc", "def", "ghi"]
+
+// Deserialization
+int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
+// ==> ints2 will be same as ints
+```
+
+We also support multi-dimensional arrays, with arbitrarily complex element types.
+
+### Collections Examples
+
+```java
+Gson gson = new Gson();
+Collection<Integer> ints = Arrays.asList(1,2,3,4,5);
+
+// Serialization
+String json = gson.toJson(ints);  // ==> [1,2,3,4,5]
+
+// Deserialization
+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
+```
+
+Fairly hideous: note how we define the type of collection.
+Unfortunately, there is no way to get around this in Java.
+
+#### Collections Limitations
+
+Gson can serialize collection of arbitrary objects but can not deserialize from it, because there is no way for the user to indicate the type of the resulting object. Instead, while deserializing, the Collection must be of a specific, generic type.
+This makes sense, and is rarely a problem when following good Java coding practices.
+
+### Maps Examples
+
+Gson by default serializes any `java.util.Map` implementation as a JSON object. Because JSON objects only support strings as member names, Gson converts the Map keys to strings by calling `toString()` on them, and using `"null"` for `null` keys:
+
+```java
+Gson gson = new Gson();
+Map<String, String> stringMap = new LinkedHashMap<>();
+stringMap.put("key", "value");
+stringMap.put(null, "null-entry");
+
+// Serialization
+String json = gson.toJson(stringMap); // ==> {"key":"value","null":"null-entry"}
+
+Map<Integer, Integer> intMap = new LinkedHashMap<>();
+intMap.put(2, 4);
+intMap.put(3, 6);
+
+// Serialization
+String json = gson.toJson(intMap); // ==> {"2":4,"3":6}
+```
+
+For deserialization Gson uses the `read` method of the `TypeAdapter` registered for the Map key type. Similar to the Collection example shown above, for deserialization a `TypeToken` has to be used to tell Gson what types the Map keys and values have:
+
+```java
+Gson gson = new Gson();
+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}
+```
+
+Gson also supports using complex types as Map keys. This feature can be enabled with [`GsonBuilder.enableComplexMapKeySerialization()`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#enableComplexMapKeySerialization()). If enabled, Gson uses the `write` method of the `TypeAdapter` registered for the Map key type to serialize the keys, instead of using `toString()`. When any of the keys is serialized by the adapter as JSON array or JSON object, Gson will serialize the complete Map as JSON array, consisting of key-value pairs (encoded as JSON array). Otherwise, if none of the keys is serialized as a JSON array or JSON object, Gson will use a JSON object to encode the Map:
+
+```java
+class PersonName {
+  String firstName;
+  String lastName;
+
+  PersonName(String firstName, String lastName) {
+    this.firstName = firstName;
+    this.lastName = lastName;
+  }
+
+  // ... equals and hashCode
+}
+
+Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+Map<PersonName, Integer> complexMap = new LinkedHashMap<>();
+complexMap.put(new PersonName("John", "Doe"), 30);
+complexMap.put(new PersonName("Jane", "Doe"), 35);
+
+// Serialization; complex map is serialized as a JSON array containing key-value pairs (as JSON arrays)
+String json = gson.toJson(complexMap);
+// ==> [[{"firstName":"John","lastName":"Doe"},30],[{"firstName":"Jane","lastName":"Doe"},35]]
+
+Map<String, String> stringMap = new LinkedHashMap<>();
+stringMap.put("key", "value");
+// Serialization; non-complex map is serialized as a regular JSON object
+String json = gson.toJson(stringMap); // ==> {"key":"value"}
+```
+
+**Important:** Because Gson by default uses `toString()` to serialize Map keys, this can lead to malformed encoded keys or can cause mismatch between serialization and deserialization of the keys, for example when `toString()` is not properly implemented. A workaround for this can be to use `enableComplexMapKeySerialization()` to make sure the `TypeAdapter` registered for the Map key type is used for deserialization _and_ serialization. As shown in the example above, when none of the keys are serialized by the adapter as JSON array or JSON object, the Map is serialized as a regular JSON object, as desired.
+
+Note that when deserializing enums as Map keys, if Gson is unable to find an enum constant with a matching `name()` value respectively `@SerializedName` annotation, it falls back to looking up the enum constant by its `toString()` value. This is to work around the issue described above, but only applies to enum constants.
+
+### Serializing and Deserializing Generic Types
+
+When you call `toJson(obj)`, Gson calls `obj.getClass()` to get information on the fields to serialize. Similarly, you can typically pass `MyClass.class` object in the `fromJson(json, MyClass.class)` method. This works fine if the object is a non-generic type. However, if the object is of a generic type, then the Generic type information is lost because of Java Type Erasure. Here is an example illustrating the point:
+
+```java
+class Foo<T> {
+  T value;
+}
+Gson gson = new Gson();
+Foo<Bar> foo = new Foo<Bar>();
+gson.toJson(foo); // May not serialize foo.value correctly
+
+gson.fromJson(json, foo.getClass()); // Fails to deserialize foo.value as Bar
+```
+
+The above code fails to interpret value as type Bar because Gson invokes `foo.getClass()` to get its class information, but this method returns a raw class, `Foo.class`. This means that Gson has no way of knowing that this is an object of type `Foo<Bar>`, and not just plain `Foo`.
+
+You can solve this problem by specifying the correct parameterized type for your generic type. You can do this by using the [`TypeToken`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html) class.
+
+```java
+Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
+gson.toJson(foo, fooType);
+
+gson.fromJson(json, fooType);
+```
+
+The idiom used to get `fooType` actually defines an anonymous local inner class containing a method `getType()` that returns the fully parameterized type.
+
+### Serializing and Deserializing Collection with Objects of Arbitrary Types
+
+Sometimes you are dealing with JSON array that contains mixed types. For example:
+`['hello',5,{name:'GREETINGS',source:'guest'}]`
+
+The equivalent `Collection` containing this is:
+
+```java
+Collection collection = new ArrayList();
+collection.add("hello");
+collection.add(5);
+collection.add(new Event("GREETINGS", "guest"));
+```
+
+where the `Event` class is defined as:
+
+```java
+class Event {
+  private String name;
+  private String source;
+  private Event(String name, String source) {
+    this.name = name;
+    this.source = source;
+  }
+}
+```
+
+You can serialize the collection with Gson without doing anything specific: `toJson(collection)` would write out the desired output.
+
+However, deserialization with `fromJson(json, Collection.class)` will not work since Gson has no way of knowing how to map the input to the types. Gson requires that you provide a genericized version of the collection type in `fromJson()`. So, you have three options:
+
+1. Use Gson's parser API (low-level streaming parser or the DOM parser JsonParser) to parse the array elements and then use `Gson.fromJson()` on each of the array elements.This is the preferred approach. [Here is an example](extras/src/main/java/com/google/gson/extras/examples/rawcollections/RawCollectionsExample.java) that demonstrates how to do this.
+
+2. Register a type adapter for `Collection.class` that looks at each of the array members and maps them to appropriate objects. The disadvantage of this approach is that it will screw up deserialization of other collection types in Gson.
+
+3. Register a type adapter for `MyCollectionMemberType` and use `fromJson()` with `Collection<MyCollectionMemberType>`.
+
+This approach is practical only if the array appears as a top-level element or if you can change the field type holding the collection to be of type `Collection<MyCollectionMemberType>`.
+
+### Built-in Serializers and Deserializers
+
+Gson has built-in serializers and deserializers for commonly used classes whose default representation may be inappropriate, for instance
+
+* `java.net.URL` to match it with strings like `"https://github.com/google/gson/"`
+* `java.net.URI` to match it with strings like `"/google/gson/"`
+
+For many more, see the internal class [`TypeAdapters`](gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java).
+
+You can also find source code for some commonly used classes such as JodaTime at [this page](https://sites.google.com/site/gson/gson-type-adapters-for-common-classes-1).
+
+### Custom Serialization and Deserialization
+
+Sometimes the default representation is not what you want. This is often the case when dealing with library classes (DateTime, etc.).
+Gson allows you to register your own custom serializers and deserializers. This is done by defining two parts:
+
+* JSON Serializers: Need to define custom serialization for an object
+* JSON Deserializers: Needed to define custom deserialization for a type
+
+* Instance Creators: Not needed if no-args constructor is available or a deserializer is registered
+
+```java
+GsonBuilder gson = new GsonBuilder();
+gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter());
+gson.registerTypeAdapter(MyType.class, new MySerializer());
+gson.registerTypeAdapter(MyType.class, new MyDeserializer());
+gson.registerTypeAdapter(MyType.class, new MyInstanceCreator());
+```
+
+`registerTypeAdapter` call checks
+1. if the type adapter implements more than one of these interfaces, in that case it registers the adapter for all of them.
+2. if the type adapter is for the Object class or JsonElement or any of its subclasses, in that case it throws IllegalArgumentException because overriding the built-in adapters for these types is not supported.
+
+#### Writing a Serializer
+
+Here is an example of how to write a custom serializer for JodaTime `DateTime` class.
+
+```java
+private class DateTimeSerializer implements JsonSerializer<DateTime> {
+  public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context) {
+    return new JsonPrimitive(src.toString());
+  }
+}
+```
+
+Gson calls `serialize()` when it runs into a `DateTime` object during serialization.
+
+#### Writing a Deserializer
+
+Here is an example of how to write a custom deserializer for JodaTime DateTime class.
+
+```java
+private class DateTimeDeserializer implements JsonDeserializer<DateTime> {
+  public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException {
+    return new DateTime(json.getAsJsonPrimitive().getAsString());
+  }
+}
+```
+
+Gson calls `deserialize` when it needs to deserialize a JSON string fragment into a DateTime object
+
+**Finer points with Serializers and Deserializers**
+
+Often you want to register a single handler for all generic types corresponding to a raw type
+
+* For example, suppose you have an `Id` class for id representation/translation (i.e. an internal vs. external representation).
+* `Id<T>` type that has same serialization for all generic types
+  * Essentially write out the id value
+* Deserialization is very similar but not exactly the same
+  * Need to call `new Id(Class<T>, String)` which returns an instance of `Id<T>`
+
+Gson supports registering a single handler for this. You can also register a specific handler for a specific generic type (say `Id<RequiresSpecialHandling>` needed special handling).
+The `Type` parameter for the `toJson()` and `fromJson()` contains the generic type information to help you write a single handler for all generic types corresponding to the same raw type.
+
+### Writing an Instance Creator
+
+While deserializing an Object, Gson needs to create a default instance of the class.
+Well-behaved classes that are meant for serialization and deserialization should have a no-argument constructor.
+
+* Doesn't matter whether public or private
+
+Typically, Instance Creators are needed when you are dealing with a library class that does NOT define a no-argument constructor
+
+**Instance Creator Example**
+
+```java
+private class MoneyInstanceCreator implements InstanceCreator<Money> {
+  public Money createInstance(Type type) {
+    return new Money("1000000", CurrencyCode.USD);
+  }
+}
+```
+
+Type could be of a corresponding generic type
+
+* Very useful to invoke constructors which need specific generic type information
+* For example, if the `Id` class stores the class for which the Id is being created
+
+#### InstanceCreator for a Parameterized Type
+
+Sometimes the type that you are trying to instantiate is a parameterized type. Generally, this is not a problem since the actual instance is of raw type. Here is an example:
+
+```java
+class MyList<T> extends ArrayList<T> {
+}
+
+class MyListInstanceCreator implements InstanceCreator<MyList<?>> {
+  @SuppressWarnings("unchecked")
+  public MyList<?> createInstance(Type type) {
+    // No need to use a parameterized list since the actual instance will have the raw type anyway.
+    return new MyList();
+  }
+}
+```
+
+However, sometimes you do need to create instance based on the actual parameterized type. In this case, you can use the type parameter being passed to the `createInstance` method. Here is an example:
+
+```java
+public class Id<T> {
+  private final Class<T> classOfId;
+  private final long value;
+  public Id(Class<T> classOfId, long value) {
+    this.classOfId = classOfId;
+    this.value = value;
+  }
+}
+
+class IdInstanceCreator implements InstanceCreator<Id<?>> {
+  public Id<?> createInstance(Type type) {
+    Type[] typeParameters = ((ParameterizedType)type).getActualTypeArguments();
+    Type idType = typeParameters[0]; // Id has only one parameterized type T
+    return new Id((Class)idType, 0L);
+  }
+}
+```
+
+In the above example, an instance of the Id class can not be created without actually passing in the actual type for the parameterized type. We solve this problem by using the passed method parameter, `type`. The `type` object in this case is the Java parameterized type representation of `Id<Foo>` where the actual instance should be bound to `Id<Foo>`. Since `Id` class has just one parameterized type parameter, `T`, we use the zeroth element of the type array returned by `getActualTypeArgument()` which will hold `Foo.class` in this case.
+
+### Compact Vs. Pretty Printing for JSON Output Format
+
+The default JSON output that is provided by Gson is a compact JSON format. This means that there will not be any whitespace in the output JSON structure. Therefore, there will be no whitespace between field names and its value, object fields, and objects within arrays in the JSON output. As well, "null" fields will be ignored in the output (NOTE: null values will still be included in collections/arrays of objects). See the [Null Object Support](#null-object-support) section for information on configure Gson to output all null values.
+
+If you would like to use the Pretty Print feature, you must configure your `Gson` instance using the `GsonBuilder`. The `JsonFormatter` is not exposed through our public API, so the client is unable to configure the default print settings/margins for the JSON output. For now, we only provide a default `JsonPrintFormatter` that has default line length of 80 character, 2 character indentation, and 4 character right margin.
+
+The following is an example shows how to configure a `Gson` instance to use the default `JsonPrintFormatter` instead of the `JsonCompactFormatter`:
+
+```java
+Gson gson = new GsonBuilder().setPrettyPrinting().create();
+String jsonOutput = gson.toJson(someObject);
+```
+
+### Null Object Support
+
+The default behaviour that is implemented in Gson is that `null` object fields are ignored. This allows for a more compact output format; however, the client must define a default value for these fields as the JSON format is converted back into its Java form.
+
+Here's how you would configure a `Gson` instance to output null:
+
+```java
+Gson gson = new GsonBuilder().serializeNulls().create();
+```
+
+NOTE: when serializing `null`s with Gson, it will add a `JsonNull` element to the `JsonElement` structure. Therefore, this object can be used in custom serialization/deserialization.
+
+Here's an example:
+
+```java
+public class Foo {
+  private final String s;
+  private final int i;
+
+  public Foo() {
+    this(null, 5);
+  }
+
+  public Foo(String s, int i) {
+    this.s = s;
+    this.i = i;
+  }
+}
+
+Gson gson = new GsonBuilder().serializeNulls().create();
+Foo foo = new Foo();
+String json = gson.toJson(foo);
+System.out.println(json);
+
+json = gson.toJson(null);
+System.out.println(json);
+```
+
+The output is:
+
+```json
+{"s":null,"i":5}
+null
+```
+
+### Versioning Support
+
+Multiple versions of the same object can be maintained by using [@Since](gson/src/main/java/com/google/gson/annotations/Since.java) annotation. This annotation can be used on Classes, Fields and, in a future release, Methods. In order to leverage this feature, you must configure your `Gson` instance to ignore any field/object that is greater than some version number. If no version is set on the `Gson` instance then it will serialize and deserialize all fields and classes regardless of the version.
+
+```java
+public class VersionedClass {
+  @Since(1.1) private final String newerField;
+  @Since(1.0) private final String newField;
+  private final String field;
+
+  public VersionedClass() {
+    this.newerField = "newer";
+    this.newField = "new";
+    this.field = "old";
+  }
+}
+
+VersionedClass versionedObject = new VersionedClass();
+Gson gson = new GsonBuilder().setVersion(1.0).create();
+String jsonOutput = gson.toJson(versionedObject);
+System.out.println(jsonOutput);
+System.out.println();
+
+gson = new Gson();
+jsonOutput = gson.toJson(versionedObject);
+System.out.println(jsonOutput);
+```
+
+The output is:
+
+```json
+{"newField":"new","field":"old"}
+
+{"newerField":"newer","newField":"new","field":"old"}
+```
+
+### Excluding Fields From Serialization and Deserialization
+
+Gson supports numerous mechanisms for excluding top-level classes, fields and field types. Below are pluggable mechanisms that allow field and class exclusion. If none of the below mechanisms satisfy your needs then you can always use [custom serializers and deserializers](#custom-serialization-and-deserialization).
+
+#### Java Modifier Exclusion
+
+By default, if you mark a field as `transient`, it will be excluded. As well, if a field is marked as `static` then by default it will be excluded. If you want to include some transient fields then you can do the following:
+
+```java
+import java.lang.reflect.Modifier;
+Gson gson = new GsonBuilder()
+    .excludeFieldsWithModifiers(Modifier.STATIC)
+    .create();
+```
+
+NOTE: you can give any number of the `Modifier` constants to the `excludeFieldsWithModifiers` method. For example:
+
+```java
+Gson gson = new GsonBuilder()
+    .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE)
+    .create();
+```
+
+#### Gson's `@Expose`
+
+This feature provides a way where you can mark certain fields of your objects to be excluded for consideration for serialization and deserialization to JSON. To use this annotation, you must create Gson by using `new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()`. The Gson instance created will exclude all fields in a class that are not marked with `@Expose` annotation.
+
+#### User Defined Exclusion Strategies
+
+If the above mechanisms for excluding fields and class type do not work for you then you can always write your own exclusion strategy and plug it into Gson. See the [`ExclusionStrategy`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ExclusionStrategy.html) JavaDoc for more information.
+
+The following example shows how to exclude fields marked with a specific `@Foo` annotation and excludes top-level types (or declared field type) of class `String`.
+
+```java
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD})
+public @interface Foo {
+  // Field tag only annotation
+}
+
+public class SampleObjectForTest {
+  @Foo private final int annotatedField;
+  private final String stringField;
+  private final long longField;
+
+  public SampleObjectForTest() {
+    annotatedField = 5;
+    stringField = "someDefaultValue";
+    longField = 1234;
+  }
+}
+
+public class MyExclusionStrategy implements ExclusionStrategy {
+  private final Class<?> typeToSkip;
+
+  private MyExclusionStrategy(Class<?> typeToSkip) {
+    this.typeToSkip = typeToSkip;
+  }
+
+  public boolean shouldSkipClass(Class<?> clazz) {
+    return (clazz == typeToSkip);
+  }
+
+  public boolean shouldSkipField(FieldAttributes f) {
+    return f.getAnnotation(Foo.class) != null;
+  }
+}
+
+public static void main(String[] args) {
+  Gson gson = new GsonBuilder()
+      .setExclusionStrategies(new MyExclusionStrategy(String.class))
+      .serializeNulls()
+      .create();
+  SampleObjectForTest src = new SampleObjectForTest();
+  String json = gson.toJson(src);
+  System.out.println(json);
+}
+```
+
+The output is:
+
+```json
+{"longField":1234}
+```
+
+### JSON Field Naming Support
+
+Gson supports some pre-defined field naming policies to convert the standard Java field names (i.e., camel cased names starting with lower case --- `sampleFieldNameInJava`) to a JSON field name (i.e., `sample_field_name_in_java` or `SampleFieldNameInJava`). See the [FieldNamingPolicy](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/FieldNamingPolicy.html) class for information on the pre-defined naming policies.
+
+It also has an annotation based strategy to allows clients to define custom names on a per field basis. Note, that the annotation based strategy has field name validation which will raise "Runtime" exceptions if an invalid field name is provided as the annotation value.
+
+The following is an example of how to use both Gson naming policy features:
+
+```java
+private class SomeObject {
+  @SerializedName("custom_naming") private final String someField;
+  private final String someOtherField;
+
+  public SomeObject(String a, String b) {
+    this.someField = a;
+    this.someOtherField = b;
+  }
+}
+
+SomeObject someObject = new SomeObject("first", "second");
+Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
+String jsonRepresentation = gson.toJson(someObject);
+System.out.println(jsonRepresentation);
+```
+
+The output is:
+
+```json
+{"custom_naming":"first","SomeOtherField":"second"}
+```
+
+If you have a need for custom naming policy ([see this discussion](https://groups.google.com/group/google-gson/browse_thread/thread/cb441a2d717f6892)), you can use the [@SerializedName](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation.
+
+### Sharing State Across Custom Serializers and Deserializers
+
+Sometimes you need to share state across custom serializers/deserializers ([see this discussion](https://groups.google.com/group/google-gson/browse_thread/thread/2850010691ea09fb)). You can use the following three strategies to accomplish this:
+
+1. Store shared state in static fields
+2. Declare the serializer/deserializer as inner classes of a parent type, and use the instance fields of parent type to store shared state
+3. Use Java `ThreadLocal`
+
+1 and 2 are not thread-safe options, but 3 is.
+
+### Streaming
+
+In addition Gson's object model and data binding, you can use Gson to read from and write to a [stream](https://sites.google.com/site/gson/streaming). You can also combine streaming and object model access to get the best of both approaches.
+
+## Issues in Designing Gson
+
+See the [Gson design document](GsonDesignDocument.md "Gson design document") for a discussion of issues we faced while designing Gson. It also includes a comparison of Gson with other Java libraries that can be used for JSON conversion.
+
+## Future Enhancements to Gson
+
+For the latest list of proposed enhancements or if you'd like to suggest new ones, see the [Issues section](https://github.com/google/gson/issues) under the project website.
diff --git a/gson/examples/android-proguard-example/AndroidManifest.xml b/gson/examples/android-proguard-example/AndroidManifest.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7e9b1d8b0ff54d6aa9efd2d44baf3507f6970604
--- /dev/null
+++ b/gson/examples/android-proguard-example/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.google.gson.examples.android"
+  android:versionCode="1"
+  android:versionName="1.0">
+  <uses-sdk android:minSdkVersion="3"/>
+  <application android:icon="@drawable/icon" android:label="@string/app_name">
+    <activity android:name=".GsonProguardExampleActivity" 
+              android:label="@string/app_name" 
+              android:exported="true" 
+              android:icon="@drawable/icon"
+              android:configChanges="keyboardHidden|orientation" 
+              android:enabled="true">
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+    </activity>
+  </application>
+  <uses-permission android:name="android.permission.INTERNET" />
+</manifest> 
diff --git a/gson/examples/android-proguard-example/README.md b/gson/examples/android-proguard-example/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..902960fdfa17ed347e6c4fc9d1fa83a29bee05d6
--- /dev/null
+++ b/gson/examples/android-proguard-example/README.md
@@ -0,0 +1,20 @@
+# android-proguard-example
+
+Example Android project showing how to properly configure [ProGuard](https://www.guardsquare.com/proguard).
+ProGuard is a tool for 'shrinking' and obfuscating compiled classes. It can rename methods and fields,
+or remove them if they appear to be unused. This can cause issues for Gson which uses Java reflection to
+access the fields of a class. It is necessary to configure ProGuard to make sure that Gson works correctly.
+
+Also have a look at the [ProGuard manual](https://www.guardsquare.com/manual/configuration/usage#keepoverview)
+and the [ProGuard Gson examples](https://www.guardsquare.com/manual/configuration/examples#gson) for more
+details on how ProGuard can be configured.
+
+The R8 code shrinker uses the same rule format as ProGuard, but there are differences between these two
+tools. Have a look at R8's Compatibility FAQ, and especially at the [Gson section](https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#gson).
+
+Note that the latest Gson versions (> 2.10.1) apply some of the rules shown in `proguard.cfg` automatically by default,
+see the file [`gson/META-INF/proguard/gson.pro`](/gson/src/main/resources/META-INF/proguard/gson.pro) for
+the Gson version you are using. In general if your classes are top-level classes or are `static`, have a no-args constructor and their fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional ProGuard or R8 configuration.
+
+An alternative to writing custom keep rules for your classes in the ProGuard configuration can be to use
+Android's [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep).
diff --git a/gson/examples/android-proguard-example/default.properties b/gson/examples/android-proguard-example/default.properties
new file mode 100644
index 0000000000000000000000000000000000000000..7d4fed0b2d92a8cbe0fafa31d3108d2047efd50c
--- /dev/null
+++ b/gson/examples/android-proguard-example/default.properties
@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "build.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-3
+proguard.config=proguard.cfg
\ No newline at end of file
diff --git a/gson/examples/android-proguard-example/proguard.cfg b/gson/examples/android-proguard-example/proguard.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..95f31ec6d631d645aace5717a49f7a39a97a8bca
--- /dev/null
+++ b/gson/examples/android-proguard-example/proguard.cfg
@@ -0,0 +1,32 @@
+##---------------Begin: proguard configuration for Gson  ----------
+# Gson uses generic type information stored in a class file when working with fields. Proguard
+# removes such information by default, so configure it to keep all of it.
+-keepattributes Signature
+
+# For using GSON @Expose annotation
+-keepattributes *Annotation*
+
+# Gson specific classes
+-dontwarn sun.misc.**
+#-keep class com.google.gson.stream.** { *; }
+
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.google.gson.examples.android.model.** { <fields>; }
+
+# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
+# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
+-keep class * extends com.google.gson.TypeAdapter
+-keep class * implements com.google.gson.TypeAdapterFactory
+-keep class * implements com.google.gson.JsonSerializer
+-keep class * implements com.google.gson.JsonDeserializer
+
+# Prevent R8 from leaving Data object members always null
+-keepclassmembers,allowobfuscation class * {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
+
+# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
+-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
+-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
+
+##---------------End: proguard configuration for Gson  ----------
diff --git a/gson/examples/android-proguard-example/res/drawable/icon.png b/gson/examples/android-proguard-example/res/drawable/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..a07c69fa5a0f4da5d5efe96eea12a543154dbab6
Binary files /dev/null and b/gson/examples/android-proguard-example/res/drawable/icon.png differ
diff --git a/gson/examples/android-proguard-example/res/layout/main.xml b/gson/examples/android-proguard-example/res/layout/main.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0ac46e68437ac8235a2b8cc43d9e890627e61d32
--- /dev/null
+++ b/gson/examples/android-proguard-example/res/layout/main.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<LinearLayout
+  xmlns:android="http://schemas.android.com/apk/res/android" 
+  android:orientation="vertical"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent">
+  
+  <TextView android:id="@+id/tv"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content" />
+</LinearLayout>
\ No newline at end of file
diff --git a/gson/examples/android-proguard-example/res/values/strings.xml b/gson/examples/android-proguard-example/res/values/strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ba3be81e86d77d628441fb279674480d7ff33c7e
--- /dev/null
+++ b/gson/examples/android-proguard-example/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <string name="app_name">Gson Proguard Example</string>
+</resources>
+
diff --git a/gson/examples/android-proguard-example/src/com/google/gson/examples/android/GsonProguardExampleActivity.java b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/GsonProguardExampleActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..851a905143c20d184a1749739d439b6dabdb3919
--- /dev/null
+++ b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/GsonProguardExampleActivity.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 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.examples.android;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import com.google.gson.Gson;
+import com.google.gson.examples.android.model.Cart;
+import com.google.gson.examples.android.model.LineItem;
+
+/**
+ * Activity class illustrating how to use proguard with Gson
+ *
+ * @author Inderjeet Singh
+ */
+public class GsonProguardExampleActivity extends Activity {
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+    setContentView(R.layout.main);
+    TextView tv = (TextView) findViewById(R.id.tv);
+    Gson gson = new Gson();
+    Cart cart = buildCart();
+    StringBuilder sb = new StringBuilder();
+    sb.append("Gson.toJson() example: \n");
+    sb.append("  Cart Object: ").append(cart).append('\n');
+    sb.append("  Cart JSON: ").append(gson.toJson(cart)).append('\n');
+    sb.append("\n\nGson.fromJson() example: \n");
+    String json = "{buyer:'Happy Camper',creditCard:'4111-1111-1111-1111',"
+      + "lineItems:[{name:'nails',priceInMicros:100000,quantity:100,currencyCode:'USD'}]}";
+    sb.append("Cart JSON: ").append(json).append('\n');
+    sb.append("Cart Object: ").append(gson.fromJson(json, Cart.class)).append('\n');
+    tv.setText(sb.toString());
+    tv.invalidate();
+  }
+
+  private Cart buildCart() {
+    List<LineItem> lineItems = new ArrayList<>();
+    lineItems.add(new LineItem("hammer", 1, 12000000, "USD"));
+    return new Cart(lineItems, "Happy Buyer", "4111-1111-1111-1111");
+  }
+}
diff --git a/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/Cart.java b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/Cart.java
new file mode 100644
index 0000000000000000000000000000000000000000..7582036ebc2d9c2e052d53019a29e18ea46d024e
--- /dev/null
+++ b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/Cart.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2011 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.examples.android.model;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * A model object representing a cart that can be posted to an order-processing server
+ * 
+ * @author Inderjeet Singh
+ */
+public class Cart {
+  public final List<LineItem> lineItems;
+
+  @SerializedName("buyer")
+  private final String buyerName;
+
+  private final String creditCard;
+
+  public Cart(List<LineItem> lineItems, String buyerName, String creditCard) {
+    this.lineItems = lineItems;
+    this.buyerName = buyerName;
+    this.creditCard = creditCard;
+  }
+
+  public List<LineItem> getLineItems() {
+    return lineItems;
+  }
+
+  public String getBuyerName() {
+    return buyerName;
+  }
+
+  public String getCreditCard() {
+    return creditCard;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder itemsText = new StringBuilder();
+    boolean first = true;
+    if (lineItems != null) {
+      try {
+        Class<?> fieldType = Cart.class.getField("lineItems").getType();
+        System.out.println("LineItems CLASS: " + getSimpleTypeName(fieldType));
+      } catch (SecurityException e) {
+      } catch (NoSuchFieldException e) {
+      }
+      for (LineItem item : lineItems) {
+        if (first) {
+          first = false;
+        } else {
+          itemsText.append("; ");
+        }
+        itemsText.append(item);
+      }
+    }
+    return "[BUYER: " + buyerName + "; CC: " + creditCard + "; "
+    + "LINE_ITEMS: " + itemsText.toString() + "]";
+  }
+
+  @SuppressWarnings("unchecked")
+  public static String getSimpleTypeName(Type type) {
+    if (type == null) {
+      return "null";
+    }
+    if (type instanceof Class) {
+      return ((Class)type).getSimpleName();
+    } else if (type instanceof ParameterizedType) {
+      ParameterizedType pType = (ParameterizedType) type;
+      StringBuilder sb = new StringBuilder(getSimpleTypeName(pType.getRawType()));
+      sb.append('<');
+      boolean first = true;
+      for (Type argumentType : pType.getActualTypeArguments()) {
+        if (first) {
+          first = false;
+        } else {
+          sb.append(',');
+        }
+        sb.append(getSimpleTypeName(argumentType));
+      }
+      sb.append('>');
+      return sb.toString();
+    } else if (type instanceof WildcardType) {
+      return "?";
+    }
+    return type.toString();
+  }
+
+}
diff --git a/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/LineItem.java b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/LineItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..1273ec971fca24c3f7adc4efb7148b2e4c2bc193
--- /dev/null
+++ b/gson/examples/android-proguard-example/src/com/google/gson/examples/android/model/LineItem.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2011 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.examples.android.model;
+
+/**
+ * A line item in a cart. This is not a rest resource, just a dependent object
+ *
+ * @author Inderjeet Singh
+ */
+public class LineItem {
+  private final String name;
+  private final int quantity;
+  private final long priceInMicros;
+  private final String currencyCode;
+
+  public LineItem(String name, int quantity, long priceInMicros, String currencyCode) {
+    this.name = name;
+    this.quantity = quantity;
+    this.priceInMicros = priceInMicros;
+    this.currencyCode = currencyCode;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public int getQuantity() {
+    return quantity;
+  }
+
+  public long getPriceInMicros() {
+    return priceInMicros;
+  }
+
+  public String getCurrencyCode() {
+    return currencyCode;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("(item: %s, qty: %s, price: %.2f %s)",
+        name, quantity, priceInMicros / 1000000d, currencyCode);
+  }
+}
diff --git a/gson/extras/README.md b/gson/extras/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..41447726e2deaff9c14e573a75dbf7ce49f110d1
--- /dev/null
+++ b/gson/extras/README.md
@@ -0,0 +1,6 @@
+# extras
+
+This Maven module contains the source code for supplementary Gson features which
+are not included by default.
+
+The artifacts created by this module are currently not deployed to Maven Central.
diff --git a/gson/extras/pom.xml b/gson/extras/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c792292ebc3925fc3cfe848b2d64afdd0317ac2a
--- /dev/null
+++ b/gson/extras/pom.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2011 Google LLC
+
+  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.
+-->
+<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>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gson-extras</artifactId>
+  <inceptionYear>2008</inceptionYear>
+  <name>Gson Extras</name>
+  <description>Google Gson grab bag of utilities, type adapters, etc.</description>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+  </properties>
+
+  <licenses>
+    <license>
+      <name>Apache-2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <organization>
+    <name>Google, Inc.</name>
+    <url>https://www.google.com</url>
+  </organization>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${project.parent.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.annotation</groupId>
+      <artifactId>jsr250-api</artifactId>
+      <version>1.0</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <configuration>
+            <!-- Currently not deployed -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <developers>
+    <developer>
+      <name>Inderjeet Singh</name>
+    </developer>
+    <developer>
+      <name>Joel Leitch</name>
+      <organization>Google Inc.</organization>
+    </developer>
+    <developer>
+      <name>Jesse Wilson</name>
+      <organization>Square Inc.</organization>
+    </developer>
+  </developers>
+</project>
diff --git a/gson/extras/src/main/java/com/google/gson/extras/examples/rawcollections/RawCollectionsExample.java b/gson/extras/src/main/java/com/google/gson/extras/examples/rawcollections/RawCollectionsExample.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d9cffa86e1756504cd0de1e18495253879d14f6
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/extras/examples/rawcollections/RawCollectionsExample.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 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.extras.examples.rawcollections;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonParser;
+import java.util.ArrayList;
+import java.util.Collection;
+
+@SuppressWarnings({"PrivateConstructorForUtilityClass", "SystemOut"})
+public class RawCollectionsExample {
+  static class Event {
+    private String name;
+    private String source;
+
+    private Event(String name, String source) {
+      this.name = name;
+      this.source = source;
+    }
+
+    @Override
+    public String toString() {
+      return String.format("(name=%s, source=%s)", name, source);
+    }
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  public static void main(String[] args) {
+    Gson gson = new Gson();
+    Collection collection = new ArrayList();
+    collection.add("hello");
+    collection.add(5);
+    collection.add(new Event("GREETINGS", "guest"));
+    String json = gson.toJson(collection);
+    System.out.println("Using Gson.toJson() on a raw collection: " + json);
+    JsonArray array = JsonParser.parseString(json).getAsJsonArray();
+    String message = gson.fromJson(array.get(0), String.class);
+    int number = gson.fromJson(array.get(1), int.class);
+    Event event = gson.fromJson(array.get(2), Event.class);
+    System.out.printf("Using Gson.fromJson() to get: %s, %d, %s", message, number, event);
+  }
+}
diff --git a/gson/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/gson/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..dba381531e1b5d3a3b257c6839580cd7e0cc78d0
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2011 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.graph;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonElement;
+import com.google.gson.ReflectionAccessFilter;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.ObjectConstructor;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Queue;
+
+/** Writes a graph of objects as a list of named nodes. */
+// TODO: proper documentation
+public final class GraphAdapterBuilder {
+  private final Map<Type, InstanceCreator<?>> instanceCreators;
+  private final ConstructorConstructor constructorConstructor;
+
+  public GraphAdapterBuilder() {
+    this.instanceCreators = new HashMap<>();
+    this.constructorConstructor =
+        new ConstructorConstructor(
+            instanceCreators, true, Collections.<ReflectionAccessFilter>emptyList());
+  }
+
+  public GraphAdapterBuilder addType(Type type) {
+    final ObjectConstructor<?> objectConstructor = constructorConstructor.get(TypeToken.get(type));
+    InstanceCreator<Object> instanceCreator =
+        new InstanceCreator<Object>() {
+          @Override
+          public Object createInstance(Type type) {
+            return objectConstructor.construct();
+          }
+        };
+    return addType(type, instanceCreator);
+  }
+
+  public GraphAdapterBuilder addType(Type type, InstanceCreator<?> instanceCreator) {
+    if (type == null || instanceCreator == null) {
+      throw new NullPointerException();
+    }
+    instanceCreators.put(type, instanceCreator);
+    return this;
+  }
+
+  public void registerOn(GsonBuilder gsonBuilder) {
+    Factory factory = new Factory(instanceCreators);
+    gsonBuilder.registerTypeAdapterFactory(factory);
+    for (Map.Entry<Type, InstanceCreator<?>> entry : instanceCreators.entrySet()) {
+      gsonBuilder.registerTypeAdapter(entry.getKey(), factory);
+    }
+  }
+
+  static class Factory implements TypeAdapterFactory, InstanceCreator<Object> {
+    private final Map<Type, InstanceCreator<?>> instanceCreators;
+
+    @SuppressWarnings("ThreadLocalUsage")
+    private final ThreadLocal<Graph> graphThreadLocal = new ThreadLocal<>();
+
+    Factory(Map<Type, InstanceCreator<?>> instanceCreators) {
+      this.instanceCreators = instanceCreators;
+    }
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+      if (!instanceCreators.containsKey(type.getType())) {
+        return null;
+      }
+
+      final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(this, type);
+      final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
+      return new TypeAdapter<T>() {
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+            return;
+          }
+
+          Graph graph = graphThreadLocal.get();
+          boolean writeEntireGraph = false;
+
+          /*
+           * We have one of two cases:
+           *  1. We've encountered the first known object in this graph. Write
+           *     out the graph, starting with that object.
+           *  2. We've encountered another graph object in the course of #1.
+           *     Just write out this object's name. We'll circle back to writing
+           *     out the object's value as a part of #1.
+           */
+
+          if (graph == null) {
+            writeEntireGraph = true;
+            graph = new Graph(new IdentityHashMap<Object, Element<?>>());
+          }
+
+          @SuppressWarnings("unchecked") // graph.map guarantees consistency between value and T
+          Element<T> element = (Element<T>) graph.map.get(value);
+          if (element == null) {
+            element = new Element<>(value, graph.nextName(), typeAdapter, null);
+            graph.map.put(value, element);
+            graph.queue.add(element);
+          }
+
+          if (writeEntireGraph) {
+            graphThreadLocal.set(graph);
+            try {
+              out.beginObject();
+              Element<?> current;
+              while ((current = graph.queue.poll()) != null) {
+                out.name(current.id);
+                current.write(out);
+              }
+              out.endObject();
+            } finally {
+              graphThreadLocal.remove();
+            }
+          } else {
+            out.value(element.id);
+          }
+        }
+
+        @Override
+        public T read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+
+          /*
+           * Again we have one of two cases:
+           *  1. We've encountered the first known object in this graph. Read
+           *     the entire graph in as a map from names to their JsonElements.
+           *     Then convert the first JsonElement to its Java object.
+           *  2. We've encountered another graph object in the course of #1.
+           *     Read in its name, then deserialize its value from the
+           *     JsonElement in our map. We need to do this lazily because we
+           *     don't know which TypeAdapter to use until a value is
+           *     encountered in the wild.
+           */
+
+          String currentName = null;
+          Graph graph = graphThreadLocal.get();
+          boolean readEntireGraph = false;
+
+          if (graph == null) {
+            graph = new Graph(new HashMap<Object, Element<?>>());
+            readEntireGraph = true;
+
+            // read the entire tree into memory
+            in.beginObject();
+            while (in.hasNext()) {
+              String name = in.nextName();
+              if (currentName == null) {
+                currentName = name;
+              }
+              JsonElement element = elementAdapter.read(in);
+              graph.map.put(name, new Element<>(null, name, typeAdapter, element));
+            }
+            in.endObject();
+          } else {
+            currentName = in.nextString();
+          }
+
+          if (readEntireGraph) {
+            graphThreadLocal.set(graph);
+          }
+          try {
+            @SuppressWarnings("unchecked") // graph.map guarantees consistency between value and T
+            Element<T> element = (Element<T>) graph.map.get(currentName);
+            // now that we know the typeAdapter for this name, go from JsonElement to 'T'
+            if (element.value == null) {
+              element.typeAdapter = typeAdapter;
+              element.read(graph);
+            }
+            return element.value;
+          } finally {
+            if (readEntireGraph) {
+              graphThreadLocal.remove();
+            }
+          }
+        }
+      };
+    }
+
+    /**
+     * Hook for the graph adapter to get a reference to a deserialized value before that value is
+     * fully populated. This is useful to deserialize values that directly or indirectly reference
+     * themselves: we can hand out an instance before read() returns.
+     *
+     * <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.
+     */
+    @Override
+    public Object createInstance(Type type) {
+      Graph graph = graphThreadLocal.get();
+      if (graph == null || graph.nextCreate == null) {
+        throw new IllegalStateException("Unexpected call to createInstance() for " + type);
+      }
+      InstanceCreator<?> creator = instanceCreators.get(type);
+      Object result = creator.createInstance(type);
+      graph.nextCreate.value = result;
+      graph.nextCreate = null;
+      return result;
+    }
+  }
+
+  static class Graph {
+    /**
+     * The graph elements. On serialization keys are objects (using an identity hash map) and on
+     * deserialization keys are the string names (using a standard hash map).
+     */
+    private final Map<Object, Element<?>> map;
+
+    /** The queue of elements to write during serialization. Unused during deserialization. */
+    private final Queue<Element<?>> queue = new ArrayDeque<>();
+
+    /**
+     * 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<Object> nextCreate;
+
+    private Graph(Map<Object, Element<?>> map) {
+      this.map = map;
+    }
+
+    /** Returns a unique name for an element to be inserted into the graph. */
+    public String nextName() {
+      return "0x" + Integer.toHexString(map.size() + 1);
+    }
+  }
+
+  /** An element of the graph during serialization or deserialization. */
+  static class Element<T> {
+    /** This element's name in the top level graph object. */
+    private final String id;
+
+    /** The value if known. During deserialization this is lazily populated. */
+    private T value;
+
+    /** This element's type adapter if known. During deserialization this is lazily populated. */
+    private TypeAdapter<T> typeAdapter;
+
+    /** The element to deserialize. Unused in serialization. */
+    private final JsonElement element;
+
+    Element(T value, String id, TypeAdapter<T> typeAdapter, JsonElement element) {
+      this.value = value;
+      this.id = id;
+      this.typeAdapter = typeAdapter;
+      this.element = element;
+    }
+
+    void write(JsonWriter out) throws IOException {
+      typeAdapter.write(out, value);
+    }
+
+    @SuppressWarnings("unchecked")
+    void read(Graph graph) {
+      if (graph.nextCreate != null) {
+        throw new IllegalStateException("Unexpected recursive call to read() for " + id);
+      }
+      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/gson/extras/src/main/java/com/google/gson/interceptors/Intercept.java b/gson/extras/src/main/java/com/google/gson/interceptors/Intercept.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d5f69a2c8f07cafad5c842173a5cec74d86ad91
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/interceptors/Intercept.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.interceptors;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Use this annotation to indicate various interceptors for class instances after they have been
+ * processed by Gson. For example, you can use it to validate an instance after it has been
+ * deserialized from Json. Here is an example of how this annotation is used:
+ *
+ * <p>Here is an example of how this annotation is used:
+ *
+ * <pre>
+ * &#64;Intercept(postDeserialize=UserValidator.class)
+ * public class User {
+ *   String name;
+ *   String password;
+ *   String emailAddress;
+ * }
+ *
+ * public class UserValidator implements JsonPostDeserializer&lt;User&gt; {
+ *   public void postDeserialize(User user) {
+ *     // Do some checks on user
+ *     if (user.name == null || user.password == null) {
+ *       throw new JsonParseException("name and password are required fields.");
+ *     }
+ *     if (user.emailAddress == null) {
+ *       emailAddress = "unknown"; // assign a default value.
+ *     }
+ *   }
+ * }
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Intercept {
+
+  /**
+   * Specify the class that provides the methods that should be invoked after an instance has been
+   * deserialized.
+   */
+  @SuppressWarnings("rawtypes")
+  public Class<? extends JsonPostDeserializer> postDeserialize();
+}
diff --git a/gson/extras/src/main/java/com/google/gson/interceptors/InterceptorFactory.java b/gson/extras/src/main/java/com/google/gson/interceptors/InterceptorFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..7868a133528c8b1361b0100e076cf250d536803a
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/interceptors/InterceptorFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 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.interceptors;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+/** A type adapter factory that implements {@code @Intercept}. */
+public final class InterceptorFactory implements TypeAdapterFactory {
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+    Intercept intercept = type.getRawType().getAnnotation(Intercept.class);
+    if (intercept == null) {
+      return null;
+    }
+
+    TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+    return new InterceptorAdapter<>(delegate, intercept);
+  }
+
+  static class InterceptorAdapter<T> extends TypeAdapter<T> {
+    private final TypeAdapter<T> delegate;
+    private final JsonPostDeserializer<T> postDeserializer;
+
+    @SuppressWarnings("unchecked") // ?
+    public InterceptorAdapter(TypeAdapter<T> delegate, Intercept intercept) {
+      try {
+        this.delegate = delegate;
+        this.postDeserializer = intercept.postDeserialize().getDeclaredConstructor().newInstance();
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+      delegate.write(out, value);
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      T result = delegate.read(in);
+      postDeserializer.postDeserialize(result);
+      return result;
+    }
+  }
+}
diff --git a/gson/extras/src/main/java/com/google/gson/interceptors/JsonPostDeserializer.java b/gson/extras/src/main/java/com/google/gson/interceptors/JsonPostDeserializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a8fcf96c9c8b55a46407d36a72440da31ec30e9
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/interceptors/JsonPostDeserializer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2012 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.interceptors;
+
+import com.google.gson.InstanceCreator;
+
+/**
+ * This interface is implemented by a class that wishes to inspect or modify an object after it has
+ * been deserialized. You must define a no-args constructor or register an {@link InstanceCreator}
+ * for such a class.
+ *
+ * @author Inderjeet Singh
+ */
+public interface JsonPostDeserializer<T> {
+
+  /** This method is called by Gson after the object has been deserialized from Json. */
+  public void postDeserialize(T object);
+}
diff --git a/gson/extras/src/main/java/com/google/gson/typeadapters/PostConstructAdapterFactory.java b/gson/extras/src/main/java/com/google/gson/typeadapters/PostConstructAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..924a9089ffdff948cf4a44c13e8808eca52c93a0
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/typeadapters/PostConstructAdapterFactory.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 Gson Authors
+ *
+ * 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.typeadapters;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+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.InvocationTargetException;
+import java.lang.reflect.Method;
+import javax.annotation.PostConstruct;
+
+public class PostConstructAdapterFactory implements TypeAdapterFactory {
+  // copied from https://gist.github.com/swankjesse/20df26adaf639ed7fd160f145a0b661a
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+    for (Class<?> t = type.getRawType();
+        (t != Object.class) && (t.getSuperclass() != null);
+        t = t.getSuperclass()) {
+      for (Method m : t.getDeclaredMethods()) {
+        if (m.isAnnotationPresent(PostConstruct.class)) {
+          m.setAccessible(true);
+          TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+          return new PostConstructAdapter<>(delegate, m);
+        }
+      }
+    }
+    return null;
+  }
+
+  static final class PostConstructAdapter<T> extends TypeAdapter<T> {
+    private final TypeAdapter<T> delegate;
+    private final Method method;
+
+    public PostConstructAdapter(TypeAdapter<T> delegate, Method method) {
+      this.delegate = delegate;
+      this.method = method;
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      T result = delegate.read(in);
+      if (result != null) {
+        try {
+          method.invoke(result);
+        } catch (IllegalAccessException e) {
+          throw new AssertionError(e);
+        } catch (InvocationTargetException e) {
+          if (e.getCause() instanceof RuntimeException) {
+            throw (RuntimeException) e.getCause();
+          }
+          throw new RuntimeException(e.getCause());
+        }
+      }
+      return result;
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+      delegate.write(out, value);
+    }
+  }
+}
diff --git a/gson/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/gson/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa70755abd76eefcf141082b95f26d6304ec9816
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2011 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.typeadapters;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Adapts values whose runtime type may differ from their declaration type. This is necessary when a
+ * field's type is not the same type that GSON should create when deserializing that field. For
+ * example, consider these types:
+ *
+ * <pre>{@code
+ * abstract class Shape {
+ *   int x;
+ *   int y;
+ * }
+ * class Circle extends Shape {
+ *   int radius;
+ * }
+ * class Rectangle extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Diamond extends Shape {
+ *   int width;
+ *   int height;
+ * }
+ * class Drawing {
+ *   Shape bottomShape;
+ *   Shape topShape;
+ * }
+ * }</pre>
+ *
+ * <p>Without additional type information, the serialized JSON is ambiguous. Is the bottom shape in
+ * this drawing a rectangle or a diamond?
+ *
+ * <pre>{@code
+ * {
+ *   "bottomShape": {
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }</pre>
+ *
+ * This class addresses this problem by adding type information to the serialized JSON and honoring
+ * that type information when the JSON is deserialized:
+ *
+ * <pre>{@code
+ * {
+ *   "bottomShape": {
+ *     "type": "Diamond",
+ *     "width": 10,
+ *     "height": 5,
+ *     "x": 0,
+ *     "y": 0
+ *   },
+ *   "topShape": {
+ *     "type": "Circle",
+ *     "radius": 2,
+ *     "x": 4,
+ *     "y": 1
+ *   }
+ * }
+ * }</pre>
+ *
+ * Both the type field name ({@code "type"}) and the type labels ({@code "Rectangle"}) are
+ * configurable.
+ *
+ * <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
+ * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
+ *     = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }</pre>
+ *
+ * Next register all of your subtypes. Every subtype must be explicitly registered. This protects
+ * your application from injection attacks. If you don't supply an explicit type label, the type's
+ * simple name will be used.
+ *
+ * <pre>{@code
+ * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }</pre>
+ *
+ * Finally, register the type adapter factory in your application's GSON builder:
+ *
+ * <pre>{@code
+ * Gson gson = new GsonBuilder()
+ *     .registerTypeAdapterFactory(shapeAdapterFactory)
+ *     .create();
+ * }</pre>
+ *
+ * Like {@code GsonBuilder}, this API supports chaining:
+ *
+ * <pre>{@code
+ * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *     .registerSubtype(Rectangle.class)
+ *     .registerSubtype(Circle.class)
+ *     .registerSubtype(Diamond.class);
+ * }</pre>
+ *
+ * <h2>Serialization and deserialization</h2>
+ *
+ * In order to serialize and deserialize a polymorphic object, you must specify the base type
+ * explicitly.
+ *
+ * <pre>{@code
+ * Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+ * }</pre>
+ *
+ * And then:
+ *
+ * <pre>{@code
+ * Shape shape = gson.fromJson(json, Shape.class);
+ * }</pre>
+ */
+public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
+  private final Class<?> baseType;
+  private final String typeFieldName;
+  private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
+  private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
+  private final boolean maintainType;
+  private boolean recognizeSubtypes;
+
+  private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
+    if (typeFieldName == null || baseType == null) {
+      throw new NullPointerException();
+    }
+    this.baseType = baseType;
+    this.typeFieldName = typeFieldName;
+    this.maintainType = 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.
+   *
+   * @param maintainType true if the type field should be included in deserialized objects
+   */
+  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.
+   */
+  public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
+    return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
+  }
+
+  /**
+   * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type field
+   * name.
+   */
+  public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
+    return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
+  }
+
+  /**
+   * Ensures that this factory will handle not just the given {@code baseType}, but any subtype of
+   * that type.
+   */
+  @CanIgnoreReturnValue
+  public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
+    this.recognizeSubtypes = true;
+    return this;
+  }
+
+  /**
+   * Registers {@code type} identified by {@code label}. Labels are case sensitive.
+   *
+   * @throws IllegalArgumentException if either {@code type} or {@code label} have already been
+   *     registered on this type adapter.
+   */
+  @CanIgnoreReturnValue
+  public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
+    if (type == null || label == null) {
+      throw new NullPointerException();
+    }
+    if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
+      throw new IllegalArgumentException("types and labels must be unique");
+    }
+    labelToSubtype.put(label, type);
+    subtypeToLabel.put(type, label);
+    return this;
+  }
+
+  /**
+   * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
+   * case sensitive.
+   *
+   * @throws IllegalArgumentException if either {@code type} or its simple name have already been
+   *     registered on this type adapter.
+   */
+  @CanIgnoreReturnValue
+  public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
+    return registerSubtype(type, type.getSimpleName());
+  }
+
+  @Override
+  public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
+    if (type == null) {
+      return null;
+    }
+    Class<?> rawType = type.getRawType();
+    boolean handle =
+        recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
+    if (!handle) {
+      return null;
+    }
+
+    final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
+    final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
+    final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
+    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
+      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
+      labelToDelegate.put(entry.getKey(), delegate);
+      subtypeToDelegate.put(entry.getValue(), delegate);
+    }
+
+    return new TypeAdapter<R>() {
+      @Override
+      public R read(JsonReader in) throws IOException {
+        JsonElement jsonElement = jsonElementAdapter.read(in);
+        JsonElement labelJsonElement;
+        if (maintainType) {
+          labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
+        } else {
+          labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
+        }
+
+        if (labelJsonElement == null) {
+          throw new JsonParseException(
+              "cannot deserialize "
+                  + baseType
+                  + " because it does not define a field named "
+                  + typeFieldName);
+        }
+        String label = labelJsonElement.getAsString();
+        @SuppressWarnings("unchecked") // registration requires that subtype extends T
+        TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
+        if (delegate == null) {
+          throw new JsonParseException(
+              "cannot deserialize "
+                  + baseType
+                  + " subtype named "
+                  + label
+                  + "; did you forget to register a subtype?");
+        }
+        return delegate.fromJsonTree(jsonElement);
+      }
+
+      @Override
+      public void write(JsonWriter out, R value) throws IOException {
+        Class<?> srcType = value.getClass();
+        String label = subtypeToLabel.get(srcType);
+        @SuppressWarnings("unchecked") // registration requires that subtype extends T
+        TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
+        if (delegate == null) {
+          throw new JsonParseException(
+              "cannot serialize " + srcType.getName() + "; did you forget to register a subtype?");
+        }
+        JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
+
+        if (maintainType) {
+          jsonElementAdapter.write(out, jsonObject);
+          return;
+        }
+
+        JsonObject clone = new JsonObject();
+
+        if (jsonObject.has(typeFieldName)) {
+          throw new JsonParseException(
+              "cannot serialize "
+                  + srcType.getName()
+                  + " 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());
+        }
+        jsonElementAdapter.write(out, clone);
+      }
+    }.nullSafe();
+  }
+}
diff --git a/gson/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java b/gson/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b2356c1b2a5560e6809d00a8466185d4f88da78
--- /dev/null
+++ b/gson/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2011 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.typeadapters;
+
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public final class UtcDateTypeAdapter extends TypeAdapter<Date> {
+  private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
+
+  @Override
+  public void write(JsonWriter out, Date date) throws IOException {
+    if (date == null) {
+      out.nullValue();
+    } else {
+      String value = format(date, true, UTC_TIME_ZONE);
+      out.value(value);
+    }
+  }
+
+  @Override
+  public Date read(JsonReader in) throws IOException {
+    try {
+      switch (in.peek()) {
+        case NULL:
+          in.nextNull();
+          return null;
+        default:
+          String date = in.nextString();
+          // Instead of using iso8601Format.parse(value), we use Jackson's date parsing
+          // This is because Android doesn't support XXX because it is JDK 1.6
+          return parse(date, new ParsePosition(0));
+      }
+    } catch (ParseException e) {
+      throw new JsonParseException(e);
+    }
+  }
+
+  // Date parsing code from Jackson databind ISO8601Utils.java
+  // https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
+  private static final String GMT_ID = "GMT";
+
+  /**
+   * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   *
+   * @param date the date to format
+   * @param millis true to include millis precision otherwise false
+   * @param tz timezone to use for the formatting (GMT will produce 'Z')
+   * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   */
+  private static String format(Date date, boolean millis, TimeZone tz) {
+    Calendar calendar = new GregorianCalendar(tz, Locale.US);
+    calendar.setTime(date);
+
+    // estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
+    int capacity = "yyyy-MM-ddThh:mm:ss".length();
+    capacity += millis ? ".sss".length() : 0;
+    capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length();
+    StringBuilder formatted = new StringBuilder(capacity);
+
+    padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
+    formatted.append('-');
+    padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
+    formatted.append('-');
+    padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
+    formatted.append('T');
+    padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
+    formatted.append(':');
+    padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
+    formatted.append(':');
+    padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
+    if (millis) {
+      formatted.append('.');
+      padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
+    }
+
+    int offset = tz.getOffset(calendar.getTimeInMillis());
+    if (offset != 0) {
+      int hours = Math.abs((offset / (60 * 1000)) / 60);
+      int minutes = Math.abs((offset / (60 * 1000)) % 60);
+      formatted.append(offset < 0 ? '-' : '+');
+      padInt(formatted, hours, "hh".length());
+      formatted.append(':');
+      padInt(formatted, minutes, "mm".length());
+    } else {
+      formatted.append('Z');
+    }
+
+    return formatted.toString();
+  }
+
+  /**
+   * Zero pad a number to a specified length
+   *
+   * @param buffer buffer to use for padding
+   * @param value the integer value to pad if necessary.
+   * @param length the length of the string we should zero pad
+   */
+  private static void padInt(StringBuilder buffer, int value, int length) {
+    String strValue = Integer.toString(value);
+    for (int i = length - strValue.length(); i > 0; i--) {
+      buffer.append('0');
+    }
+    buffer.append(strValue);
+  }
+
+  /**
+   * Parse a date from ISO-8601 formatted string. It expects a format
+   * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
+   *
+   * @param date ISO string to parse in the appropriate format.
+   * @param pos The position to start parsing from, updated to where parsing stopped.
+   * @return the parsed date
+   * @throws ParseException if the date is not in the appropriate format
+   */
+  private static Date parse(String date, ParsePosition pos) throws ParseException {
+    Exception fail = null;
+    try {
+      int offset = pos.getIndex();
+
+      // extract year
+      int year = parseInt(date, offset, offset += 4);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract month
+      int month = parseInt(date, offset, offset += 2);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract day
+      int day = parseInt(date, offset, offset += 2);
+      // default time value
+      int hour = 0;
+      int minutes = 0;
+      int seconds = 0;
+      // always use 0 otherwise returned date will include millis of current time
+      int milliseconds = 0;
+      if (checkOffset(date, offset, 'T')) {
+
+        // extract hours, minutes, seconds and milliseconds
+        hour = parseInt(date, offset += 1, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+
+        minutes = parseInt(date, offset, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+        // second and milliseconds can be optional
+        if (date.length() > offset) {
+          char c = date.charAt(offset);
+          if (c != 'Z' && c != '+' && c != '-') {
+            seconds = parseInt(date, offset, offset += 2);
+            // milliseconds can be optional in the format
+            if (checkOffset(date, offset, '.')) {
+              milliseconds = parseInt(date, offset += 1, offset += 3);
+            }
+          }
+        }
+      }
+
+      // extract timezone
+      String timezoneId;
+      if (date.length() <= offset) {
+        throw new IllegalArgumentException("No time zone indicator");
+      }
+      char timezoneIndicator = date.charAt(offset);
+      if (timezoneIndicator == '+' || timezoneIndicator == '-') {
+        String timezoneOffset = date.substring(offset);
+        timezoneId = GMT_ID + timezoneOffset;
+        offset += timezoneOffset.length();
+      } else if (timezoneIndicator == 'Z') {
+        timezoneId = GMT_ID;
+        offset += 1;
+      } else {
+        throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator);
+      }
+
+      TimeZone timezone = TimeZone.getTimeZone(timezoneId);
+      if (!timezone.getID().equals(timezoneId)) {
+        throw new IndexOutOfBoundsException();
+      }
+
+      Calendar calendar = new GregorianCalendar(timezone);
+      calendar.setLenient(false);
+      calendar.set(Calendar.YEAR, year);
+      calendar.set(Calendar.MONTH, month - 1);
+      calendar.set(Calendar.DAY_OF_MONTH, day);
+      calendar.set(Calendar.HOUR_OF_DAY, hour);
+      calendar.set(Calendar.MINUTE, minutes);
+      calendar.set(Calendar.SECOND, seconds);
+      calendar.set(Calendar.MILLISECOND, milliseconds);
+
+      pos.setIndex(offset);
+      return calendar.getTime();
+      // If we get a ParseException it'll already have the right message/offset.
+      // Other exception types can convert here.
+    } catch (IndexOutOfBoundsException e) {
+      fail = e;
+    } catch (NumberFormatException e) {
+      fail = e;
+    } catch (IllegalArgumentException e) {
+      fail = e;
+    }
+    String input = (date == null) ? null : ("'" + date + "'");
+    throw new ParseException(
+        "Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
+  }
+
+  /**
+   * Check if the expected character exist at the given offset in the value.
+   *
+   * @param value the string to check at the specified offset
+   * @param offset the offset to look for the expected character
+   * @param expected the expected character
+   * @return true if the expected character exist at the given offset
+   */
+  private static boolean checkOffset(String value, int offset, char expected) {
+    return (offset < value.length()) && (value.charAt(offset) == expected);
+  }
+
+  /**
+   * Parse an integer located between 2 given offsets in a string
+   *
+   * @param value the string to parse
+   * @param beginIndex the start index for the integer in the string
+   * @param endIndex the end index for the integer in the string
+   * @return the int
+   * @throws NumberFormatException if the value is not a number
+   */
+  private static int parseInt(String value, int beginIndex, int endIndex)
+      throws NumberFormatException {
+    if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
+      throw new NumberFormatException(value);
+    }
+    // use same logic as in Integer.parseInt() but less generic we're not supporting negative values
+    int i = beginIndex;
+    int result = 0;
+    int digit;
+    if (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value);
+      }
+      result = -digit;
+    }
+    while (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value);
+      }
+      result *= 10;
+      result -= digit;
+    }
+    return -result;
+  }
+}
diff --git a/gson/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java b/gson/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f4b986f532a4ba650f55581977e57ad0025420e
--- /dev/null
+++ b/gson/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2011 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.graph;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+public final class GraphAdapterBuilderTest {
+  @Test
+  public void testSerialization() {
+    Roshambo rock = new Roshambo("ROCK");
+    Roshambo scissors = new Roshambo("SCISSORS");
+    Roshambo paper = new Roshambo("PAPER");
+    rock.beats = scissors;
+    scissors.beats = paper;
+    paper.beats = rock;
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    assertEquals(
+        "{'0x1':{'name':'ROCK','beats':'0x2'},"
+            + "'0x2':{'name':'SCISSORS','beats':'0x3'},"
+            + "'0x3':{'name':'PAPER','beats':'0x1'}}",
+        gson.toJson(rock).replace('"', '\''));
+  }
+
+  @Test
+  public void testDeserialization() {
+    String json =
+        "{'0x1':{'name':'ROCK','beats':'0x2'},"
+            + "'0x2':{'name':'SCISSORS','beats':'0x3'},"
+            + "'0x3':{'name':'PAPER','beats':'0x1'}}";
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    Roshambo rock = gson.fromJson(json, Roshambo.class);
+    assertEquals("ROCK", rock.name);
+    Roshambo scissors = rock.beats;
+    assertEquals("SCISSORS", scissors.name);
+    Roshambo paper = scissors.beats;
+    assertEquals("PAPER", paper.name);
+    assertSame(rock, paper.beats);
+  }
+
+  @Test
+  public void testDeserializationDirectSelfReference() {
+    String json = "{'0x1':{'name':'SUICIDE','beats':'0x1'}}";
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    Roshambo suicide = gson.fromJson(json, Roshambo.class);
+    assertEquals("SUICIDE", suicide.name);
+    assertSame(suicide, suicide.beats);
+  }
+
+  @Test
+  public void testSerializeListOfLists() {
+    Type listOfListsType = new TypeToken<List<List<?>>>() {}.getType();
+    Type listOfAnyType = new TypeToken<List<?>>() {}.getType();
+
+    List<List<?>> listOfLists = new ArrayList<>();
+    listOfLists.add(listOfLists);
+    listOfLists.add(new ArrayList<>());
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder()
+        .addType(listOfListsType)
+        .addType(listOfAnyType)
+        .registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    String json = gson.toJson(listOfLists, listOfListsType);
+    assertEquals("{'0x1':['0x1','0x2'],'0x2':[]}", json.replace('"', '\''));
+  }
+
+  @Test
+  public void testDeserializeListOfLists() {
+    Type listOfAnyType = new TypeToken<List<?>>() {}.getType();
+    Type listOfListsType = new TypeToken<List<List<?>>>() {}.getType();
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder()
+        .addType(listOfListsType)
+        .addType(listOfAnyType)
+        .registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    List<List<?>> listOfLists = gson.fromJson("{'0x1':['0x1','0x2'],'0x2':[]}", listOfListsType);
+    assertEquals(2, listOfLists.size());
+    assertSame(listOfLists, listOfLists.get(0));
+    assertEquals(Collections.emptyList(), listOfLists.get(1));
+  }
+
+  @Test
+  public void testSerializationWithMultipleTypes() {
+    Company google = new Company("Google");
+    // Employee constructor adds `this` to the given Company object
+    Employee unused1 = new Employee("Jesse", google);
+    Employee unused2 = new Employee("Joel", google);
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder()
+        .addType(Company.class)
+        .addType(Employee.class)
+        .registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    assertEquals(
+        "{'0x1':{'name':'Google','employees':['0x2','0x3']},"
+            + "'0x2':{'name':'Jesse','company':'0x1'},"
+            + "'0x3':{'name':'Joel','company':'0x1'}}",
+        gson.toJson(google).replace('"', '\''));
+  }
+
+  @Test
+  public void testDeserializationWithMultipleTypes() {
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    new GraphAdapterBuilder()
+        .addType(Company.class)
+        .addType(Employee.class)
+        .registerOn(gsonBuilder);
+    Gson gson = gsonBuilder.create();
+
+    String json =
+        "{'0x1':{'name':'Google','employees':['0x2','0x3']},"
+            + "'0x2':{'name':'Jesse','company':'0x1'},"
+            + "'0x3':{'name':'Joel','company':'0x1'}}";
+    Company company = gson.fromJson(json, Company.class);
+    assertEquals("Google", company.name);
+    Employee jesse = company.employees.get(0);
+    assertEquals("Jesse", jesse.name);
+    assertEquals(company, jesse.company);
+    Employee joel = company.employees.get(1);
+    assertEquals("Joel", joel.name);
+    assertEquals(company, joel.company);
+  }
+
+  static class Roshambo {
+    String name;
+    Roshambo beats;
+
+    Roshambo(String name) {
+      this.name = name;
+    }
+  }
+
+  static class Employee {
+    final String name;
+    final Company company;
+
+    Employee(String name, Company company) {
+      this.name = name;
+      this.company = company;
+      this.company.employees.add(this);
+    }
+  }
+
+  static class Company {
+    final String name;
+    final List<Employee> employees = new ArrayList<>();
+
+    Company(String name) {
+      this.name = name;
+    }
+  }
+}
diff --git a/gson/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java b/gson/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a957fc744c1d9881dbbf47fca9332247222c16f
--- /dev/null
+++ b/gson/extras/src/test/java/com/google/gson/interceptors/InterceptorTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 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.interceptors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+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.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link Intercept} and {@link JsonPostDeserializer}.
+ *
+ * @author Inderjeet Singh
+ */
+public final class InterceptorTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    this.gson =
+        new GsonBuilder()
+            .registerTypeAdapterFactory(new InterceptorFactory())
+            .enableComplexMapKeySerialization()
+            .create();
+  }
+
+  @Test
+  public void testExceptionsPropagated() {
+    try {
+      gson.fromJson("{}", User.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testTopLevelClass() {
+    User user = gson.fromJson("{name:'bob',password:'pwd'}", User.class);
+    assertEquals(User.DEFAULT_EMAIL, user.email);
+  }
+
+  @Test
+  public void testList() {
+    List<User> list =
+        gson.fromJson("[{name:'bob',password:'pwd'}]", new TypeToken<List<User>>() {}.getType());
+    User user = list.get(0);
+    assertEquals(User.DEFAULT_EMAIL, user.email);
+  }
+
+  @Test
+  public void testCollection() {
+    Collection<User> list =
+        gson.fromJson(
+            "[{name:'bob',password:'pwd'}]", new TypeToken<Collection<User>>() {}.getType());
+    User user = list.iterator().next();
+    assertEquals(User.DEFAULT_EMAIL, user.email);
+  }
+
+  @Test
+  public void testMapKeyAndValues() {
+    Type mapType = new TypeToken<Map<User, Address>>() {}.getType();
+    try {
+      gson.fromJson("[[{name:'bob',password:'pwd'},{}]]", mapType);
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+    Map<User, Address> map =
+        gson.fromJson(
+            "[[{name:'bob',password:'pwd'},{city:'Mountain View',state:'CA',zip:'94043'}]]",
+            mapType);
+    Entry<User, Address> entry = map.entrySet().iterator().next();
+    assertEquals(User.DEFAULT_EMAIL, entry.getKey().email);
+    assertEquals(Address.DEFAULT_FIRST_LINE, entry.getValue().firstLine);
+  }
+
+  @Test
+  public void testField() {
+    UserGroup userGroup = gson.fromJson("{user:{name:'bob',password:'pwd'}}", UserGroup.class);
+    assertEquals(User.DEFAULT_EMAIL, userGroup.user.email);
+  }
+
+  @Test
+  public void testCustomTypeAdapter() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                User.class,
+                new TypeAdapter<User>() {
+                  @Override
+                  public void write(JsonWriter out, User value) throws IOException {
+                    throw new UnsupportedOperationException();
+                  }
+
+                  @Override
+                  public User read(JsonReader in) throws IOException {
+                    in.beginObject();
+                    String unused1 = in.nextName();
+                    String name = in.nextString();
+                    String unused2 = in.nextName();
+                    String password = in.nextString();
+                    in.endObject();
+                    return new User(name, password);
+                  }
+                })
+            .registerTypeAdapterFactory(new InterceptorFactory())
+            .create();
+    UserGroup userGroup = gson.fromJson("{user:{name:'bob',password:'pwd'}}", UserGroup.class);
+    assertEquals(User.DEFAULT_EMAIL, userGroup.user.email);
+  }
+
+  @Test
+  public void testDirectInvocationOfTypeAdapter() throws Exception {
+    TypeAdapter<UserGroup> adapter = gson.getAdapter(UserGroup.class);
+    UserGroup userGroup = adapter.fromJson("{\"user\":{\"name\":\"bob\",\"password\":\"pwd\"}}");
+    assertEquals(User.DEFAULT_EMAIL, userGroup.user.email);
+  }
+
+  @SuppressWarnings("unused")
+  private static final class UserGroup {
+    User user;
+    String city;
+  }
+
+  @Intercept(postDeserialize = UserValidator.class)
+  @SuppressWarnings("unused")
+  private static final class User {
+    static final String DEFAULT_EMAIL = "invalid@invalid.com";
+    String name;
+    String password;
+    String email;
+    Address address;
+
+    public User(String name, String password) {
+      this.name = name;
+      this.password = password;
+    }
+  }
+
+  public static final class UserValidator implements JsonPostDeserializer<User> {
+    @Override
+    public void postDeserialize(User user) {
+      if (user.name == null || user.password == null) {
+        throw new JsonSyntaxException("name and password are required fields.");
+      }
+      if (user.email == null) {
+        user.email = User.DEFAULT_EMAIL;
+      }
+    }
+  }
+
+  @Intercept(postDeserialize = AddressValidator.class)
+  @SuppressWarnings("unused")
+  private static final class Address {
+    static final String DEFAULT_FIRST_LINE = "unknown";
+    String firstLine;
+    String secondLine;
+    String city;
+    String state;
+    String zip;
+  }
+
+  public static final class AddressValidator implements JsonPostDeserializer<Address> {
+    @Override
+    public void postDeserialize(Address address) {
+      if (address.city == null || address.state == null || address.zip == null) {
+        throw new JsonSyntaxException("Address city, state and zip are required fields.");
+      }
+      if (address.firstLine == null) {
+        address.firstLine = Address.DEFAULT_FIRST_LINE;
+      }
+    }
+  }
+}
diff --git a/gson/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java b/gson/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ce937f751a6ecaebd50dc293a174a8aaeb91832
--- /dev/null
+++ b/gson/extras/src/test/java/com/google/gson/typeadapters/PostConstructAdapterFactoryTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 Gson Authors
+ *
+ * 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.typeadapters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.Arrays;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import org.junit.Test;
+
+public class PostConstructAdapterFactoryTest {
+  @Test
+  public void test() throws Exception {
+    Gson gson =
+        new GsonBuilder().registerTypeAdapterFactory(new PostConstructAdapterFactory()).create();
+    gson.fromJson("{\"bread\": \"white\", \"cheese\": \"cheddar\"}", Sandwich.class);
+    try {
+      gson.fromJson("{\"bread\": \"cheesey bread\", \"cheese\": \"swiss\"}", Sandwich.class);
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertEquals("too cheesey", expected.getMessage());
+    }
+  }
+
+  @Test
+  public void testList() {
+    MultipleSandwiches sandwiches =
+        new MultipleSandwiches(
+            Arrays.asList(new Sandwich("white", "cheddar"), new Sandwich("whole wheat", "swiss")));
+
+    Gson gson =
+        new GsonBuilder().registerTypeAdapterFactory(new PostConstructAdapterFactory()).create();
+
+    // Throws NullPointerException without the fix in https://github.com/google/gson/pull/1103
+    String json = gson.toJson(sandwiches);
+    assertEquals(
+        "{\"sandwiches\":[{\"bread\":\"white\",\"cheese\":\"cheddar\"},"
+            + "{\"bread\":\"whole wheat\",\"cheese\":\"swiss\"}]}",
+        json);
+
+    MultipleSandwiches sandwichesFromJson = gson.fromJson(json, MultipleSandwiches.class);
+    assertEquals(sandwiches, sandwichesFromJson);
+  }
+
+  @SuppressWarnings({"overrides", "EqualsHashCode"}) // for missing hashCode() override
+  static class Sandwich {
+    public String bread;
+    public String cheese;
+
+    public Sandwich(String bread, String cheese) {
+      this.bread = bread;
+      this.cheese = cheese;
+    }
+
+    @PostConstruct
+    private void validate() {
+      if (bread.equals("cheesey bread") && cheese != null) {
+        throw new IllegalArgumentException("too cheesey");
+      }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o == this) {
+        return true;
+      }
+      if (!(o instanceof Sandwich)) {
+        return false;
+      }
+      final Sandwich other = (Sandwich) o;
+      if (this.bread == null ? other.bread != null : !this.bread.equals(other.bread)) {
+        return false;
+      }
+      if (this.cheese == null ? other.cheese != null : !this.cheese.equals(other.cheese)) {
+        return false;
+      }
+      return true;
+    }
+  }
+
+  @SuppressWarnings({"overrides", "EqualsHashCode"}) // for missing hashCode() override
+  static class MultipleSandwiches {
+    public List<Sandwich> sandwiches;
+
+    public MultipleSandwiches(List<Sandwich> sandwiches) {
+      this.sandwiches = sandwiches;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o == this) {
+        return true;
+      }
+      if (!(o instanceof MultipleSandwiches)) {
+        return false;
+      }
+      final MultipleSandwiches other = (MultipleSandwiches) o;
+      if (this.sandwiches == null
+          ? other.sandwiches != null
+          : !this.sandwiches.equals(other.sandwiches)) {
+        return false;
+      }
+      return true;
+    }
+  }
+}
diff --git a/gson/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java b/gson/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4018caedc9d338fd1ff526b8e0c8a1230533d7b
--- /dev/null
+++ b/gson/extras/src/test/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactoryTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2011 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.typeadapters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapterFactory;
+import org.junit.Test;
+
+public final class RuntimeTypeAdapterFactoryTest {
+
+  @Test
+  public void testRuntimeTypeAdapter() {
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class).registerSubtype(CreditCard.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(rta).create();
+
+    CreditCard original = new CreditCard("Jesse", 234);
+    assertEquals(
+        "{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}",
+        gson.toJson(original, BillingInstrument.class));
+    BillingInstrument deserialized =
+        gson.fromJson("{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
+    assertEquals("Jesse", deserialized.ownerName);
+    assertTrue(deserialized instanceof CreditCard);
+  }
+
+  @Test
+  public void testRuntimeTypeAdapterRecognizeSubtypes() {
+    // We don't have an explicit factory for CreditCard.class, but we do have one for
+    // BillingInstrument.class that has recognizeSubtypes(). So it should recognize CreditCard, and
+    // when we call gson.toJson(original) below, without an explicit type, it should be invoked.
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class)
+            .recognizeSubtypes()
+            .registerSubtype(CreditCard.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(rta).create();
+
+    CreditCard original = new CreditCard("Jesse", 234);
+    assertEquals(
+        "{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}", gson.toJson(original));
+    BillingInstrument deserialized =
+        gson.fromJson("{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
+    assertEquals("Jesse", deserialized.ownerName);
+    assertTrue(deserialized instanceof CreditCard);
+  }
+
+  @Test
+  public void testRuntimeTypeIsBaseType() {
+    TypeAdapterFactory rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class)
+            .registerSubtype(BillingInstrument.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(rta).create();
+
+    BillingInstrument original = new BillingInstrument("Jesse");
+    assertEquals(
+        "{\"type\":\"BillingInstrument\",\"ownerName\":\"Jesse\"}",
+        gson.toJson(original, BillingInstrument.class));
+    BillingInstrument deserialized =
+        gson.fromJson("{type:'BillingInstrument',ownerName:'Jesse'}", BillingInstrument.class);
+    assertEquals("Jesse", deserialized.ownerName);
+  }
+
+  @Test
+  public void testNullBaseType() {
+    try {
+      RuntimeTypeAdapterFactory.of(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testNullTypeFieldName() {
+    try {
+      RuntimeTypeAdapterFactory.of(BillingInstrument.class, null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testNullSubtype() {
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class);
+    try {
+      rta.registerSubtype(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testNullLabel() {
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class);
+    try {
+      rta.registerSubtype(CreditCard.class, null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testDuplicateSubtype() {
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class);
+    rta.registerSubtype(CreditCard.class, "CC");
+    try {
+      rta.registerSubtype(CreditCard.class, "Visa");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDuplicateLabel() {
+    RuntimeTypeAdapterFactory<BillingInstrument> rta =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class);
+    rta.registerSubtype(CreditCard.class, "CC");
+    try {
+      rta.registerSubtype(BankTransfer.class, "CC");
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDeserializeMissingTypeField() {
+    TypeAdapterFactory billingAdapter =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class).registerSubtype(CreditCard.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create();
+    try {
+      gson.fromJson("{ownerName:'Jesse'}", BillingInstrument.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testDeserializeMissingSubtype() {
+    TypeAdapterFactory billingAdapter =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class).registerSubtype(BankTransfer.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create();
+    try {
+      gson.fromJson("{type:'CreditCard',ownerName:'Jesse'}", BillingInstrument.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeMissingSubtype() {
+    TypeAdapterFactory billingAdapter =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class).registerSubtype(BankTransfer.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create();
+    try {
+      gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeCollidingTypeFieldName() {
+    TypeAdapterFactory billingAdapter =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class, "cvv")
+            .registerSubtype(CreditCard.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create();
+    try {
+      gson.toJson(new CreditCard("Jesse", 456), BillingInstrument.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeWrappedNullValue() {
+    TypeAdapterFactory billingAdapter =
+        RuntimeTypeAdapterFactory.of(BillingInstrument.class)
+            .registerSubtype(CreditCard.class)
+            .registerSubtype(BankTransfer.class);
+    Gson gson = new GsonBuilder().registerTypeAdapterFactory(billingAdapter).create();
+    String serialized =
+        gson.toJson(new BillingInstrumentWrapper(null), BillingInstrumentWrapper.class);
+    BillingInstrumentWrapper deserialized =
+        gson.fromJson(serialized, BillingInstrumentWrapper.class);
+    assertNull(deserialized.instrument);
+  }
+
+  static class BillingInstrumentWrapper {
+    BillingInstrument instrument;
+
+    BillingInstrumentWrapper(BillingInstrument instrument) {
+      this.instrument = instrument;
+    }
+  }
+
+  static class BillingInstrument {
+    private final String ownerName;
+
+    BillingInstrument(String ownerName) {
+      this.ownerName = ownerName;
+    }
+  }
+
+  static class CreditCard extends BillingInstrument {
+    int cvv;
+
+    CreditCard(String ownerName, int cvv) {
+      super(ownerName);
+      this.cvv = cvv;
+    }
+  }
+
+  static class BankTransfer extends BillingInstrument {
+    int bankAccount;
+
+    BankTransfer(String ownerName, int bankAccount) {
+      super(ownerName);
+      this.bankAccount = bankAccount;
+    }
+  }
+}
diff --git a/gson/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java b/gson/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..771e7648e9ec41ae1b059560bbfcc1efd4ef2fce
--- /dev/null
+++ b/gson/extras/src/test/java/com/google/gson/typeadapters/UtcDateTypeAdapterTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 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.typeadapters;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.Test;
+
+@SuppressWarnings("JavaUtilDate")
+public final class UtcDateTypeAdapterTest {
+  private final Gson gson =
+      new GsonBuilder().registerTypeAdapter(Date.class, new UtcDateTypeAdapter()).create();
+
+  @Test
+  public void testLocalTimeZone() {
+    Date expected = new Date();
+    String json = gson.toJson(expected);
+    Date actual = gson.fromJson(json, Date.class);
+    assertEquals(expected.getTime(), actual.getTime());
+  }
+
+  @Test
+  public void testDifferentTimeZones() {
+    for (String timeZone : TimeZone.getAvailableIDs()) {
+      Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
+      Date expected = cal.getTime();
+      String json = gson.toJson(expected);
+      // System.out.println(json + ": " + timeZone);
+      Date actual = gson.fromJson(json, Date.class);
+      assertEquals(expected.getTime(), actual.getTime());
+    }
+  }
+
+  /**
+   * JDK 1.7 introduced support for XXX format to indicate UTC date. But Android is older JDK. We
+   * want to make sure that this date is parseable in Android.
+   */
+  @Test
+  public void testUtcDatesOnJdkBefore1_7() {
+    Gson gson =
+        new GsonBuilder().registerTypeAdapter(Date.class, new UtcDateTypeAdapter()).create();
+    Date unused = gson.fromJson("'2014-12-05T04:00:00.000Z'", Date.class);
+  }
+
+  @Test
+  public void testUtcWithJdk7Default() {
+    Date expected = new Date();
+    SimpleDateFormat iso8601Format =
+        new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US);
+    iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC"));
+    String expectedJson = "\"" + iso8601Format.format(expected) + "\"";
+    String actualJson = gson.toJson(expected);
+    assertEquals(expectedJson, actualJson);
+    Date actual = gson.fromJson(expectedJson, Date.class);
+    assertEquals(expected.getTime(), actual.getTime());
+  }
+
+  @Test
+  public void testNullDateSerialization() {
+    String json = gson.toJson(null, Date.class);
+    assertEquals("null", json);
+  }
+
+  @Test
+  public void testWellFormedParseException() {
+    try {
+      gson.fromJson("2017-06-20T14:32:30", Date.class);
+      fail("No exception");
+    } catch (JsonParseException exe) {
+      assertEquals(
+          "java.text.ParseException: Failed to parse date ['2017-06-20T14']: 2017-06-20T14",
+          exe.getMessage());
+    }
+  }
+}
diff --git a/gson/graal-native-image-test/README.md b/gson/graal-native-image-test/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b3b2b353c22d50a5c3ecef8d51e1bc3fc1eb7210
--- /dev/null
+++ b/gson/graal-native-image-test/README.md
@@ -0,0 +1,19 @@
+# graal-native-image-test
+
+This Maven module contains integration tests for using Gson in a GraalVM Native Image.
+
+Execution requires using GraalVM as JDK, and can be quite resource intensive. Native Image tests are therefore not enabled by default and the tests are only executed as regular unit tests. To run Native Image tests, make sure your `PATH` and `JAVA_HOME` environment variables point to GraalVM and then run:
+
+```
+mvn clean test --activate-profiles native-image-test
+```
+
+Technically it would also be possible to directly configure Native Image test execution for the `gson` module instead of having this separate Maven module. However, maintaining the reflection metadata for the unit tests would be quite cumbersome and would hinder future changes to the `gson` unit tests because many of them just happen to use reflection, without all of them being relevant for Native Image testing.
+
+## Reflection metadata
+
+Native Image creation requires configuring which class members are accessed using reflection, see the [GraalVM documentation](https://www.graalvm.org/22.3/reference-manual/native-image/metadata/#specifying-reflection-metadata-in-json).
+
+The file [`reflect-config.json`](./src/test/resources/META-INF/native-image/reflect-config.json) contains this reflection metadata.
+
+You can also run with `-Dagent=true` to let the Maven plugin automatically generate a metadata file, see the [plugin documentation](https://graalvm.github.io/native-build-tools/latest/maven-plugin.html#agent-support-running-tests).
diff --git a/gson/graal-native-image-test/pom.xml b/gson/graal-native-image-test/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..02d395c83971a7291be267c04ed9d70da335d5a2
--- /dev/null
+++ b/gson/graal-native-image-test/pom.xml
@@ -0,0 +1,173 @@
+<!--
+  Copyright 2023 Google Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+  <artifactId>graal-native-image-test</artifactId>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+
+    <!-- GraalVM is JDK >= 17, however for build with regular JDK these tests
+      are also executed with JDK 11, so for them exclude JDK 17 specific tests -->
+    <maven.compiler.testRelease>11</maven.compiler.testRelease>
+    <excludeTestCompilation>**/Java17*</excludeTestCompilation>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${project.parent.version}</version>
+    </dependency>
+
+    <!-- Graal Native Maven Plugin requires using JUnit Platform (JUnit 5), see
+      https://graalvm.github.io/native-build-tools/latest/maven-plugin.html#testing-support
+      This also supports using JUnit Vintage to run JUnit 4 tests, but for simplicity
+      completely use JUnit 5 here and no JUnit 4 at all -->
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <version>5.10.1</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>com.github.siom79.japicmp</groupId>
+          <artifactId>japicmp-maven-plugin</artifactId>
+          <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.codehaus.mojo</groupId>
+          <artifactId>animal-sniffer-maven-plugin</artifactId>
+          <configuration>
+            <!-- This module is not supposed to be consumed as library, so no need to check used classes -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <configuration>
+            <!-- This module has no 'main' source code; skip creating JAR and avoid warning on console -->
+            <skipIfEmpty>true</skipIfEmpty>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <configuration>
+            <!-- This module has no 'main' source code, so no JAR is created which could be installed;
+              see maven-jar-plugin configuration above -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <configuration>
+            <!-- Not deployed -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <!-- Adjust standard `default-testCompile` 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>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>JDK17</id>
+      <activation>
+        <jdk>[17,)</jdk>
+      </activation>
+      <properties>
+        <maven.compiler.testRelease>17</maven.compiler.testRelease>
+        <excludeTestCompilation />
+      </properties>
+    </profile>
+
+    <profile>
+      <id>native-image-test</id>
+      <activation>
+        <activeByDefault>false</activeByDefault>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.graalvm.buildtools</groupId>
+            <artifactId>native-maven-plugin</artifactId>
+            <version>0.9.28</version>
+            <extensions>true</extensions>
+            <executions>
+              <execution>
+                <id>test-native</id>
+                <goals>
+                  <goal>test</goal>
+                </goals>
+                <configuration>
+                  <quickBuild>true</quickBuild>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java b/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea9123628d82b55fdd2ec8384df877444f0e68e1
--- /dev/null
+++ b/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.gson.native_test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+
+class Java17RecordReflectionTest {
+  public record PublicRecord(int i) {}
+
+  @Test
+  void testPublicRecord() {
+    Gson gson = new Gson();
+    PublicRecord r = gson.fromJson("{\"i\":1}", PublicRecord.class);
+    assertThat(r.i).isEqualTo(1);
+  }
+
+  // Private record has implicit private canonical constructor
+  private record PrivateRecord(int i) {}
+
+  @Test
+  void testPrivateRecord() {
+    Gson gson = new Gson();
+    PrivateRecord r = gson.fromJson("{\"i\":1}", PrivateRecord.class);
+    assertThat(r.i).isEqualTo(1);
+  }
+
+  @Test
+  void testLocalRecord() {
+    record LocalRecordDeserialization(int i) {}
+
+    Gson gson = new Gson();
+    LocalRecordDeserialization r = gson.fromJson("{\"i\":1}", LocalRecordDeserialization.class);
+    assertThat(r.i).isEqualTo(1);
+  }
+
+  @Test
+  void testLocalRecordSerialization() {
+    record LocalRecordSerialization(int i) {}
+
+    Gson gson = new Gson();
+    assertThat(gson.toJson(new LocalRecordSerialization(1))).isEqualTo("{\"i\":1}");
+  }
+
+  private record RecordWithSerializedName(@SerializedName("custom-name") int i) {}
+
+  @Test
+  void testSerializedName() {
+    Gson gson = new Gson();
+    RecordWithSerializedName r =
+        gson.fromJson("{\"custom-name\":1}", RecordWithSerializedName.class);
+    assertThat(r.i).isEqualTo(1);
+
+    assertThat(gson.toJson(new RecordWithSerializedName(2))).isEqualTo("{\"custom-name\":2}");
+  }
+
+  private record RecordWithCustomConstructor(int i) {
+    @SuppressWarnings("unused")
+    RecordWithCustomConstructor {
+      i += 5;
+    }
+  }
+
+  @Test
+  void testCustomConstructor() {
+    Gson gson = new Gson();
+    RecordWithCustomConstructor r = gson.fromJson("{\"i\":1}", RecordWithCustomConstructor.class);
+    assertThat(r.i).isEqualTo(6);
+  }
+
+  private record RecordWithCustomAccessor(int i) {
+    @SuppressWarnings("UnusedMethod")
+    @Override
+    public int i() {
+      return i + 5;
+    }
+  }
+
+  @Test
+  void testCustomAccessor() {
+    Gson gson = new Gson();
+    assertThat(gson.toJson(new RecordWithCustomAccessor(2))).isEqualTo("{\"i\":7}");
+  }
+
+  @JsonAdapter(RecordWithCustomClassAdapter.CustomAdapter.class)
+  private record RecordWithCustomClassAdapter(int i) {
+    private static class CustomAdapter extends TypeAdapter<RecordWithCustomClassAdapter> {
+      @Override
+      public RecordWithCustomClassAdapter read(JsonReader in) throws IOException {
+        return new RecordWithCustomClassAdapter(in.nextInt() + 5);
+      }
+
+      @Override
+      public void write(JsonWriter out, RecordWithCustomClassAdapter value) throws IOException {
+        out.value(value.i + 6);
+      }
+    }
+  }
+
+  @Test
+  void testCustomClassAdapter() {
+    Gson gson = new Gson();
+    RecordWithCustomClassAdapter r = gson.fromJson("1", RecordWithCustomClassAdapter.class);
+    assertThat(r.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new RecordWithCustomClassAdapter(1))).isEqualTo("7");
+  }
+
+  private record RecordWithCustomFieldAdapter(
+      @JsonAdapter(RecordWithCustomFieldAdapter.CustomAdapter.class) int i) {
+    private static class CustomAdapter extends TypeAdapter<Integer> {
+      @Override
+      public Integer read(JsonReader in) throws IOException {
+        return in.nextInt() + 5;
+      }
+
+      @Override
+      public void write(JsonWriter out, Integer value) throws IOException {
+        out.value(value + 6);
+      }
+    }
+  }
+
+  @Test
+  void testCustomFieldAdapter() {
+    Gson gson = new Gson();
+    RecordWithCustomFieldAdapter r = gson.fromJson("{\"i\":1}", RecordWithCustomFieldAdapter.class);
+    assertThat(r.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new RecordWithCustomFieldAdapter(1))).isEqualTo("{\"i\":7}");
+  }
+
+  private record RecordWithRegisteredAdapter(int i) {}
+
+  @Test
+  void testCustomAdapter() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                RecordWithRegisteredAdapter.class,
+                new TypeAdapter<RecordWithRegisteredAdapter>() {
+                  @Override
+                  public RecordWithRegisteredAdapter read(JsonReader in) throws IOException {
+                    return new RecordWithRegisteredAdapter(in.nextInt() + 5);
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, RecordWithRegisteredAdapter value)
+                      throws IOException {
+                    out.value(value.i + 6);
+                  }
+                })
+            .create();
+
+    RecordWithRegisteredAdapter r = gson.fromJson("1", RecordWithRegisteredAdapter.class);
+    assertThat(r.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new RecordWithRegisteredAdapter(1))).isEqualTo("7");
+  }
+}
diff --git a/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java b/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6caa10ebae17e0a33e8c97781cbe04fb646da7fc
--- /dev/null
+++ b/gson/graal-native-image-test/src/test/java/com/google/gson/native_test/ReflectionTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.gson.native_test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class ReflectionTest {
+  private static class ClassWithDefaultConstructor {
+    private int i;
+  }
+
+  @Test
+  void testDefaultConstructor() {
+    Gson gson = new Gson();
+
+    ClassWithDefaultConstructor c = gson.fromJson("{\"i\":1}", ClassWithDefaultConstructor.class);
+    assertThat(c.i).isEqualTo(1);
+  }
+
+  private static class ClassWithCustomDefaultConstructor {
+    private int i;
+
+    private ClassWithCustomDefaultConstructor() {
+      i = 1;
+    }
+  }
+
+  @Test
+  void testCustomDefaultConstructor() {
+    Gson gson = new Gson();
+
+    ClassWithCustomDefaultConstructor c =
+        gson.fromJson("{\"i\":2}", ClassWithCustomDefaultConstructor.class);
+    assertThat(c.i).isEqualTo(2);
+
+    c = gson.fromJson("{}", ClassWithCustomDefaultConstructor.class);
+    assertThat(c.i).isEqualTo(1);
+  }
+
+  private static class ClassWithoutDefaultConstructor {
+    private int i = -1;
+
+    // Explicit constructor with args to remove implicit no-args default constructor
+    private ClassWithoutDefaultConstructor(int i) {
+      this.i = i;
+    }
+  }
+
+  /**
+   * Tests deserializing a class without default constructor.
+   *
+   * <p>This should use JDK Unsafe, and would normally require specifying {@code "unsafeAllocated":
+   * true} in the reflection metadata for GraalVM, though for some reason it also seems to work
+   * without it? Possibly because GraalVM seems to have special support for Gson, see its class
+   * {@code com.oracle.svm.thirdparty.gson.GsonFeature}.
+   */
+  @Test
+  void testClassWithoutDefaultConstructor() {
+    Gson gson = new Gson();
+
+    ClassWithoutDefaultConstructor c =
+        gson.fromJson("{\"i\":1}", ClassWithoutDefaultConstructor.class);
+    assertThat(c.i).isEqualTo(1);
+
+    c = gson.fromJson("{}", ClassWithoutDefaultConstructor.class);
+    // Class is instantiated with JDK Unsafe, therefore field keeps its default value instead of
+    // assigned -1
+    assertThat(c.i).isEqualTo(0);
+  }
+
+  @Test
+  void testInstanceCreator() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ClassWithoutDefaultConstructor.class,
+                new InstanceCreator<ClassWithoutDefaultConstructor>() {
+                  @Override
+                  public ClassWithoutDefaultConstructor createInstance(Type type) {
+                    return new ClassWithoutDefaultConstructor(-2);
+                  }
+                })
+            .create();
+
+    ClassWithoutDefaultConstructor c =
+        gson.fromJson("{\"i\":1}", ClassWithoutDefaultConstructor.class);
+    assertThat(c.i).isEqualTo(1);
+
+    c = gson.fromJson("{}", ClassWithoutDefaultConstructor.class);
+    // Uses default value specified by InstanceCreator
+    assertThat(c.i).isEqualTo(-2);
+  }
+
+  private static class ClassWithFinalField {
+    // Initialize with value which is not inlined by compiler
+    private final int i = nonConstant();
+
+    private static int nonConstant() {
+      return "a".length(); // = 1
+    }
+  }
+
+  @Test
+  void testFinalField() {
+    Gson gson = new Gson();
+
+    ClassWithFinalField c = gson.fromJson("{\"i\":2}", ClassWithFinalField.class);
+    assertThat(c.i).isEqualTo(2);
+
+    c = gson.fromJson("{}", ClassWithFinalField.class);
+    assertThat(c.i).isEqualTo(1);
+  }
+
+  private static class ClassWithSerializedName {
+    @SerializedName("custom-name")
+    private int i;
+  }
+
+  @Test
+  void testSerializedName() {
+    Gson gson = new Gson();
+    ClassWithSerializedName c = gson.fromJson("{\"custom-name\":1}", ClassWithSerializedName.class);
+    assertThat(c.i).isEqualTo(1);
+
+    c = new ClassWithSerializedName();
+    c.i = 2;
+    assertThat(gson.toJson(c)).isEqualTo("{\"custom-name\":2}");
+  }
+
+  @JsonAdapter(ClassWithCustomClassAdapter.CustomAdapter.class)
+  private static class ClassWithCustomClassAdapter {
+    private static class CustomAdapter extends TypeAdapter<ClassWithCustomClassAdapter> {
+      @Override
+      public ClassWithCustomClassAdapter read(JsonReader in) throws IOException {
+        return new ClassWithCustomClassAdapter(in.nextInt() + 5);
+      }
+
+      @Override
+      public void write(JsonWriter out, ClassWithCustomClassAdapter value) throws IOException {
+        out.value(value.i + 6);
+      }
+    }
+
+    private int i;
+
+    private ClassWithCustomClassAdapter(int i) {
+      this.i = i;
+    }
+  }
+
+  @Test
+  void testCustomClassAdapter() {
+    Gson gson = new Gson();
+    ClassWithCustomClassAdapter c = gson.fromJson("1", ClassWithCustomClassAdapter.class);
+    assertThat(c.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new ClassWithCustomClassAdapter(1))).isEqualTo("7");
+  }
+
+  private static class ClassWithCustomFieldAdapter {
+    private static class CustomAdapter extends TypeAdapter<Integer> {
+      @Override
+      public Integer read(JsonReader in) throws IOException {
+        return in.nextInt() + 5;
+      }
+
+      @Override
+      public void write(JsonWriter out, Integer value) throws IOException {
+        out.value(value + 6);
+      }
+    }
+
+    @JsonAdapter(ClassWithCustomFieldAdapter.CustomAdapter.class)
+    private int i;
+
+    private ClassWithCustomFieldAdapter(int i) {
+      this.i = i;
+    }
+
+    private ClassWithCustomFieldAdapter() {
+      this(-1);
+    }
+  }
+
+  @Test
+  void testCustomFieldAdapter() {
+    Gson gson = new Gson();
+    ClassWithCustomFieldAdapter c = gson.fromJson("{\"i\":1}", ClassWithCustomFieldAdapter.class);
+    assertThat(c.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new ClassWithCustomFieldAdapter(1))).isEqualTo("{\"i\":7}");
+  }
+
+  private static class ClassWithRegisteredAdapter {
+    private int i;
+
+    private ClassWithRegisteredAdapter(int i) {
+      this.i = i;
+    }
+  }
+
+  @Test
+  void testCustomAdapter() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ClassWithRegisteredAdapter.class,
+                new TypeAdapter<ClassWithRegisteredAdapter>() {
+                  @Override
+                  public ClassWithRegisteredAdapter read(JsonReader in) throws IOException {
+                    return new ClassWithRegisteredAdapter(in.nextInt() + 5);
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, ClassWithRegisteredAdapter value)
+                      throws IOException {
+                    out.value(value.i + 6);
+                  }
+                })
+            .create();
+
+    ClassWithRegisteredAdapter c = gson.fromJson("1", ClassWithRegisteredAdapter.class);
+    assertThat(c.i).isEqualTo(6);
+
+    assertThat(gson.toJson(new ClassWithRegisteredAdapter(1))).isEqualTo("7");
+  }
+
+  @Test
+  void testGenerics() {
+    Gson gson = new Gson();
+
+    List<ClassWithDefaultConstructor> list =
+        gson.fromJson("[{\"i\":1}]", new TypeToken<List<ClassWithDefaultConstructor>>() {});
+    assertThat(list).hasSize(1);
+    assertThat(list.get(0).i).isEqualTo(1);
+
+    @SuppressWarnings("unchecked")
+    List<ClassWithDefaultConstructor> list2 =
+        (List<ClassWithDefaultConstructor>)
+            gson.fromJson(
+                "[{\"i\":1}]",
+                TypeToken.getParameterized(List.class, ClassWithDefaultConstructor.class));
+    assertThat(list2).hasSize(1);
+    assertThat(list2.get(0).i).isEqualTo(1);
+  }
+}
diff --git a/gson/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json b/gson/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json
new file mode 100644
index 0000000000000000000000000000000000000000..e5220767d3a20effd71c5dabf9cfe9624987b9b4
--- /dev/null
+++ b/gson/graal-native-image-test/src/test/resources/META-INF/native-image/reflect-config.json
@@ -0,0 +1,101 @@
+[
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithDefaultConstructor",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomDefaultConstructor",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithoutDefaultConstructor",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithFinalField",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithSerializedName",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomFieldAdapter",
+  "allDeclaredFields":true,
+  "allDeclaredConstructors": true
+},
+
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomClassAdapter$CustomAdapter",
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.ReflectionTest$ClassWithCustomFieldAdapter$CustomAdapter",
+  "allDeclaredConstructors": true
+},
+
+
+
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$PublicRecord",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$PrivateRecord",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithSerializedName",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomConstructor",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomAccessor",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomFieldAdapter",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$1LocalRecordDeserialization",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$1LocalRecordSerialization",
+  "allDeclaredFields":true,
+  "allPublicMethods": true,
+  "allDeclaredConstructors": true
+},
+
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomClassAdapter$CustomAdapter",
+  "allDeclaredConstructors": true
+},
+{
+  "name":"com.google.gson.native_test.Java17RecordReflectionTest$RecordWithCustomFieldAdapter$CustomAdapter",
+  "allDeclaredConstructors": true
+}
+]
diff --git a/gson/gson/LICENSE b/gson/gson/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..8763058a27bcdabdcaa82d21684755c39fb33f9c
--- /dev/null
+++ b/gson/gson/LICENSE
@@ -0,0 +1,203 @@
+Google Gson
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2008-2011 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.
diff --git a/gson/gson/README.md b/gson/gson/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..75ec9fc92a956e5f71aba12685fbce09b9caa5b7
--- /dev/null
+++ b/gson/gson/README.md
@@ -0,0 +1,4 @@
+# gson
+
+This Maven module contains the Gson source code. The artifacts created by this module
+are deployed to Maven Central under the coordinates `com.google.code.gson:gson`.
diff --git a/gson/gson/bnd.bnd b/gson/gson/bnd.bnd
new file mode 100644
index 0000000000000000000000000000000000000000..626a0c5becdff4a65f4a0b4c6407db83cb5515f9
--- /dev/null
+++ b/gson/gson/bnd.bnd
@@ -0,0 +1,19 @@
+Bundle-SymbolicName: com.google.gson
+Bundle-Name: ${project.name}
+Bundle-Description: ${project.description}
+Bundle-Vendor: Google Gson Project
+Bundle-ContactAddress: ${project.parent.url}
+Bundle-RequiredExecutionEnvironment: JavaSE-1.7, JavaSE-1.8
+Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.7))"
+
+# Optional dependency for JDK's sun.misc.Unsafe
+# https://bnd.bndtools.org/chapters/920-faq.html#remove-unwanted-imports-
+Import-Package: sun.misc;resolution:=optional, *
+
+-removeheaders: Private-Package
+
+-exportcontents:\
+    com.google.gson,\
+    com.google.gson.annotations,\
+    com.google.gson.reflect,\
+    com.google.gson.stream
diff --git a/gson/gson/pom.xml b/gson/gson/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e72f89b60c1a36d39478a2d89136d065c3fc062b
--- /dev/null
+++ b/gson/gson/pom.xml
@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2008 Google LLC
+
+  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.
+-->
+<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>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gson</artifactId>
+  <name>Gson</name>
+
+  <licenses>
+    <license>
+      <name>Apache-2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+
+    <excludeTestCompilation>**/Java17*</excludeTestCompilation>
+  </properties>
+
+  <dependencies>
+    <!-- This dependency can be considered optional; omitting it during runtime will most likely
+      not cause any issues. However, it is not declared with `<optional>true</optional>` because
+      that can lead to cryptic compiler warnings for consumers, and to be on the safe side in case
+      there are actually issues which could occur when the dependency is missing at runtime.
+      See also discussion at https://github.com/google/gson/pull/2320#issuecomment-1455233938 -->
+    <dependency>
+      <groupId>com.google.errorprone</groupId>
+      <artifactId>error_prone_annotations</artifactId>
+      <version>2.24.1</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava-testlib</artifactId>
+      <version>33.0.0-jre</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <!--
+        Plugins for source generation and compilation
+      -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>templating-maven-plugin</artifactId>
+        <version>3.0.0</version>
+        <executions>
+          <execution>
+            <id>filtering-java-templates</id>
+            <goals>
+              <goal>filter-sources</goal>
+            </goals>
+            <configuration>
+              <sourceDirectory>${project.basedir}/src/main/java-templates</sourceDirectory>
+              <outputDirectory>${project.build.directory}/generated-sources/java-templates</outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <!-- Adjust standard `default-compile` execution -->
+          <execution>
+            <id>default-compile</id>
+            <configuration>
+              <excludes>
+                <!-- module-info.java is compiled using ModiTect -->
+                <exclude>module-info.java</exclude>
+              </excludes>
+            </configuration>
+          </execution>
+          <!-- Adjust standard `default-testCompile` 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.4.0</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>
+        <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>
+        <executions>
+          <execution>
+            <id>pre-obfuscate-class</id>
+            <phase>process-test-classes</phase>
+            <goals>
+              <goal>rename</goal>
+            </goals>
+            <configuration>
+              <fileSets>
+                <fileSet>
+                  <sourceFile>${project.build.directory}/test-classes/com/google/gson/functional/EnumWithObfuscatedTest.class</sourceFile>
+                  <destinationFile>${project.build.directory}/test-classes-obfuscated-injar/com/google/gson/functional/EnumWithObfuscatedTest.class</destinationFile>
+                </fileSet>
+                <fileSet>
+                  <sourceFile>${project.build.directory}/test-classes/com/google/gson/functional/EnumWithObfuscatedTest$Gender.class</sourceFile>
+                  <destinationFile>${project.build.directory}/test-classes-obfuscated-injar/com/google/gson/functional/EnumWithObfuscatedTest$Gender.class</destinationFile>
+                </fileSet>
+              </fileSets>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>com.github.wvengen</groupId>
+        <artifactId>proguard-maven-plugin</artifactId>
+        <version>2.6.0</version>
+        <executions>
+          <execution>
+            <id>obfuscate-test-class</id>
+            <phase>process-test-classes</phase>
+            <goals>
+              <goal>proguard</goal>
+            </goals>
+          </execution>
+        </executions>
+        <!-- Upgrades ProGuard to version newer than the one included by plugin by default -->
+        <dependencies>
+          <dependency>
+            <groupId>com.guardsquare</groupId>
+            <artifactId>proguard-base</artifactId>
+            <version>7.4.1</version>
+          </dependency>
+          <dependency>
+            <groupId>com.guardsquare</groupId>
+            <artifactId>proguard-core</artifactId>
+            <version>9.1.1</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <obfuscate>true</obfuscate>
+          <injar>test-classes-obfuscated-injar</injar>
+          <outjar>test-classes-obfuscated-outjar</outjar>
+          <inFilter>**/*.class</inFilter>
+          <proguardInclude>${project.basedir}/src/test/resources/testcases-proguard.conf</proguardInclude>
+          <libs>
+            <lib>${project.build.directory}/classes</lib>
+            <lib>${java.home}/jmods/java.base.jmod</lib>
+          </libs>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>3.3.1</version>
+        <executions>
+          <execution>
+            <id>post-obfuscate-class</id>
+            <phase>process-test-classes</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${project.build.directory}/test-classes/com/google/gson/functional</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${project.build.directory}/test-classes-obfuscated-outjar/com/google/gson/functional</directory>
+                  <includes>
+                    <include>EnumWithObfuscatedTest.class</include>
+                    <include>EnumWithObfuscatedTest$Gender.class</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </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.1.0</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/gson/src/main/java-templates/com/google/gson/internal/GsonBuildConfig.java b/gson/gson/src/main/java-templates/com/google/gson/internal/GsonBuildConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..48ca18fc3adaa1bd21e84207a75d768a7b31c66d
--- /dev/null
+++ b/gson/gson/src/main/java-templates/com/google/gson/internal/GsonBuildConfig.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 The Gson authors
+ *
+ * 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.internal;
+
+/**
+ * Build configuration for Gson. This file is automatically populated by templating-maven-plugin and
+ * .java/.class files are generated for use in Gson.
+ *
+ * @author Inderjeet Singh
+ */
+public final class GsonBuildConfig {
+  // Based on https://stackoverflow.com/questions/2469922/generate-a-version-java-file-in-maven
+
+  /** This field is automatically populated by Maven when a build is triggered */
+  public static final String VERSION = "${project.version}";
+
+  private GsonBuildConfig() {}
+}
diff --git a/gson/gson/src/main/java/com/google/gson/ExclusionStrategy.java b/gson/gson/src/main/java/com/google/gson/ExclusionStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f5d36b28d7bb3a5d8d58a917f0d6f53bcc14886
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/ExclusionStrategy.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+/**
+ * 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.
+ *
+ * <p><strong>Exclude fields and objects based on a particular class type:</strong>
+ *
+ * <pre class="code">
+ * private static class SpecificClassExclusionStrategy implements ExclusionStrategy {
+ *   private final Class&lt;?&gt; excludedThisClass;
+ *
+ *   public SpecificClassExclusionStrategy(Class&lt;?&gt; excludedThisClass) {
+ *     this.excludedThisClass = excludedThisClass;
+ *   }
+ *
+ *   public boolean shouldSkipClass(Class&lt;?&gt; clazz) {
+ *     return excludedThisClass.equals(clazz);
+ *   }
+ *
+ *   public boolean shouldSkipField(FieldAttributes f) {
+ *     return excludedThisClass.equals(f.getDeclaredClass());
+ *   }
+ * }
+ * </pre>
+ *
+ * <p><strong>Excludes fields and objects based on a particular annotation:</strong>
+ *
+ * <pre class="code">
+ * public &#64;interface FooAnnotation {
+ *   // some implementation here
+ * }
+ *
+ * // Excludes any field (or class) that is tagged with an "&#64;FooAnnotation"
+ * private static class FooAnnotationExclusionStrategy implements ExclusionStrategy {
+ *   public boolean shouldSkipClass(Class&lt;?&gt; clazz) {
+ *     return clazz.getAnnotation(FooAnnotation.class) != null;
+ *   }
+ *
+ *   public boolean shouldSkipField(FieldAttributes f) {
+ *     return f.getAnnotation(FooAnnotation.class) != null;
+ *   }
+ * }
+ * </pre>
+ *
+ * <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 samples:
+ *
+ * <pre class="code">
+ * ExclusionStrategy excludeStrings = new UserDefinedExclusionStrategy(String.class);
+ * Gson gson = new GsonBuilder()
+ *     .setExclusionStrategies(excludeStrings)
+ *     .create();
+ * </pre>
+ *
+ * <p>For certain model classes, you may only want to serialize a field, but exclude it for
+ * deserialization. To do that, you can write an {@code ExclusionStrategy} as per normal; however,
+ * you would register it with the {@link
+ * GsonBuilder#addDeserializationExclusionStrategy(ExclusionStrategy)} method. For example:
+ *
+ * <pre class="code">
+ * ExclusionStrategy excludeStrings = new UserDefinedExclusionStrategy(String.class);
+ * Gson gson = new GsonBuilder()
+ *     .addDeserializationExclusionStrategy(excludeStrings)
+ *     .create();
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @see GsonBuilder#setExclusionStrategies(ExclusionStrategy...)
+ * @see GsonBuilder#addDeserializationExclusionStrategy(ExclusionStrategy)
+ * @see GsonBuilder#addSerializationExclusionStrategy(ExclusionStrategy)
+ * @since 1.4
+ */
+public interface ExclusionStrategy {
+
+  /**
+   * Decides if a field should be skipped during serialization or deserialization.
+   *
+   * @param f the field object that is under test
+   * @return true if the field should be ignored; otherwise false
+   */
+  public boolean shouldSkipField(FieldAttributes f);
+
+  /**
+   * Decides if a class should be serialized or deserialized
+   *
+   * @param clazz the class object that is under test
+   * @return true if the class should be ignored; otherwise false
+   */
+  public boolean shouldSkipClass(Class<?> clazz);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/FieldAttributes.java b/gson/gson/src/main/java/com/google/gson/FieldAttributes.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4d3709fb0809fb4773ddc8c4b8b61d7ae996c38
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/FieldAttributes.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2009 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;
+
+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.
+ *
+ * <p>This class is immutable; therefore, it can be safely shared across threads.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.4
+ */
+public final class FieldAttributes {
+  private final Field field;
+
+  /**
+   * Constructs a Field Attributes object from the {@code f}.
+   *
+   * @param f the field to pull attributes from
+   */
+  public FieldAttributes(Field f) {
+    this.field = Objects.requireNonNull(f);
+  }
+
+  /**
+   * Gets the declaring Class that contains this field
+   *
+   * @return the declaring class that contains this field
+   */
+  public Class<?> getDeclaringClass() {
+    return field.getDeclaringClass();
+  }
+
+  /**
+   * Gets the name of the field
+   *
+   * @return the name of the field
+   */
+  public String getName() {
+    return field.getName();
+  }
+
+  /**
+   * Returns the declared generic type of the field.
+   *
+   * <p>For example, assume the following class definition:
+   *
+   * <pre class="code">
+   * public class Foo {
+   *   private String bar;
+   *   private List&lt;String&gt; red;
+   * }
+   *
+   * Type listParameterizedType = new TypeToken&lt;List&lt;String&gt;&gt;() {}.getType();
+   * </pre>
+   *
+   * <p>This method would return {@code String.class} for the {@code bar} field and {@code
+   * listParameterizedType} for the {@code red} field.
+   *
+   * @return the specific type declared for this field
+   */
+  public Type getDeclaredType() {
+    return field.getGenericType();
+  }
+
+  /**
+   * Returns the {@code Class} object that was declared for this field.
+   *
+   * <p>For example, assume the following class definition:
+   *
+   * <pre class="code">
+   * public class Foo {
+   *   private String bar;
+   *   private List&lt;String&gt; red;
+   * }
+   * </pre>
+   *
+   * <p>This method would return {@code String.class} for the {@code bar} field and {@code
+   * List.class} for the {@code red} field.
+   *
+   * @return the specific class object that was declared for the field
+   */
+  public Class<?> getDeclaredClass() {
+    return field.getType();
+  }
+
+  /**
+   * Returns the {@code T} annotation object from this field if it exists; otherwise returns {@code
+   * null}.
+   *
+   * @param annotation the class of the annotation that will be retrieved
+   * @return the annotation instance if it is bound to the field; otherwise {@code null}
+   */
+  public <T extends Annotation> T getAnnotation(Class<T> annotation) {
+    return field.getAnnotation(annotation);
+  }
+
+  /**
+   * Returns the annotations that are present on this field.
+   *
+   * @return an array of all the annotations set on the field
+   * @since 1.4
+   */
+  public Collection<Annotation> getAnnotations() {
+    return Arrays.asList(field.getAnnotations());
+  }
+
+  /**
+   * Returns {@code true} if the field is defined with the {@code modifier}.
+   *
+   * <p>This method is meant to be called as:
+   *
+   * <pre class="code">
+   * boolean hasPublicModifier = fieldAttribute.hasModifier(java.lang.reflect.Modifier.PUBLIC);
+   * </pre>
+   *
+   * @see java.lang.reflect.Modifier
+   */
+  public boolean hasModifier(int modifier) {
+    return (field.getModifiers() & modifier) != 0;
+  }
+
+  @Override
+  public String toString() {
+    return field.toString();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/FieldNamingPolicy.java b/gson/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6d452d1a939d1aa164f87cd45a53698e9b9bceb
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
@@ -0,0 +1,213 @@
+/*
+ * 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;
+
+import java.lang.reflect.Field;
+import java.util.Locale;
+
+/**
+ * An enumeration that defines a few standard naming conventions for JSON field names. This
+ * enumeration should be used in conjunction with {@link com.google.gson.GsonBuilder} to configure a
+ * {@link com.google.gson.Gson} instance to properly translate Java field names into the desired
+ * JSON field names.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public enum FieldNamingPolicy implements FieldNamingStrategy {
+
+  /** Using this naming policy with Gson will ensure that the field name is unchanged. */
+  IDENTITY() {
+    @Override
+    public String translateName(Field f) {
+      return f.getName();
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will ensure that the first "letter" of the Java field name
+   * is capitalized when serialized to its JSON form.
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; SomeFieldName
+   *   <li>_someFieldName ---&gt; _SomeFieldName
+   * </ul>
+   */
+  UPPER_CAMEL_CASE() {
+    @Override
+    public String translateName(Field f) {
+      return upperCaseFirstLetter(f.getName());
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will ensure that the first "letter" of the Java field name
+   * is capitalized when serialized to its JSON form and the words will be separated by a space.
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; Some Field Name
+   *   <li>_someFieldName ---&gt; _Some Field Name
+   * </ul>
+   *
+   * @since 1.4
+   */
+  UPPER_CAMEL_CASE_WITH_SPACES() {
+    @Override
+    public String translateName(Field f) {
+      return upperCaseFirstLetter(separateCamelCase(f.getName(), ' '));
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will modify the Java Field name from its camel cased form to
+   * an upper case field name where each word is separated by an underscore (_).
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; SOME_FIELD_NAME
+   *   <li>_someFieldName ---&gt; _SOME_FIELD_NAME
+   *   <li>aStringField ---&gt; A_STRING_FIELD
+   *   <li>aURL ---&gt; A_U_R_L
+   * </ul>
+   *
+   * @since 2.9.0
+   */
+  UPPER_CASE_WITH_UNDERSCORES() {
+    @Override
+    public String translateName(Field f) {
+      return separateCamelCase(f.getName(), '_').toUpperCase(Locale.ENGLISH);
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will modify the Java Field name from its camel cased form to
+   * a lower case field name where each word is separated by an underscore (_).
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; some_field_name
+   *   <li>_someFieldName ---&gt; _some_field_name
+   *   <li>aStringField ---&gt; a_string_field
+   *   <li>aURL ---&gt; a_u_r_l
+   * </ul>
+   */
+  LOWER_CASE_WITH_UNDERSCORES() {
+    @Override
+    public String translateName(Field f) {
+      return separateCamelCase(f.getName(), '_').toLowerCase(Locale.ENGLISH);
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will modify the Java Field name from its camel cased form to
+   * a lower case field name where each word is separated by a dash (-).
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; some-field-name
+   *   <li>_someFieldName ---&gt; _some-field-name
+   *   <li>aStringField ---&gt; a-string-field
+   *   <li>aURL ---&gt; a-u-r-l
+   * </ul>
+   *
+   * 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.
+   *
+   * @since 1.4
+   */
+  LOWER_CASE_WITH_DASHES() {
+    @Override
+    public String translateName(Field f) {
+      return separateCamelCase(f.getName(), '-').toLowerCase(Locale.ENGLISH);
+    }
+  },
+
+  /**
+   * Using this naming policy with Gson will modify the Java Field name from its camel cased form to
+   * a lower case field name where each word is separated by a dot (.).
+   *
+   * <p>Here are a few examples of the form "Java Field Name" ---&gt; "JSON Field Name":
+   *
+   * <ul>
+   *   <li>someFieldName ---&gt; some.field.name
+   *   <li>_someFieldName ---&gt; _some.field.name
+   *   <li>aStringField ---&gt; a.string.field
+   *   <li>aURL ---&gt; a.u.r.l
+   * </ul>
+   *
+   * 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.4
+   */
+  LOWER_CASE_WITH_DOTS() {
+    @Override
+    public String translateName(Field f) {
+      return separateCamelCase(f.getName(), '.').toLowerCase(Locale.ENGLISH);
+    }
+  };
+
+  /**
+   * Converts the field name that uses camel-case define word separation into separate words that
+   * are separated by the provided {@code separator}.
+   */
+  static String separateCamelCase(String name, char separator) {
+    StringBuilder translation = new StringBuilder();
+    for (int i = 0, length = name.length(); i < length; i++) {
+      char character = name.charAt(i);
+      if (Character.isUpperCase(character) && translation.length() != 0) {
+        translation.append(separator);
+      }
+      translation.append(character);
+    }
+    return translation.toString();
+  }
+
+  /** Ensures the JSON field names begins with an upper case letter. */
+  static String upperCaseFirstLetter(String s) {
+    int length = s.length();
+    for (int i = 0; i < length; i++) {
+      char c = s.charAt(i);
+      if (Character.isLetter(c)) {
+        if (Character.isUpperCase(c)) {
+          return s;
+        }
+
+        char uppercased = Character.toUpperCase(c);
+        // For leading letter only need one substring
+        if (i == 0) {
+          return uppercased + s.substring(1);
+        } else {
+          return s.substring(0, i) + uppercased + s.substring(i + 1);
+        }
+      }
+    }
+
+    return s;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/FieldNamingStrategy.java b/gson/gson/src/main/java/com/google/gson/FieldNamingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..541588696e45aa0f9ec4d38b7e1bacae27c7bc2c
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/FieldNamingStrategy.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import java.lang.reflect.Field;
+
+/**
+ * A mechanism for providing custom field naming in Gson. This allows the client code to translate
+ * field names into a particular convention that is not supported as a normal Java field declaration
+ * rules. For example, Java does not support "-" characters in a field name.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.3
+ */
+public interface FieldNamingStrategy {
+
+  /**
+   * Translates the field name into its JSON field name representation.
+   *
+   * @param f the field object that we are translating
+   * @return the translated field name.
+   * @since 1.3
+   */
+  public String translateName(Field f);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/FormattingStyle.java b/gson/gson/src/main/java/com/google/gson/FormattingStyle.java
new file mode 100644
index 0000000000000000000000000000000000000000..11b9490812d26a1aba7227d36ae53e0dcee5bb98
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/FormattingStyle.java
@@ -0,0 +1,147 @@
+/*
+ * 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;
+
+import com.google.gson.stream.JsonWriter;
+import java.util.Objects;
+
+/**
+ * A class used to control what the serialization output looks like.
+ *
+ * <p>It currently has the following configuration methods, but more methods might be added in the
+ * future:
+ *
+ * <ul>
+ *   <li>{@link #withNewline(String)}
+ *   <li>{@link #withIndent(String)}
+ *   <li>{@link #withSpaceAfterSeparators(boolean)}
+ * </ul>
+ *
+ * @see GsonBuilder#setFormattingStyle(FormattingStyle)
+ * @see JsonWriter#setFormattingStyle(FormattingStyle)
+ * @see <a href="https://en.wikipedia.org/wiki/Newline">Wikipedia Newline article</a>
+ * @since $next-version$
+ */
+public class FormattingStyle {
+  private final String newline;
+  private final String indent;
+  private final boolean spaceAfterSeparators;
+
+  /**
+   * The default compact formatting style:
+   *
+   * <ul>
+   *   <li>no newline
+   *   <li>no indent
+   *   <li>no space after {@code ','} and {@code ':'}
+   * </ul>
+   */
+  public static final FormattingStyle COMPACT = new FormattingStyle("", "", false);
+
+  /**
+   * The default pretty printing formatting style:
+   *
+   * <ul>
+   *   <li>{@code "\n"} as newline
+   *   <li>two spaces as indent
+   *   <li>a space between {@code ':'} and the subsequent value
+   * </ul>
+   */
+  public static final FormattingStyle PRETTY = new FormattingStyle("\n", "  ", true);
+
+  private FormattingStyle(String newline, String indent, boolean spaceAfterSeparators) {
+    Objects.requireNonNull(newline, "newline == null");
+    Objects.requireNonNull(indent, "indent == null");
+    if (!newline.matches("[\r\n]*")) {
+      throw new IllegalArgumentException(
+          "Only combinations of \\n and \\r are allowed in newline.");
+    }
+    if (!indent.matches("[ \t]*")) {
+      throw new IllegalArgumentException(
+          "Only combinations of spaces and tabs are allowed in indent.");
+    }
+    this.newline = newline;
+    this.indent = indent;
+    this.spaceAfterSeparators = spaceAfterSeparators;
+  }
+
+  /**
+   * Creates a {@link FormattingStyle} with the specified newline setting.
+   *
+   * <p>It can be used to accommodate certain OS convention, for example hardcode {@code "\n"} for
+   * Linux and macOS, {@code "\r\n"} for Windows, or call {@link java.lang.System#lineSeparator()}
+   * to match the current OS.
+   *
+   * <p>Only combinations of {@code \n} and {@code \r} are allowed.
+   *
+   * @param newline the string value that will be used as newline.
+   * @return a newly created {@link FormattingStyle}
+   */
+  public FormattingStyle withNewline(String newline) {
+    return new FormattingStyle(newline, this.indent, this.spaceAfterSeparators);
+  }
+
+  /**
+   * Creates a {@link FormattingStyle} with the specified indent string.
+   *
+   * <p>Only combinations of spaces and tabs allowed in indent.
+   *
+   * @param indent the string value that will be used as indent.
+   * @return a newly created {@link FormattingStyle}
+   */
+  public FormattingStyle withIndent(String indent) {
+    return new FormattingStyle(this.newline, indent, this.spaceAfterSeparators);
+  }
+
+  /**
+   * Creates a {@link FormattingStyle} which either uses a space after the separators {@code ','}
+   * and {@code ':'} in the JSON output, or not.
+   *
+   * <p>This setting has no effect on the {@linkplain #withNewline(String) configured newline}. If a
+   * non-empty newline is configured, it will always be added after {@code ','} and no space is
+   * added after the {@code ','} in that case.
+   *
+   * @param spaceAfterSeparators whether to output a space after {@code ','} and {@code ':'}.
+   * @return a newly created {@link FormattingStyle}
+   */
+  public FormattingStyle withSpaceAfterSeparators(boolean spaceAfterSeparators) {
+    return new FormattingStyle(this.newline, this.indent, spaceAfterSeparators);
+  }
+
+  /**
+   * Returns the string value that will be used as a newline.
+   *
+   * @return the newline value.
+   */
+  public String getNewline() {
+    return this.newline;
+  }
+
+  /**
+   * Returns the string value that will be used as indent.
+   *
+   * @return the indent value.
+   */
+  public String getIndent() {
+    return this.indent;
+  }
+
+  /** Returns whether a space will be used after {@code ','} and {@code ':'}. */
+  public boolean usesSpaceAfterSeparators() {
+    return this.spaceAfterSeparators;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/Gson.java b/gson/gson/src/main/java/com/google/gson/Gson.java
new file mode 100644
index 0000000000000000000000000000000000000000..058d0bef823c629ca6264de512d5cc631547e968
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/Gson.java
@@ -0,0 +1,1532 @@
+/*
+ * 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;
+
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.Excluder;
+import com.google.gson.internal.GsonBuildConfig;
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.internal.Primitives;
+import com.google.gson.internal.Streams;
+import com.google.gson.internal.bind.ArrayTypeAdapter;
+import com.google.gson.internal.bind.CollectionTypeAdapterFactory;
+import com.google.gson.internal.bind.DefaultDateTypeAdapter;
+import com.google.gson.internal.bind.JsonAdapterAnnotationTypeAdapterFactory;
+import com.google.gson.internal.bind.JsonTreeReader;
+import com.google.gson.internal.bind.JsonTreeWriter;
+import com.google.gson.internal.bind.MapTypeAdapterFactory;
+import com.google.gson.internal.bind.NumberTypeAdapter;
+import com.google.gson.internal.bind.ObjectTypeAdapter;
+import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
+import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter;
+import com.google.gson.internal.bind.TypeAdapters;
+import com.google.gson.internal.sql.SqlTypesSupport;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicLongArray;
+
+/**
+ * This is the main class for using Gson. Gson is typically used by first constructing a Gson
+ * instance and then invoking {@link #toJson(Object)} or {@link #fromJson(String, Class)} methods on
+ * it. Gson instances are Thread-safe so you can reuse them freely across multiple threads.
+ *
+ * <p>You can create a Gson instance by invoking {@code new Gson()} if the default configuration is
+ * all you need. You can also use {@link GsonBuilder} to build a Gson instance with various
+ * configuration options such as versioning support, pretty printing, custom newline, custom indent,
+ * custom {@link JsonSerializer}s, {@link JsonDeserializer}s, and {@link InstanceCreator}s.
+ *
+ * <p>Here is an example of how Gson is used for a simple Class:
+ *
+ * <pre>
+ * Gson gson = new Gson(); // Or use new GsonBuilder().create();
+ * MyType target = new MyType();
+ * String json = gson.toJson(target); // serializes target to JSON
+ * MyType target2 = gson.fromJson(json, MyType.class); // deserializes json into target2
+ * </pre>
+ *
+ * <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>
+ * 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();
+ * // 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://github.com/google/gson/blob/main/UserGuide.md">Gson User Guide</a>
+ * for a more complete set of examples.
+ *
+ * <h2 id="default-lenient">JSON Strictness handling</h2>
+ *
+ * For legacy reasons most of the {@code Gson} methods allow JSON data which does not comply with
+ * the JSON specification when no explicit {@linkplain Strictness strictness} is set (the default).
+ * To specify the strictness of a {@code Gson} instance, you should set it through {@link
+ * GsonBuilder#setStrictness(Strictness)}.
+ *
+ * <p>For older Gson versions, which don't have the strictness mode API, the following workarounds
+ * can be used:
+ *
+ * <h3>Serialization</h3>
+ *
+ * <ol>
+ *   <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be serialized
+ *   <li>When using an existing {@code JsonWriter}, manually apply the writer settings of this
+ *       {@code Gson} instance listed by {@link #newJsonWriter(Writer)}.<br>
+ *       Otherwise, when not using an existing {@code JsonWriter}, use {@link
+ *       #newJsonWriter(Writer)} to construct one.
+ *   <li>Call {@link TypeAdapter#write(JsonWriter, Object)}
+ * </ol>
+ *
+ * <h3>Deserialization</h3>
+ *
+ * <ol>
+ *   <li>Use {@link #getAdapter(Class)} to obtain the adapter for the type to be deserialized
+ *   <li>When using an existing {@code JsonReader}, manually apply the reader settings of this
+ *       {@code Gson} instance listed by {@link #newJsonReader(Reader)}.<br>
+ *       Otherwise, when not using an existing {@code JsonReader}, use {@link
+ *       #newJsonReader(Reader)} to construct one.
+ *   <li>Call {@link TypeAdapter#read(JsonReader)}
+ *   <li>Call {@link JsonReader#peek()} and verify that the result is {@link JsonToken#END_DOCUMENT}
+ *       to make sure there is no trailing data
+ * </ol>
+ *
+ * Note that the {@code JsonReader} created this way is only 'legacy strict', it mostly adheres to
+ * the JSON specification but allows small deviations. See {@link
+ * JsonReader#setStrictness(Strictness)} for details.
+ *
+ * @see TypeToken
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @author Jesse Wilson
+ */
+public final class Gson {
+
+  static final boolean DEFAULT_JSON_NON_EXECUTABLE = false;
+  // Strictness of `null` is the legacy mode where some Gson APIs are always lenient
+  static final Strictness DEFAULT_STRICTNESS = null;
+  static final FormattingStyle DEFAULT_FORMATTING_STYLE = FormattingStyle.COMPACT;
+  static final boolean DEFAULT_ESCAPE_HTML = true;
+  static final boolean DEFAULT_SERIALIZE_NULLS = false;
+  static final boolean DEFAULT_COMPLEX_MAP_KEYS = false;
+  static final boolean DEFAULT_SPECIALIZE_FLOAT_VALUES = false;
+  static final boolean DEFAULT_USE_JDK_UNSAFE = true;
+  static final String DEFAULT_DATE_PATTERN = null;
+  static final FieldNamingStrategy DEFAULT_FIELD_NAMING_STRATEGY = FieldNamingPolicy.IDENTITY;
+  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 String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
+
+  /**
+   * This thread local guards against reentrant calls to {@link #getAdapter(TypeToken)}. In certain
+   * object graphs, creating an adapter for a type may recursively require an adapter for the same
+   * type! Without intervention, the recursive lookup would stack overflow. We cheat by returning a
+   * proxy type adapter, {@link FutureTypeAdapter}, which is wired up once the initial adapter has
+   * been created.
+   *
+   * <p>The map stores the type adapters for ongoing {@code getAdapter} calls, with the type token
+   * provided to {@code getAdapter} as key and either {@code FutureTypeAdapter} or a regular {@code
+   * TypeAdapter} as value.
+   */
+  @SuppressWarnings("ThreadLocalUsage")
+  private final ThreadLocal<Map<TypeToken<?>, TypeAdapter<?>>> threadLocalAdapterResults =
+      new ThreadLocal<>();
+
+  private final ConcurrentMap<TypeToken<?>, TypeAdapter<?>> typeTokenCache =
+      new ConcurrentHashMap<>();
+
+  private final ConstructorConstructor constructorConstructor;
+  private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
+
+  final List<TypeAdapterFactory> factories;
+
+  final Excluder excluder;
+  final FieldNamingStrategy fieldNamingStrategy;
+  final Map<Type, InstanceCreator<?>> instanceCreators;
+  final boolean serializeNulls;
+  final boolean complexMapKeySerialization;
+  final boolean generateNonExecutableJson;
+  final boolean htmlSafe;
+  final FormattingStyle formattingStyle;
+  final Strictness strictness;
+  final boolean serializeSpecialFloatingPointValues;
+  final boolean useJdkUnsafe;
+  final String datePattern;
+  final int dateStyle;
+  final int timeStyle;
+  final LongSerializationPolicy longSerializationPolicy;
+  final List<TypeAdapterFactory> builderFactories;
+  final List<TypeAdapterFactory> builderHierarchyFactories;
+  final ToNumberStrategy objectToNumberStrategy;
+  final ToNumberStrategy numberToNumberStrategy;
+  final List<ReflectionAccessFilter> reflectionFilters;
+
+  /**
+   * Constructs a Gson object with default configuration. The default configuration has the
+   * following settings:
+   *
+   * <ul>
+   *   <li>The JSON generated by {@code toJson} methods is in compact representation. This means
+   *       that all the unneeded white-space is removed. You can change this behavior with {@link
+   *       GsonBuilder#setPrettyPrinting()}.
+   *   <li>When the JSON generated contains more than one line, the kind of newline and indent to
+   *       use can be configured with {@link GsonBuilder#setFormattingStyle(FormattingStyle)}.
+   *   <li>The generated JSON omits all the fields that are null. Note that nulls in arrays are kept
+   *       as is since an array is an ordered list. Moreover, if a field is not null, but its
+   *       generated JSON is empty, the field is kept. You can configure Gson to serialize null
+   *       values by setting {@link GsonBuilder#serializeNulls()}.
+   *   <li>Gson provides default serialization and deserialization for Enums, {@link Map}, {@link
+   *       java.net.URL}, {@link java.net.URI}, {@link java.util.Locale}, {@link java.util.Date},
+   *       {@link java.math.BigDecimal}, and {@link java.math.BigInteger} classes. If you would
+   *       prefer to change the default representation, you can do so by registering a type adapter
+   *       through {@link GsonBuilder#registerTypeAdapter(Type, Object)}.
+   *   <li>The default Date format is same as {@link java.text.DateFormat#DEFAULT}. This format
+   *       ignores the millisecond portion of the date during serialization. You can change this by
+   *       invoking {@link GsonBuilder#setDateFormat(int)} or {@link
+   *       GsonBuilder#setDateFormat(String)}.
+   *   <li>By default, Gson ignores the {@link com.google.gson.annotations.Expose} annotation. You
+   *       can enable Gson to serialize/deserialize only those fields marked with this annotation
+   *       through {@link GsonBuilder#excludeFieldsWithoutExposeAnnotation()}.
+   *   <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>The default field naming policy for the output JSON is same as in Java. So, a Java class
+   *       field {@code versionNumber} will be output as {@code "versionNumber"} in 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>By default, Gson excludes {@code transient} or {@code static} fields from consideration
+   *       for serialization and deserialization. You can change this behavior through {@link
+   *       GsonBuilder#excludeFieldsWithModifiers(int...)}.
+   *   <li>No explicit strictness is set. You can change this by calling {@link
+   *       GsonBuilder#setStrictness(Strictness)}.
+   * </ul>
+   */
+  public Gson() {
+    this(
+        Excluder.DEFAULT,
+        DEFAULT_FIELD_NAMING_STRATEGY,
+        Collections.<Type, InstanceCreator<?>>emptyMap(),
+        DEFAULT_SERIALIZE_NULLS,
+        DEFAULT_COMPLEX_MAP_KEYS,
+        DEFAULT_JSON_NON_EXECUTABLE,
+        DEFAULT_ESCAPE_HTML,
+        DEFAULT_FORMATTING_STYLE,
+        DEFAULT_STRICTNESS,
+        DEFAULT_SPECIALIZE_FLOAT_VALUES,
+        DEFAULT_USE_JDK_UNSAFE,
+        LongSerializationPolicy.DEFAULT,
+        DEFAULT_DATE_PATTERN,
+        DateFormat.DEFAULT,
+        DateFormat.DEFAULT,
+        Collections.<TypeAdapterFactory>emptyList(),
+        Collections.<TypeAdapterFactory>emptyList(),
+        Collections.<TypeAdapterFactory>emptyList(),
+        DEFAULT_OBJECT_TO_NUMBER_STRATEGY,
+        DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
+        Collections.<ReflectionAccessFilter>emptyList());
+  }
+
+  Gson(
+      Excluder excluder,
+      FieldNamingStrategy fieldNamingStrategy,
+      Map<Type, InstanceCreator<?>> instanceCreators,
+      boolean serializeNulls,
+      boolean complexMapKeySerialization,
+      boolean generateNonExecutableGson,
+      boolean htmlSafe,
+      FormattingStyle formattingStyle,
+      Strictness strictness,
+      boolean serializeSpecialFloatingPointValues,
+      boolean useJdkUnsafe,
+      LongSerializationPolicy longSerializationPolicy,
+      String datePattern,
+      int dateStyle,
+      int timeStyle,
+      List<TypeAdapterFactory> builderFactories,
+      List<TypeAdapterFactory> builderHierarchyFactories,
+      List<TypeAdapterFactory> factoriesToBeAdded,
+      ToNumberStrategy objectToNumberStrategy,
+      ToNumberStrategy numberToNumberStrategy,
+      List<ReflectionAccessFilter> reflectionFilters) {
+    this.excluder = excluder;
+    this.fieldNamingStrategy = fieldNamingStrategy;
+    this.instanceCreators = instanceCreators;
+    this.constructorConstructor =
+        new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters);
+    this.serializeNulls = serializeNulls;
+    this.complexMapKeySerialization = complexMapKeySerialization;
+    this.generateNonExecutableJson = generateNonExecutableGson;
+    this.htmlSafe = htmlSafe;
+    this.formattingStyle = formattingStyle;
+    this.strictness = strictness;
+    this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues;
+    this.useJdkUnsafe = useJdkUnsafe;
+    this.longSerializationPolicy = longSerializationPolicy;
+    this.datePattern = datePattern;
+    this.dateStyle = dateStyle;
+    this.timeStyle = timeStyle;
+    this.builderFactories = builderFactories;
+    this.builderHierarchyFactories = builderHierarchyFactories;
+    this.objectToNumberStrategy = objectToNumberStrategy;
+    this.numberToNumberStrategy = numberToNumberStrategy;
+    this.reflectionFilters = reflectionFilters;
+
+    List<TypeAdapterFactory> factories = new ArrayList<>();
+
+    // built-in type adapters that cannot be overridden
+    factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
+    factories.add(ObjectTypeAdapter.getFactory(objectToNumberStrategy));
+
+    // the excluder must precede all adapters that handle user-defined types
+    factories.add(excluder);
+
+    // users' type adapters
+    factories.addAll(factoriesToBeAdded);
+
+    // type adapters for basic platform types
+    factories.add(TypeAdapters.STRING_FACTORY);
+    factories.add(TypeAdapters.INTEGER_FACTORY);
+    factories.add(TypeAdapters.BOOLEAN_FACTORY);
+    factories.add(TypeAdapters.BYTE_FACTORY);
+    factories.add(TypeAdapters.SHORT_FACTORY);
+    TypeAdapter<Number> longAdapter = longAdapter(longSerializationPolicy);
+    factories.add(TypeAdapters.newFactory(long.class, Long.class, longAdapter));
+    factories.add(
+        TypeAdapters.newFactory(
+            double.class, Double.class, doubleAdapter(serializeSpecialFloatingPointValues)));
+    factories.add(
+        TypeAdapters.newFactory(
+            float.class, Float.class, floatAdapter(serializeSpecialFloatingPointValues)));
+    factories.add(NumberTypeAdapter.getFactory(numberToNumberStrategy));
+    factories.add(TypeAdapters.ATOMIC_INTEGER_FACTORY);
+    factories.add(TypeAdapters.ATOMIC_BOOLEAN_FACTORY);
+    factories.add(TypeAdapters.newFactory(AtomicLong.class, atomicLongAdapter(longAdapter)));
+    factories.add(
+        TypeAdapters.newFactory(AtomicLongArray.class, atomicLongArrayAdapter(longAdapter)));
+    factories.add(TypeAdapters.ATOMIC_INTEGER_ARRAY_FACTORY);
+    factories.add(TypeAdapters.CHARACTER_FACTORY);
+    factories.add(TypeAdapters.STRING_BUILDER_FACTORY);
+    factories.add(TypeAdapters.STRING_BUFFER_FACTORY);
+    factories.add(TypeAdapters.newFactory(BigDecimal.class, TypeAdapters.BIG_DECIMAL));
+    factories.add(TypeAdapters.newFactory(BigInteger.class, TypeAdapters.BIG_INTEGER));
+    // Add adapter for LazilyParsedNumber because user can obtain it from Gson and then try to
+    // serialize it again
+    factories.add(
+        TypeAdapters.newFactory(LazilyParsedNumber.class, TypeAdapters.LAZILY_PARSED_NUMBER));
+    factories.add(TypeAdapters.URL_FACTORY);
+    factories.add(TypeAdapters.URI_FACTORY);
+    factories.add(TypeAdapters.UUID_FACTORY);
+    factories.add(TypeAdapters.CURRENCY_FACTORY);
+    factories.add(TypeAdapters.LOCALE_FACTORY);
+    factories.add(TypeAdapters.INET_ADDRESS_FACTORY);
+    factories.add(TypeAdapters.BIT_SET_FACTORY);
+    factories.add(DefaultDateTypeAdapter.DEFAULT_STYLE_FACTORY);
+    factories.add(TypeAdapters.CALENDAR_FACTORY);
+
+    if (SqlTypesSupport.SUPPORTS_SQL_TYPES) {
+      factories.add(SqlTypesSupport.TIME_FACTORY);
+      factories.add(SqlTypesSupport.DATE_FACTORY);
+      factories.add(SqlTypesSupport.TIMESTAMP_FACTORY);
+    }
+
+    factories.add(ArrayTypeAdapter.FACTORY);
+    factories.add(TypeAdapters.CLASS_FACTORY);
+
+    // type adapters for composite and user-defined types
+    factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
+    factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
+    this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
+    factories.add(jsonAdapterFactory);
+    factories.add(TypeAdapters.ENUM_FACTORY);
+    factories.add(
+        new ReflectiveTypeAdapterFactory(
+            constructorConstructor,
+            fieldNamingStrategy,
+            excluder,
+            jsonAdapterFactory,
+            reflectionFilters));
+
+    this.factories = Collections.unmodifiableList(factories);
+  }
+
+  /**
+   * Returns a new GsonBuilder containing all custom factories and configuration used by the current
+   * instance.
+   *
+   * @return a GsonBuilder instance.
+   * @since 2.8.3
+   */
+  public GsonBuilder newBuilder() {
+    return new GsonBuilder(this);
+  }
+
+  /**
+   * @deprecated This method by accident exposes an internal Gson class; it might be removed in a
+   *     future version.
+   */
+  @Deprecated
+  public Excluder excluder() {
+    return excluder;
+  }
+
+  /**
+   * Returns the field naming strategy used by this Gson instance.
+   *
+   * @see GsonBuilder#setFieldNamingStrategy(FieldNamingStrategy)
+   */
+  public FieldNamingStrategy fieldNamingStrategy() {
+    return fieldNamingStrategy;
+  }
+
+  /**
+   * Returns whether this Gson instance is serializing JSON object properties with {@code null}
+   * values, or just omits them.
+   *
+   * @see GsonBuilder#serializeNulls()
+   */
+  public boolean serializeNulls() {
+    return serializeNulls;
+  }
+
+  /**
+   * Returns whether this Gson instance produces JSON output which is HTML-safe, that means all HTML
+   * characters are escaped.
+   *
+   * @see GsonBuilder#disableHtmlEscaping()
+   */
+  public boolean htmlSafe() {
+    return htmlSafe;
+  }
+
+  private TypeAdapter<Number> doubleAdapter(boolean serializeSpecialFloatingPointValues) {
+    if (serializeSpecialFloatingPointValues) {
+      return TypeAdapters.DOUBLE;
+    }
+    return new TypeAdapter<Number>() {
+      @Override
+      public Double read(JsonReader in) throws IOException {
+        if (in.peek() == JsonToken.NULL) {
+          in.nextNull();
+          return null;
+        }
+        return in.nextDouble();
+      }
+
+      @Override
+      public void write(JsonWriter out, Number value) throws IOException {
+        if (value == null) {
+          out.nullValue();
+          return;
+        }
+        double doubleValue = value.doubleValue();
+        checkValidFloatingPoint(doubleValue);
+        out.value(doubleValue);
+      }
+    };
+  }
+
+  private TypeAdapter<Number> floatAdapter(boolean serializeSpecialFloatingPointValues) {
+    if (serializeSpecialFloatingPointValues) {
+      return TypeAdapters.FLOAT;
+    }
+    return new TypeAdapter<Number>() {
+      @Override
+      public Float read(JsonReader in) throws IOException {
+        if (in.peek() == JsonToken.NULL) {
+          in.nextNull();
+          return null;
+        }
+        return (float) in.nextDouble();
+      }
+
+      @Override
+      public void write(JsonWriter out, Number value) throws IOException {
+        if (value == null) {
+          out.nullValue();
+          return;
+        }
+        float floatValue = value.floatValue();
+        checkValidFloatingPoint(floatValue);
+        // 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);
+      }
+    };
+  }
+
+  static void checkValidFloatingPoint(double value) {
+    if (Double.isNaN(value) || Double.isInfinite(value)) {
+      throw new IllegalArgumentException(
+          value
+              + " is not a valid double value as per JSON specification. To override this"
+              + " behavior, use GsonBuilder.serializeSpecialFloatingPointValues() method.");
+    }
+  }
+
+  private static TypeAdapter<Number> longAdapter(LongSerializationPolicy longSerializationPolicy) {
+    if (longSerializationPolicy == LongSerializationPolicy.DEFAULT) {
+      return TypeAdapters.LONG;
+    }
+    return new TypeAdapter<Number>() {
+      @Override
+      public Number read(JsonReader in) throws IOException {
+        if (in.peek() == JsonToken.NULL) {
+          in.nextNull();
+          return null;
+        }
+        return in.nextLong();
+      }
+
+      @Override
+      public void write(JsonWriter out, Number value) throws IOException {
+        if (value == null) {
+          out.nullValue();
+          return;
+        }
+        out.value(value.toString());
+      }
+    };
+  }
+
+  private static TypeAdapter<AtomicLong> atomicLongAdapter(final TypeAdapter<Number> longAdapter) {
+    return new TypeAdapter<AtomicLong>() {
+      @Override
+      public void write(JsonWriter out, AtomicLong value) throws IOException {
+        longAdapter.write(out, value.get());
+      }
+
+      @Override
+      public AtomicLong read(JsonReader in) throws IOException {
+        Number value = longAdapter.read(in);
+        return new AtomicLong(value.longValue());
+      }
+    }.nullSafe();
+  }
+
+  private static TypeAdapter<AtomicLongArray> atomicLongArrayAdapter(
+      final TypeAdapter<Number> longAdapter) {
+    return new TypeAdapter<AtomicLongArray>() {
+      @Override
+      public void write(JsonWriter out, AtomicLongArray value) throws IOException {
+        out.beginArray();
+        for (int i = 0, length = value.length(); i < length; i++) {
+          longAdapter.write(out, value.get(i));
+        }
+        out.endArray();
+      }
+
+      @Override
+      public AtomicLongArray read(JsonReader in) throws IOException {
+        List<Long> list = new ArrayList<>();
+        in.beginArray();
+        while (in.hasNext()) {
+          long value = longAdapter.read(in).longValue();
+          list.add(value);
+        }
+        in.endArray();
+        int length = list.size();
+        AtomicLongArray array = new AtomicLongArray(length);
+        for (int i = 0; i < length; ++i) {
+          array.set(i, list.get(i));
+        }
+        return array;
+      }
+    }.nullSafe();
+  }
+
+  /**
+   * Returns the type adapter for {@code type}.
+   *
+   * <p>When calling this method concurrently from multiple threads and requesting an adapter for
+   * the same type this method may return different {@code TypeAdapter} instances. However, that
+   * should normally not be an issue because {@code TypeAdapter} implementations are supposed to be
+   * stateless.
+   *
+   * @throws IllegalArgumentException if this Gson instance cannot serialize and deserialize {@code
+   *     type}.
+   */
+  public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
+    Objects.requireNonNull(type, "type must not be null");
+    TypeAdapter<?> cached = typeTokenCache.get(type);
+    if (cached != null) {
+      @SuppressWarnings("unchecked")
+      TypeAdapter<T> adapter = (TypeAdapter<T>) cached;
+      return adapter;
+    }
+
+    Map<TypeToken<?>, TypeAdapter<?>> threadCalls = threadLocalAdapterResults.get();
+    boolean isInitialAdapterRequest = false;
+    if (threadCalls == null) {
+      threadCalls = new HashMap<>();
+      threadLocalAdapterResults.set(threadCalls);
+      isInitialAdapterRequest = true;
+    } else {
+      // the key and value type parameters always agree
+      @SuppressWarnings("unchecked")
+      TypeAdapter<T> ongoingCall = (TypeAdapter<T>) threadCalls.get(type);
+      if (ongoingCall != null) {
+        return ongoingCall;
+      }
+    }
+
+    TypeAdapter<T> candidate = null;
+    try {
+      FutureTypeAdapter<T> call = new FutureTypeAdapter<>();
+      threadCalls.put(type, call);
+
+      for (TypeAdapterFactory factory : factories) {
+        candidate = factory.create(this, type);
+        if (candidate != null) {
+          call.setDelegate(candidate);
+          // Replace future adapter with actual adapter
+          threadCalls.put(type, candidate);
+          break;
+        }
+      }
+    } finally {
+      if (isInitialAdapterRequest) {
+        threadLocalAdapterResults.remove();
+      }
+    }
+
+    if (candidate == null) {
+      throw new IllegalArgumentException(
+          "GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
+    }
+
+    if (isInitialAdapterRequest) {
+      /*
+       * Publish resolved adapters to all threads
+       * Can only do this for the initial request because cyclic dependency TypeA -> TypeB -> TypeA
+       * would otherwise publish adapter for TypeB which uses not yet resolved adapter for TypeA
+       * See https://github.com/google/gson/issues/625
+       */
+      typeTokenCache.putAll(threadCalls);
+    }
+    return candidate;
+  }
+
+  /**
+   * Returns the type adapter for {@code type}.
+   *
+   * @throws IllegalArgumentException if this Gson instance cannot serialize and deserialize {@code
+   *     type}.
+   */
+  public <T> TypeAdapter<T> getAdapter(Class<T> type) {
+    return getAdapter(TypeToken.get(type));
+  }
+
+  /**
+   * This method is used to get an alternate type adapter for the specified type. This is used to
+   * access a type adapter that is overridden by a {@link TypeAdapterFactory} that you may have
+   * registered. This feature is typically used when you want to register a type adapter that does a
+   * little bit of work but then delegates further processing to the Gson default type adapter. Here
+   * is an example:
+   *
+   * <p>Let's say we want to write a type adapter that counts the number of objects being read from
+   * or written to JSON. We can achieve this by writing a type adapter factory that uses the {@code
+   * getDelegateAdapter} method:
+   *
+   * <pre>{@code
+   * class StatsTypeAdapterFactory implements TypeAdapterFactory {
+   *   public int numReads = 0;
+   *   public int numWrites = 0;
+   *   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+   *     final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+   *     return new TypeAdapter<T>() {
+   *       public void write(JsonWriter out, T value) throws IOException {
+   *         ++numWrites;
+   *         delegate.write(out, value);
+   *       }
+   *       public T read(JsonReader in) throws IOException {
+   *         ++numReads;
+   *         return delegate.read(in);
+   *       }
+   *     };
+   *   }
+   * }
+   * }</pre>
+   *
+   * This factory can now be used like this:
+   *
+   * <pre>{@code
+   * StatsTypeAdapterFactory stats = new StatsTypeAdapterFactory();
+   * Gson gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
+   * // Call gson.toJson() and fromJson methods on objects
+   * System.out.println("Num JSON reads: " + stats.numReads);
+   * System.out.println("Num JSON writes: " + stats.numWrites);
+   * }</pre>
+   *
+   * Note that this call will skip all factories registered before {@code skipPast}. In case of
+   * multiple TypeAdapterFactories registered it is up to the caller of this function to ensure that
+   * the order of registration does not prevent this method from reaching a factory they would
+   * expect to reply from this call. Note that since you can not override the type adapter factories
+   * for some types, see {@link GsonBuilder#registerTypeAdapter(Type, Object)}, our stats factory
+   * will not count the number of instances of those types that will be read or written.
+   *
+   * <p>If {@code skipPast} is a factory which has neither been registered on the {@link
+   * GsonBuilder} nor specified with the {@link JsonAdapter @JsonAdapter} annotation on a class,
+   * then this method behaves as if {@link #getAdapter(TypeToken)} had been called. This also means
+   * that for fields with {@code @JsonAdapter} annotation this method behaves normally like {@code
+   * getAdapter} (except for corner cases where a custom {@link InstanceCreator} is used to create
+   * an instance of the factory).
+   *
+   * @param skipPast The type adapter factory that needs to be skipped while searching for a
+   *     matching type adapter. In most cases, you should just pass <i>this</i> (the type adapter
+   *     factory from where {@code getDelegateAdapter} method is being invoked).
+   * @param type Type for which the delegate adapter is being searched for.
+   * @since 2.2
+   */
+  public <T> TypeAdapter<T> getDelegateAdapter(TypeAdapterFactory skipPast, TypeToken<T> type) {
+    Objects.requireNonNull(skipPast, "skipPast must not be null");
+    Objects.requireNonNull(type, "type must not be null");
+
+    if (jsonAdapterFactory.isClassJsonAdapterFactory(type, skipPast)) {
+      skipPast = jsonAdapterFactory;
+    }
+
+    boolean skipPastFound = false;
+    for (TypeAdapterFactory factory : factories) {
+      if (!skipPastFound) {
+        if (factory == skipPast) {
+          skipPastFound = true;
+        }
+        continue;
+      }
+
+      TypeAdapter<T> candidate = factory.create(this, type);
+      if (candidate != null) {
+        return candidate;
+      }
+    }
+
+    if (skipPastFound) {
+      throw new IllegalArgumentException("GSON cannot serialize or deserialize " + type);
+    } else {
+      // Probably a factory from @JsonAdapter on a field
+      return getAdapter(type);
+    }
+  }
+
+  /**
+   * This method serializes the specified object into its equivalent representation as a tree of
+   * {@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 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
+   * @return JSON representation of {@code src}.
+   * @since 1.4
+   * @see #toJsonTree(Object, Type)
+   */
+  public JsonElement toJsonTree(Object src) {
+    if (src == null) {
+      return JsonNull.INSTANCE;
+    }
+    return toJsonTree(src, src.getClass());
+  }
+
+  /**
+   * This method serializes the specified object, including those of generic types, into its
+   * equivalent representation as a tree of {@link JsonElement}s. This method must be used if the
+   * specified object is a generic type. For non-generic objects, use {@link #toJsonTree(Object)}
+   * instead.
+   *
+   * @param src the object for which JSON representation is to be created
+   * @param typeOfSrc 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 typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
+   * </pre>
+   *
+   * @return JSON representation of {@code src}.
+   * @since 1.4
+   * @see #toJsonTree(Object)
+   */
+  public JsonElement toJsonTree(Object src, Type typeOfSrc) {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    toJson(src, typeOfSrc, writer);
+    return writer.get();
+  }
+
+  /**
+   * 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 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
+   * @return JSON representation of {@code src}.
+   * @see #toJson(Object, Appendable)
+   * @see #toJson(Object, Type)
+   */
+  public String toJson(Object src) {
+    if (src == null) {
+      return toJson(JsonNull.INSTANCE);
+    }
+    return toJson(src, src.getClass());
+  }
+
+  /**
+   * 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)} instead. If you want to write out
+   * the object to a {@link Appendable}, use {@link #toJson(Object, Type, 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 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 typeOfSrc = new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}.getType();
+   * </pre>
+   *
+   * @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();
+    toJson(src, typeOfSrc, writer);
+    return writer.toString();
+  }
+
+  /**
+   * 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 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
+   * @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) {
+      toJson(src, src.getClass(), writer);
+    } else {
+      toJson(JsonNull.INSTANCE, writer);
+    }
+  }
+
+  /**
+   * This method serializes the specified object, including those of generic types, into its
+   * 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 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 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
+   * @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 {
+      JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer));
+      toJson(src, typeOfSrc, jsonWriter);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+  }
+
+  /**
+   * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to {@code writer}.
+   *
+   * <p>If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness)
+   * explicit strictness setting}, this setting will be used for writing the JSON regardless of the
+   * {@linkplain JsonWriter#getStrictness() strictness} of the provided {@link JsonWriter}. For
+   * legacy reasons, if the {@code Gson} instance has no explicit strictness setting and the writer
+   * does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link
+   * Strictness#LENIENT} mode.<br>
+   * Note that in all cases the old strictness setting of the writer will be restored when this
+   * method returns.
+   *
+   * <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
+   * (configured by the {@link GsonBuilder}) are applied, and the original settings of the writer
+   * are restored once this method returns.
+   *
+   * @param src the object for which JSON representation is to be created
+   * @param typeOfSrc the type of the object 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
+   */
+  public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException {
+    @SuppressWarnings("unchecked")
+    TypeAdapter<Object> adapter = (TypeAdapter<Object>) getAdapter(TypeToken.get(typeOfSrc));
+
+    Strictness oldStrictness = writer.getStrictness();
+    if (this.strictness != null) {
+      writer.setStrictness(this.strictness);
+    } else if (writer.getStrictness() != Strictness.STRICT) {
+      writer.setStrictness(Strictness.LENIENT);
+    }
+
+    boolean oldHtmlSafe = writer.isHtmlSafe();
+    boolean oldSerializeNulls = writer.getSerializeNulls();
+
+    writer.setHtmlSafe(htmlSafe);
+    writer.setSerializeNulls(serializeNulls);
+    try {
+      adapter.write(writer, src);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    } catch (AssertionError e) {
+      throw new AssertionError(
+          "AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);
+    } finally {
+      writer.setStrictness(oldStrictness);
+      writer.setHtmlSafe(oldHtmlSafe);
+      writer.setSerializeNulls(oldSerializeNulls);
+    }
+  }
+
+  /**
+   * Converts a tree of {@link JsonElement}s into its equivalent JSON representation.
+   *
+   * @param jsonElement root of a tree of {@link JsonElement}s
+   * @return JSON String representation of the tree.
+   * @since 1.4
+   */
+  public String toJson(JsonElement jsonElement) {
+    StringWriter writer = new StringWriter();
+    toJson(jsonElement, writer);
+    return writer.toString();
+  }
+
+  /**
+   * 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
+   * @throws JsonIOException if there was a problem writing to the writer
+   * @since 1.4
+   */
+  public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOException {
+    try {
+      JsonWriter jsonWriter = newJsonWriter(Streams.writerForAppendable(writer));
+      toJson(jsonElement, jsonWriter);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+  }
+
+  /**
+   * Writes the JSON for {@code jsonElement} to {@code writer}.
+   *
+   * <p>If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness)
+   * explicit strictness setting}, this setting will be used for writing the JSON regardless of the
+   * {@linkplain JsonWriter#getStrictness() strictness} of the provided {@link JsonWriter}. For
+   * legacy reasons, if the {@code Gson} instance has no explicit strictness setting and the writer
+   * does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link
+   * Strictness#LENIENT} mode.<br>
+   * Note that in all cases the old strictness setting of the writer will be restored when this
+   * method returns.
+   *
+   * <p>The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance
+   * (configured by the {@link GsonBuilder}) are applied, and the original settings of the writer
+   * are restored once this method returns.
+   *
+   * @param jsonElement the JSON element to be written
+   * @param writer the JSON writer to which the provided element will be written
+   * @throws JsonIOException if there was a problem writing to the writer
+   */
+  public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException {
+    Strictness oldStrictness = writer.getStrictness();
+    boolean oldHtmlSafe = writer.isHtmlSafe();
+    boolean oldSerializeNulls = writer.getSerializeNulls();
+
+    writer.setHtmlSafe(htmlSafe);
+    writer.setSerializeNulls(serializeNulls);
+
+    if (this.strictness != null) {
+      writer.setStrictness(this.strictness);
+    } else if (writer.getStrictness() != Strictness.STRICT) {
+      writer.setStrictness(Strictness.LENIENT);
+    }
+
+    try {
+      Streams.write(jsonElement, writer);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    } catch (AssertionError e) {
+      throw new AssertionError(
+          "AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);
+    } finally {
+      writer.setStrictness(oldStrictness);
+      writer.setHtmlSafe(oldHtmlSafe);
+      writer.setSerializeNulls(oldSerializeNulls);
+    }
+  }
+
+  /**
+   * Returns a new JSON writer configured for the settings on this Gson instance.
+   *
+   * <p>The following settings are considered:
+   *
+   * <ul>
+   *   <li>{@link GsonBuilder#disableHtmlEscaping()}
+   *   <li>{@link GsonBuilder#generateNonExecutableJson()}
+   *   <li>{@link GsonBuilder#serializeNulls()}
+   *   <li>{@link GsonBuilder#setStrictness(Strictness)}. If no {@linkplain
+   *       GsonBuilder#setStrictness(Strictness) explicit strictness has been set} the created
+   *       writer will have a strictness of {@link Strictness#LEGACY_STRICT}. Otherwise, the
+   *       strictness of the {@code Gson} instance will be used for the created writer.
+   *   <li>{@link GsonBuilder#setPrettyPrinting()}
+   *   <li>{@link GsonBuilder#setFormattingStyle(FormattingStyle)}
+   * </ul>
+   */
+  public JsonWriter newJsonWriter(Writer writer) throws IOException {
+    if (generateNonExecutableJson) {
+      writer.write(JSON_NON_EXECUTABLE_PREFIX);
+    }
+    JsonWriter jsonWriter = new JsonWriter(writer);
+    jsonWriter.setFormattingStyle(formattingStyle);
+    jsonWriter.setHtmlSafe(htmlSafe);
+    jsonWriter.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness);
+    jsonWriter.setSerializeNulls(serializeNulls);
+    return jsonWriter;
+  }
+
+  /**
+   * Returns a new JSON reader configured for the settings on this Gson instance.
+   *
+   * <p>The following settings are considered:
+   *
+   * <ul>
+   *   <li>{@link GsonBuilder#setStrictness(Strictness)}. If no {@linkplain
+   *       GsonBuilder#setStrictness(Strictness) explicit strictness has been set} the created
+   *       reader will have a strictness of {@link Strictness#LEGACY_STRICT}. Otherwise, the
+   *       strictness of the {@code Gson} instance will be used for the created reader.
+   * </ul>
+   */
+  public JsonReader newJsonReader(Reader reader) {
+    JsonReader jsonReader = new JsonReader(reader);
+    jsonReader.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness);
+    return jsonReader;
+  }
+
+  /**
+   * 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 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,
+   * 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. 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 classOfT the class of T
+   * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code
+   *     null} or if {@code json} is empty.
+   * @throws JsonSyntaxException if json is not a valid representation for an object of type
+   *     classOfT
+   * @see #fromJson(Reader, Class)
+   * @see #fromJson(String, TypeToken)
+   */
+  public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
+    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
+   * 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, 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. Use {@link #fromJson(JsonReader, Type)} if this behavior is not desired.
+   *
+   * @param <T> the type of the desired object
+   * @param json the string from which the object is to be deserialized
+   * @param typeOfT The specific genericized type of src
+   * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code
+   *     null} or if {@code json} is empty.
+   * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+   * @see #fromJson(Reader, Type)
+   * @see #fromJson(String, Class)
+   * @see #fromJson(String, TypeToken)
+   */
+  @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
+  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);
+    return fromJson(reader, typeOfT);
+  }
+
+  /**
+   * 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 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, 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. 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 classOfT the class of T
+   * @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 typeOfT
+   * @since 1.2
+   * @see #fromJson(String, Class)
+   * @see #fromJson(Reader, TypeToken)
+   */
+  public <T> T fromJson(Reader json, Class<T> classOfT)
+      throws JsonSyntaxException, JsonIOException {
+    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
+   * 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, 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(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
+   * @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 typeOfT
+   * @since 1.2
+   * @see #fromJson(String, Type)
+   * @see #fromJson(Reader, Class)
+   * @see #fromJson(Reader, TypeToken)
+   */
+  @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
+  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 = fromJson(jsonReader, typeOfT);
+    assertFullConsumption(object, jsonReader);
+    return object;
+  }
+
+  // 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 converts it to an object of type {@code
+   * typeOfT}. Returns {@code null}, if the {@code reader} is at EOF.
+   *
+   * <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.
+   *
+   * <p>If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness)
+   * explicit strictness setting}, this setting will be used for reading the JSON regardless of the
+   * {@linkplain JsonReader#getStrictness() strictness} of the provided {@link JsonReader}. For
+   * legacy reasons, if the {@code Gson} instance has no explicit strictness setting and the reader
+   * does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link
+   * Strictness#LENIENT} mode.<br>
+   * Note that in all cases the old strictness setting of the reader will be restored when 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
+   * @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", "TypeParameterUnusedInFormals"})
+  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>If the {@code Gson} instance has an {@linkplain GsonBuilder#setStrictness(Strictness)
+   * explicit strictness setting}, this setting will be used for reading the JSON regardless of the
+   * {@linkplain JsonReader#getStrictness() strictness} of the provided {@link JsonReader}. For
+   * legacy reasons, if the {@code Gson} instance has no explicit strictness setting and the reader
+   * does not have the strictness {@link Strictness#STRICT}, the JSON will be written in {@link
+   * Strictness#LENIENT} mode.<br>
+   * Note that in all cases the old strictness setting of the reader will be restored when 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;
+    Strictness oldStrictness = reader.getStrictness();
+
+    if (this.strictness != null) {
+      reader.setStrictness(this.strictness);
+    } else if (reader.getStrictness() != Strictness.STRICT) {
+      reader.setStrictness(Strictness.LENIENT);
+    }
+
+    try {
+      JsonToken unused = reader.peek();
+      isEmpty = false;
+      TypeAdapter<T> typeAdapter = getAdapter(typeOfT);
+      return typeAdapter.read(reader);
+    } catch (EOFException e) {
+      /*
+       * For compatibility with JSON 1.5 and earlier, we return null for empty
+       * documents instead of throwing.
+       */
+      if (isEmpty) {
+        return null;
+      }
+      throw new JsonSyntaxException(e);
+    } catch (IllegalStateException e) {
+      throw new JsonSyntaxException(e);
+    } catch (IOException e) {
+      // TODO(inder): Figure out whether it is indeed right to rethrow this as JsonSyntaxException
+      throw new JsonSyntaxException(e);
+    } catch (AssertionError e) {
+      throw new AssertionError(
+          "AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e);
+    } finally {
+      reader.setStrictness(oldStrictness);
+    }
+  }
+
+  /**
+   * 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 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, 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}
+   *     or if {@code json} is empty.
+   * @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 {
+    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
+   * 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", "TypeParameterUnusedInFormals"})
+  public <T> T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException {
+    return (T) fromJson(json, TypeToken.get(typeOfT));
+  }
+
+  /**
+   * This method deserializes the JSON read from the specified parse tree into an object of the
+   * specified type. This method is useful if the specified object is a generic type. For
+   * non-generic objects, use {@link #fromJson(JsonElement, Class)} instead.
+   *
+   * @param <T> the type of the desired object
+   * @param json the root of the parse tree of {@link JsonElement}s from which the object is to be
+   *     deserialized
+   * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of
+   *     {@code TypeToken} with the specific generic type arguments. For example, to get the type
+   *     for {@code Collection<Foo>}, you should use:
+   *     <pre>
+   * new TypeToken&lt;Collection&lt;Foo&gt;&gt;(){}
+   * </pre>
+   *
+   * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null}
+   *     or if {@code json} is empty.
+   * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT
+   * @see #fromJson(Reader, TypeToken)
+   * @see #fromJson(JsonElement, Class)
+   * @since 2.10
+   */
+  public <T> T fromJson(JsonElement json, TypeToken<T> typeOfT) throws JsonSyntaxException {
+    if (json == null) {
+      return null;
+    }
+    return fromJson(new JsonTreeReader(json), typeOfT);
+  }
+
+  private static void assertFullConsumption(Object obj, JsonReader reader) {
+    try {
+      if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) {
+        throw new JsonSyntaxException("JSON document was not fully consumed.");
+      }
+    } catch (MalformedJsonException e) {
+      throw new JsonSyntaxException(e);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+  }
+
+  /**
+   * Proxy type adapter for cyclic type graphs.
+   *
+   * <p><b>Important:</b> Setting the delegate adapter is not thread-safe; instances of {@code
+   * FutureTypeAdapter} must only be published to other threads after the delegate has been set.
+   *
+   * @see Gson#threadLocalAdapterResults
+   */
+  static class FutureTypeAdapter<T> extends SerializationDelegatingTypeAdapter<T> {
+    private TypeAdapter<T> delegate = null;
+
+    public void setDelegate(TypeAdapter<T> typeAdapter) {
+      if (delegate != null) {
+        throw new AssertionError("Delegate is already set");
+      }
+      delegate = typeAdapter;
+    }
+
+    private TypeAdapter<T> delegate() {
+      TypeAdapter<T> delegate = this.delegate;
+      if (delegate == null) {
+        // Can occur when adapter is leaked to other thread or when adapter is used for
+        // (de-)serialization
+        // directly within the TypeAdapterFactory which requested it
+        throw new IllegalStateException(
+            "Adapter for type with cyclic dependency has been used"
+                + " before dependency has been resolved");
+      }
+      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 {
+      delegate().write(out, value);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "{serializeNulls:"
+        + serializeNulls
+        + ",factories:"
+        + factories
+        + ",instanceCreators:"
+        + constructorConstructor
+        + "}";
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/gson/src/main/java/com/google/gson/GsonBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d9bef027d76b08715e201d900ae224ec6c9d2cd
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/GsonBuilder.java
@@ -0,0 +1,939 @@
+/*
+ * 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;
+
+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;
+import static com.google.gson.Gson.DEFAULT_FORMATTING_STYLE;
+import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE;
+import static com.google.gson.Gson.DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
+import static com.google.gson.Gson.DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
+import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS;
+import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
+import static com.google.gson.Gson.DEFAULT_STRICTNESS;
+import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.errorprone.annotations.InlineMe;
+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.text.SimpleDateFormat;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 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 use {@code
+ * new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its various
+ * configuration methods, and finally calling create.
+ *
+ * <p>The following example shows how to use the {@code GsonBuilder} to construct a Gson instance:
+ *
+ * <pre>
+ * Gson gson = new GsonBuilder()
+ *     .registerTypeAdapter(Id.class, new IdTypeAdapter())
+ *     .enableComplexMapKeySerialization()
+ *     .serializeNulls()
+ *     .setDateFormat(DateFormat.LONG)
+ *     .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
+ *     .setPrettyPrinting()
+ *     .setVersion(1.0)
+ *     .create();
+ * </pre>
+ *
+ * <p>Notes:
+ *
+ * <ul>
+ *   <li>The order of invocation of configuration methods does not matter.
+ *   <li>The default serialization of {@link Date} and its subclasses in Gson does not contain
+ *       time-zone information. So, if you are using date/time instances, use {@code GsonBuilder}
+ *       and its {@code setDateFormat} methods.
+ *   <li>By default no explicit {@link Strictness} is set; some of the {@link Gson} methods behave
+ *       as if {@link Strictness#LEGACY_STRICT} was used whereas others behave as if {@link
+ *       Strictness#LENIENT} was used. Prefer explicitly setting a strictness with {@link
+ *       #setStrictness(Strictness)} to avoid this legacy behavior.
+ * </ul>
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @author Jesse Wilson
+ */
+public final class GsonBuilder {
+  private Excluder excluder = Excluder.DEFAULT;
+  private LongSerializationPolicy longSerializationPolicy = LongSerializationPolicy.DEFAULT;
+  private FieldNamingStrategy fieldNamingPolicy = FieldNamingPolicy.IDENTITY;
+  private final Map<Type, InstanceCreator<?>> instanceCreators = new HashMap<>();
+  private final List<TypeAdapterFactory> factories = new ArrayList<>();
+
+  /** tree-style hierarchy factories. These come after factories for backwards compatibility. */
+  private final List<TypeAdapterFactory> hierarchyFactories = new ArrayList<>();
+
+  private boolean serializeNulls = DEFAULT_SERIALIZE_NULLS;
+  private String datePattern = DEFAULT_DATE_PATTERN;
+  private int dateStyle = DateFormat.DEFAULT;
+  private int timeStyle = DateFormat.DEFAULT;
+  private boolean complexMapKeySerialization = DEFAULT_COMPLEX_MAP_KEYS;
+  private boolean serializeSpecialFloatingPointValues = DEFAULT_SPECIALIZE_FLOAT_VALUES;
+  private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML;
+  private FormattingStyle formattingStyle = DEFAULT_FORMATTING_STYLE;
+  private boolean generateNonExecutableJson = DEFAULT_JSON_NON_EXECUTABLE;
+  private Strictness strictness = DEFAULT_STRICTNESS;
+  private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE;
+  private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
+  private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
+  private final ArrayDeque<ReflectionAccessFilter> reflectionFilters = new ArrayDeque<>();
+
+  /**
+   * Creates a GsonBuilder instance that can be used to build Gson with various configuration
+   * settings. GsonBuilder follows the builder pattern, and it is typically used by first invoking
+   * various configuration methods to set desired options, and finally calling {@link #create()}.
+   */
+  public GsonBuilder() {}
+
+  /**
+   * Constructs a GsonBuilder instance from a Gson instance. The newly constructed GsonBuilder has
+   * the same configuration as the previously built Gson instance.
+   *
+   * @param gson the gson instance whose configuration should be applied to a new GsonBuilder.
+   */
+  GsonBuilder(Gson gson) {
+    this.excluder = gson.excluder;
+    this.fieldNamingPolicy = gson.fieldNamingStrategy;
+    this.instanceCreators.putAll(gson.instanceCreators);
+    this.serializeNulls = gson.serializeNulls;
+    this.complexMapKeySerialization = gson.complexMapKeySerialization;
+    this.generateNonExecutableJson = gson.generateNonExecutableJson;
+    this.escapeHtmlChars = gson.htmlSafe;
+    this.formattingStyle = gson.formattingStyle;
+    this.strictness = gson.strictness;
+    this.serializeSpecialFloatingPointValues = gson.serializeSpecialFloatingPointValues;
+    this.longSerializationPolicy = gson.longSerializationPolicy;
+    this.datePattern = gson.datePattern;
+    this.dateStyle = gson.dateStyle;
+    this.timeStyle = gson.timeStyle;
+    this.factories.addAll(gson.builderFactories);
+    this.hierarchyFactories.addAll(gson.builderHierarchyFactories);
+    this.useJdkUnsafe = gson.useJdkUnsafe;
+    this.objectToNumberStrategy = gson.objectToNumberStrategy;
+    this.numberToNumberStrategy = gson.numberToNumberStrategy;
+    this.reflectionFilters.addAll(gson.reflectionFilters);
+  }
+
+  /**
+   * Configures Gson to enable versioning support. Versioning support works based on the annotation
+   * types {@link Since} and {@link Until}. It allows including or excluding fields and classes
+   * based on the specified version. See the documentation of these annotation types for more
+   * information.
+   *
+   * <p>By default versioning support is disabled and usage of {@code @Since} and {@code @Until} has
+   * no effect.
+   *
+   * @param 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
+   */
+  @CanIgnoreReturnValue
+  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 {@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, {@link
+   *     java.lang.reflect.Modifier#TRANSIENT}, {@link java.lang.reflect.Modifier#STATIC}.
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder excludeFieldsWithModifiers(int... modifiers) {
+    Objects.requireNonNull(modifiers);
+    excluder = excluder.withModifiers(modifiers);
+    return this;
+  }
+
+  /**
+   * Makes the output JSON non-executable in Javascript by prefixing the generated JSON with some
+   * special text. This prevents attacks from third-party sites through script sourcing. See <a
+   * href="http://code.google.com/p/google-gson/issues/detail?id=42">Gson Issue 42</a> for details.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.3
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder generateNonExecutableJson() {
+    this.generateNonExecutableJson = true;
+    return this;
+  }
+
+  /**
+   * 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder excludeFieldsWithoutExposeAnnotation() {
+    excluder = excluder.excludeFieldsWithoutExposeAnnotation();
+    return this;
+  }
+
+  /**
+   * Configures Gson to serialize null fields. By default, Gson omits all fields that are null
+   * during serialization.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.2
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder serializeNulls() {
+    this.serializeNulls = true;
+    return this;
+  }
+
+  /**
+   * Configures Gson to serialize {@code Map} objects with complex keys as JSON arrays. Enabling
+   * this feature will only change the serialized form if the map key is a complex type (i.e.
+   * non-primitive) in its <strong>serialized</strong> JSON form. The default implementation of map
+   * serialization uses {@code toString()} on the key; however, when this is called then one of the
+   * following cases apply:
+   *
+   * <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}.
+   *
+   * <p>Below is an example:
+   *
+   * <pre>{@code
+   * Gson gson = new GsonBuilder()
+   *     .register(Point.class, new MyPointTypeAdapter())
+   *     .enableComplexMapKeySerialization()
+   *     .create();
+   *
+   * Map<Point, String> original = new LinkedHashMap<>();
+   * original.put(new Point(5, 6), "a");
+   * original.put(new Point(8, 8), "b");
+   * System.out.println(gson.toJson(original, type));
+   * }</pre>
+   *
+   * The above code prints this JSON object:
+   *
+   * <pre>{@code
+   * {
+   *   "(5,6)": "a",
+   *   "(8,8)": "b"
+   * }
+   * }</pre>
+   *
+   * <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":3}}.
+   *
+   * <p>Given the assumption above, a {@code Map<Point, String>} will be serialized as an array of
+   * arrays (can be viewed as an entry set of pairs).
+   *
+   * <p>Below is an example of serializing complex types as JSON arrays:
+   *
+   * <pre>{@code
+   * Gson gson = new GsonBuilder()
+   *     .enableComplexMapKeySerialization()
+   *     .create();
+   *
+   * Map<Point, String> original = new LinkedHashMap<>();
+   * original.put(new Point(5, 6), "a");
+   * original.put(new Point(8, 8), "b");
+   * System.out.println(gson.toJson(original, type));
+   * }</pre>
+   *
+   * The JSON output would look as follows:
+   *
+   * <pre>{@code
+   * [
+   *   [
+   *     {
+   *       "x": 5,
+   *       "y": 6
+   *     },
+   *     "a"
+   *   ],
+   *   [
+   *     {
+   *       "x": 8,
+   *       "y": 8
+   *     },
+   *     "b"
+   *   ]
+   * ]
+   * }</pre>
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.7
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder enableComplexMapKeySerialization() {
+    complexMapKeySerialization = true;
+    return this;
+  }
+
+  /**
+   * 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder disableInnerClassSerialization() {
+    excluder = excluder.disableInnerClassSerialization();
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply a specific serialization policy for {@code Long} and {@code long}
+   * objects.
+   *
+   * @param serializationPolicy the particular policy to use for serializing longs.
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.3
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setLongSerializationPolicy(LongSerializationPolicy serializationPolicy) {
+    this.longSerializationPolicy = Objects.requireNonNull(serializationPolicy);
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply a specific naming policy to an object's fields during serialization
+   * and deserialization.
+   *
+   * <p>This method just delegates to {@link #setFieldNamingStrategy(FieldNamingStrategy)}.
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setFieldNamingPolicy(FieldNamingPolicy namingConvention) {
+    return setFieldNamingStrategy(namingConvention);
+  }
+
+  /**
+   * Configures Gson to apply a specific naming strategy to an object's fields during serialization
+   * and deserialization.
+   *
+   * <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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setFieldNamingStrategy(FieldNamingStrategy fieldNamingStrategy) {
+    this.fieldNamingPolicy = Objects.requireNonNull(fieldNamingStrategy);
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply a specific number strategy during deserialization of {@link Object}.
+   *
+   * @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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) {
+    this.objectToNumberStrategy = Objects.requireNonNull(objectToNumberStrategy);
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply a specific number strategy during deserialization of {@link Number}.
+   *
+   * @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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setNumberToNumberStrategy(ToNumberStrategy numberToNumberStrategy) {
+    this.numberToNumberStrategy = Objects.requireNonNull(numberToNumberStrategy);
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply a set of exclusion strategies during both serialization and
+   * 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setExclusionStrategies(ExclusionStrategy... strategies) {
+    Objects.requireNonNull(strategies);
+    for (ExclusionStrategy strategy : strategies) {
+      excluder = excluder.withExclusionStrategy(strategy, true, true);
+    }
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply the passed in exclusion strategy during serialization. If this method
+   * is invoked numerous times with different exclusion strategy objects then the exclusion
+   * strategies that were added will be applied as a disjunction rule. This means that if one of the
+   * added exclusion strategies suggests that a field (or 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder addSerializationExclusionStrategy(ExclusionStrategy strategy) {
+    Objects.requireNonNull(strategy);
+    excluder = excluder.withExclusionStrategy(strategy, true, false);
+    return this;
+  }
+
+  /**
+   * Configures Gson to apply the passed in exclusion strategy during deserialization. If this
+   * method is invoked numerous times with different exclusion strategy objects then the exclusion
+   * strategies that were added will be applied as a disjunction rule. This means that if one of the
+   * added exclusion strategies suggests that a field (or 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strategy) {
+    Objects.requireNonNull(strategy);
+    excluder = excluder.withExclusionStrategy(strategy, false, true);
+    return this;
+  }
+
+  /**
+   * Configures Gson to output JSON that fits in a page for pretty printing. This option only
+   * affects JSON serialization.
+   *
+   * <p>This is a convenience method which simply calls {@link #setFormattingStyle(FormattingStyle)}
+   * with {@link FormattingStyle#PRETTY}.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setPrettyPrinting() {
+    return setFormattingStyle(FormattingStyle.PRETTY);
+  }
+
+  /**
+   * Configures Gson to output JSON that uses a certain kind of formatting style (for example
+   * newline and indent). This option only affects JSON serialization. By default Gson produces
+   * compact JSON output without any formatting.
+   *
+   * @param formattingStyle the formatting style to use.
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since $next-version$
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setFormattingStyle(FormattingStyle formattingStyle) {
+    this.formattingStyle = Objects.requireNonNull(formattingStyle);
+    return this;
+  }
+
+  /**
+   * Sets the strictness of this builder to {@link Strictness#LENIENT}.
+   *
+   * @deprecated This method is equivalent to calling {@link #setStrictness(Strictness)} with {@link
+   *     Strictness#LENIENT}: {@code setStrictness(Strictness.LENIENT)}
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern.
+   * @see JsonReader#setStrictness(Strictness)
+   * @see JsonWriter#setStrictness(Strictness)
+   * @see #setStrictness(Strictness)
+   */
+  @Deprecated
+  @InlineMe(
+      replacement = "this.setStrictness(Strictness.LENIENT)",
+      imports = "com.google.gson.Strictness")
+  @CanIgnoreReturnValue
+  public GsonBuilder setLenient() {
+    return setStrictness(Strictness.LENIENT);
+  }
+
+  /**
+   * Sets the strictness of this builder to the provided parameter.
+   *
+   * <p>This changes how strict the <a href="https://www.ietf.org/rfc/rfc8259.txt">RFC 8259 JSON
+   * specification</a> is enforced when parsing or writing JSON. For details on this, refer to
+   * {@link JsonReader#setStrictness(Strictness)} and {@link JsonWriter#setStrictness(Strictness)}.
+   *
+   * @param strictness the new strictness mode. May not be {@code null}.
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern.
+   * @see JsonReader#setStrictness(Strictness)
+   * @see JsonWriter#setStrictness(Strictness)
+   * @since $next-version$
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setStrictness(Strictness strictness) {
+    this.strictness = Objects.requireNonNull(strictness);
+    return this;
+  }
+
+  /**
+   * By default, Gson escapes HTML characters such as &lt; &gt; etc. Use this option to configure
+   * Gson to pass-through HTML characters as is.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.3
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder disableHtmlEscaping() {
+    this.escapeHtmlChars = false;
+    return this;
+  }
+
+  /**
+   * Configures Gson to serialize {@code Date} objects according to the pattern provided. You can
+   * call this method or {@link #setDateFormat(int)} multiple times, but only the last invocation
+   * will be used to decide the serialization format.
+   *
+   * <p>The date format will be used to serialize and deserialize {@link java.util.Date} and in case
+   * the {@code java.sql} module is present, also {@link java.sql.Timestamp} and {@link
+   * java.sql.Date}.
+   *
+   * <p>Note that this pattern must abide by the convention provided by {@code SimpleDateFormat}
+   * class. See the documentation in {@link java.text.SimpleDateFormat} for more information on
+   * valid date and time patterns.
+   *
+   * @param pattern the pattern that dates will be serialized/deserialized to/from; can be {@code
+   *     null} to reset the pattern
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @throws IllegalArgumentException if the pattern is invalid
+   * @since 1.2
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setDateFormat(String pattern) {
+    if (pattern != null) {
+      try {
+        new SimpleDateFormat(pattern);
+      } catch (IllegalArgumentException e) {
+        // Throw exception if it is an invalid date format
+        throw new IllegalArgumentException("The date pattern '" + pattern + "' is not valid", e);
+      }
+    }
+    this.datePattern = pattern;
+    return this;
+  }
+
+  /**
+   * Configures Gson to serialize {@code Date} objects according to the date style value provided.
+   * You can call this method or {@link #setDateFormat(String)} multiple times, but only the last
+   * invocation will be used to decide the serialization format. This methods leaves the current
+   * 'time style' unchanged.
+   *
+   * <p>Note that this style value should be one of the predefined constants in the {@link
+   * DateFormat} class, such as {@link DateFormat#MEDIUM}. See the documentation of the {@link
+   * DateFormat} class for more information on the valid style constants.
+   *
+   * @param dateStyle the predefined date style that date objects will be serialized/deserialized
+   *     to/from
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @throws IllegalArgumentException if the style is invalid
+   * @since 1.2
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setDateFormat(int dateStyle) {
+    this.dateStyle = checkDateFormatStyle(dateStyle);
+    this.datePattern = null;
+    return this;
+  }
+
+  /**
+   * Configures Gson to serialize {@code Date} objects according to the style value provided. You
+   * can call this method or {@link #setDateFormat(String)} multiple times, but only the last
+   * invocation will be used to decide the serialization format.
+   *
+   * <p>Note that this style value should be one of the predefined constants in the {@link
+   * DateFormat} class, such as {@link DateFormat#MEDIUM}. See the documentation of the {@link
+   * DateFormat} class for more information on the valid style constants.
+   *
+   * @param dateStyle the predefined date style that date objects will be serialized/deserialized
+   *     to/from
+   * @param timeStyle the predefined style for the time portion of the date objects
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @throws IllegalArgumentException if the style values are invalid
+   * @since 1.2
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder setDateFormat(int dateStyle, int timeStyle) {
+    this.dateStyle = checkDateFormatStyle(dateStyle);
+    this.timeStyle = checkDateFormatStyle(timeStyle);
+    this.datePattern = null;
+    return this;
+  }
+
+  private static int checkDateFormatStyle(int style) {
+    // Valid DateFormat styles are: 0, 1, 2, 3 (FULL, LONG, MEDIUM, SHORT)
+    if (style < 0 || style > 3) {
+      throw new IllegalArgumentException("Invalid style: " + style);
+    }
+    return style;
+  }
+
+  /**
+   * Configures Gson for custom serialization or deserialization. This method combines the
+   * registration of an {@link TypeAdapter}, {@link InstanceCreator}, {@link JsonSerializer}, and a
+   * {@link JsonDeserializer}. It is best used when a single object {@code typeAdapter} implements
+   * all the required interfaces for custom serialization with Gson. If a type adapter was
+   * previously registered for the specified {@code type}, it is overwritten.
+   *
+   * <p>This registers the type specified and no other types: you must manually register related
+   * 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
+   * @throws IllegalArgumentException if the type adapter being registered is for {@code Object}
+   *     class or {@link JsonElement} or any of its subclasses
+   */
+  @CanIgnoreReturnValue
+  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 (isTypeObjectOrJsonElement(type)) {
+      throw new IllegalArgumentException("Cannot override built-in adapter for " + type);
+    }
+
+    if (typeAdapter instanceof InstanceCreator<?>) {
+      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<?>) {
+      @SuppressWarnings({"unchecked", "rawtypes"})
+      TypeAdapterFactory factory =
+          TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter) typeAdapter);
+      factories.add(factory);
+    }
+    return this;
+  }
+
+  private static boolean isTypeObjectOrJsonElement(Type type) {
+    return type instanceof Class
+        && (type == Object.class || JsonElement.class.isAssignableFrom((Class<?>) type));
+  }
+
+  /**
+   * Register a factory for type adapters. Registering a factory is useful when the type adapter
+   * needs to be configured based on the type of the field being processed. Gson 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory) {
+    Objects.requireNonNull(factory);
+    factories.add(factory);
+    return this;
+  }
+
+  /**
+   * Configures Gson for custom serialization or deserialization for an inheritance type hierarchy.
+   * This method combines the registration of a {@link TypeAdapter}, {@link JsonSerializer} and a
+   * {@link JsonDeserializer}. If a type adapter was previously registered for the specified type
+   * hierarchy, it is overridden. If a type adapter is registered for a specific type in the type
+   * hierarchy, it will be invoked instead of the one registered for the type hierarchy.
+   *
+   * @param baseType the class definition for the type adapter being registered for the base class
+   *     or interface
+   * @param typeAdapter This object must implement at least one of {@link TypeAdapter}, {@link
+   *     JsonSerializer} or {@link JsonDeserializer} interfaces.
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @throws IllegalArgumentException if the type adapter being registered is for {@link
+   *     JsonElement} or any of its subclasses
+   * @since 1.7
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder registerTypeHierarchyAdapter(Class<?> baseType, Object typeAdapter) {
+    Objects.requireNonNull(baseType);
+    $Gson$Preconditions.checkArgument(
+        typeAdapter instanceof JsonSerializer<?>
+            || typeAdapter instanceof JsonDeserializer<?>
+            || typeAdapter instanceof TypeAdapter<?>);
+
+    if (JsonElement.class.isAssignableFrom(baseType)) {
+      throw new IllegalArgumentException("Cannot override built-in adapter for " + baseType);
+    }
+
+    if (typeAdapter instanceof JsonDeserializer || typeAdapter instanceof JsonSerializer) {
+      hierarchyFactories.add(TreeTypeAdapter.newTypeHierarchyFactory(baseType, typeAdapter));
+    }
+    if (typeAdapter instanceof TypeAdapter<?>) {
+      @SuppressWarnings({"unchecked", "rawtypes"})
+      TypeAdapterFactory factory =
+          TypeAdapters.newTypeHierarchyFactory(baseType, (TypeAdapter) typeAdapter);
+      factories.add(factory);
+    }
+    return this;
+  }
+
+  /**
+   * Section 6 of <a href="https://www.ietf.org/rfc/rfc8259.txt">JSON specification</a> disallows
+   * special double values (NaN, Infinity, -Infinity). However, <a
+   * href="http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf">Javascript
+   * specification</a> (see section 4.3.20, 4.3.22, 4.3.23) allows these values as valid Javascript
+   * values. Moreover, most JavaScript engines will accept these special values in JSON without
+   * problem. So, at a practical level, it makes sense to accept these values as valid JSON even
+   * though JSON specification disallows them.
+   *
+   * <p>Gson always accepts these special values during deserialization. However, it outputs
+   * strictly compliant JSON. Hence, if it encounters a float value {@link Float#NaN}, {@link
+   * Float#POSITIVE_INFINITY}, {@link Float#NEGATIVE_INFINITY}, or a double value {@link
+   * Double#NaN}, {@link Double#POSITIVE_INFINITY}, {@link Double#NEGATIVE_INFINITY}, it will throw
+   * an {@link IllegalArgumentException}. This method provides a way to override the default
+   * behavior when you know that the JSON receiver will be able to handle these special values.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 1.3
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder serializeSpecialFloatingPointValues() {
+    this.serializeSpecialFloatingPointValues = true;
+    return this;
+  }
+
+  /**
+   * Disables usage of JDK's {@code sun.misc.Unsafe}.
+   *
+   * <p>By default Gson uses {@code Unsafe} to create instances of classes which don't have a
+   * no-args constructor. However, {@code Unsafe} might not be available for all Java runtimes. For
+   * example Android does not provide {@code Unsafe}, or only with limited functionality.
+   * Additionally {@code Unsafe} creates instances without executing any constructor or initializer
+   * block, or performing initialization of field values. This can lead to surprising and difficult
+   * to debug errors. Therefore, to get reliable behavior regardless of which runtime is used, and
+   * to detect classes which cannot be deserialized in an early stage of development, this method
+   * allows disabling usage of {@code Unsafe}.
+   *
+   * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+   * @since 2.9.0
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder disableJdkUnsafe() {
+    this.useJdkUnsafe = false;
+    return this;
+  }
+
+  /**
+   * Adds a reflection access filter. A reflection access filter prevents Gson from using reflection
+   * for the serialization and deserialization of certain classes. The logic in the filter specifies
+   * which classes those are.
+   *
+   * <p>Filters will be invoked in reverse registration order, that is, the most recently added
+   * filter will be invoked first.
+   *
+   * <p>By default Gson has no filters configured and will try to use reflection for 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
+   */
+  @CanIgnoreReturnValue
+  public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) {
+    Objects.requireNonNull(filter);
+    reflectionFilters.addFirst(filter);
+    return this;
+  }
+
+  /**
+   * Creates a {@link Gson} instance based on the current configuration. This method is free of
+   * side-effects to this {@code GsonBuilder} instance and hence can be called multiple times.
+   *
+   * @return an instance of Gson configured with the options currently set in this builder
+   */
+  public Gson create() {
+    List<TypeAdapterFactory> factories =
+        new ArrayList<>(this.factories.size() + this.hierarchyFactories.size() + 3);
+    factories.addAll(this.factories);
+    Collections.reverse(factories);
+
+    List<TypeAdapterFactory> hierarchyFactories = new ArrayList<>(this.hierarchyFactories);
+    Collections.reverse(hierarchyFactories);
+    factories.addAll(hierarchyFactories);
+
+    addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories);
+
+    return new Gson(
+        excluder,
+        fieldNamingPolicy,
+        new HashMap<>(instanceCreators),
+        serializeNulls,
+        complexMapKeySerialization,
+        generateNonExecutableJson,
+        escapeHtmlChars,
+        formattingStyle,
+        strictness,
+        serializeSpecialFloatingPointValues,
+        useJdkUnsafe,
+        longSerializationPolicy,
+        datePattern,
+        dateStyle,
+        timeStyle,
+        new ArrayList<>(this.factories),
+        new ArrayList<>(this.hierarchyFactories),
+        factories,
+        objectToNumberStrategy,
+        numberToNumberStrategy,
+        new ArrayList<>(reflectionFilters));
+  }
+
+  private static void addTypeAdaptersForDate(
+      String datePattern, int dateStyle, int timeStyle, List<TypeAdapterFactory> factories) {
+    TypeAdapterFactory dateAdapterFactory;
+    boolean sqlTypesSupported = SqlTypesSupport.SUPPORTS_SQL_TYPES;
+    TypeAdapterFactory sqlTimestampAdapterFactory = null;
+    TypeAdapterFactory sqlDateAdapterFactory = null;
+
+    if (datePattern != null && !datePattern.trim().isEmpty()) {
+      dateAdapterFactory = DefaultDateTypeAdapter.DateType.DATE.createAdapterFactory(datePattern);
+
+      if (sqlTypesSupported) {
+        sqlTimestampAdapterFactory =
+            SqlTypesSupport.TIMESTAMP_DATE_TYPE.createAdapterFactory(datePattern);
+        sqlDateAdapterFactory = SqlTypesSupport.DATE_DATE_TYPE.createAdapterFactory(datePattern);
+      }
+    } else if (dateStyle != DateFormat.DEFAULT && timeStyle != DateFormat.DEFAULT) {
+      dateAdapterFactory =
+          DefaultDateTypeAdapter.DateType.DATE.createAdapterFactory(dateStyle, timeStyle);
+
+      if (sqlTypesSupported) {
+        sqlTimestampAdapterFactory =
+            SqlTypesSupport.TIMESTAMP_DATE_TYPE.createAdapterFactory(dateStyle, timeStyle);
+        sqlDateAdapterFactory =
+            SqlTypesSupport.DATE_DATE_TYPE.createAdapterFactory(dateStyle, timeStyle);
+      }
+    } else {
+      return;
+    }
+
+    factories.add(dateAdapterFactory);
+    if (sqlTypesSupported) {
+      factories.add(sqlTimestampAdapterFactory);
+      factories.add(sqlDateAdapterFactory);
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/InstanceCreator.java b/gson/gson/src/main/java/com/google/gson/InstanceCreator.java
new file mode 100644
index 0000000000000000000000000000000000000000..48cf6a485469fb3554ee5c7d26f894c2487bb68d
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/InstanceCreator.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+
+/**
+ * This interface is implemented to create instances of a class that does not define a no-args
+ * constructor. If you can modify the class, you should instead add a private, or public no-args
+ * constructor. However, that is not possible for library classes, such as JDK classes, or a
+ * third-party library that you do not have source-code of. In such cases, you should define an
+ * instance creator for the class. Implementations of this interface should be registered with
+ * {@link GsonBuilder#registerTypeAdapter(Type, Object)} method before Gson will be able to use
+ * them.
+ *
+ * <p>Let us look at an example where defining an InstanceCreator might be useful. The {@code Id}
+ * class defined below does not have a default no-args constructor.
+ *
+ * <pre>
+ * public class Id&lt;T&gt; {
+ *   private final Class&lt;T&gt; clazz;
+ *   private final long value;
+ *   public Id(Class&lt;T&gt; clazz, long value) {
+ *     this.clazz = clazz;
+ *     this.value = value;
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>If Gson encounters an object of type {@code Id} during deserialization, it will throw an
+ * exception. The easiest way to solve this problem will be to add a (public or private) no-args
+ * constructor as follows:
+ *
+ * <pre>
+ * private Id() {
+ *   this(Object.class, 0L);
+ * }
+ * </pre>
+ *
+ * <p>However, let us assume that the developer does not have access to the source-code of the
+ * {@code Id} class, or does not want to define a no-args constructor for it. The developer can
+ * solve this problem by defining an {@code InstanceCreator} for {@code Id}:
+ *
+ * <pre>
+ * class IdInstanceCreator implements InstanceCreator&lt;Id&gt; {
+ *   public Id createInstance(Type type) {
+ *     return new Id(Object.class, 0L);
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>Note that it does not matter what the fields of the created instance contain since Gson will
+ * overwrite them with the deserialized values specified in JSON. You should also ensure that a
+ * <i>new</i> object is returned, not a common object since its fields will be overwritten. The
+ * developer will need to register {@code IdInstanceCreator} with Gson as follows:
+ *
+ * <pre>
+ * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdInstanceCreator()).create();
+ * </pre>
+ *
+ * @param <T> the type of object that will be created by this implementation.
+ * @see GsonBuilder#registerTypeAdapter(Type, Object)
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public interface InstanceCreator<T> {
+
+  /**
+   * Gson invokes this call-back method during deserialization to create an instance of the
+   * specified type. The fields of the returned instance are overwritten with the data present in
+   * the JSON. Since the prior contents of the object are destroyed and overwritten, do not return
+   * an instance that is useful elsewhere. In particular, do not return a common instance, always
+   * use {@code new} to create a new instance.
+   *
+   * @param type the parameterized T represented as a {@link Type}.
+   * @return a default object instance of type T.
+   */
+  public T createInstance(Type type);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonArray.java b/gson/gson/src/main/java/com/google/gson/JsonArray.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d1cbd2f17ad2218e98d99d45a513dae89d1f248
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonArray.java
@@ -0,0 +1,433 @@
+/*
+ * 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;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.internal.NonNullElementWrapperList;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A class representing an array type in JSON. An array is a list of {@link JsonElement}s each of
+ * which can be of a different type. This is an ordered list, meaning that the order in which
+ * 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 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.
+   *
+   * @since 2.8.2
+   */
+  @Override
+  public JsonArray deepCopy() {
+    if (!elements.isEmpty()) {
+      JsonArray result = new JsonArray(elements.size());
+      for (JsonElement element : elements) {
+        result.add(element.deepCopy());
+      }
+      return result;
+    }
+    return new JsonArray();
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * Adds the specified element to self.
+   *
+   * @param element the element that needs to be added to the array.
+   */
+  public void add(JsonElement element) {
+    if (element == null) {
+      element = JsonNull.INSTANCE;
+    }
+    elements.add(element);
+  }
+
+  /**
+   * Adds all the elements of the specified array to self.
+   *
+   * @param array the array whose elements need to be added to the array.
+   */
+  public void addAll(JsonArray array) {
+    elements.addAll(array.elements);
+  }
+
+  /**
+   * Replaces the element at the specified position in this array with the specified element.
+   *
+   * @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
+   */
+  @CanIgnoreReturnValue
+  public JsonElement set(int index, JsonElement 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
+   */
+  @CanIgnoreReturnValue
+  public boolean remove(JsonElement element) {
+    return elements.remove(element);
+  }
+
+  /**
+   * 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
+   * @since 2.3
+   */
+  @CanIgnoreReturnValue
+  public JsonElement remove(int index) {
+    return elements.remove(index);
+  }
+
+  /**
+   * 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
+   */
+  public boolean contains(JsonElement element) {
+    return elements.contains(element);
+  }
+
+  /**
+   * Returns the number of elements in the array.
+   *
+   * @return the number of elements in the array.
+   */
+  public int size() {
+    return elements.size();
+  }
+
+  /**
+   * Returns true if the array is empty.
+   *
+   * @return true if the array is empty.
+   * @since 2.8.7
+   */
+  public boolean isEmpty() {
+    return elements.isEmpty();
+  }
+
+  /**
+   * Returns an iterator to navigate the elements of the array. Since the array is an ordered list,
+   * the iterator navigates the elements in the order they were inserted.
+   *
+   * @return an iterator to navigate the elements of the array.
+   */
+  @Override
+  public Iterator<JsonElement> iterator() {
+    return elements.iterator();
+  }
+
+  /**
+   * 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 i-th index.
+   * @throws IndexOutOfBoundsException if {@code i} is negative or greater than or equal to the
+   *     {@link #size()} of the array.
+   */
+  public JsonElement get(int i) {
+    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. This
+   * method calls {@link JsonElement#getAsNumber()} on the element, therefore any of the exceptions
+   * declared by that method can occur.
+   *
+   * @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() {
+    return getAsSingleElement().getAsNumber();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsString();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsDouble();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsBigDecimal();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsBigInteger();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsFloat();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsLong();
+  }
+
+  /**
+   * 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 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() {
+    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() {
+    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() {
+    return getAsSingleElement().getAsCharacter();
+  }
+
+  /**
+   * 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 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() {
+    return getAsSingleElement().getAsShort();
+  }
+
+  /**
+   * 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 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() {
+    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/gson/src/main/java/com/google/gson/JsonDeserializationContext.java b/gson/gson/src/main/java/com/google/gson/JsonDeserializationContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..caf7fb7b8ec18a1d4a841fd2a8b1346d403ced79
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonDeserializationContext.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+
+/**
+ * Context for deserialization that is passed to a custom deserializer during invocation of its
+ * {@link JsonDeserializer#deserialize(JsonElement, Type, JsonDeserializationContext)} method.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public interface JsonDeserializationContext {
+
+  /**
+   * Invokes default deserialization on the specified object. It should never be invoked on the
+   * element received as a parameter of the {@link JsonDeserializer#deserialize(JsonElement, Type,
+   * JsonDeserializationContext)} method. Doing so will result in an infinite loop since Gson will
+   * in-turn call the custom deserializer again.
+   *
+   * @param json the parse tree.
+   * @param typeOfT type of the expected return value.
+   * @param <T> The type of the deserialized object.
+   * @return An object of type typeOfT.
+   * @throws JsonParseException if the parse tree does not contain expected data.
+   */
+  @SuppressWarnings("TypeParameterUnusedInFormals")
+  public <T> T deserialize(JsonElement json, Type typeOfT) throws JsonParseException;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonDeserializer.java b/gson/gson/src/main/java/com/google/gson/JsonDeserializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c83b63e0e7995a2f0f1eb460c490a7ea4cc6a91
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonDeserializer.java
@@ -0,0 +1,95 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+
+/**
+ * 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>Let us look at example where defining a deserializer will be useful. The {@code Id} class
+ * defined below has two fields: {@code clazz} and {@code value}.
+ *
+ * <pre>
+ * public class Id&lt;T&gt; {
+ *   private final Class&lt;T&gt; clazz;
+ *   private final long value;
+ *   public Id(Class&lt;T&gt; clazz, long value) {
+ *     this.clazz = clazz;
+ *     this.value = value;
+ *   }
+ *   public long getValue() {
+ *     return value;
+ *   }
+ * }
+ * </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 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
+ * deserializer:
+ *
+ * <pre>
+ * class IdDeserializer implements JsonDeserializer&lt;Id&gt; {
+ *   public Id deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ *       throws JsonParseException {
+ *     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:
+ *
+ * <pre>
+ * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdDeserializer()).create();
+ * </pre>
+ *
+ * <p>Deserializers should be stateless and thread-safe, otherwise the thread-safety guarantees of
+ * {@link Gson} might not apply.
+ *
+ * <p>New applications should prefer {@link TypeAdapter}, whose streaming API is more efficient than
+ * this interface's tree API.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @param <T> type for which the deserializer is being registered. It is possible that a
+ *     deserializer may be asked to deserialize a specific generic type of the T.
+ */
+public interface JsonDeserializer<T> {
+
+  /**
+   * Gson invokes this call-back method during deserialization when it encounters a field of the
+   * specified type.
+   *
+   * <p>In the implementation of this call-back method, you should consider invoking {@link
+   * JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects for any
+   * non-trivial field of the returned object. However, you should never invoke it on the same type
+   * passing {@code json} since that will cause an infinite loop (Gson will call your call-back
+   * method again).
+   *
+   * @param json The Json data being deserialized
+   * @param typeOfT The type of the Object to deserialize to
+   * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T}
+   * @throws JsonParseException if json is not in the expected format of {@code typeOfT}
+   */
+  public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonElement.java b/gson/gson/src/main/java/com/google/gson/JsonElement.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa3a7af5f627993f4cf584a6f6cabc53c6002090
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonElement.java
@@ -0,0 +1,338 @@
+/*
+ * 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;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * 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
+ * @author Joel Leitch
+ */
+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 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.
+   */
+  public boolean isJsonArray() {
+    return this instanceof JsonArray;
+  }
+
+  /**
+   * 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.
+   */
+  public boolean isJsonObject() {
+    return this instanceof JsonObject;
+  }
+
+  /**
+   * 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.
+   */
+  public boolean isJsonPrimitive() {
+    return this instanceof JsonPrimitive;
+  }
+
+  /**
+   * 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
+   */
+  public boolean isJsonNull() {
+    return this instanceof JsonNull;
+  }
+
+  /**
+   * 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 this element as a {@link JsonObject}.
+   * @throws IllegalStateException if this element is of another type.
+   */
+  public JsonObject getAsJsonObject() {
+    if (isJsonObject()) {
+      return (JsonObject) this;
+    }
+    throw new IllegalStateException("Not a JSON Object: " + this);
+  }
+
+  /**
+   * 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 this element as a {@link JsonArray}.
+   * @throws IllegalStateException if this element is of another type.
+   */
+  public JsonArray getAsJsonArray() {
+    if (isJsonArray()) {
+      return (JsonArray) this;
+    }
+    throw new IllegalStateException("Not a JSON Array: " + this);
+  }
+
+  /**
+   * 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 this element as a {@link JsonPrimitive}.
+   * @throws IllegalStateException if this element is of another type.
+   */
+  public JsonPrimitive getAsJsonPrimitive() {
+    if (isJsonPrimitive()) {
+      return (JsonPrimitive) this;
+    }
+    throw new IllegalStateException("Not a JSON Primitive: " + this);
+  }
+
+  /**
+   * 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 this element as a {@link JsonNull}.
+   * @throws IllegalStateException if this element is of another type.
+   * @since 1.2
+   */
+  @CanIgnoreReturnValue // When this method is used only to verify that the value is JsonNull
+  public JsonNull getAsJsonNull() {
+    if (isJsonNull()) {
+      return (JsonNull) this;
+    }
+    throw new IllegalStateException("Not a JSON Null: " + this);
+  }
+
+  /**
+   * Convenience method to get this element as a boolean value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a {@link Number}.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a string value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive double value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive float value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive long value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive integer value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive byte value.
+   *
+   * @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
+   */
+  public byte getAsByte() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get the first character of the string value of this element.
+   *
+   * @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
+   *     a string's first character.
+   */
+  @Deprecated
+  public char getAsCharacter() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a {@link BigDecimal}.
+   *
+   * @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
+   */
+  public BigDecimal getAsBigDecimal() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a {@link BigInteger}.
+   *
+   * @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
+   */
+  public BigInteger getAsBigInteger() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive short value.
+   *
+   * @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() {
+    throw new UnsupportedOperationException(getClass().getSimpleName());
+  }
+
+  /** Returns a String representation of this element. */
+  @Override
+  public String toString() {
+    try {
+      StringWriter stringWriter = new StringWriter();
+      JsonWriter jsonWriter = new JsonWriter(stringWriter);
+      // Make writer lenient because toString() must not fail, even if for example JsonPrimitive
+      // contains NaN
+      jsonWriter.setStrictness(Strictness.LENIENT);
+      Streams.write(this, jsonWriter);
+      return stringWriter.toString();
+    } catch (IOException e) {
+      throw new AssertionError(e);
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonIOException.java b/gson/gson/src/main/java/com/google/gson/JsonIOException.java
new file mode 100644
index 0000000000000000000000000000000000000000..45f453b2887ebf9fde2da857a6af5124c41b4dab
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonIOException.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * This exception is raised when Gson was unable to read an input stream or write to one.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class JsonIOException extends JsonParseException {
+  private static final long serialVersionUID = 1L;
+
+  public JsonIOException(String msg) {
+    super(msg);
+  }
+
+  public JsonIOException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+
+  /**
+   * Creates exception with the specified cause. Consider using {@link #JsonIOException(String,
+   * Throwable)} instead if you can describe what happened.
+   *
+   * @param cause root exception that caused this exception to be thrown.
+   */
+  public JsonIOException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonNull.java b/gson/gson/src/main/java/com/google/gson/JsonNull.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cc78ff10683e1e2614e6e484446236077ab15d3
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonNull.java
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ * A class representing a JSON {@code null} value.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.2
+ */
+public final class JsonNull extends JsonElement {
+  /**
+   * Singleton for {@code JsonNull}.
+   *
+   * @since 1.8
+   */
+  public static final JsonNull INSTANCE = new JsonNull();
+
+  /**
+   * Creates a new {@code JsonNull} object.
+   *
+   * @deprecated Deprecated since Gson version 1.8, use {@link #INSTANCE} instead.
+   */
+  @Deprecated
+  public JsonNull() {
+    // Do nothing
+  }
+
+  /**
+   * Returns the same instance since it is an immutable value.
+   *
+   * @since 2.8.2
+   */
+  @Override
+  public JsonNull deepCopy() {
+    return INSTANCE;
+  }
+
+  /** All instances of {@code JsonNull} have the same hash code since they are indistinguishable. */
+  @Override
+  public int hashCode() {
+    return JsonNull.class.hashCode();
+  }
+
+  /** All instances of {@code JsonNull} are considered equal. */
+  @Override
+  public boolean equals(Object other) {
+    return other instanceof JsonNull;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonObject.java b/gson/gson/src/main/java/com/google/gson/JsonObject.java
new file mode 100644
index 0000000000000000000000000000000000000000..d13be19dce5963450a713c753799963d42a2593b
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonObject.java
@@ -0,0 +1,256 @@
+/*
+ * 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;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.internal.LinkedTreeMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class representing an object type in Json. An object consists of name-value pairs where names
+ * are strings, and values are any other type of {@link JsonElement}. This allows for a creating a
+ * tree of JsonElements. The member elements of this object are maintained in order they were added.
+ * This class does not support {@code null} values. If {@code null} is provided as value argument to
+ * any of the methods, it is converted to a {@link JsonNull}.
+ *
+ * <p>{@code JsonObject} does not implement the {@link Map} interface, but a {@code Map} view of it
+ * can be obtained with {@link #asMap()}.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class JsonObject extends JsonElement {
+  private final LinkedTreeMap<String, JsonElement> members = new LinkedTreeMap<>(false);
+
+  /** 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
+  public JsonObject deepCopy() {
+    JsonObject result = new JsonObject();
+    for (Map.Entry<String, JsonElement> entry : members.entrySet()) {
+      result.add(entry.getKey(), entry.getValue().deepCopy());
+    }
+    return result;
+  }
+
+  /**
+   * Adds a member, which is a name-value pair, to self. The name must be a String, but the value
+   * 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.
+   * @param value the member object.
+   */
+  public void add(String property, JsonElement value) {
+    members.put(property, value == null ? JsonNull.INSTANCE : value);
+  }
+
+  /**
+   * 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, or {@code null} if no member with
+   *     this name exists.
+   * @since 1.3
+   */
+  @CanIgnoreReturnValue
+  public JsonElement remove(String property) {
+    return members.remove(property);
+  }
+
+  /**
+   * 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.
+   */
+  public void addProperty(String property, String value) {
+    add(property, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value));
+  }
+
+  /**
+   * 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.
+   */
+  public void addProperty(String property, Number value) {
+    add(property, value == null ? JsonNull.INSTANCE : new JsonPrimitive(value));
+  }
+
+  /**
+   * Convenience method to add a boolean member. The specified value is converted to a {@link
+   * JsonPrimitive} of Boolean.
+   *
+   * @param property name of 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));
+  }
+
+  /**
+   * Convenience method to add a char member. The specified value is converted to a {@link
+   * JsonPrimitive} of Character.
+   *
+   * @param property name of 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));
+  }
+
+  /**
+   * Returns a set of members of this object. The set is ordered, and the order is in which the
+   * elements were added.
+   *
+   * @return a set of members of this object.
+   */
+  public Set<Map.Entry<String, JsonElement>> entrySet() {
+    return members.entrySet();
+  }
+
+  /**
+   * Returns a set of members key values.
+   *
+   * @return a set of member keys as Strings
+   * @since 2.8.1
+   */
+  public Set<String> keySet() {
+    return members.keySet();
+  }
+
+  /**
+   * 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();
+  }
+
+  /**
+   * Returns true if the number of key/value pairs in the object is zero.
+   *
+   * @return true if the number of key/value pairs in the object is zero.
+   * @since 2.10.1
+   */
+  public boolean isEmpty() {
+    return members.size() == 0;
+  }
+
+  /**
+   * Convenience method to check if a member with the specified name is present in this object.
+   *
+   * @param memberName name of the member that is being checked for presence.
+   * @return true if there is a member with the specified name, false otherwise.
+   */
+  public boolean has(String memberName) {
+    return members.containsKey(memberName);
+  }
+
+  /**
+   * Returns the member with the specified name.
+   *
+   * @param memberName name of the member that is being requested.
+   * @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 {@link JsonPrimitive}.
+   *
+   * @param memberName name of the member being requested.
+   * @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 {@link JsonArray}.
+   *
+   * @param memberName name of the member being requested.
+   * @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 {@link JsonObject}.
+   *
+   * @param memberName name of the member being requested.
+   * @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/gson/src/main/java/com/google/gson/JsonParseException.java b/gson/gson/src/main/java/com/google/gson/JsonParseException.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e58f0bb72e4e30524eb854a52e1bbc41eee5711
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonParseException.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+/**
+ * This exception is raised if there is a serious issue that occurs during parsing of a Json string.
+ * One of the main usages for this class is for the Gson infrastructure. If the incoming Json is
+ * bad/malicious, an instance of this exception is raised.
+ *
+ * <p>This exception is a {@link RuntimeException} because it is exposed to the client. Using a
+ * {@link RuntimeException} avoids bad coding practices on the client side where they catch the
+ * exception and do nothing. It is often the case that you want to blow up if there is a parsing
+ * error (i.e. often clients do not know how to recover from a {@link JsonParseException}.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class JsonParseException extends RuntimeException {
+  static final long serialVersionUID = -4086729973971783390L;
+
+  /**
+   * Creates exception with the specified message. If you are wrapping another exception, consider
+   * using {@link #JsonParseException(String, Throwable)} instead.
+   *
+   * @param msg error message describing a possible cause of this exception.
+   */
+  public JsonParseException(String msg) {
+    super(msg);
+  }
+
+  /**
+   * Creates exception with the specified message and cause.
+   *
+   * @param msg error message describing what happened.
+   * @param cause root exception that caused this exception to be thrown.
+   */
+  public JsonParseException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+
+  /**
+   * Creates exception with the specified cause. Consider using {@link #JsonParseException(String,
+   * Throwable)} instead if you can describe what happened.
+   *
+   * @param cause root exception that caused this exception to be thrown.
+   */
+  public JsonParseException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonParser.java b/gson/gson/src/main/java/com/google/gson/JsonParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9cfa5ecb6869f1035a8ee2fc03dccef6077327f
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonParser.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.google.errorprone.annotations.InlineMe;
+import com.google.gson.internal.Streams;
+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.io.StringReader;
+
+/**
+ * A parser to parse JSON into a parse tree of {@link JsonElement}s.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.3
+ */
+public final class JsonParser {
+  /**
+   * @deprecated No need to instantiate this class, use the static methods instead.
+   */
+  @Deprecated
+  public JsonParser() {}
+
+  /**
+   * Parses the specified JSON string into a parse tree. An exception is thrown if the JSON string
+   * has multiple top-level JSON elements, or if there is trailing data.
+   *
+   * <p>The JSON string is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}.
+   *
+   * @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));
+  }
+
+  /**
+   * Parses the complete JSON string provided by the reader into a parse tree. An exception is
+   * thrown if the JSON string has multiple top-level JSON elements, or if there is trailing data.
+   *
+   * <p>The JSON data is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}.
+   *
+   * @param reader JSON text
+   * @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 {
+      JsonReader jsonReader = new JsonReader(reader);
+      JsonElement element = parseReader(jsonReader);
+      if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) {
+        throw new JsonSyntaxException("Did not consume the entire document.");
+      }
+      return element;
+    } catch (MalformedJsonException e) {
+      throw new JsonSyntaxException(e);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    } catch (NumberFormatException e) {
+      throw new JsonSyntaxException(e);
+    }
+  }
+
+  /**
+   * Returns the next value from the JSON stream as a parse tree. Unlike the other {@code parse}
+   * 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#setStrictness(Strictness) lenient mode},
+   * regardless of the strictness setting of the provided reader. The strictness setting of the
+   * reader is restored once this method returns.
+   *
+   * @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 {
+    Strictness strictness = reader.getStrictness();
+    reader.setStrictness(Strictness.LENIENT);
+    try {
+      return Streams.parse(reader);
+    } catch (StackOverflowError e) {
+      throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
+    } catch (OutOfMemoryError e) {
+      throw new JsonParseException("Failed parsing JSON source: " + reader + " to Json", e);
+    } finally {
+      reader.setStrictness(strictness);
+    }
+  }
+
+  /**
+   * @deprecated Use {@link JsonParser#parseString}
+   */
+  @Deprecated
+  @InlineMe(replacement = "JsonParser.parseString(json)", imports = "com.google.gson.JsonParser")
+  public JsonElement parse(String json) throws JsonSyntaxException {
+    return parseString(json);
+  }
+
+  /**
+   * @deprecated Use {@link JsonParser#parseReader(Reader)}
+   */
+  @Deprecated
+  @InlineMe(replacement = "JsonParser.parseReader(json)", imports = "com.google.gson.JsonParser")
+  public JsonElement parse(Reader json) throws JsonIOException, JsonSyntaxException {
+    return parseReader(json);
+  }
+
+  /**
+   * @deprecated Use {@link JsonParser#parseReader(JsonReader)}
+   */
+  @Deprecated
+  @InlineMe(replacement = "JsonParser.parseReader(json)", imports = "com.google.gson.JsonParser")
+  public JsonElement parse(JsonReader json) throws JsonIOException, JsonSyntaxException {
+    return parseReader(json);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonPrimitive.java b/gson/gson/src/main/java/com/google/gson/JsonPrimitive.java
new file mode 100644
index 0000000000000000000000000000000000000000..2985ce98abb07bf8e8b24c041381843f06990ddb
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonPrimitive.java
@@ -0,0 +1,320 @@
+/*
+ * 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;
+
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.internal.NumberLimits;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Objects;
+
+/**
+ * A class representing a JSON primitive value. A primitive value is either a String, a Java
+ * primitive, or a Java primitive wrapper type.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class JsonPrimitive extends JsonElement {
+
+  private final Object value;
+
+  /**
+   * Create a primitive containing a boolean value.
+   *
+   * @param bool the value to create the primitive with.
+   */
+  // "deprecation" suppression for superclass constructor
+  // "UnnecessaryBoxedVariable" Error Prone warning is correct since method does not accept
+  // null, but cannot be changed anymore since this is public API
+  @SuppressWarnings({"deprecation", "UnnecessaryBoxedVariable"})
+  public JsonPrimitive(Boolean bool) {
+    value = Objects.requireNonNull(bool);
+  }
+
+  /**
+   * Create a primitive containing a {@link Number}.
+   *
+   * @param number the value to create the primitive with.
+   */
+  @SuppressWarnings("deprecation") // superclass constructor
+  public JsonPrimitive(Number number) {
+    value = Objects.requireNonNull(number);
+  }
+
+  /**
+   * Create a primitive containing a String value.
+   *
+   * @param string the value to create the primitive with.
+   */
+  @SuppressWarnings("deprecation") // superclass constructor
+  public JsonPrimitive(String 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.
+   *
+   * @param c the value to create the primitive with.
+   */
+  // "deprecation" suppression for superclass constructor
+  // "UnnecessaryBoxedVariable" Error Prone warning is correct since method does not accept
+  // null, but cannot be changed anymore since this is public API
+  @SuppressWarnings({"deprecation", "UnnecessaryBoxedVariable"})
+  public JsonPrimitive(Character c) {
+    // convert characters to strings since in JSON, characters are represented as a single
+    // character string
+    value = Objects.requireNonNull(c).toString();
+  }
+
+  /**
+   * Returns the same value as primitives are immutable.
+   *
+   * @since 2.8.2
+   */
+  @Override
+  public JsonPrimitive deepCopy() {
+    return this;
+  }
+
+  /**
+   * Check whether this primitive contains a boolean value.
+   *
+   * @return true if this primitive contains a boolean value, false otherwise.
+   */
+  public boolean isBoolean() {
+    return value instanceof Boolean;
+  }
+
+  /**
+   * 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() {
+    if (isBoolean()) {
+      return (Boolean) value;
+    }
+    // Check to see if the value as a String is "true" in any case.
+    return Boolean.parseBoolean(getAsString());
+  }
+
+  /**
+   * Check whether this primitive contains a Number.
+   *
+   * @return true if this primitive contains a Number, false otherwise.
+   */
+  public boolean isNumber() {
+    return value instanceof 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}).
+   *
+   * @throws UnsupportedOperationException if this primitive is neither a number nor a string.
+   */
+  @Override
+  public Number getAsNumber() {
+    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");
+  }
+
+  /**
+   * Check whether this primitive contains a String value.
+   *
+   * @return true if this primitive contains a String value, false otherwise.
+   */
+  public boolean isString() {
+    return value instanceof String;
+  }
+
+  // Don't add Javadoc, inherit it from super implementation; no exceptions are thrown here
+  @Override
+  public String getAsString() {
+    if (value instanceof String) {
+      return (String) value;
+    } else if (isNumber()) {
+      return getAsNumber().toString();
+    } else if (isBoolean()) {
+      return ((Boolean) value).toString();
+    }
+    throw new AssertionError("Unexpected value type: " + value.getClass());
+  }
+
+  /**
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public double getAsDouble() {
+    return isNumber() ? getAsNumber().doubleValue() : Double.parseDouble(getAsString());
+  }
+
+  /**
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public BigDecimal getAsBigDecimal() {
+    return value instanceof BigDecimal
+        ? (BigDecimal) value
+        : NumberLimits.parseBigDecimal(getAsString());
+  }
+
+  /**
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public BigInteger getAsBigInteger() {
+    return value instanceof BigInteger
+        ? (BigInteger) value
+        : isIntegral(this)
+            ? BigInteger.valueOf(this.getAsNumber().longValue())
+            : NumberLimits.parseBigInteger(this.getAsString());
+  }
+
+  /**
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public float getAsFloat() {
+    return isNumber() ? getAsNumber().floatValue() : Float.parseFloat(getAsString());
+  }
+
+  /**
+   * Convenience method to get this element as a primitive long.
+   *
+   * @return this element as a primitive long.
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public long getAsLong() {
+    return isNumber() ? getAsNumber().longValue() : Long.parseLong(getAsString());
+  }
+
+  /**
+   * @throws NumberFormatException {@inheritDoc}
+   */
+  @Override
+  public short getAsShort() {
+    return isNumber() ? getAsNumber().shortValue() : Short.parseShort(getAsString());
+  }
+
+  /**
+   * @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() {
+    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) {
+      return 31;
+    }
+    // Using recommended hashing algorithm from Effective Java for longs and doubles
+    if (isIntegral(this)) {
+      long value = getAsNumber().longValue();
+      return (int) (value ^ (value >>> 32));
+    }
+    if (value instanceof Number) {
+      long value = Double.doubleToLongBits(getAsNumber().doubleValue());
+      return (int) (value ^ (value >>> 32));
+    }
+    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) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    JsonPrimitive other = (JsonPrimitive) obj;
+    if (value == null) {
+      return other.value == null;
+    }
+    if (isIntegral(this) && isIntegral(other)) {
+      return (this.value instanceof BigInteger || other.value instanceof BigInteger)
+          ? this.getAsBigInteger().equals(other.getAsBigInteger())
+          : this.getAsNumber().longValue() == other.getAsNumber().longValue();
+    }
+    if (value instanceof Number && other.value instanceof Number) {
+      double a = getAsNumber().doubleValue();
+      // Java standard types other than double return true for two NaN. So, need
+      // special handling for double.
+      double b = other.getAsNumber().doubleValue();
+      return a == b || (Double.isNaN(a) && Double.isNaN(b));
+    }
+    return value.equals(other.value);
+  }
+
+  /**
+   * Returns true if the specified number is an integral type (Long, Integer, Short, Byte,
+   * BigInteger)
+   */
+  private static boolean isIntegral(JsonPrimitive primitive) {
+    if (primitive.value instanceof Number) {
+      Number number = (Number) primitive.value;
+      return number instanceof BigInteger
+          || number instanceof Long
+          || number instanceof Integer
+          || number instanceof Short
+          || number instanceof Byte;
+    }
+    return false;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonSerializationContext.java b/gson/gson/src/main/java/com/google/gson/JsonSerializationContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5c05b07b3e36d3da83689d448f22070111d1c22
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonSerializationContext.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+
+/**
+ * Context for serialization that is passed to a custom serializer during invocation of its {@link
+ * JsonSerializer#serialize(Object, Type, JsonSerializationContext)} method.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public interface JsonSerializationContext {
+
+  /**
+   * Invokes default serialization on the specified object.
+   *
+   * @param src the object that needs to be serialized.
+   * @return a tree of {@link JsonElement}s corresponding to the serialized form of {@code src}.
+   */
+  public JsonElement serialize(Object src);
+
+  /**
+   * Invokes default serialization on the specified object passing the specific type information. It
+   * should never be invoked on the element received as a parameter of the {@link
+   * JsonSerializer#serialize(Object, Type, JsonSerializationContext)} method. Doing so will result
+   * in an infinite loop since Gson will in-turn call the custom serializer again.
+   *
+   * @param src the object that needs to be serialized.
+   * @param typeOfSrc the actual genericized type of src object.
+   * @return a tree of {@link JsonElement}s corresponding to the serialized form of {@code src}.
+   */
+  public JsonElement serialize(Object src, Type typeOfSrc);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonSerializer.java b/gson/gson/src/main/java/com/google/gson/JsonSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..550fd28505ff5cd7a4d2e61f7394204d5bbad474
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonSerializer.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+
+/**
+ * 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)}.
+ *
+ * <p>Let us look at example where defining a serializer will be useful. The {@code Id} class
+ * defined below has two fields: {@code clazz} and {@code value}.
+ *
+ * <pre>
+ * public class Id&lt;T&gt; {
+ *   private final Class&lt;T&gt; clazz;
+ *   private final long value;
+ *
+ *   public Id(Class&lt;T&gt; clazz, long value) {
+ *     this.clazz = clazz;
+ *     this.value = value;
+ *   }
+ *
+ *   public long getValue() {
+ *     return value;
+ *   }
+ * }
+ * </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 the value
+ * instead, which is {@code 20} in this case. You can achieve that by writing a custom serializer:
+ *
+ * <pre>
+ * class IdSerializer implements JsonSerializer&lt;Id&gt; {
+ *   public JsonElement serialize(Id id, Type typeOfId, JsonSerializationContext context) {
+ *     return new JsonPrimitive(id.getValue());
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>You will also need to register {@code IdSerializer} with Gson as follows:
+ *
+ * <pre>
+ * Gson gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdSerializer()).create();
+ * </pre>
+ *
+ * <p>Serializers should be stateless and thread-safe, otherwise the thread-safety guarantees of
+ * {@link Gson} might not apply.
+ *
+ * <p>New applications should prefer {@link TypeAdapter}, whose streaming API is more efficient than
+ * this interface's tree API.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @param <T> type for which the serializer is being registered. It is possible that a serializer
+ *     may be asked to serialize a specific generic type of the T.
+ */
+public interface JsonSerializer<T> {
+
+  /**
+   * Gson invokes this call-back method during serialization when it encounters a field of the
+   * specified type.
+   *
+   * <p>In the implementation of this call-back method, you should consider invoking {@link
+   * JsonSerializationContext#serialize(Object, Type)} method to create JsonElements for any
+   * non-trivial field of the {@code src} object. However, you should never invoke it on the {@code
+   * src} object itself since that will cause an infinite loop (Gson will call your call-back method
+   * again).
+   *
+   * @param src the object that needs to be converted to Json.
+   * @param typeOfSrc the actual type (fully genericized version) of the source object.
+   * @return a JsonElement corresponding to the specified object.
+   */
+  public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonStreamParser.java b/gson/gson/src/main/java/com/google/gson/JsonStreamParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..058d10da29a992febe74716ba2a5a1ed9ad07841
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonStreamParser.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2009 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;
+
+import com.google.gson.internal.Streams;
+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.io.StringReader;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A streaming parser that allows reading of multiple {@link JsonElement}s from the specified reader
+ * asynchronously. The JSON data is parsed in lenient mode, see also {@link
+ * JsonReader#setStrictness(Strictness)}.
+ *
+ * <p>This class is conditionally thread-safe (see Item 70, Effective Java second edition). To
+ * properly use this class across multiple threads, you will need to add some external
+ * synchronization. For example:
+ *
+ * <pre>
+ * JsonStreamParser parser = new JsonStreamParser("['first'] {'second':10} 'third'");
+ * JsonElement element;
+ * synchronized (parser) {  // synchronize on an object shared by threads
+ *   if (parser.hasNext()) {
+ *     element = parser.next();
+ *   }
+ * }
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @since 1.4
+ */
+public final class JsonStreamParser implements Iterator<JsonElement> {
+  private final JsonReader parser;
+  private final Object lock;
+
+  /**
+   * @param json The string containing JSON elements concatenated to each other.
+   * @since 1.4
+   */
+  public JsonStreamParser(String json) {
+    this(new StringReader(json));
+  }
+
+  /**
+   * @param reader The data stream containing JSON elements concatenated to each other.
+   * @since 1.4
+   */
+  public JsonStreamParser(Reader reader) {
+    parser = new JsonReader(reader);
+    parser.setStrictness(Strictness.LENIENT);
+    lock = new Object();
+  }
+
+  /**
+   * Returns the next available {@link JsonElement} on the reader. Throws a {@link
+   * NoSuchElementException} if no element is available.
+   *
+   * @return the next available {@code JsonElement} on the reader.
+   * @throws JsonParseException if the incoming stream is malformed JSON.
+   * @throws NoSuchElementException if no {@code JsonElement} is available.
+   * @since 1.4
+   */
+  @Override
+  public JsonElement next() throws JsonParseException {
+    if (!hasNext()) {
+      throw new NoSuchElementException();
+    }
+
+    try {
+      return Streams.parse(parser);
+    } catch (StackOverflowError e) {
+      throw new JsonParseException("Failed parsing JSON source to Json", e);
+    } catch (OutOfMemoryError e) {
+      throw new JsonParseException("Failed parsing JSON source to Json", e);
+    }
+  }
+
+  /**
+   * Returns true if a {@link JsonElement} is available on the input for consumption
+   *
+   * @return true if a {@link JsonElement} is available on the input, false otherwise
+   * @throws JsonParseException if the incoming stream is malformed JSON.
+   * @since 1.4
+   */
+  @Override
+  public boolean hasNext() {
+    synchronized (lock) {
+      try {
+        return parser.peek() != JsonToken.END_DOCUMENT;
+      } catch (MalformedJsonException e) {
+        throw new JsonSyntaxException(e);
+      } catch (IOException e) {
+        throw new JsonIOException(e);
+      }
+    }
+  }
+
+  /**
+   * This optional {@link Iterator} method is not relevant for stream parsing and hence is not
+   * implemented.
+   *
+   * @since 1.4
+   */
+  @Override
+  public void remove() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/JsonSyntaxException.java b/gson/gson/src/main/java/com/google/gson/JsonSyntaxException.java
new file mode 100644
index 0000000000000000000000000000000000000000..7bafe26676ca9a3976114c80212c5b1457e2ea08
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/JsonSyntaxException.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010 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;
+
+/**
+ * This exception is raised when Gson attempts to read (or write) a malformed JSON element.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class JsonSyntaxException extends JsonParseException {
+
+  private static final long serialVersionUID = 1L;
+
+  public JsonSyntaxException(String msg) {
+    super(msg);
+  }
+
+  public JsonSyntaxException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+
+  /**
+   * Creates exception with the specified cause. Consider using {@link #JsonSyntaxException(String,
+   * Throwable)} instead if you can describe what actually happened.
+   *
+   * @param cause root exception that caused this exception to be thrown.
+   */
+  public JsonSyntaxException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/LongSerializationPolicy.java b/gson/gson/src/main/java/com/google/gson/LongSerializationPolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..df7c8fa1677622049bd9dd9847e39b2480776e5d
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/LongSerializationPolicy.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2009 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;
+
+/**
+ * Defines the expected format for a {@code long} or {@code Long} type when it is serialized.
+ *
+ * @since 1.3
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public enum LongSerializationPolicy {
+  /**
+   * This is the "default" serialization policy that will output a {@code Long} object as a JSON
+   * number. For example, assume an object has a long field named "f" then the serialized output
+   * would be: {@code {"f":123}}
+   *
+   * <p>A {@code null} value is serialized as {@link JsonNull}.
+   */
+  DEFAULT() {
+    @Override
+    public JsonElement serialize(Long value) {
+      if (value == null) {
+        return JsonNull.INSTANCE;
+      }
+      return new JsonPrimitive(value);
+    }
+  },
+
+  /**
+   * Serializes a long value as a quoted string. For example, assume an object has a long field
+   * named "f" then the serialized output would be: {@code {"f":"123"}}
+   *
+   * <p>A {@code null} value is serialized as {@link JsonNull}.
+   */
+  STRING() {
+    @Override
+    public JsonElement serialize(Long value) {
+      if (value == null) {
+        return JsonNull.INSTANCE;
+      }
+      return new JsonPrimitive(value.toString());
+    }
+  };
+
+  /**
+   * Serialize this {@code value} using this serialization policy.
+   *
+   * @param value the long value to be serialized into a {@link JsonElement}
+   * @return the serialized version of {@code value}
+   */
+  public abstract JsonElement serialize(Long value);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java b/gson/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..6581da8c9ea32cb77ae856339e49df559df8dc94
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
@@ -0,0 +1,228 @@
+/*
+ * 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;
+
+import com.google.gson.internal.ReflectionAccessFilterHelper;
+import java.lang.reflect.AccessibleObject;
+
+/**
+ * Filter for determining whether reflection based serialization and deserialization is allowed for
+ * a class.
+ *
+ * <p>A filter can be useful in multiple scenarios, for example when upgrading to newer Java
+ * versions which use the Java Platform Module System (JPMS). A filter then allows to {@linkplain
+ * FilterResult#BLOCK_INACCESSIBLE prevent making inaccessible members accessible}, even if the used
+ * Java version might still allow illegal access (but logs a warning), or if {@code java} command
+ * line arguments are used to open the inaccessible packages to other parts of the application. This
+ * interface defines some convenience filters for this task, such as {@link
+ * #BLOCK_INACCESSIBLE_JAVA}.
+ *
+ * <p>A filter can also be useful to prevent mixing model classes of a project with other non-model
+ * classes; the filter could {@linkplain FilterResult#BLOCK_ALL block all reflective access} to
+ * non-model classes.
+ *
+ * <p>A reflection access filter is similar to an {@link ExclusionStrategy} with the major
+ * difference that a filter will cause an exception to be thrown when access is disallowed while an
+ * exclusion strategy just skips 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 {
+    /**
+     * Reflection access for the class is allowed.
+     *
+     * <p>Note that this does not affect the Java access checks in any way, it only permits Gson to
+     * try using reflection for a class. The Java runtime might still deny such access.
+     */
+    ALLOW,
+    /**
+     * The filter is indecisive whether reflection access should be allowed. The next registered
+     * filter will be consulted to get the result. If there is no next filter, this result acts like
+     * {@link #ALLOW}.
+     */
+    INDECISIVE,
+    /**
+     * Blocks reflection access if a member of the class is not accessible by default and would have
+     * to be made accessible. This is unaffected by any {@code java} command line arguments being
+     * used to make packages accessible, or by module declaration directives which <i>open</i> the
+     * complete module or certain packages for reflection and will consider such packages
+     * inaccessible.
+     *
+     * <p>Note that this <b>only works for Java 9 and higher</b>, for older Java versions its
+     * functionality will be limited and it might behave like {@link #ALLOW}. Access checks are only
+     * performed as defined by the Java Language Specification (<a
+     * href="https://docs.oracle.com/javase/specs/jls/se11/html/jls-6.html#jls-6.6">JLS 11
+     * &sect;6.6</a>), restrictions imposed by a {@link SecurityManager} are not considered.
+     *
+     * <p>This result type is mainly intended to help enforce the access checks of the Java Platform
+     * Module System. It allows detecting illegal access, even if the used Java version would only
+     * log a warning, or is configured to open packages for reflection using command line arguments.
+     *
+     * @see AccessibleObject#canAccess(Object)
+     */
+    BLOCK_INACCESSIBLE,
+    /**
+     * Blocks all reflection access for the class. Other means for serializing and deserializing the
+     * class, such as a {@link TypeAdapter}, have to be used.
+     */
+    BLOCK_ALL
+  }
+
+  /**
+   * Blocks all reflection access to members of standard Java classes which are not accessible by
+   * default. However, reflection access is still allowed for classes for which all fields are
+   * accessible and which have an accessible no-args constructor (or for which an {@link
+   * InstanceCreator} has been registered).
+   *
+   * <p>If this filter encounters a class other than a standard Java class it returns {@link
+   * FilterResult#INDECISIVE}.
+   *
+   * <p>This filter is mainly intended to help enforcing the access checks of Java Platform Module
+   * System. It allows detecting illegal access, even if the used Java version would only log a
+   * warning, or is configured to open packages for reflection. However, this filter <b>only works
+   * for Java 9 and higher</b>, when using an older Java version its functionality will be limited.
+   *
+   * <p>Note that this filter might not cover all standard Java classes. Currently only classes in a
+   * {@code java.*} or {@code javax.*} package are considered. The set of detected classes might be
+   * expanded in the future without prior notice.
+   *
+   * @see FilterResult#BLOCK_INACCESSIBLE
+   */
+  ReflectionAccessFilter BLOCK_INACCESSIBLE_JAVA =
+      new ReflectionAccessFilter() {
+        @Override
+        public FilterResult check(Class<?> rawClass) {
+          return ReflectionAccessFilterHelper.isJavaType(rawClass)
+              ? FilterResult.BLOCK_INACCESSIBLE
+              : FilterResult.INDECISIVE;
+        }
+
+        @Override
+        public String toString() {
+          return "ReflectionAccessFilter#BLOCK_INACCESSIBLE_JAVA";
+        }
+      };
+
+  /**
+   * Blocks all reflection access to members of standard Java classes.
+   *
+   * <p>If this filter encounters a class other than a standard Java class it returns {@link
+   * FilterResult#INDECISIVE}.
+   *
+   * <p>This filter is mainly intended to prevent depending on implementation details of the Java
+   * platform and to help applications prepare for upgrading to the Java Platform Module System.
+   *
+   * <p>Note that this filter might not cover all standard Java classes. Currently only classes in a
+   * {@code java.*} or {@code javax.*} package are considered. The set of detected classes might be
+   * expanded in the future without prior notice.
+   *
+   * @see #BLOCK_INACCESSIBLE_JAVA
+   * @see FilterResult#BLOCK_ALL
+   */
+  ReflectionAccessFilter BLOCK_ALL_JAVA =
+      new ReflectionAccessFilter() {
+        @Override
+        public FilterResult check(Class<?> rawClass) {
+          return ReflectionAccessFilterHelper.isJavaType(rawClass)
+              ? FilterResult.BLOCK_ALL
+              : FilterResult.INDECISIVE;
+        }
+
+        @Override
+        public String toString() {
+          return "ReflectionAccessFilter#BLOCK_ALL_JAVA";
+        }
+      };
+
+  /**
+   * Blocks all reflection access to members of standard Android classes.
+   *
+   * <p>If this filter encounters a class other than a standard Android class it returns {@link
+   * FilterResult#INDECISIVE}.
+   *
+   * <p>This filter is mainly intended to prevent depending on implementation details of the Android
+   * platform.
+   *
+   * <p>Note that this filter might not cover all standard Android classes. Currently only classes
+   * in an {@code android.*} or {@code androidx.*} package, and standard Java classes in a {@code
+   * java.*} or {@code javax.*} package are considered. The set of detected classes might be
+   * expanded in the future without prior notice.
+   *
+   * @see FilterResult#BLOCK_ALL
+   */
+  ReflectionAccessFilter BLOCK_ALL_ANDROID =
+      new ReflectionAccessFilter() {
+        @Override
+        public FilterResult check(Class<?> rawClass) {
+          return ReflectionAccessFilterHelper.isAndroidType(rawClass)
+              ? FilterResult.BLOCK_ALL
+              : FilterResult.INDECISIVE;
+        }
+
+        @Override
+        public String toString() {
+          return "ReflectionAccessFilter#BLOCK_ALL_ANDROID";
+        }
+      };
+
+  /**
+   * Blocks all reflection access to members of classes belonging to programming language platforms,
+   * such as Java, Android, Kotlin or Scala.
+   *
+   * <p>If this filter encounters a class other than a standard platform class it returns {@link
+   * FilterResult#INDECISIVE}.
+   *
+   * <p>This filter is mainly intended to prevent depending on implementation details of the
+   * platform classes.
+   *
+   * <p>Note that this filter might not cover all platform classes. Currently it combines the
+   * filters {@link #BLOCK_ALL_JAVA} and {@link #BLOCK_ALL_ANDROID}, and checks for other
+   * language-specific platform classes like {@code kotlin.*}. The set of detected classes might be
+   * expanded in the future without prior notice.
+   *
+   * @see FilterResult#BLOCK_ALL
+   */
+  ReflectionAccessFilter BLOCK_ALL_PLATFORM =
+      new ReflectionAccessFilter() {
+        @Override
+        public FilterResult check(Class<?> rawClass) {
+          return ReflectionAccessFilterHelper.isAnyPlatformType(rawClass)
+              ? FilterResult.BLOCK_ALL
+              : FilterResult.INDECISIVE;
+        }
+
+        @Override
+        public String toString() {
+          return "ReflectionAccessFilter#BLOCK_ALL_PLATFORM";
+        }
+      };
+
+  /**
+   * Checks if reflection access should be allowed for a class.
+   *
+   * @param rawClass Class to check
+   * @return Result indicating whether reflection access is allowed
+   */
+  FilterResult check(Class<?> rawClass);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/Strictness.java b/gson/gson/src/main/java/com/google/gson/Strictness.java
new file mode 100644
index 0000000000000000000000000000000000000000..775214f36b42bf590cadb69467468d9b1e410b65
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/Strictness.java
@@ -0,0 +1,28 @@
+package com.google.gson;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * Modes that indicate how strictly a JSON {@linkplain JsonReader reader} or {@linkplain JsonWriter
+ * writer} follows the syntax laid out in the <a href="https://www.ietf.org/rfc/rfc8259.txt">RFC
+ * 8259 JSON specification</a>.
+ *
+ * <p>You can look at {@link JsonReader#setStrictness(Strictness)} to see how the strictness affects
+ * the {@link JsonReader} and you can look at {@link JsonWriter#setStrictness(Strictness)} to see
+ * how the strictness affects the {@link JsonWriter}.
+ *
+ * @see JsonReader#setStrictness(Strictness)
+ * @see JsonWriter#setStrictness(Strictness)
+ * @since $next-version$
+ */
+public enum Strictness {
+  /** Allow large deviations from the JSON specification. */
+  LENIENT,
+
+  /** Allow certain small deviations from the JSON specification for legacy reasons. */
+  LEGACY_STRICT,
+
+  /** Strict compliance with the JSON specification. */
+  STRICT
+}
diff --git a/gson/gson/src/main/java/com/google/gson/ToNumberPolicy.java b/gson/gson/src/main/java/com/google/gson/ToNumberPolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..6380e5d9867865b14d4c9582436cf4a512d52d87
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/ToNumberPolicy.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2021 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;
+
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.internal.NumberLimits;
+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 strategies to
+ * overcome some historical Gson limitations while deserializing numbers as {@link Object} and
+ * {@link Number}.
+ *
+ * @see ToNumberStrategy
+ * @since 2.8.9
+ */
+public enum ToNumberPolicy implements ToNumberStrategy {
+
+  /**
+   * Using this policy will ensure that numbers will be read as {@link Double} values. This is the
+   * default strategy used during deserialization of numbers as {@link Object}.
+   */
+  DOUBLE {
+    @Override
+    public Double readNumber(JsonReader in) throws IOException {
+      return in.nextDouble();
+    }
+  },
+
+  /**
+   * Using this policy will ensure that numbers will be read as a lazily parsed number backed by a
+   * string. This is the default strategy used during deserialization of numbers as {@link Number}.
+   */
+  LAZILY_PARSED_NUMBER {
+    @Override
+    public Number readNumber(JsonReader in) throws IOException {
+      return new LazilyParsedNumber(in.nextString());
+    }
+  },
+
+  /**
+   * Using this policy will ensure that numbers will be read as {@link Long} or {@link Double}
+   * values depending on how JSON numbers are represented: {@code Long} if the JSON number can be
+   * parsed as a {@code Long} value, or otherwise {@code Double} if it can be parsed as a {@code
+   * Double} value. If the parsed double-precision number results in a positive or negative infinity
+   * ({@link Double#isInfinite()}) or a NaN ({@link Double#isNaN()}) value and the {@code
+   * JsonReader} is not {@link JsonReader#isLenient() lenient}, a {@link MalformedJsonException} is
+   * thrown.
+   */
+  LONG_OR_DOUBLE {
+    @Override
+    public Number readNumber(JsonReader in) throws IOException, JsonParseException {
+      String value = in.nextString();
+      try {
+        return Long.parseLong(value);
+      } catch (NumberFormatException longE) {
+        try {
+          Double d = Double.valueOf(value);
+          if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) {
+            throw new MalformedJsonException(
+                "JSON forbids NaN and infinities: " + d + "; at path " + in.getPreviousPath());
+          }
+          return d;
+        } catch (NumberFormatException doubleE) {
+          throw new JsonParseException(
+              "Cannot parse " + value + "; at path " + in.getPreviousPath(), doubleE);
+        }
+      }
+    }
+  },
+
+  /**
+   * Using this policy will ensure that numbers will be read as numbers of arbitrary length using
+   * {@link BigDecimal}.
+   */
+  BIG_DECIMAL {
+    @Override
+    public BigDecimal readNumber(JsonReader in) throws IOException {
+      String value = in.nextString();
+      try {
+        return NumberLimits.parseBigDecimal(value);
+      } catch (NumberFormatException e) {
+        throw new JsonParseException(
+            "Cannot parse " + value + "; at path " + in.getPreviousPath(), e);
+      }
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/ToNumberStrategy.java b/gson/gson/src/main/java/com/google/gson/ToNumberStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbbd415aee1c98e1d8ab5a207abaae0636ca03ce
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/ToNumberStrategy.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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;
+
+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} when a concrete type of the deserialized number is unknown in advance. By default,
+ * Gson uses the following deserialization strategies:
+ *
+ * <ul>
+ *   <li>{@link Double} values are returned for JSON numbers if the deserialization type is declared
+ *       as {@code Object}, see {@link ToNumberPolicy#DOUBLE};
+ *   <li>Lazily parsed number values are returned if the deserialization type is declared as {@code
+ *       Number}, see {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}.
+ * </ul>
+ *
+ * <p>For historical reasons, Gson does not support deserialization of arbitrary-length numbers for
+ * {@code Object} and {@code Number} by default, potentially causing precision loss. However, <a
+ * href="https://tools.ietf.org/html/rfc8259#section-6">RFC 8259</a> permits this:
+ *
+ * <pre>
+ *   This specification allows implementations to set limits on the range
+ *   and precision of numbers accepted.  Since software that implements
+ *   IEEE 754 binary64 (double precision) numbers [IEEE754] is generally
+ *   available and widely used, good interoperability can be achieved by
+ *   implementations that expect no more precision or range than these
+ *   provide, in the sense that implementations will approximate JSON
+ *   numbers within the expected precision.  A JSON number such as 1E400
+ *   or 3.141592653589793238462643383279 may indicate potential
+ *   interoperability problems, since it suggests that the software that
+ *   created it expects receiving software to have greater capabilities
+ *   for numeric magnitude and precision than is widely available.
+ * </pre>
+ *
+ * <p>To overcome the precision loss, use for example {@link ToNumberPolicy#LONG_OR_DOUBLE} or
+ * {@link ToNumberPolicy#BIG_DECIMAL}.
+ *
+ * @see ToNumberPolicy
+ * @see GsonBuilder#setObjectToNumberStrategy(ToNumberStrategy)
+ * @see GsonBuilder#setNumberToNumberStrategy(ToNumberStrategy)
+ * @since 2.8.9
+ */
+public interface ToNumberStrategy {
+
+  /**
+   * Reads a number from the given JSON reader. A strategy is supposed to read a single value from
+   * the reader, and the read value is guaranteed never to be {@code null}.
+   *
+   * @param in JSON reader to read a number from
+   * @return number read from the JSON reader.
+   */
+  public Number readNumber(JsonReader in) throws IOException;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/gson/src/main/java/com/google/gson/TypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..90d330e382a9512dcdfebb6c3500fe03e0eda900
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/TypeAdapter.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2011 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;
+
+import com.google.gson.internal.bind.JsonTreeReader;
+import com.google.gson.internal.bind.JsonTreeWriter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+/**
+ * Converts Java objects to and from JSON.
+ *
+ * <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 adapter for an (X,Y) coordinate point:
+ *
+ * <pre>{@code
+ * public class PointAdapter extends TypeAdapter<Point> {
+ *   public Point read(JsonReader reader) throws IOException {
+ *     if (reader.peek() == JsonToken.NULL) {
+ *       reader.nextNull();
+ *       return null;
+ *     }
+ *     String xy = reader.nextString();
+ *     String[] parts = xy.split(",");
+ *     int x = Integer.parseInt(parts[0]);
+ *     int y = Integer.parseInt(parts[1]);
+ *     return new Point(x, y);
+ *   }
+ *   public void write(JsonWriter writer, Point value) throws IOException {
+ *     if (value == null) {
+ *       writer.nullValue();
+ *       return;
+ *     }
+ *     String xy = value.getX() + "," + value.getY();
+ *     writer.value(xy);
+ *   }
+ * }
+ * }</pre>
+ *
+ * With this type adapter installed, Gson will convert {@code Points} to JSON as strings like {@code
+ * "5,8"} rather than objects like {@code {"x":5,"y":8}}. In this case the type adapter binds a rich
+ * Java class to a compact JSON value.
+ *
+ * <p>The {@link #read(JsonReader) read()} method must read exactly one value and {@link
+ * #write(JsonWriter,Object) write()} must write exactly one value. For primitive types this is
+ * means readers should make exactly one call to {@code nextBoolean()}, {@code nextDouble()}, {@code
+ * nextInt()}, {@code nextLong()}, {@code nextString()} or {@code nextNull()}. Writers should make
+ * exactly one call to one of {@code value()} or {@code nullValue()}. For arrays, type adapters
+ * should start with a call to {@code beginArray()}, convert all elements, and finish with a call to
+ * {@code endArray()}. For objects, they should start with {@code beginObject()}, convert the
+ * object, and finish with {@code endObject()}. Failing to convert a value or converting too many
+ * values may cause the application to crash.
+ *
+ * <p>Type adapters should be prepared to read null from the stream and write it to the stream.
+ * Alternatively, they should use {@link #nullSafe()} method while registering the type adapter with
+ * Gson. If your {@code Gson} instance has been configured to {@link GsonBuilder#serializeNulls()},
+ * these nulls will be written to the final document. Otherwise the value (and the corresponding
+ * name when writing to a JSON object) will be omitted automatically. In either case your type
+ * adapter must handle null.
+ *
+ * <p>Type adapters should be stateless and thread-safe, otherwise the thread-safety guarantees of
+ * {@link Gson} might not apply.
+ *
+ * <p>To use a custom type adapter with Gson, you must <i>register</i> it with a {@link
+ * GsonBuilder}:
+ *
+ * <pre>{@code
+ * GsonBuilder builder = new GsonBuilder();
+ * builder.registerTypeAdapter(Point.class, new PointAdapter());
+ * // if PointAdapter didn't check for nulls in its read/write methods, you should instead use
+ * // builder.registerTypeAdapter(Point.class, new PointAdapter().nullSafe());
+ * ...
+ * Gson gson = builder.create();
+ * }</pre>
+ *
+ * @since 2.1
+ */
+// non-Javadoc:
+//
+// <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
+//   String json = "{'origin':'0,0','points':['1,2','3,4']}";
+//   TypeAdapter<Graph> graphAdapter = gson.getAdapter(Graph.class);
+//   Graph graph = graphAdapter.fromJson(json);
+// }</pre>
+// And an example for serialization: <pre>{@code
+//   Graph graph = new Graph(...);
+//   TypeAdapter<Graph> graphAdapter = gson.getAdapter(Graph.class);
+//   String json = graphAdapter.toJson(graph);
+// }</pre>
+//
+// <p>Type adapters are <strong>type-specific</strong>. For example, a {@code
+// TypeAdapter<Date>} can convert {@code Date} instances to JSON and JSON to
+// instances of {@code Date}, but cannot convert any other types.
+//
+public abstract class TypeAdapter<T> {
+
+  public TypeAdapter() {}
+
+  /**
+   * Writes one JSON value (an array, object, string, number, boolean or null) for {@code value}.
+   *
+   * @param value the Java object to write. May be null.
+   */
+  public abstract void write(JsonWriter out, T value) throws IOException;
+
+  /**
+   * Converts {@code value} to a JSON document and writes it to {@code out}.
+   *
+   * <p>A {@link JsonWriter} with default configuration is used for writing the JSON data. To
+   * customize this behavior, create a {@link JsonWriter}, configure it and then use {@link
+   * #write(JsonWriter, Object)} instead.
+   *
+   * @param value the Java object to convert. May be {@code null}.
+   * @since 2.2
+   */
+  public final void toJson(Writer out, T value) throws IOException {
+    JsonWriter writer = new JsonWriter(out);
+    write(writer, value);
+  }
+
+  /**
+   * Converts {@code value} to a JSON document.
+   *
+   * <p>A {@link JsonWriter} with default configuration is used for writing the JSON data. To
+   * customize this behavior, create a {@link JsonWriter}, configure it and then use {@link
+   * #write(JsonWriter, Object)} instead.
+   *
+   * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #write(JsonWriter,
+   *     Object)}
+   * @param value the Java object to convert. May be {@code null}.
+   * @since 2.2
+   */
+  public final String toJson(T value) {
+    StringWriter stringWriter = new StringWriter();
+    try {
+      toJson(stringWriter, value);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * Converts {@code value} to a JSON tree.
+   *
+   * @param value the Java object to convert. May be {@code 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) {
+    try {
+      JsonTreeWriter jsonWriter = new JsonTreeWriter();
+      write(jsonWriter, value);
+      return jsonWriter.get();
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+  }
+
+  /**
+   * Reads one JSON value (an array, object, string, number, boolean or null) and converts it to a
+   * Java object. Returns the converted object.
+   *
+   * @return the converted Java object. May be {@code null}.
+   */
+  public abstract T read(JsonReader in) throws IOException;
+
+  /**
+   * Converts the JSON document in {@code in} to a Java object.
+   *
+   * <p>A {@link JsonReader} with default configuration (that is with {@link
+   * Strictness#LEGACY_STRICT} as strictness) is used for reading the JSON data. To customize this
+   * behavior, create a {@link JsonReader}, configure it and then use {@link #read(JsonReader)}
+   * instead.
+   *
+   * <p>No exception is thrown if the JSON data has multiple top-level JSON elements, or if there is
+   * trailing data.
+   *
+   * @return the converted Java object. May be {@code null}.
+   * @since 2.2
+   */
+  public final T fromJson(Reader in) throws IOException {
+    JsonReader reader = new JsonReader(in);
+    return read(reader);
+  }
+
+  /**
+   * Converts the JSON document in {@code json} to a Java object.
+   *
+   * <p>A {@link JsonReader} with default configuration (that is with {@link
+   * Strictness#LEGACY_STRICT} as strictness) is used for reading the JSON data. To customize this
+   * behavior, create a {@link JsonReader}, configure it and then use {@link #read(JsonReader)}
+   * instead.
+   *
+   * <p>No exception is thrown if the JSON data has multiple top-level JSON elements, or if there is
+   * trailing data.
+   *
+   * @return the converted Java object. May be {@code null}.
+   * @since 2.2
+   */
+  public final T fromJson(String json) throws IOException {
+    return fromJson(new StringReader(json));
+  }
+
+  /**
+   * Converts {@code jsonTree} to a Java object.
+   *
+   * @param jsonTree the JSON element to convert. May be {@link JsonNull}.
+   * @return the converted Java object. May be {@code null}.
+   * @throws JsonIOException wrapping {@code IOException}s thrown by {@link #read(JsonReader)}
+   * @since 2.2
+   */
+  public final T fromJsonTree(JsonElement jsonTree) {
+    try {
+      JsonReader jsonReader = new JsonTreeReader(jsonTree);
+      return read(jsonReader);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    }
+  }
+
+  /**
+   * This wrapper method is used to make a type adapter null tolerant. In general, a type adapter is
+   * required to handle nulls in write and read methods. Here is how this is typically done:<br>
+   *
+   * <pre>{@code
+   * Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class,
+   *   new TypeAdapter<Foo>() {
+   *     public Foo read(JsonReader in) throws IOException {
+   *       if (in.peek() == JsonToken.NULL) {
+   *         in.nextNull();
+   *         return null;
+   *       }
+   *       // read a Foo from in and return it
+   *     }
+   *     public void write(JsonWriter out, Foo src) throws IOException {
+   *       if (src == null) {
+   *         out.nullValue();
+   *         return;
+   *       }
+   *       // write src as JSON to out
+   *     }
+   *   }).create();
+   * }</pre>
+   *
+   * You can avoid this boilerplate handling of nulls by wrapping your type adapter with this
+   * method. Here is how we will rewrite the above example:
+   *
+   * <pre>{@code
+   * Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class,
+   *   new TypeAdapter<Foo>() {
+   *     public Foo read(JsonReader in) throws IOException {
+   *       // read a Foo from in and return it
+   *     }
+   *     public void write(JsonWriter out, Foo src) throws IOException {
+   *       // write src as JSON to out
+   *     }
+   *   }.nullSafe()).create();
+   * }</pre>
+   *
+   * Note that we didn't need to check for nulls in our type adapter after we used nullSafe.
+   */
+  public final TypeAdapter<T> nullSafe() {
+    return new TypeAdapter<T>() {
+      @Override
+      public void write(JsonWriter out, T value) throws IOException {
+        if (value == null) {
+          out.nullValue();
+        } else {
+          TypeAdapter.this.write(out, value);
+        }
+      }
+
+      @Override
+      public T read(JsonReader reader) throws IOException {
+        if (reader.peek() == JsonToken.NULL) {
+          reader.nextNull();
+          return null;
+        }
+        return TypeAdapter.this.read(reader);
+      }
+    };
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..03873406350b7a71e4690b9c2e8d37230c2ea1fa
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2011 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;
+
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Creates type adapters for set of related types. Type adapter factories are most useful when
+ * several types share similar structure in their JSON form.
+ *
+ * <h2>Examples</h2>
+ *
+ * <h3>Example: Converting enums to lowercase</h3>
+ *
+ * In this example, we implement a factory that creates type adapters for all enums. The type
+ * adapters will write enums in lowercase, despite the fact that they're defined in {@code
+ * CONSTANT_CASE} in the corresponding Java model:
+ *
+ * <pre>{@code
+ * public class LowercaseEnumTypeAdapterFactory implements TypeAdapterFactory {
+ *   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+ *     Class<T> rawType = (Class<T>) type.getRawType();
+ *     if (!rawType.isEnum()) {
+ *       return null;
+ *     }
+ *
+ *     final Map<String, T> lowercaseToConstant = new HashMap<>();
+ *     for (T constant : rawType.getEnumConstants()) {
+ *       lowercaseToConstant.put(toLowercase(constant), constant);
+ *     }
+ *
+ *     return new TypeAdapter<T>() {
+ *       public void write(JsonWriter out, T value) throws IOException {
+ *         if (value == null) {
+ *           out.nullValue();
+ *         } else {
+ *           out.value(toLowercase(value));
+ *         }
+ *       }
+ *
+ *       public T read(JsonReader reader) throws IOException {
+ *         if (reader.peek() == JsonToken.NULL) {
+ *           reader.nextNull();
+ *           return null;
+ *         } else {
+ *           return lowercaseToConstant.get(reader.nextString());
+ *         }
+ *       }
+ *     };
+ *   }
+ *
+ *   private String toLowercase(Object o) {
+ *     return o.toString().toLowerCase(Locale.US);
+ *   }
+ * }
+ * }</pre>
+ *
+ * <p>Type adapter factories select which types they provide type adapters for. If a factory cannot
+ * support a given type, it must return null when that type is passed to {@link #create}. Factories
+ * should expect {@code create()} to be called on them for many types and should return null for
+ * most of those types. In the above example the factory returns null for calls to {@code create()}
+ * where {@code type} is not an enum.
+ *
+ * <p>A factory is typically called once per type, but the returned type adapter may be used many
+ * times. It is most efficient to do expensive work like reflection in {@code create()} so that the
+ * type adapter's {@code read()} and {@code write()} methods can be very fast. In this example the
+ * mapping from lowercase name to enum value is computed eagerly.
+ *
+ * <p>As with type adapters, factories must be <i>registered</i> with a {@link
+ * com.google.gson.GsonBuilder} for them to take effect:
+ *
+ * <pre>{@code
+ * GsonBuilder builder = new GsonBuilder();
+ * builder.registerTypeAdapterFactory(new LowercaseEnumTypeAdapterFactory());
+ * ...
+ * Gson gson = builder.create();
+ * }</pre>
+ *
+ * If multiple factories support the same type, the factory registered earlier takes precedence.
+ *
+ * <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 Multiset<String>} is different from the type adapter for {@code Multiset<URL>}.
+ *
+ * <p>The type adapter <i>delegates</i> to another type adapter for the multiset elements. It
+ * figures out the element type by reflecting on the multiset's type token. A {@code Gson} is passed
+ * in to {@code create} for just this purpose:
+ *
+ * <pre>{@code
+ * public class MultisetTypeAdapterFactory implements TypeAdapterFactory {
+ *   public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+ *     Type type = typeToken.getType();
+ *     if (typeToken.getRawType() != Multiset.class
+ *         || !(type instanceof ParameterizedType)) {
+ *       return null;
+ *     }
+ *
+ *     Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+ *     TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
+ *     return (TypeAdapter<T>) newMultisetAdapter(elementAdapter);
+ *   }
+ *
+ *   private <E> TypeAdapter<Multiset<E>> newMultisetAdapter(
+ *       final TypeAdapter<E> elementAdapter) {
+ *     return new TypeAdapter<Multiset<E>>() {
+ *       public void write(JsonWriter out, Multiset<E> value) throws IOException {
+ *         if (value == null) {
+ *           out.nullValue();
+ *           return;
+ *         }
+ *
+ *         out.beginArray();
+ *         for (Multiset.Entry<E> entry : value.entrySet()) {
+ *           out.value(entry.getCount());
+ *           elementAdapter.write(out, entry.getElement());
+ *         }
+ *         out.endArray();
+ *       }
+ *
+ *       public Multiset<E> read(JsonReader in) throws IOException {
+ *         if (in.peek() == JsonToken.NULL) {
+ *           in.nextNull();
+ *           return null;
+ *         }
+ *
+ *         Multiset<E> result = LinkedHashMultiset.create();
+ *         in.beginArray();
+ *         while (in.hasNext()) {
+ *           int count = in.nextInt();
+ *           E element = elementAdapter.read(in);
+ *           result.add(element, count);
+ *         }
+ *         in.endArray();
+ *         return result;
+ *       }
+ *     };
+ *   }
+ * }
+ * }</pre>
+ *
+ * Delegating from one type adapter to another is extremely powerful; it's the foundation of how
+ * Gson converts Java objects and collections. Whenever possible your factory should retrieve its
+ * delegate type adapter in the {@code create()} method; this ensures potentially-expensive type
+ * adapter creation happens only once.
+ *
+ * @since 2.1
+ */
+public interface TypeAdapterFactory {
+
+  /**
+   * Returns a type adapter for {@code type}, or null if this factory doesn't support {@code type}.
+   */
+  <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type);
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/Expose.java b/gson/gson/src/main/java/com/google/gson/annotations/Expose.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b66af4fb498664ac29abd6b28081810d1e6464b
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/Expose.java
@@ -0,0 +1,84 @@
+/*
+ * 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.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that indicates this member should be exposed for JSON serialization or
+ * deserialization.
+ *
+ * <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#excludeFieldsWithoutExposeAnnotation()} method.
+ *
+ * <p>Here is an example of how this annotation is meant to be used:
+ *
+ * <pre>
+ * public class User {
+ *   &#64;Expose private String firstName;
+ *   &#64;Expose(serialize = false) private String lastName;
+ *   &#64;Expose (serialize = false, deserialize = false) private String emailAddress;
+ *   private String password;
+ * }
+ * </pre>
+ *
+ * If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()} methods
+ * will use the {@code password} field along-with {@code firstName}, {@code lastName}, and {@code
+ * emailAddress} for serialization and deserialization. However, if you created Gson with {@code
+ * Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()} then the {@code
+ * toJson()} and {@code fromJson()} methods of Gson will exclude the {@code password} field. This is
+ * because the {@code password} field is not marked with the {@code @Expose} annotation. Gson will
+ * also exclude {@code lastName} and {@code emailAddress} from serialization since {@code serialize}
+ * is set to {@code false}. Similarly, Gson will exclude {@code emailAddress} from deserialization
+ * since {@code deserialize} is set to false.
+ *
+ * <p>Note that another way to achieve the same effect would have been to just mark the {@code
+ * password} field as {@code transient}, and Gson would have excluded it even with default settings.
+ * The {@code @Expose} annotation is useful in a style of programming where you want to explicitly
+ * specify all fields that should get considered for serialization or deserialization.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Expose {
+
+  /**
+   * If {@code true}, the field marked with this annotation is written out in the JSON while
+   * serializing. If {@code false}, the field marked with this annotation is skipped from the
+   * serialized output. Defaults to {@code true}.
+   *
+   * @since 1.4
+   */
+  public boolean serialize() default true;
+
+  /**
+   * If {@code true}, the field marked with this annotation is deserialized from the JSON. If {@code
+   * false}, the field marked with this annotation is skipped during deserialization. Defaults to
+   * {@code true}.
+   *
+   * @since 1.4
+   */
+  public boolean deserialize() default true;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java b/gson/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c8ef2fa71500f8edde27f14d6ba0ed3e4b97d96
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2014 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.annotations;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that indicates the Gson {@link TypeAdapter} to use with a class or field.
+ *
+ * <p>Here is an example of how this annotation is used:
+ *
+ * <pre>
+ * &#64;JsonAdapter(UserJsonAdapter.class)
+ * public class User {
+ *   public final String firstName, lastName;
+ *
+ *   private User(String firstName, String lastName) {
+ *     this.firstName = firstName;
+ *     this.lastName = lastName;
+ *   }
+ * }
+ *
+ * public class UserJsonAdapter extends TypeAdapter&lt;User&gt; {
+ *   &#64;Override public void write(JsonWriter out, User user) throws IOException {
+ *     // implement write: combine firstName and lastName into name
+ *     out.beginObject();
+ *     out.name("name");
+ *     out.value(user.firstName + " " + user.lastName);
+ *     out.endObject();
+ *   }
+ *
+ *   &#64;Override public User read(JsonReader in) throws IOException {
+ *     // implement read: split name into firstName and lastName
+ *     in.beginObject();
+ *     in.nextName();
+ *     String[] nameParts = in.nextString().split(" ");
+ *     in.endObject();
+ *     return new User(nameParts[0], nameParts[1]);
+ *   }
+ * }
+ * </pre>
+ *
+ * Since {@code User} class specified {@code UserJsonAdapter.class} in {@code @JsonAdapter}
+ * annotation, it will automatically be invoked to serialize/deserialize {@code User} instances.
+ *
+ * <p>Here is an example of how to apply this annotation to a field.
+ *
+ * <pre>
+ * private static final class Gadget {
+ *   &#64;JsonAdapter(UserJsonAdapter.class)
+ *   final User user;
+ *
+ *   Gadget(User user) {
+ *     this.user = user;
+ *   }
+ * }
+ * </pre>
+ *
+ * It's possible to specify different type adapters on a field, that field's type, and in the {@link
+ * GsonBuilder}. Field annotations take precedence over {@code GsonBuilder}-registered type
+ * adapters, which in turn take precedence over annotated types.
+ *
+ * <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
+ * {@link Gson} instance. By default the specified adapter will not be called for {@code null}
+ * values; set {@link #nullSafe()} to {@code false} to let the adapter handle {@code null} values
+ * itself.
+ *
+ * <p>The type adapter is created in the same way Gson creates instances of custom classes during
+ * deserialization, that means:
+ *
+ * <ol>
+ *   <li>If a custom {@link InstanceCreator} has been registered for the adapter class, it will be
+ *       used to create the instance
+ *   <li>Otherwise, if the adapter class has a no-args constructor (regardless of which visibility),
+ *       it will be invoked to create the instance
+ *   <li>Otherwise, JDK {@code Unsafe} will be used to create the instance; see {@link
+ *       GsonBuilder#disableJdkUnsafe()} for the unexpected side-effects this might have
+ * </ol>
+ *
+ * <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
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ * @author Jesse Wilson
+ */
+// Note that the above example is taken from AdaptAnnotationTest.
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.FIELD})
+public @interface JsonAdapter {
+
+  /**
+   * Either a {@link TypeAdapter} or {@link TypeAdapterFactory}, or one or both of {@link
+   * JsonDeserializer} or {@link JsonSerializer}.
+   */
+  Class<?> value();
+
+  /**
+   * Whether the adapter referenced by {@link #value()} should be made {@linkplain
+   * TypeAdapter#nullSafe() null-safe}.
+   *
+   * <p>If {@code true} (the default), it will be made null-safe and Gson will handle {@code null}
+   * Java objects on serialization and JSON {@code null} on deserialization without calling the
+   * adapter. If {@code false}, the adapter will have to handle the {@code null} values.
+   */
+  boolean nullSafe() default true;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/SerializedName.java b/gson/gson/src/main/java/com/google/gson/annotations/SerializedName.java
new file mode 100644
index 0000000000000000000000000000000000000000..91a61bc900db192893b3bee5047b12cfd4c05851
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/SerializedName.java
@@ -0,0 +1,101 @@
+/*
+ * 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.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that indicates this member should be serialized to JSON with the provided name
+ * value as its field name.
+ *
+ * <p>This annotation will override any {@link com.google.gson.FieldNamingPolicy}, including the
+ * default field naming policy, that may have been set on the {@link com.google.gson.Gson} instance.
+ * A different naming policy can set using the {@code GsonBuilder} class. See {@link
+ * com.google.gson.GsonBuilder#setFieldNamingPolicy(com.google.gson.FieldNamingPolicy)} for more
+ * information.
+ *
+ * <p>Here is an example of how this annotation is meant to be used:
+ *
+ * <pre>
+ * public class MyClass {
+ *   &#64;SerializedName("name") String a;
+ *   &#64;SerializedName(value="name1", alternate={"name2", "name3"}) String b;
+ *   String c;
+ *
+ *   public MyClass(String a, String b, String c) {
+ *     this.a = a;
+ *     this.b = b;
+ *     this.c = c;
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>The following shows the output that is generated when serializing an instance of the above
+ * example class:
+ *
+ * <pre>
+ * MyClass target = new MyClass("v1", "v2", "v3");
+ * Gson gson = new Gson();
+ * String json = gson.toJson(target);
+ * System.out.println(json);
+ *
+ * ===== OUTPUT =====
+ * {"name":"v1","name1":"v2","c":"v3"}
+ * </pre>
+ *
+ * <p>NOTE: The value you specify in this annotation must be a valid JSON field name. While
+ * deserializing, all values specified in the annotation will be deserialized into the field. For
+ * example:
+ *
+ * <pre>
+ *   MyClass target = gson.fromJson("{'name1':'v1'}", MyClass.class);
+ *   assertEquals("v1", target.b);
+ *   target = gson.fromJson("{'name2':'v2'}", MyClass.class);
+ *   assertEquals("v2", target.b);
+ *   target = gson.fromJson("{'name3':'v3'}", MyClass.class);
+ *   assertEquals("v3", target.b);
+ * </pre>
+ *
+ * Note that MyClass.b is now deserialized from either name1, name2 or name3.
+ *
+ * @see com.google.gson.FieldNamingPolicy
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.METHOD})
+public @interface SerializedName {
+
+  /**
+   * The desired name of the field when it is serialized or deserialized.
+   *
+   * @return the desired name of the field when it is serialized or deserialized
+   */
+  String value();
+
+  /**
+   * The alternative names of the field when it is deserialized
+   *
+   * @return the alternative names of the field when it is deserialized
+   */
+  String[] alternate() default {};
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/Since.java b/gson/gson/src/main/java/com/google/gson/annotations/Since.java
new file mode 100644
index 0000000000000000000000000000000000000000..e363a58ba9d20936f30850ec7084fceec0f4de43
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/Since.java
@@ -0,0 +1,65 @@
+/*
+ * 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.annotations;
+
+import com.google.gson.GsonBuilder;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that indicates the version number since a member or a type has been present. This
+ * annotation is useful to manage versioning of your JSON classes for a web-service.
+ *
+ * <p>This annotation has no effect unless you build {@link com.google.gson.Gson} with a {@code
+ * GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
+ *
+ * <p>Here is an example of how this annotation is meant to be used:
+ *
+ * <pre>
+ * public class User {
+ *   private String firstName;
+ *   private String lastName;
+ *   &#64;Since(1.0) private String emailAddress;
+ *   &#64;Since(1.0) private String password;
+ *   &#64;Since(1.1) private Address address;
+ * }
+ * </pre>
+ *
+ * <p>If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()}
+ * methods will use all the fields for serialization and deserialization. However, if you created
+ * Gson with {@code Gson gson = new GsonBuilder().setVersion(1.0).create()} then the {@code
+ * toJson()} and {@code fromJson()} methods of Gson will exclude the {@code address} field since
+ * it's version number is set to {@code 1.1}.
+ *
+ * @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 number is
+   * inclusive; annotated elements will be included if {@code gsonVersion >= value}.
+   */
+  double value();
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/Until.java b/gson/gson/src/main/java/com/google/gson/annotations/Until.java
new file mode 100644
index 0000000000000000000000000000000000000000..9086f50ee72152ad2ced50bc674993b32cfdaa16
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/Until.java
@@ -0,0 +1,70 @@
+/*
+ * 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.annotations;
+
+import com.google.gson.GsonBuilder;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation that indicates the version number until a member or a type should be present.
+ * Basically, if Gson is created with a version number that 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 {@code
+ * GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method.
+ *
+ * <p>Here is an example of how this annotation is meant to be used:
+ *
+ * <pre>
+ * public class User {
+ *   private String firstName;
+ *   private String lastName;
+ *   &#64;Until(1.1) private String emailAddress;
+ *   &#64;Until(1.1) private String password;
+ * }
+ * </pre>
+ *
+ * <p>If you created Gson with {@code new Gson()}, the {@code toJson()} and {@code fromJson()}
+ * 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 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
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.TYPE})
+public @interface Until {
+
+  /**
+   * The value indicating a version number until this member or type should be included. The number
+   * is exclusive; annotated elements will be included if {@code gsonVersion < value}.
+   */
+  double value();
+}
diff --git a/gson/gson/src/main/java/com/google/gson/annotations/package-info.java b/gson/gson/src/main/java/com/google/gson/annotations/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b380e6d3395176626d4b7dafccf547340e21ed8
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/annotations/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package provides annotations that can be used with {@link com.google.gson.Gson}.
+ *
+ * @author Inderjeet Singh, Joel Leitch
+ */
+package com.google.gson.annotations;
diff --git a/gson/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java b/gson/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java
new file mode 100644
index 0000000000000000000000000000000000000000..76c24165bfe8c3954284ad435e67ff69797ecec6
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/$Gson$Preconditions.java
@@ -0,0 +1,57 @@
+/*
+ * 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.internal;
+
+import java.util.Objects;
+
+/**
+ * A simple utility class used to check method Preconditions.
+ *
+ * <pre>
+ * public long divideBy(long value) {
+ *   Preconditions.checkArgument(value != 0);
+ *   return this.value / value;
+ * }
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class $Gson$Preconditions {
+  private $Gson$Preconditions() {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * @deprecated This is an internal Gson method. Use {@link Objects#requireNonNull(Object)}
+   *     instead.
+   */
+  // Only deprecated for now because external projects might be using this by accident
+  @Deprecated
+  public static <T> T checkNotNull(T obj) {
+    if (obj == null) {
+      throw new NullPointerException();
+    }
+    return obj;
+  }
+
+  public static void checkArgument(boolean condition) {
+    if (!condition) {
+      throw new IllegalArgumentException();
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/$Gson$Types.java b/gson/gson/src/main/java/com/google/gson/internal/$Gson$Types.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ccdaa3eb56a38229bf0061a5fdd0356348b9025
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/$Gson$Types.java
@@ -0,0 +1,687 @@
+/*
+ * 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.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;
+import java.lang.reflect.GenericDeclaration;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * Static methods for working with types.
+ *
+ * @author Bob Lee
+ * @author Jesse Wilson
+ */
+public final class $Gson$Types {
+  static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
+
+  private $Gson$Types() {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType} and
+   * enclosed by {@code ownerType}.
+   *
+   * @return a {@link java.io.Serializable serializable} parameterized type.
+   */
+  public static ParameterizedType newParameterizedTypeWithOwner(
+      Type ownerType, Type rawType, Type... typeArguments) {
+    return new ParameterizedTypeImpl(ownerType, rawType, typeArguments);
+  }
+
+  /**
+   * Returns an array type whose elements are all instances of {@code componentType}.
+   *
+   * @return a {@link java.io.Serializable serializable} generic array type.
+   */
+  public static GenericArrayType arrayOf(Type componentType) {
+    return new GenericArrayTypeImpl(componentType);
+  }
+
+  /**
+   * Returns a type that represents an unknown type that extends {@code bound}. For example, if
+   * {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If
+   * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code ?
+   * extends Object}.
+   */
+  public static WildcardType subtypeOf(Type bound) {
+    Type[] upperBounds;
+    if (bound instanceof WildcardType) {
+      upperBounds = ((WildcardType) bound).getUpperBounds();
+    } else {
+      upperBounds = new Type[] {bound};
+    }
+    return new WildcardTypeImpl(upperBounds, EMPTY_TYPE_ARRAY);
+  }
+
+  /**
+   * Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code
+   * bound} is {@code String.class}, this returns {@code ? super String}.
+   */
+  public static WildcardType supertypeOf(Type bound) {
+    Type[] lowerBounds;
+    if (bound instanceof WildcardType) {
+      lowerBounds = ((WildcardType) bound).getLowerBounds();
+    } else {
+      lowerBounds = new Type[] {bound};
+    }
+    return new WildcardTypeImpl(new Type[] {Object.class}, lowerBounds);
+  }
+
+  /**
+   * Returns a type that is functionally equal but not necessarily equal according to {@link
+   * Object#equals(Object) Object.equals()}. The returned type is {@link java.io.Serializable}.
+   */
+  public static Type canonicalize(Type type) {
+    if (type instanceof Class) {
+      Class<?> c = (Class<?>) type;
+      return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
+
+    } else if (type instanceof ParameterizedType) {
+      ParameterizedType p = (ParameterizedType) type;
+      return new ParameterizedTypeImpl(
+          p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
+
+    } else if (type instanceof GenericArrayType) {
+      GenericArrayType g = (GenericArrayType) type;
+      return new GenericArrayTypeImpl(g.getGenericComponentType());
+
+    } else if (type instanceof WildcardType) {
+      WildcardType w = (WildcardType) type;
+      return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
+
+    } else {
+      // type is either serializable as-is or unsupported
+      return type;
+    }
+  }
+
+  public static Class<?> getRawType(Type type) {
+    if (type instanceof Class<?>) {
+      // type is a normal class.
+      return (Class<?>) type;
+
+    } else if (type instanceof ParameterizedType) {
+      ParameterizedType parameterizedType = (ParameterizedType) type;
+
+      // getRawType() returns Type instead of Class; that seems to be an API mistake,
+      // see https://bugs.openjdk.org/browse/JDK-8250659
+      Type rawType = parameterizedType.getRawType();
+      checkArgument(rawType instanceof Class);
+      return (Class<?>) rawType;
+
+    } else if (type instanceof GenericArrayType) {
+      Type componentType = ((GenericArrayType) type).getGenericComponentType();
+      return Array.newInstance(getRawType(componentType), 0).getClass();
+
+    } else if (type instanceof TypeVariable) {
+      // we could use the variable's bounds, but that won't work if there are multiple.
+      // having a raw type that's more general than necessary is okay
+      return Object.class;
+
+    } else if (type instanceof WildcardType) {
+      Type[] bounds = ((WildcardType) type).getUpperBounds();
+      // Currently the JLS only permits one bound for wildcards so using first bound is safe
+      assert bounds.length == 1;
+      return getRawType(bounds[0]);
+
+    } else {
+      String className = type == null ? "null" : type.getClass().getName();
+      throw new IllegalArgumentException(
+          "Expected a Class, ParameterizedType, or GenericArrayType, but <"
+              + type
+              + "> is of type "
+              + className);
+    }
+  }
+
+  private static boolean equal(Object a, Object b) {
+    return Objects.equals(a, b);
+  }
+
+  /** Returns true if {@code a} and {@code b} are equal. */
+  public static boolean equals(Type a, Type b) {
+    if (a == b) {
+      // also handles (a == null && b == null)
+      return true;
+
+    } else if (a instanceof Class) {
+      // Class already specifies equals().
+      return a.equals(b);
+
+    } else if (a instanceof ParameterizedType) {
+      if (!(b instanceof ParameterizedType)) {
+        return false;
+      }
+
+      // TODO: save a .clone() call
+      ParameterizedType pa = (ParameterizedType) a;
+      ParameterizedType pb = (ParameterizedType) b;
+      return equal(pa.getOwnerType(), pb.getOwnerType())
+          && pa.getRawType().equals(pb.getRawType())
+          && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments());
+
+    } else if (a instanceof GenericArrayType) {
+      if (!(b instanceof GenericArrayType)) {
+        return false;
+      }
+
+      GenericArrayType ga = (GenericArrayType) a;
+      GenericArrayType gb = (GenericArrayType) b;
+      return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
+
+    } else if (a instanceof WildcardType) {
+      if (!(b instanceof WildcardType)) {
+        return false;
+      }
+
+      WildcardType wa = (WildcardType) a;
+      WildcardType wb = (WildcardType) b;
+      return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
+          && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
+
+    } else if (a instanceof TypeVariable) {
+      if (!(b instanceof TypeVariable)) {
+        return false;
+      }
+      TypeVariable<?> va = (TypeVariable<?>) a;
+      TypeVariable<?> vb = (TypeVariable<?>) b;
+      return Objects.equals(va.getGenericDeclaration(), vb.getGenericDeclaration())
+          && va.getName().equals(vb.getName());
+
+    } else {
+      // This isn't a type we support. Could be a generic array type, wildcard type, etc.
+      return false;
+    }
+  }
+
+  public static String typeToString(Type type) {
+    return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
+  }
+
+  /**
+   * Returns the generic supertype for {@code supertype}. For example, given a class {@code
+   * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
+   * result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
+   */
+  private static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> supertype) {
+    if (supertype == rawType) {
+      return context;
+    }
+
+    // we skip searching through interfaces if unknown is an interface
+    if (supertype.isInterface()) {
+      Class<?>[] interfaces = rawType.getInterfaces();
+      for (int i = 0, length = interfaces.length; i < length; i++) {
+        if (interfaces[i] == supertype) {
+          return rawType.getGenericInterfaces()[i];
+        } else if (supertype.isAssignableFrom(interfaces[i])) {
+          return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], supertype);
+        }
+      }
+    }
+
+    // check our supertypes
+    if (!rawType.isInterface()) {
+      while (rawType != Object.class) {
+        Class<?> rawSupertype = rawType.getSuperclass();
+        if (rawSupertype == supertype) {
+          return rawType.getGenericSuperclass();
+        } else if (supertype.isAssignableFrom(rawSupertype)) {
+          return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, supertype);
+        }
+        rawType = rawSupertype;
+      }
+    }
+
+    // we can't resolve this further
+    return supertype;
+  }
+
+  /**
+   * Returns the generic form of {@code supertype}. For example, if this is {@code
+   * ArrayList<String>}, this returns {@code Iterable<String>} given the input {@code
+   * Iterable.class}.
+   *
+   * @param supertype a superclass of, or interface implemented by, this.
+   */
+  private static Type getSupertype(Type context, Class<?> contextRawType, Class<?> supertype) {
+    if (context instanceof WildcardType) {
+      // Wildcards are useless for resolving supertypes. As the upper bound has the same raw type,
+      // use it instead
+      Type[] bounds = ((WildcardType) context).getUpperBounds();
+      // Currently the JLS only permits one bound for wildcards so using first bound is safe
+      assert bounds.length == 1;
+      context = bounds[0];
+    }
+    checkArgument(supertype.isAssignableFrom(contextRawType));
+    return resolve(
+        context,
+        contextRawType,
+        $Gson$Types.getGenericSupertype(context, contextRawType, supertype));
+  }
+
+  /**
+   * Returns the component type of this array type.
+   *
+   * @throws ClassCastException if this type is not an array.
+   */
+  public static Type getArrayComponentType(Type array) {
+    return array instanceof GenericArrayType
+        ? ((GenericArrayType) array).getGenericComponentType()
+        : ((Class<?>) array).getComponentType();
+  }
+
+  /**
+   * Returns the element type of this collection type.
+   *
+   * @throws IllegalArgumentException if this type is not a collection.
+   */
+  public static Type getCollectionElementType(Type context, Class<?> contextRawType) {
+    Type collectionType = getSupertype(context, contextRawType, Collection.class);
+
+    if (collectionType instanceof ParameterizedType) {
+      return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
+    }
+    return Object.class;
+  }
+
+  /**
+   * Returns a two element array containing this map's key and value types in positions 0 and 1
+   * respectively.
+   */
+  public static Type[] getMapKeyAndValueTypes(Type context, Class<?> contextRawType) {
+    /*
+     * Work around a problem with the declaration of java.util.Properties. That
+     * class should extend Hashtable<String, String>, but it's declared to
+     * extend Hashtable<Object, Object>.
+     */
+    if (context == Properties.class) {
+      return new Type[] {String.class, String.class}; // TODO: test subclasses of Properties!
+    }
+
+    Type mapType = getSupertype(context, contextRawType, Map.class);
+    // TODO: strip wildcards?
+    if (mapType instanceof ParameterizedType) {
+      ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
+      return mapParameterizedType.getActualTypeArguments();
+    }
+    return new Type[] {Object.class, Object.class};
+  }
+
+  public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
+
+    return resolve(context, contextRawType, toResolve, new HashMap<TypeVariable<?>, Type>());
+  }
+
+  private static Type resolve(
+      Type context,
+      Class<?> contextRawType,
+      Type toResolve,
+      Map<TypeVariable<?>, Type> visitedTypeVariables) {
+    // this implementation is made a little more complicated in an attempt to avoid object-creation
+    TypeVariable<?> resolving = null;
+    while (true) {
+      if (toResolve instanceof TypeVariable) {
+        TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
+        Type previouslyResolved = visitedTypeVariables.get(typeVariable);
+        if (previouslyResolved != null) {
+          // cannot reduce due to infinite recursion
+          return (previouslyResolved == Void.TYPE) ? toResolve : previouslyResolved;
+        }
+
+        // Insert a placeholder to mark the fact that we are in the process of resolving this type
+        visitedTypeVariables.put(typeVariable, Void.TYPE);
+        if (resolving == null) {
+          resolving = typeVariable;
+        }
+
+        toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
+        if (toResolve == typeVariable) {
+          break;
+        }
+
+      } else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
+        Class<?> original = (Class<?>) toResolve;
+        Type componentType = original.getComponentType();
+        Type newComponentType =
+            resolve(context, contextRawType, componentType, visitedTypeVariables);
+        toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType);
+        break;
+
+      } else if (toResolve instanceof GenericArrayType) {
+        GenericArrayType original = (GenericArrayType) toResolve;
+        Type componentType = original.getGenericComponentType();
+        Type newComponentType =
+            resolve(context, contextRawType, componentType, visitedTypeVariables);
+        toResolve = equal(componentType, newComponentType) ? original : arrayOf(newComponentType);
+        break;
+
+      } else if (toResolve instanceof ParameterizedType) {
+        ParameterizedType original = (ParameterizedType) toResolve;
+        Type ownerType = original.getOwnerType();
+        Type newOwnerType = resolve(context, contextRawType, ownerType, visitedTypeVariables);
+        boolean changed = !equal(newOwnerType, ownerType);
+
+        Type[] args = original.getActualTypeArguments();
+        for (int t = 0, length = args.length; t < length; t++) {
+          Type resolvedTypeArgument =
+              resolve(context, contextRawType, args[t], visitedTypeVariables);
+          if (!equal(resolvedTypeArgument, args[t])) {
+            if (!changed) {
+              args = args.clone();
+              changed = true;
+            }
+            args[t] = resolvedTypeArgument;
+          }
+        }
+
+        toResolve =
+            changed
+                ? newParameterizedTypeWithOwner(newOwnerType, original.getRawType(), args)
+                : original;
+        break;
+
+      } else if (toResolve instanceof WildcardType) {
+        WildcardType original = (WildcardType) toResolve;
+        Type[] originalLowerBound = original.getLowerBounds();
+        Type[] originalUpperBound = original.getUpperBounds();
+
+        if (originalLowerBound.length == 1) {
+          Type lowerBound =
+              resolve(context, contextRawType, originalLowerBound[0], visitedTypeVariables);
+          if (lowerBound != originalLowerBound[0]) {
+            toResolve = supertypeOf(lowerBound);
+            break;
+          }
+        } else if (originalUpperBound.length == 1) {
+          Type upperBound =
+              resolve(context, contextRawType, originalUpperBound[0], visitedTypeVariables);
+          if (upperBound != originalUpperBound[0]) {
+            toResolve = subtypeOf(upperBound);
+            break;
+          }
+        }
+        toResolve = original;
+        break;
+
+      } else {
+        break;
+      }
+    }
+    // ensure that any in-process resolution gets updated with the final result
+    if (resolving != null) {
+      visitedTypeVariables.put(resolving, toResolve);
+    }
+    return toResolve;
+  }
+
+  private static Type resolveTypeVariable(
+      Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
+    Class<?> declaredByRaw = declaringClassOf(unknown);
+
+    // we can't reduce this further
+    if (declaredByRaw == null) {
+      return unknown;
+    }
+
+    Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
+    if (declaredBy instanceof ParameterizedType) {
+      int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
+      return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
+    }
+
+    return unknown;
+  }
+
+  private static int indexOf(Object[] array, Object toFind) {
+    for (int i = 0, length = array.length; i < length; i++) {
+      if (toFind.equals(array[i])) {
+        return i;
+      }
+    }
+    throw new NoSuchElementException();
+  }
+
+  /**
+   * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
+   * a class.
+   */
+  private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
+    GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
+    return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
+  }
+
+  static void checkNotPrimitive(Type type) {
+    checkArgument(!(type instanceof Class<?>) || !((Class<?>) type).isPrimitive());
+  }
+
+  /**
+   * Whether an {@linkplain ParameterizedType#getOwnerType() owner type} must be specified when
+   * constructing a {@link ParameterizedType} for {@code rawType}.
+   *
+   * <p>Note that this method might not require an owner type for all cases where Java reflection
+   * would create parameterized types with owner type.
+   */
+  public static boolean requiresOwnerType(Type rawType) {
+    if (rawType instanceof Class<?>) {
+      Class<?> rawTypeAsClass = (Class<?>) rawType;
+      return !Modifier.isStatic(rawTypeAsClass.getModifiers())
+          && rawTypeAsClass.getDeclaringClass() != null;
+    }
+    return false;
+  }
+
+  // Here and below we put @SuppressWarnings("serial") on fields of type `Type`. Recent Java
+  // compilers complain that the declared type is not Serializable. But in this context we go out of
+  // our way to ensure that the Type in question is either Class (which is serializable) or one of
+  // the nested Type implementations here (which are also serializable).
+  private static final class ParameterizedTypeImpl implements ParameterizedType, Serializable {
+    @SuppressWarnings("serial")
+    private final Type ownerType;
+
+    @SuppressWarnings("serial")
+    private final Type rawType;
+
+    @SuppressWarnings("serial")
+    private final Type[] typeArguments;
+
+    public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
+      // TODO: Should this enforce that rawType is a Class? See JDK implementation of
+      // the ParameterizedType interface and https://bugs.openjdk.org/browse/JDK-8250659
+      requireNonNull(rawType);
+      if (ownerType == null && requiresOwnerType(rawType)) {
+        throw new IllegalArgumentException("Must specify owner type for " + rawType);
+      }
+
+      this.ownerType = ownerType == null ? null : canonicalize(ownerType);
+      this.rawType = canonicalize(rawType);
+      this.typeArguments = typeArguments.clone();
+      for (int t = 0, length = this.typeArguments.length; t < length; t++) {
+        requireNonNull(this.typeArguments[t]);
+        checkNotPrimitive(this.typeArguments[t]);
+        this.typeArguments[t] = canonicalize(this.typeArguments[t]);
+      }
+    }
+
+    @Override
+    public Type[] getActualTypeArguments() {
+      return typeArguments.clone();
+    }
+
+    @Override
+    public Type getRawType() {
+      return rawType;
+    }
+
+    @Override
+    public Type getOwnerType() {
+      return ownerType;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof ParameterizedType
+          && $Gson$Types.equals(this, (ParameterizedType) other);
+    }
+
+    private static int hashCodeOrZero(Object o) {
+      return o != null ? o.hashCode() : 0;
+    }
+
+    @Override
+    public int hashCode() {
+      return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType);
+    }
+
+    @Override
+    public String toString() {
+      int length = typeArguments.length;
+      if (length == 0) {
+        return typeToString(rawType);
+      }
+
+      StringBuilder stringBuilder = new StringBuilder(30 * (length + 1));
+      stringBuilder
+          .append(typeToString(rawType))
+          .append("<")
+          .append(typeToString(typeArguments[0]));
+      for (int i = 1; i < length; i++) {
+        stringBuilder.append(", ").append(typeToString(typeArguments[i]));
+      }
+      return stringBuilder.append(">").toString();
+    }
+
+    private static final long serialVersionUID = 0;
+  }
+
+  private static final class GenericArrayTypeImpl implements GenericArrayType, Serializable {
+    @SuppressWarnings("serial")
+    private final Type componentType;
+
+    public GenericArrayTypeImpl(Type componentType) {
+      requireNonNull(componentType);
+      this.componentType = canonicalize(componentType);
+    }
+
+    @Override
+    public Type getGenericComponentType() {
+      return componentType;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof GenericArrayType && $Gson$Types.equals(this, (GenericArrayType) o);
+    }
+
+    @Override
+    public int hashCode() {
+      return componentType.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return typeToString(componentType) + "[]";
+    }
+
+    private static final long serialVersionUID = 0;
+  }
+
+  /**
+   * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
+   * support what the target Java version supports - at most one bound, see also
+   * https://bugs.openjdk.java.net/browse/JDK-8250660. If a lower bound is set, the upper bound must
+   * be Object.class.
+   */
+  private static final class WildcardTypeImpl implements WildcardType, Serializable {
+    @SuppressWarnings("serial")
+    private final Type upperBound;
+
+    @SuppressWarnings("serial")
+    private final Type lowerBound;
+
+    public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
+      checkArgument(lowerBounds.length <= 1);
+      checkArgument(upperBounds.length == 1);
+
+      if (lowerBounds.length == 1) {
+        requireNonNull(lowerBounds[0]);
+        checkNotPrimitive(lowerBounds[0]);
+        checkArgument(upperBounds[0] == Object.class);
+        this.lowerBound = canonicalize(lowerBounds[0]);
+        this.upperBound = Object.class;
+
+      } else {
+        requireNonNull(upperBounds[0]);
+        checkNotPrimitive(upperBounds[0]);
+        this.lowerBound = null;
+        this.upperBound = canonicalize(upperBounds[0]);
+      }
+    }
+
+    @Override
+    public Type[] getUpperBounds() {
+      return new Type[] {upperBound};
+    }
+
+    @Override
+    public Type[] getLowerBounds() {
+      return lowerBound != null ? new Type[] {lowerBound} : EMPTY_TYPE_ARRAY;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof WildcardType && $Gson$Types.equals(this, (WildcardType) other);
+    }
+
+    @Override
+    public int hashCode() {
+      // this equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds());
+      return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode());
+    }
+
+    @Override
+    public String toString() {
+      if (lowerBound != null) {
+        return "? super " + typeToString(lowerBound);
+      } else if (upperBound == Object.class) {
+        return "?";
+      } else {
+        return "? extends " + typeToString(upperBound);
+      }
+    }
+
+    private static final long serialVersionUID = 0;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..b4deb74726982de5704a17702607100a1f64c145
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2011 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.internal;
+
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonIOException;
+import com.google.gson.ReflectionAccessFilter;
+import com.google.gson.ReflectionAccessFilter.FilterResult;
+import com.google.gson.internal.reflect.ReflectionHelper;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/** Returns a function that can construct an instance of a requested type. */
+public final class ConstructorConstructor {
+  private final Map<Type, InstanceCreator<?>> instanceCreators;
+  private final boolean useJdkUnsafe;
+  private final List<ReflectionAccessFilter> reflectionFilters;
+
+  public ConstructorConstructor(
+      Map<Type, InstanceCreator<?>> instanceCreators,
+      boolean useJdkUnsafe,
+      List<ReflectionAccessFilter> reflectionFilters) {
+    this.instanceCreators = instanceCreators;
+    this.useJdkUnsafe = useJdkUnsafe;
+    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)) {
+      // R8 performs aggressive optimizations where it removes the default constructor of a class
+      // and makes the class `abstract`; check for that here explicitly
+      /*
+       * Note: Ideally should only show this R8-specific message when it is clear that R8 was
+       * used (e.g. when `c.getDeclaredConstructors().length == 0`), but on Android where this
+       * issue with R8 occurs most, R8 seems to keep some constructors for some reason while
+       * still making the class abstract
+       */
+      return "Abstract classes can't be instantiated! Adjust the R8 configuration or register"
+          + " an InstanceCreator or a TypeAdapter for this type. Class name: "
+          + c.getName()
+          + "\nSee "
+          + TroubleshootingGuide.createUrl("r8-abstract-class");
+    }
+    return null;
+  }
+
+  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
+    final Type type = typeToken.getType();
+    final Class<? super T> rawType = typeToken.getRawType();
+
+    // first try an instance creator
+
+    @SuppressWarnings("unchecked") // types must agree
+    final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
+    if (typeCreator != null) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          return typeCreator.createInstance(type);
+        }
+      };
+    }
+
+    // Next try raw type match for instance creators
+    @SuppressWarnings("unchecked") // types must agree
+    final InstanceCreator<T> rawTypeCreator = (InstanceCreator<T>) instanceCreators.get(rawType);
+    if (rawTypeCreator != null) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          return rawTypeCreator.createInstance(type);
+        }
+      };
+    }
+
+    // First consider special constructors before checking for no-args constructors
+    // below to avoid matching internal no-args constructors which might be added in
+    // future JDK versions
+    ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
+    if (specialConstructor != null) {
+      return specialConstructor;
+    }
+
+    FilterResult filterResult =
+        ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
+    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
+    if (defaultConstructor != null) {
+      return defaultConstructor;
+    }
+
+    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
+    if (defaultImplementation != null) {
+      return defaultImplementation;
+    }
+
+    // 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 = checkInstantiable(rawType);
+    if (exceptionMessage != null) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          throw new JsonIOException(exceptionMessage);
+        }
+      };
+    }
+
+    // Consider usage of Unsafe as reflection, so don't use if BLOCK_ALL
+    // Additionally, since it is not calling any constructor at all, don't use if BLOCK_INACCESSIBLE
+    if (filterResult == FilterResult.ALLOW) {
+      // finally try unsafe
+      return newUnsafeAllocator(rawType);
+    } else {
+      final String message =
+          "Unable to create instance of "
+              + rawType
+              + "; 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.";
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          throw new JsonIOException(message);
+        }
+      };
+    }
+  }
+
+  /**
+   * Creates constructors for special JDK collection types which do not have a public no-args
+   * constructor.
+   */
+  private static <T> ObjectConstructor<T> newSpecialCollectionConstructor(
+      final Type type, Class<? super T> rawType) {
+    if (EnumSet.class.isAssignableFrom(rawType)) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          if (type instanceof ParameterizedType) {
+            Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+            if (elementType instanceof Class) {
+              @SuppressWarnings({"unchecked", "rawtypes"})
+              T set = (T) EnumSet.noneOf((Class) elementType);
+              return set;
+            } else {
+              throw new JsonIOException("Invalid EnumSet type: " + type.toString());
+            }
+          } else {
+            throw new JsonIOException("Invalid EnumSet type: " + type.toString());
+          }
+        }
+      };
+    }
+    // Only support creation of EnumMap, but not of custom subtypes; for them type parameters
+    // and constructor parameter might have completely different meaning
+    else if (rawType == EnumMap.class) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          if (type instanceof ParameterizedType) {
+            Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+            if (elementType instanceof Class) {
+              @SuppressWarnings({"unchecked", "rawtypes"})
+              T map = (T) new EnumMap((Class) elementType);
+              return map;
+            } else {
+              throw new JsonIOException("Invalid EnumMap type: " + type.toString());
+            }
+          } else {
+            throw new JsonIOException("Invalid EnumMap type: " + type.toString());
+          }
+        }
+      };
+    }
+
+    return null;
+  }
+
+  private static <T> ObjectConstructor<T> newDefaultConstructor(
+      Class<? super T> rawType, FilterResult filterResult) {
+    // Cannot invoke constructor of abstract class
+    if (Modifier.isAbstract(rawType.getModifiers())) {
+      return null;
+    }
+
+    final Constructor<? super T> constructor;
+    try {
+      constructor = rawType.getDeclaredConstructor();
+    } catch (NoSuchMethodException e) {
+      return null;
+    }
+
+    boolean canAccess =
+        filterResult == FilterResult.ALLOW
+            || (ReflectionAccessFilterHelper.canAccess(constructor, null)
+                // Be a bit more lenient here for BLOCK_ALL; if constructor is accessible and public
+                // then allow calling it
+                && (filterResult != FilterResult.BLOCK_ALL
+                    || Modifier.isPublic(constructor.getModifiers())));
+
+    if (!canAccess) {
+      final String message =
+          "Unable to invoke no-args constructor of "
+              + rawType
+              + ";"
+              + " 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.";
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          throw new JsonIOException(message);
+        }
+      };
+    }
+
+    // Only try to make accessible if allowed; in all other cases checks above should
+    // have verified that constructor is accessible
+    if (filterResult == FilterResult.ALLOW) {
+      final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor);
+      if (exceptionMessage != null) {
+        /*
+         * Create ObjectConstructor which throws exception.
+         * This keeps backward compatibility (compared to returning `null` which
+         * would then choose another way of creating object).
+         * And it supports types which are only serialized but not deserialized
+         * (compared to directly throwing exception here), e.g. when runtime type
+         * of object is inaccessible, but compile-time type is accessible.
+         */
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            // New exception is created every time to avoid keeping reference
+            // to exception with potentially long stack trace, causing a
+            // memory leak
+            throw new JsonIOException(exceptionMessage);
+          }
+        };
+      }
+    }
+
+    return new ObjectConstructor<T>() {
+      @Override
+      public T construct() {
+        try {
+          @SuppressWarnings("unchecked") // T is the same raw type as is requested
+          T newInstance = (T) constructor.newInstance();
+          return newInstance;
+        }
+        // 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: JsonParseException ?
+          throw new RuntimeException(
+              "Failed to invoke constructor '"
+                  + ReflectionHelper.constructorToString(constructor)
+                  + "' with no args",
+              e.getCause());
+        } catch (IllegalAccessException e) {
+          throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+        }
+      }
+    };
+  }
+
+  /** Constructors for common interface types like Map and List and their subtypes. */
+  @SuppressWarnings("unchecked") // use runtime checks to guarantee that 'T' is what it is
+  private static <T> ObjectConstructor<T> newDefaultImplementationConstructor(
+      final Type type, Class<? super T> rawType) {
+
+    /*
+     * IMPORTANT: Must only create instances for classes with public no-args constructor.
+     * For classes with special constructors / factory methods (e.g. EnumSet)
+     * `newSpecialCollectionConstructor` defined above must be used, to avoid no-args
+     * constructor check (which is called before this method) detecting internal no-args
+     * constructors which might be added in a future JDK version
+     */
+
+    if (Collection.class.isAssignableFrom(rawType)) {
+      if (SortedSet.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new TreeSet<>();
+          }
+        };
+      } else if (Set.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new LinkedHashSet<>();
+          }
+        };
+      } else if (Queue.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new ArrayDeque<>();
+          }
+        };
+      } else {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new ArrayList<>();
+          }
+        };
+      }
+    }
+
+    if (Map.class.isAssignableFrom(rawType)) {
+      if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new ConcurrentSkipListMap<>();
+          }
+        };
+      } else if (ConcurrentMap.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new ConcurrentHashMap<>();
+          }
+        };
+      } else if (SortedMap.class.isAssignableFrom(rawType)) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new TreeMap<>();
+          }
+        };
+      } else if (type instanceof ParameterizedType
+          && !String.class.isAssignableFrom(
+              TypeToken.get(((ParameterizedType) type).getActualTypeArguments()[0]).getRawType())) {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new LinkedHashMap<>();
+          }
+        };
+      } else {
+        return new ObjectConstructor<T>() {
+          @Override
+          public T construct() {
+            return (T) new LinkedTreeMap<>();
+          }
+        };
+      }
+    }
+
+    return null;
+  }
+
+  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
+    if (useJdkUnsafe) {
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          try {
+            @SuppressWarnings("unchecked")
+            T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
+            return newInstance;
+          } catch (Exception e) {
+            throw new RuntimeException(
+                ("Unable to create instance of "
+                    + rawType
+                    + ". Registering an InstanceCreator or a TypeAdapter for this type, or adding a"
+                    + " no-args constructor may fix this problem."),
+                e);
+          }
+        }
+      };
+    } else {
+      String exceptionMessage =
+          "Unable to create instance of "
+              + rawType
+              + "; usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter"
+              + " for this type, adding a no-args constructor, or enabling usage of JDK Unsafe may"
+              + " fix this problem.";
+
+      // Check if R8 removed all constructors
+      if (rawType.getDeclaredConstructors().length == 0) {
+        // R8 with Unsafe disabled might not be common enough to warrant a separate Troubleshooting
+        // Guide entry
+        exceptionMessage +=
+            " Or adjust your R8 configuration to keep the no-args constructor of the class.";
+      }
+
+      // Explicit final variable to allow usage in the anonymous class below
+      final String exceptionMessageF = exceptionMessage;
+
+      return new ObjectConstructor<T>() {
+        @Override
+        public T construct() {
+          throw new JsonIOException(exceptionMessageF);
+        }
+      };
+    }
+  }
+
+  @Override
+  public String toString() {
+    return instanceCreators.toString();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/Excluder.java b/gson/gson/src/main/java/com/google/gson/internal/Excluder.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a6ba9db34f04b387db050b5eb403885e2b54c44
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/Excluder.java
@@ -0,0 +1,254 @@
+/*
+ * 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.internal;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.Since;
+import com.google.gson.annotations.Until;
+import com.google.gson.internal.reflect.ReflectionHelper;
+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.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class selects which fields and types to omit. It is configurable, supporting version
+ * attributes {@link Since} and {@link Until}, modifiers, synthetic fields, anonymous and local
+ * classes, inner classes, and fields with the {@link Expose} annotation.
+ *
+ * <p>This class is a type adapter factory; types that are excluded will be adapted to null. It may
+ * delegate to another type adapter if only one direction is excluded.
+ *
+ * @author Joel Leitch
+ * @author Jesse Wilson
+ */
+public final class Excluder implements TypeAdapterFactory, Cloneable {
+  private static final double IGNORE_VERSIONS = -1.0d;
+  public static final Excluder DEFAULT = new Excluder();
+
+  private double version = IGNORE_VERSIONS;
+  private int modifiers = Modifier.TRANSIENT | Modifier.STATIC;
+  private boolean serializeInnerClasses = true;
+  private boolean requireExpose;
+  private List<ExclusionStrategy> serializationStrategies = Collections.emptyList();
+  private List<ExclusionStrategy> deserializationStrategies = Collections.emptyList();
+
+  @Override
+  protected Excluder clone() {
+    try {
+      return (Excluder) super.clone();
+    } catch (CloneNotSupportedException e) {
+      throw new AssertionError(e);
+    }
+  }
+
+  public Excluder withVersion(double ignoreVersionsAfter) {
+    Excluder result = clone();
+    result.version = ignoreVersionsAfter;
+    return result;
+  }
+
+  public Excluder withModifiers(int... modifiers) {
+    Excluder result = clone();
+    result.modifiers = 0;
+    for (int modifier : modifiers) {
+      result.modifiers |= modifier;
+    }
+    return result;
+  }
+
+  public Excluder disableInnerClassSerialization() {
+    Excluder result = clone();
+    result.serializeInnerClasses = false;
+    return result;
+  }
+
+  public Excluder excludeFieldsWithoutExposeAnnotation() {
+    Excluder result = clone();
+    result.requireExpose = true;
+    return result;
+  }
+
+  public Excluder withExclusionStrategy(
+      ExclusionStrategy exclusionStrategy, boolean serialization, boolean deserialization) {
+    Excluder result = clone();
+    if (serialization) {
+      result.serializationStrategies = new ArrayList<>(serializationStrategies);
+      result.serializationStrategies.add(exclusionStrategy);
+    }
+    if (deserialization) {
+      result.deserializationStrategies = new ArrayList<>(deserializationStrategies);
+      result.deserializationStrategies.add(exclusionStrategy);
+    }
+    return result;
+  }
+
+  @Override
+  public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
+    Class<?> rawType = type.getRawType();
+
+    final boolean skipSerialize = excludeClass(rawType, true);
+    final boolean skipDeserialize = excludeClass(rawType, false);
+
+    if (!skipSerialize && !skipDeserialize) {
+      return null;
+    }
+
+    return new TypeAdapter<T>() {
+      /**
+       * The delegate is lazily created because it may not be needed, and creating it may fail.
+       * Field has to be {@code volatile} because {@link Gson} guarantees to be thread-safe.
+       */
+      private volatile TypeAdapter<T> delegate;
+
+      @Override
+      public T read(JsonReader in) throws IOException {
+        if (skipDeserialize) {
+          in.skipValue();
+          return null;
+        }
+        return delegate().read(in);
+      }
+
+      @Override
+      public void write(JsonWriter out, T value) throws IOException {
+        if (skipSerialize) {
+          out.nullValue();
+          return;
+        }
+        delegate().write(out, value);
+      }
+
+      private TypeAdapter<T> delegate() {
+        // A race might lead to `delegate` being assigned by multiple threads but the last
+        // assignment will stick
+        TypeAdapter<T> d = delegate;
+        return d != null ? d : (delegate = gson.getDelegateAdapter(Excluder.this, type));
+      }
+    };
+  }
+
+  public boolean excludeField(Field field, boolean serialize) {
+    if ((modifiers & field.getModifiers()) != 0) {
+      return true;
+    }
+
+    if (version != Excluder.IGNORE_VERSIONS
+        && !isValidVersion(field.getAnnotation(Since.class), field.getAnnotation(Until.class))) {
+      return true;
+    }
+
+    if (field.isSynthetic()) {
+      return true;
+    }
+
+    if (requireExpose) {
+      Expose annotation = field.getAnnotation(Expose.class);
+      if (annotation == null || (serialize ? !annotation.serialize() : !annotation.deserialize())) {
+        return true;
+      }
+    }
+
+    if (excludeClass(field.getType(), serialize)) {
+      return true;
+    }
+
+    List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies;
+    if (!list.isEmpty()) {
+      FieldAttributes fieldAttributes = new FieldAttributes(field);
+      for (ExclusionStrategy exclusionStrategy : list) {
+        if (exclusionStrategy.shouldSkipField(fieldAttributes)) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  // public for unit tests; can otherwise be private
+  public boolean excludeClass(Class<?> clazz, boolean serialize) {
+    if (version != Excluder.IGNORE_VERSIONS
+        && !isValidVersion(clazz.getAnnotation(Since.class), clazz.getAnnotation(Until.class))) {
+      return true;
+    }
+
+    if (!serializeInnerClasses && isInnerClass(clazz)) {
+      return true;
+    }
+
+    /*
+     * Exclude anonymous and local classes because they can have synthetic fields capturing enclosing
+     * values which makes serialization and deserialization unreliable.
+     * Don't exclude anonymous enum subclasses because enum types have a built-in adapter.
+     *
+     * Exclude only for deserialization; for serialization allow because custom adapter might be
+     * used; if no custom adapter exists reflection-based adapter otherwise excludes value.
+     *
+     * Cannot allow deserialization reliably here because some custom adapters like Collection adapter
+     * fall back to creating instances using Unsafe, which would likely lead to runtime exceptions
+     * for anonymous and local classes if they capture values.
+     */
+    if (!serialize
+        && !Enum.class.isAssignableFrom(clazz)
+        && ReflectionHelper.isAnonymousOrNonStaticLocal(clazz)) {
+      return true;
+    }
+
+    List<ExclusionStrategy> list = serialize ? serializationStrategies : deserializationStrategies;
+    for (ExclusionStrategy exclusionStrategy : list) {
+      if (exclusionStrategy.shouldSkipClass(clazz)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isInnerClass(Class<?> clazz) {
+    return clazz.isMemberClass() && !ReflectionHelper.isStatic(clazz);
+  }
+
+  private boolean isValidVersion(Since since, Until until) {
+    return isValidSince(since) && isValidUntil(until);
+  }
+
+  private boolean isValidSince(Since annotation) {
+    if (annotation != null) {
+      double annotationVersion = annotation.value();
+      return version >= annotationVersion;
+    }
+    return true;
+  }
+
+  private boolean isValidUntil(Until annotation) {
+    if (annotation != null) {
+      double annotationVersion = annotation.value();
+      return version < annotationVersion;
+    }
+    return true;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/JavaVersion.java b/gson/gson/src/main/java/com/google/gson/internal/JavaVersion.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc08057dca56ee2535e114fc5cebfdf324f19905
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/JavaVersion.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Gson authors
+ *
+ * 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.internal;
+
+/** Utility to check the major Java version of the current JVM. */
+public final class JavaVersion {
+  // Oracle defines naming conventions at
+  // http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html
+  // However, many alternate implementations differ. For example, Debian used 9-debian as the
+  // version string
+
+  private static final int majorJavaVersion = determineMajorJavaVersion();
+
+  private static int determineMajorJavaVersion() {
+    String javaVersion = System.getProperty("java.version");
+    return parseMajorJavaVersion(javaVersion);
+  }
+
+  // Visible for testing only
+  static int parseMajorJavaVersion(String javaVersion) {
+    int version = parseDotted(javaVersion);
+    if (version == -1) {
+      version = extractBeginningInt(javaVersion);
+    }
+    if (version == -1) {
+      return 6; // Choose minimum supported JDK version as default
+    }
+    return version;
+  }
+
+  // Parses both legacy 1.8 style and newer 9.0.4 style
+  private static int parseDotted(String javaVersion) {
+    try {
+      String[] parts = javaVersion.split("[._]", 3);
+      int firstVer = Integer.parseInt(parts[0]);
+      if (firstVer == 1 && parts.length > 1) {
+        return Integer.parseInt(parts[1]);
+      } else {
+        return firstVer;
+      }
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+
+  private static int extractBeginningInt(String javaVersion) {
+    try {
+      StringBuilder num = new StringBuilder();
+      for (int i = 0; i < javaVersion.length(); ++i) {
+        char c = javaVersion.charAt(i);
+        if (Character.isDigit(c)) {
+          num.append(c);
+        } else {
+          break;
+        }
+      }
+      return Integer.parseInt(num.toString());
+    } catch (NumberFormatException e) {
+      return -1;
+    }
+  }
+
+  /**
+   * Gets the major Java version
+   *
+   * @return the major Java version, i.e. '8' for Java 1.8, '9' for Java 9 etc.
+   */
+  public static int getMajorJavaVersion() {
+    return majorJavaVersion;
+  }
+
+  /**
+   * Gets a boolean value depending if the application is running on Java 9 or later
+   *
+   * @return {@code true} if the application is running on Java 9 or later; and {@code false}
+   *     otherwise.
+   */
+  public static boolean isJava9OrLater() {
+    return majorJavaVersion >= 9;
+  }
+
+  private JavaVersion() {}
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java b/gson/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java
new file mode 100644
index 0000000000000000000000000000000000000000..e3d4873d795145837bcb932f5f2ef4637d752b80
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/JsonReaderInternalAccess.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 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.internal;
+
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+
+/** Internal-only APIs of JsonReader available only to other classes in Gson. */
+public abstract class JsonReaderInternalAccess {
+  // Suppress warnings because field is initialized by `JsonReader` class during class loading
+  // (and therefore should be thread-safe), and any usage appears after `JsonReader` was loaded
+  @SuppressWarnings({"ConstantField", "NonFinalStaticField"})
+  public static volatile JsonReaderInternalAccess INSTANCE;
+
+  /** Changes the type of the current property name token to a string value. */
+  public abstract void promoteNameToValue(JsonReader reader) throws IOException;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java b/gson/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd9f9b9b571b87a6f1b66b9e048340ced5f326b0
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/LazilyParsedNumber.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2011 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.internal;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamException;
+import java.math.BigDecimal;
+
+/**
+ * This class holds a number value that is lazily converted to a specific number type
+ *
+ * @author Inderjeet Singh
+ */
+@SuppressWarnings("serial") // ignore warning about missing serialVersionUID
+public final class LazilyParsedNumber extends Number {
+  private final String value;
+
+  /**
+   * @param value must not be null
+   */
+  public LazilyParsedNumber(String value) {
+    this.value = value;
+  }
+
+  private BigDecimal asBigDecimal() {
+    return NumberLimits.parseBigDecimal(value);
+  }
+
+  @Override
+  public int intValue() {
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      try {
+        return (int) Long.parseLong(value);
+      } catch (NumberFormatException nfe) {
+        return asBigDecimal().intValue();
+      }
+    }
+  }
+
+  @Override
+  public long longValue() {
+    try {
+      return Long.parseLong(value);
+    } catch (NumberFormatException e) {
+      return asBigDecimal().longValue();
+    }
+  }
+
+  @Override
+  public float floatValue() {
+    return Float.parseFloat(value);
+  }
+
+  @Override
+  public double doubleValue() {
+    return Double.parseDouble(value);
+  }
+
+  @Override
+  public String toString() {
+    return value;
+  }
+
+  /**
+   * If somebody is unlucky enough to have to serialize one of these, serialize it as a BigDecimal
+   * so that they won't need Gson on the other side to deserialize it.
+   */
+  private Object writeReplace() throws ObjectStreamException {
+    return asBigDecimal();
+  }
+
+  private void readObject(ObjectInputStream in) throws IOException {
+    // Don't permit directly deserializing this class; writeReplace() should have written a
+    // replacement
+    throw new InvalidObjectException("Deserialization is unsupported");
+  }
+
+  @Override
+  public int hashCode() {
+    return value.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj instanceof LazilyParsedNumber) {
+      LazilyParsedNumber other = (LazilyParsedNumber) obj;
+      return value.equals(other.value);
+    }
+    return false;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java b/gson/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..099dd573c0c9de696d03e3ffbc3faf1681f56222
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/LinkedTreeMap.java
@@ -0,0 +1,680 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ * Copyright (C) 2012 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.internal;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Comparator;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A map of comparable keys to values. Unlike {@code TreeMap}, this class uses insertion order for
+ * iteration order. Comparison order is only used as an optimization for efficient insertion and
+ * removal.
+ *
+ * <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>() {
+        @Override
+        public int compare(Comparable a, Comparable b) {
+          return a.compareTo(b);
+        }
+      };
+
+  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;
+
+  /**
+   * 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(boolean allowNullValues) {
+    this((Comparator<? super K>) NATURAL_ORDER, allowNullValues);
+  }
+
+  /**
+   * Create a tree map ordered by {@code comparator}. This map's keys may only be null if {@code
+   * comparator} permits.
+   *
+   * @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
+   */
+  // unsafe! if comparator is null, this assumes K is comparable
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  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() {
+    return size;
+  }
+
+  @Override
+  public V get(Object key) {
+    Node<K, V> node = findByObject(key);
+    return node != null ? node.value : null;
+  }
+
+  @Override
+  public boolean containsKey(Object key) {
+    return findByObject(key) != null;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public V put(K key, V value) {
+    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;
+    return result;
+  }
+
+  @Override
+  public void clear() {
+    root = null;
+    size = 0;
+    modCount++;
+
+    // Clear iteration order
+    Node<K, V> header = this.header;
+    header.next = header.prev = header;
+  }
+
+  @Override
+  public V remove(Object key) {
+    Node<K, V> node = removeInternalByKey(key);
+    return node != null ? node.value : null;
+  }
+
+  /**
+   * Returns the node at or adjacent to the given key, creating it if requested.
+   *
+   * @throws ClassCastException if {@code key} and the tree's keys aren't mutually comparable.
+   */
+  Node<K, V> find(K key, boolean create) {
+    Comparator<? super K> comparator = this.comparator;
+    Node<K, V> nearest = root;
+    int comparison = 0;
+
+    if (nearest != null) {
+      // Micro-optimization: avoid polymorphic calls to Comparator.compare().
+      @SuppressWarnings("unchecked") // Throws a ClassCastException below if there's trouble.
+      Comparable<Object> comparableKey =
+          (comparator == NATURAL_ORDER) ? (Comparable<Object>) key : null;
+
+      while (true) {
+        comparison =
+            (comparableKey != null)
+                ? comparableKey.compareTo(nearest.key)
+                : comparator.compare(key, nearest.key);
+
+        // We found the requested key.
+        if (comparison == 0) {
+          return nearest;
+        }
+
+        // If it exists, the key is in a subtree. Go deeper.
+        Node<K, V> child = (comparison < 0) ? nearest.left : nearest.right;
+        if (child == null) {
+          break;
+        }
+
+        nearest = child;
+      }
+    }
+
+    // The key doesn't exist in this tree.
+    if (!create) {
+      return null;
+    }
+
+    // Create the node and add it to the tree or the table.
+    Node<K, V> header = this.header;
+    Node<K, V> created;
+    if (nearest == null) {
+      // Check that the value is comparable if we didn't do any comparisons.
+      if (comparator == NATURAL_ORDER && !(key instanceof Comparable)) {
+        throw new ClassCastException(key.getClass().getName() + " is not Comparable");
+      }
+      created = new Node<>(allowNullValues, nearest, key, header, header.prev);
+      root = created;
+    } else {
+      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
+        nearest.right = created;
+      }
+      rebalance(nearest, true);
+    }
+    size++;
+    modCount++;
+
+    return created;
+  }
+
+  @SuppressWarnings("unchecked")
+  Node<K, V> findByObject(Object key) {
+    try {
+      return key != null ? find((K) key, false) : null;
+    } catch (ClassCastException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Returns this map's entry that has the same key and value as {@code entry}, or null if this map
+   * has no such entry.
+   *
+   * <p>This method uses the comparator for key equality rather than {@code equals}. If this map's
+   * comparator isn't consistent with equals (such as {@code String.CASE_INSENSITIVE_ORDER}), then
+   * {@code remove()} and {@code contains()} will violate the collections API.
+   */
+  Node<K, V> findByEntry(Entry<?, ?> entry) {
+    Node<K, V> mine = findByObject(entry.getKey());
+    boolean valuesEqual = mine != null && equal(mine.value, entry.getValue());
+    return valuesEqual ? mine : null;
+  }
+
+  private static boolean equal(Object a, Object b) {
+    return Objects.equals(a, b);
+  }
+
+  /**
+   * Removes {@code node} from this tree, rearranging the tree's structure as necessary.
+   *
+   * @param unlink true to also unlink this node from the iteration linked list.
+   */
+  void removeInternal(Node<K, V> node, boolean unlink) {
+    if (unlink) {
+      node.prev.next = node.next;
+      node.next.prev = node.prev;
+    }
+
+    Node<K, V> left = node.left;
+    Node<K, V> right = node.right;
+    Node<K, V> originalParent = node.parent;
+    if (left != null && right != null) {
+
+      /*
+       * To remove a node with both left and right subtrees, move an
+       * adjacent node from one of those subtrees into this node's place.
+       *
+       * Removing the adjacent node may change this node's subtrees. This
+       * node may no longer have two subtrees once the adjacent node is
+       * gone!
+       */
+
+      Node<K, V> adjacent = (left.height > right.height) ? left.last() : right.first();
+      removeInternal(adjacent, false); // takes care of rebalance and size--
+
+      int leftHeight = 0;
+      left = node.left;
+      if (left != null) {
+        leftHeight = left.height;
+        adjacent.left = left;
+        left.parent = adjacent;
+        node.left = null;
+      }
+
+      int rightHeight = 0;
+      right = node.right;
+      if (right != null) {
+        rightHeight = right.height;
+        adjacent.right = right;
+        right.parent = adjacent;
+        node.right = null;
+      }
+
+      adjacent.height = Math.max(leftHeight, rightHeight) + 1;
+      replaceInParent(node, adjacent);
+      return;
+    } else if (left != null) {
+      replaceInParent(node, left);
+      node.left = null;
+    } else if (right != null) {
+      replaceInParent(node, right);
+      node.right = null;
+    } else {
+      replaceInParent(node, null);
+    }
+
+    rebalance(originalParent, false);
+    size--;
+    modCount++;
+  }
+
+  Node<K, V> removeInternalByKey(Object key) {
+    Node<K, V> node = findByObject(key);
+    if (node != null) {
+      removeInternal(node, true);
+    }
+    return node;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private void replaceInParent(Node<K, V> node, Node<K, V> replacement) {
+    Node<K, V> parent = node.parent;
+    node.parent = null;
+    if (replacement != null) {
+      replacement.parent = parent;
+    }
+
+    if (parent != null) {
+      if (parent.left == node) {
+        parent.left = replacement;
+      } else {
+        assert parent.right == node;
+        parent.right = replacement;
+      }
+    } else {
+      root = replacement;
+    }
+  }
+
+  /**
+   * Rebalances the tree by making any AVL rotations necessary between the newly-unbalanced node and
+   * the tree's root.
+   *
+   * @param insert true if the node was unbalanced by an insert; false if it was by a removal.
+   */
+  private void rebalance(Node<K, V> unbalanced, boolean insert) {
+    for (Node<K, V> node = unbalanced; node != null; node = node.parent) {
+      Node<K, V> left = node.left;
+      Node<K, V> right = node.right;
+      int leftHeight = left != null ? left.height : 0;
+      int rightHeight = right != null ? right.height : 0;
+
+      int delta = leftHeight - rightHeight;
+      if (delta == -2) {
+        Node<K, V> rightLeft = right.left;
+        Node<K, V> rightRight = right.right;
+        int rightRightHeight = rightRight != null ? rightRight.height : 0;
+        int rightLeftHeight = rightLeft != null ? rightLeft.height : 0;
+
+        int rightDelta = rightLeftHeight - rightRightHeight;
+        if (rightDelta == -1 || (rightDelta == 0 && !insert)) {
+          rotateLeft(node); // AVL right right
+        } else {
+          assert (rightDelta == 1);
+          rotateRight(right); // AVL right left
+          rotateLeft(node);
+        }
+        if (insert) {
+          break; // no further rotations will be necessary
+        }
+
+      } else if (delta == 2) {
+        Node<K, V> leftLeft = left.left;
+        Node<K, V> leftRight = left.right;
+        int leftRightHeight = leftRight != null ? leftRight.height : 0;
+        int leftLeftHeight = leftLeft != null ? leftLeft.height : 0;
+
+        int leftDelta = leftLeftHeight - leftRightHeight;
+        if (leftDelta == 1 || (leftDelta == 0 && !insert)) {
+          rotateRight(node); // AVL left left
+        } else {
+          assert (leftDelta == -1);
+          rotateLeft(left); // AVL left right
+          rotateRight(node);
+        }
+        if (insert) {
+          break; // no further rotations will be necessary
+        }
+
+      } else if (delta == 0) {
+        node.height = leftHeight + 1; // leftHeight == rightHeight
+        if (insert) {
+          break; // the insert caused balance, so rebalancing is done!
+        }
+
+      } else {
+        assert (delta == -1 || delta == 1);
+        node.height = Math.max(leftHeight, rightHeight) + 1;
+        if (!insert) {
+          break; // the height hasn't changed, so rebalancing is done!
+        }
+      }
+    }
+  }
+
+  /** Rotates the subtree so that its root's right child is the new root. */
+  private void rotateLeft(Node<K, V> root) {
+    Node<K, V> left = root.left;
+    Node<K, V> pivot = root.right;
+    Node<K, V> pivotLeft = pivot.left;
+    Node<K, V> pivotRight = pivot.right;
+
+    // move the pivot's left child to the root's right
+    root.right = pivotLeft;
+    if (pivotLeft != null) {
+      pivotLeft.parent = root;
+    }
+
+    replaceInParent(root, pivot);
+
+    // move the root to the pivot's left
+    pivot.left = root;
+    root.parent = pivot;
+
+    // fix heights
+    root.height =
+        Math.max(left != null ? left.height : 0, pivotLeft != null ? pivotLeft.height : 0) + 1;
+    pivot.height = Math.max(root.height, pivotRight != null ? pivotRight.height : 0) + 1;
+  }
+
+  /** Rotates the subtree so that its root's left child is the new root. */
+  private void rotateRight(Node<K, V> root) {
+    Node<K, V> pivot = root.left;
+    Node<K, V> right = root.right;
+    Node<K, V> pivotLeft = pivot.left;
+    Node<K, V> pivotRight = pivot.right;
+
+    // move the pivot's right child to the root's left
+    root.left = pivotRight;
+    if (pivotRight != null) {
+      pivotRight.parent = root;
+    }
+
+    replaceInParent(root, pivot);
+
+    // move the root to the pivot's right
+    pivot.right = root;
+    root.parent = pivot;
+
+    // fixup heights
+    root.height =
+        Math.max(right != null ? right.height : 0, pivotRight != null ? pivotRight.height : 0) + 1;
+    pivot.height = Math.max(root.height, pivotLeft != null ? pivotLeft.height : 0) + 1;
+  }
+
+  private EntrySet entrySet;
+  private KeySet keySet;
+
+  @Override
+  public Set<Entry<K, V>> entrySet() {
+    EntrySet result = entrySet;
+    return result != null ? result : (entrySet = new EntrySet());
+  }
+
+  @Override
+  public Set<K> keySet() {
+    KeySet result = keySet;
+    return result != null ? result : (keySet = new KeySet());
+  }
+
+  static final class Node<K, V> implements Entry<K, V> {
+    Node<K, V> parent;
+    Node<K, V> left;
+    Node<K, V> right;
+    Node<K, V> next;
+    Node<K, V> prev;
+    final K key;
+    final boolean allowNullValue;
+    V value;
+    int height;
+
+    /** Create the header entry */
+    Node(boolean allowNullValue) {
+      key = null;
+      this.allowNullValue = allowNullValue;
+      next = prev = this;
+    }
+
+    /** Create a regular entry */
+    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;
+      prev.next = this;
+      next.prev = this;
+    }
+
+    @Override
+    public K getKey() {
+      return key;
+    }
+
+    @Override
+    public V getValue() {
+      return value;
+    }
+
+    @Override
+    public V setValue(V value) {
+      if (value == null && !allowNullValue) {
+        throw new NullPointerException("value == null");
+      }
+      V oldValue = this.value;
+      this.value = value;
+      return oldValue;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof Entry) {
+        Entry<?, ?> other = (Entry<?, ?>) o;
+        return (key == null ? other.getKey() == null : key.equals(other.getKey()))
+            && (value == null ? other.getValue() == null : value.equals(other.getValue()));
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
+    }
+
+    @Override
+    public String toString() {
+      return key + "=" + value;
+    }
+
+    /** Returns the first node in this subtree. */
+    public Node<K, V> first() {
+      Node<K, V> node = this;
+      Node<K, V> child = node.left;
+      while (child != null) {
+        node = child;
+        child = node.left;
+      }
+      return node;
+    }
+
+    /** Returns the last node in this subtree. */
+    public Node<K, V> last() {
+      Node<K, V> node = this;
+      Node<K, V> child = node.right;
+      while (child != null) {
+        node = child;
+        child = node.right;
+      }
+      return node;
+    }
+  }
+
+  private abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
+    Node<K, V> next = header.next;
+    Node<K, V> lastReturned = null;
+    int expectedModCount = modCount;
+
+    LinkedTreeMapIterator() {}
+
+    @Override
+    @SuppressWarnings("ReferenceEquality")
+    public final boolean hasNext() {
+      return next != header;
+    }
+
+    @SuppressWarnings("ReferenceEquality")
+    final Node<K, V> nextNode() {
+      Node<K, V> e = next;
+      if (e == header) {
+        throw new NoSuchElementException();
+      }
+      if (modCount != expectedModCount) {
+        throw new ConcurrentModificationException();
+      }
+      next = e.next;
+      return lastReturned = e;
+    }
+
+    @Override
+    public final void remove() {
+      if (lastReturned == null) {
+        throw new IllegalStateException();
+      }
+      removeInternal(lastReturned, true);
+      lastReturned = null;
+      expectedModCount = modCount;
+    }
+  }
+
+  class EntrySet extends AbstractSet<Entry<K, V>> {
+    @Override
+    public int size() {
+      return size;
+    }
+
+    @Override
+    public Iterator<Entry<K, V>> iterator() {
+      return new LinkedTreeMapIterator<Entry<K, V>>() {
+        @Override
+        public Entry<K, V> next() {
+          return nextNode();
+        }
+      };
+    }
+
+    @Override
+    public boolean contains(Object o) {
+      return o instanceof Entry && findByEntry((Entry<?, ?>) o) != null;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+      if (!(o instanceof Entry)) {
+        return false;
+      }
+
+      Node<K, V> node = findByEntry((Entry<?, ?>) o);
+      if (node == null) {
+        return false;
+      }
+      removeInternal(node, true);
+      return true;
+    }
+
+    @Override
+    public void clear() {
+      LinkedTreeMap.this.clear();
+    }
+  }
+
+  final class KeySet extends AbstractSet<K> {
+    @Override
+    public int size() {
+      return size;
+    }
+
+    @Override
+    public Iterator<K> iterator() {
+      return new LinkedTreeMapIterator<K>() {
+        @Override
+        public K next() {
+          return nextNode().key;
+        }
+      };
+    }
+
+    @Override
+    public boolean contains(Object o) {
+      return containsKey(o);
+    }
+
+    @Override
+    public boolean remove(Object key) {
+      return removeInternalByKey(key) != null;
+    }
+
+    @Override
+    public void clear() {
+      LinkedTreeMap.this.clear();
+    }
+  }
+
+  /**
+   * If somebody is unlucky enough to have to serialize one of these, serialize it as a
+   * LinkedHashMap so that they won't need Gson on the other side to deserialize it. Using
+   * serialization defeats our DoS defence, so most apps shouldn't use it.
+   */
+  private Object writeReplace() throws ObjectStreamException {
+    return new LinkedHashMap<>(this);
+  }
+
+  private void readObject(ObjectInputStream in) throws IOException {
+    // Don't permit directly deserializing this class; writeReplace() should have written a
+    // replacement
+    throw new InvalidObjectException("Deserialization is unsupported");
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java b/gson/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
new file mode 100644
index 0000000000000000000000000000000000000000..51008f840b34347f91427acbb7e4f189a5adeb96
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2018 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.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;
+
+  @SuppressWarnings("NonApiType")
+  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();
+  }
+
+  @SuppressWarnings("UngroupedOverloads") // this is intentionally ungrouped, see comment above
+  @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/gson/src/main/java/com/google/gson/internal/NumberLimits.java b/gson/gson/src/main/java/com/google/gson/internal/NumberLimits.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d6c0981e954f14c32d7fad7ce77cc7fd8096a9a
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/NumberLimits.java
@@ -0,0 +1,36 @@
+package com.google.gson.internal;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * This class enforces limits on numbers parsed from JSON to avoid potential performance problems
+ * when extremely large numbers are used.
+ */
+public class NumberLimits {
+  private NumberLimits() {}
+
+  private static final int MAX_NUMBER_STRING_LENGTH = 10_000;
+
+  private static void checkNumberStringLength(String s) {
+    if (s.length() > MAX_NUMBER_STRING_LENGTH) {
+      throw new NumberFormatException("Number string too large: " + s.substring(0, 30) + "...");
+    }
+  }
+
+  public static BigDecimal parseBigDecimal(String s) throws NumberFormatException {
+    checkNumberStringLength(s);
+    BigDecimal decimal = new BigDecimal(s);
+
+    // Cast to long to avoid issues with abs when value is Integer.MIN_VALUE
+    if (Math.abs((long) decimal.scale()) >= 10_000) {
+      throw new NumberFormatException("Number has unsupported scale: " + s);
+    }
+    return decimal;
+  }
+
+  public static BigInteger parseBigInteger(String s) throws NumberFormatException {
+    checkNumberStringLength(s);
+    return new BigInteger(s);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java b/gson/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..30f3b1b7786925a8c358ddaf9471d9b883cb1b88
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/ObjectConstructor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.internal;
+
+/**
+ * Defines a generic object construction factory. The purpose of this class is to construct a
+ * default instance of a class that can be used for object navigation while deserialization from its
+ * JSON representation.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public interface ObjectConstructor<T> {
+
+  /** Returns a new instance. */
+  public T construct();
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java b/gson/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..552503f25fc160cd15bcc5a323eae00a9455c157
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/PreJava9DateFormatProvider.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 The Gson authors
+ *
+ * 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.internal;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/** Provides DateFormats for US locale with patterns which were the default ones before Java 9. */
+public class PreJava9DateFormatProvider {
+  private PreJava9DateFormatProvider() {}
+
+  /**
+   * Returns the same DateFormat as {@code DateFormat.getDateInstance(style, Locale.US)} in Java 8
+   * or below.
+   */
+  public static DateFormat getUsDateFormat(int style) {
+    return new SimpleDateFormat(getDateFormatPattern(style), Locale.US);
+  }
+
+  /**
+   * Returns the same DateFormat as {@code DateFormat.getDateTimeInstance(dateStyle, timeStyle,
+   * Locale.US)} in Java 8 or below.
+   */
+  public static DateFormat getUsDateTimeFormat(int dateStyle, int timeStyle) {
+    String pattern =
+        getDatePartOfDateTimePattern(dateStyle) + " " + getTimePartOfDateTimePattern(timeStyle);
+    return new SimpleDateFormat(pattern, Locale.US);
+  }
+
+  private static String getDateFormatPattern(int style) {
+    switch (style) {
+      case DateFormat.SHORT:
+        return "M/d/yy";
+      case DateFormat.MEDIUM:
+        return "MMM d, y";
+      case DateFormat.LONG:
+        return "MMMM d, y";
+      case DateFormat.FULL:
+        return "EEEE, MMMM d, y";
+      default:
+        throw new IllegalArgumentException("Unknown DateFormat style: " + style);
+    }
+  }
+
+  private static String getDatePartOfDateTimePattern(int dateStyle) {
+    switch (dateStyle) {
+      case DateFormat.SHORT:
+        return "M/d/yy";
+      case DateFormat.MEDIUM:
+        return "MMM d, yyyy";
+      case DateFormat.LONG:
+        return "MMMM d, yyyy";
+      case DateFormat.FULL:
+        return "EEEE, MMMM d, yyyy";
+      default:
+        throw new IllegalArgumentException("Unknown DateFormat style: " + dateStyle);
+    }
+  }
+
+  private static String getTimePartOfDateTimePattern(int timeStyle) {
+    switch (timeStyle) {
+      case DateFormat.SHORT:
+        return "h:mm a";
+      case DateFormat.MEDIUM:
+        return "h:mm:ss a";
+      case DateFormat.FULL:
+      case DateFormat.LONG:
+        return "h:mm:ss a z";
+      default:
+        throw new IllegalArgumentException("Unknown DateFormat style: " + timeStyle);
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/Primitives.java b/gson/gson/src/main/java/com/google/gson/internal/Primitives.java
new file mode 100644
index 0000000000000000000000000000000000000000..3397ea20e53e6ee71f29665eed053d8a4962f8aa
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/Primitives.java
@@ -0,0 +1,100 @@
+/*
+ * 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.internal;
+
+import java.lang.reflect.Type;
+
+/**
+ * Contains static utility methods pertaining to primitive types and their corresponding wrapper
+ * types.
+ *
+ * @author Kevin Bourrillion
+ */
+public final class Primitives {
+  private Primitives() {}
+
+  /** Returns true if this type is a primitive. */
+  public static boolean isPrimitive(Type type) {
+    return type instanceof Class<?> && ((Class<?>) type).isPrimitive();
+  }
+
+  /**
+   * Returns {@code true} if {@code type} is one of the nine primitive-wrapper types, such as {@link
+   * Integer}.
+   *
+   * @see Class#isPrimitive
+   */
+  public static boolean isWrapperType(Type type) {
+    return type == Integer.class
+        || type == Float.class
+        || type == Byte.class
+        || type == Double.class
+        || type == Long.class
+        || type == Character.class
+        || type == Boolean.class
+        || type == Short.class
+        || type == Void.class;
+  }
+
+  /**
+   * Returns the corresponding wrapper type of {@code type} if it is a primitive type; otherwise
+   * returns {@code type} itself. Idempotent.
+   *
+   * <pre>
+   *     wrap(int.class) == Integer.class
+   *     wrap(Integer.class) == Integer.class
+   *     wrap(String.class) == String.class
+   * </pre>
+   */
+  @SuppressWarnings({"unchecked", "MissingBraces"})
+  public static <T> Class<T> wrap(Class<T> type) {
+    if (type == int.class) return (Class<T>) Integer.class;
+    if (type == float.class) return (Class<T>) Float.class;
+    if (type == byte.class) return (Class<T>) Byte.class;
+    if (type == double.class) return (Class<T>) Double.class;
+    if (type == long.class) return (Class<T>) Long.class;
+    if (type == char.class) return (Class<T>) Character.class;
+    if (type == boolean.class) return (Class<T>) Boolean.class;
+    if (type == short.class) return (Class<T>) Short.class;
+    if (type == void.class) return (Class<T>) Void.class;
+    return type;
+  }
+
+  /**
+   * Returns the corresponding primitive type of {@code type} if it is a wrapper type; otherwise
+   * returns {@code type} itself. Idempotent.
+   *
+   * <pre>
+   *     unwrap(Integer.class) == int.class
+   *     unwrap(int.class) == int.class
+   *     unwrap(String.class) == String.class
+   * </pre>
+   */
+  @SuppressWarnings({"unchecked", "MissingBraces"})
+  public static <T> Class<T> unwrap(Class<T> type) {
+    if (type == Integer.class) return (Class<T>) int.class;
+    if (type == Float.class) return (Class<T>) float.class;
+    if (type == Byte.class) return (Class<T>) byte.class;
+    if (type == Double.class) return (Class<T>) double.class;
+    if (type == Long.class) return (Class<T>) long.class;
+    if (type == Character.class) return (Class<T>) char.class;
+    if (type == Boolean.class) return (Class<T>) boolean.class;
+    if (type == Short.class) return (Class<T>) short.class;
+    if (type == Void.class) return (Class<T>) void.class;
+    return type;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java b/gson/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f66ee0799c93d8672ff6fe8eb42678358ad6d84
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java
@@ -0,0 +1,121 @@
+/*
+ * 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.internal;
+
+import com.google.gson.ReflectionAccessFilter;
+import com.google.gson.ReflectionAccessFilter.FilterResult;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/** Internal helper class for {@link ReflectionAccessFilter}. */
+public class ReflectionAccessFilterHelper {
+  private ReflectionAccessFilterHelper() {}
+
+  // Platform type detection is based on Moshi's Util.isPlatformType(Class)
+  // See
+  // https://github.com/square/moshi/blob/3c108919ee1cce88a433ffda04eeeddc0341eae7/moshi/src/main/java/com/squareup/moshi/internal/Util.java#L141
+
+  public static boolean isJavaType(Class<?> c) {
+    return isJavaType(c.getName());
+  }
+
+  private static boolean isJavaType(String className) {
+    return className.startsWith("java.") || className.startsWith("javax.");
+  }
+
+  public static boolean isAndroidType(Class<?> c) {
+    return isAndroidType(c.getName());
+  }
+
+  private static boolean isAndroidType(String className) {
+    return className.startsWith("android.")
+        || className.startsWith("androidx.")
+        || isJavaType(className);
+  }
+
+  public static boolean isAnyPlatformType(Class<?> c) {
+    String className = c.getName();
+    return isAndroidType(className) // Covers Android and Java
+        || className.startsWith("kotlin.")
+        || className.startsWith("kotlinx.")
+        || className.startsWith("scala.");
+  }
+
+  /**
+   * Gets the result of applying all filters until the first one returns a result other than {@link
+   * FilterResult#INDECISIVE}, or {@link FilterResult#ALLOW} if the list of filters is empty or all
+   * returned {@code INDECISIVE}.
+   */
+  public static FilterResult getFilterResult(
+      List<ReflectionAccessFilter> reflectionFilters, Class<?> c) {
+    for (ReflectionAccessFilter filter : reflectionFilters) {
+      FilterResult result = filter.check(c);
+      if (result != FilterResult.INDECISIVE) {
+        return result;
+      }
+    }
+    return FilterResult.ALLOW;
+  }
+
+  /** See {@link AccessibleObject#canAccess(Object)} (Java >= 9) */
+  public static boolean canAccess(AccessibleObject accessibleObject, Object object) {
+    return AccessChecker.INSTANCE.canAccess(accessibleObject, object);
+  }
+
+  private abstract static class AccessChecker {
+    public static final AccessChecker INSTANCE;
+
+    static {
+      AccessChecker accessChecker = null;
+      // TODO: Ideally should use Multi-Release JAR for this version specific code
+      if (JavaVersion.isJava9OrLater()) {
+        try {
+          final Method canAccessMethod =
+              AccessibleObject.class.getDeclaredMethod("canAccess", Object.class);
+          accessChecker =
+              new AccessChecker() {
+                @Override
+                public boolean canAccess(AccessibleObject accessibleObject, Object object) {
+                  try {
+                    return (Boolean) canAccessMethod.invoke(accessibleObject, object);
+                  } catch (Exception e) {
+                    throw new RuntimeException("Failed invoking canAccess", e);
+                  }
+                }
+              };
+        } catch (NoSuchMethodException ignored) {
+          // OK: will assume everything is accessible
+        }
+      }
+
+      if (accessChecker == null) {
+        accessChecker =
+            new AccessChecker() {
+              @Override
+              public boolean canAccess(AccessibleObject accessibleObject, Object object) {
+                // Cannot determine whether object can be accessed, so assume it can be accessed
+                return true;
+              }
+            };
+      }
+      INSTANCE = accessChecker;
+    }
+
+    public abstract boolean canAccess(AccessibleObject accessibleObject, Object object);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/Streams.java b/gson/gson/src/main/java/com/google/gson/internal/Streams.java
new file mode 100644
index 0000000000000000000000000000000000000000..46df853f5ad61604277f3d75d3befc4141481672
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/Streams.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 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.internal;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.internal.bind.TypeAdapters;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Objects;
+
+/** Reads and writes GSON parse trees over streams. */
+public final class Streams {
+  private Streams() {
+    throw new UnsupportedOperationException();
+  }
+
+  /** Takes a reader in any state and returns the next value as a JsonElement. */
+  public static JsonElement parse(JsonReader reader) throws JsonParseException {
+    boolean isEmpty = true;
+    try {
+      JsonToken unused = reader.peek();
+      isEmpty = false;
+      return TypeAdapters.JSON_ELEMENT.read(reader);
+    } catch (EOFException e) {
+      /*
+       * For compatibility with JSON 1.5 and earlier, we return a JsonNull for
+       * empty documents instead of throwing.
+       */
+      if (isEmpty) {
+        return JsonNull.INSTANCE;
+      }
+      // The stream ended prematurely so it is likely a syntax error.
+      throw new JsonSyntaxException(e);
+    } catch (MalformedJsonException e) {
+      throw new JsonSyntaxException(e);
+    } catch (IOException e) {
+      throw new JsonIOException(e);
+    } catch (NumberFormatException e) {
+      throw new JsonSyntaxException(e);
+    }
+  }
+
+  /** Writes the JSON element to the writer, recursively. */
+  public static void write(JsonElement element, JsonWriter writer) throws IOException {
+    TypeAdapters.JSON_ELEMENT.write(writer, element);
+  }
+
+  public static Writer writerForAppendable(Appendable appendable) {
+    return appendable instanceof Writer ? (Writer) appendable : new AppendableWriter(appendable);
+  }
+
+  /** Adapts an {@link Appendable} so it can be passed anywhere a {@link Writer} is used. */
+  private static final class AppendableWriter extends Writer {
+    private final Appendable appendable;
+    private final CurrentWrite currentWrite = new CurrentWrite();
+
+    AppendableWriter(Appendable appendable) {
+      this.appendable = appendable;
+    }
+
+    @SuppressWarnings("UngroupedOverloads") // this is intentionally ungrouped, see comment below
+    @Override
+    public void write(char[] chars, int offset, int length) throws IOException {
+      currentWrite.setChars(chars);
+      appendable.append(currentWrite, offset, offset + length);
+    }
+
+    @Override
+    public void flush() {}
+
+    @Override
+    public void close() {}
+
+    // Override these methods for better performance
+    // They would otherwise unnecessarily create Strings or char arrays
+
+    @Override
+    public void write(int i) throws IOException {
+      appendable.append((char) i);
+    }
+
+    @Override
+    public void 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[]. */
+    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;
+      }
+
+      @Override
+      public char charAt(int i) {
+        return chars[i];
+      }
+
+      @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/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java b/gson/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java
new file mode 100644
index 0000000000000000000000000000000000000000..0782693b6a9352cb57b4abdaf4738aa4e0676b09
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java
@@ -0,0 +1,10 @@
+package com.google.gson.internal;
+
+public class TroubleshootingGuide {
+  private TroubleshootingGuide() {}
+
+  /** Creates a URL referring to the specified troubleshooting section. */
+  public static String createUrl(String id) {
+    return "https://github.com/google/gson/blob/main/Troubleshooting.md#" + id;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java b/gson/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a1073607aaf5c72d7077af03d57afc481613a21
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2011 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.internal;
+
+import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Do sneaky things to allocate objects without invoking their constructors.
+ *
+ * @author Joel Leitch
+ * @author Jesse Wilson
+ */
+public abstract class UnsafeAllocator {
+  public abstract <T> T newInstance(Class<T> c) throws Exception;
+
+  /**
+   * 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 = ConstructorConstructor.checkInstantiable(c);
+    if (exceptionMessage != null) {
+      throw new AssertionError(
+          "UnsafeAllocator is used for non-instantiable type: " + exceptionMessage);
+    }
+  }
+
+  public static final UnsafeAllocator INSTANCE = create();
+
+  private static UnsafeAllocator create() {
+    // try JVM
+    // public class Unsafe {
+    //   public Object allocateInstance(Class<?> type);
+    // }
+    try {
+      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+      Field f = unsafeClass.getDeclaredField("theUnsafe");
+      f.setAccessible(true);
+      final Object unsafe = f.get(null);
+      final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
+      return new UnsafeAllocator() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> T newInstance(Class<T> c) throws Exception {
+          assertInstantiable(c);
+          return (T) allocateInstance.invoke(unsafe, c);
+        }
+      };
+    } catch (Exception ignored) {
+      // OK: try the next way
+    }
+
+    // try dalvikvm, post-gingerbread
+    // public class ObjectStreamClass {
+    //   private static native int getConstructorId(Class<?> c);
+    //   private static native Object newInstance(Class<?> instantiationClass, int methodId);
+    // }
+    try {
+      Method getConstructorId =
+          ObjectStreamClass.class.getDeclaredMethod("getConstructorId", Class.class);
+      getConstructorId.setAccessible(true);
+      final int constructorId = (Integer) getConstructorId.invoke(null, Object.class);
+      final Method newInstance =
+          ObjectStreamClass.class.getDeclaredMethod("newInstance", Class.class, int.class);
+      newInstance.setAccessible(true);
+      return new UnsafeAllocator() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> T newInstance(Class<T> c) throws Exception {
+          assertInstantiable(c);
+          return (T) newInstance.invoke(null, c, constructorId);
+        }
+      };
+    } catch (Exception ignored) {
+      // OK: try the next way
+    }
+
+    // try dalvikvm, pre-gingerbread
+    // public class ObjectInputStream {
+    //   private static native Object newInstance(
+    //     Class<?> instantiationClass, Class<?> constructorClass);
+    // }
+    try {
+      final Method newInstance =
+          ObjectInputStream.class.getDeclaredMethod("newInstance", Class.class, Class.class);
+      newInstance.setAccessible(true);
+      return new UnsafeAllocator() {
+        @Override
+        @SuppressWarnings("unchecked")
+        public <T> T newInstance(Class<T> c) throws Exception {
+          assertInstantiable(c);
+          return (T) newInstance.invoke(null, c, Object.class);
+        }
+      };
+    } catch (Exception ignored) {
+      // OK: try the next way
+    }
+
+    // give up
+    return new UnsafeAllocator() {
+      @Override
+      public <T> T newInstance(Class<T> c) {
+        throw new UnsupportedOperationException(
+            "Cannot allocate "
+                + c
+                + ". Usage of JDK sun.misc.Unsafe is enabled, but it could not be used."
+                + " Make sure your runtime is configured correctly.");
+      }
+    };
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d4224d52cdc169fe26836faf8fb209c72a6e7ec9
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/ArrayTypeAdapter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+
+/** Adapt an array of objects. */
+public final class ArrayTypeAdapter<E> extends TypeAdapter<Object> {
+  public static final TypeAdapterFactory FACTORY =
+      new TypeAdapterFactory() {
+        @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()))) {
+            return null;
+          }
+
+          Type componentType = $Gson$Types.getArrayComponentType(type);
+          TypeAdapter<?> componentTypeAdapter = gson.getAdapter(TypeToken.get(componentType));
+
+          @SuppressWarnings({"unchecked", "rawtypes"})
+          TypeAdapter<T> arrayAdapter =
+              new ArrayTypeAdapter(
+                  gson, componentTypeAdapter, $Gson$Types.getRawType(componentType));
+          return arrayAdapter;
+        }
+      };
+
+  private final Class<E> componentType;
+  private final TypeAdapter<E> componentTypeAdapter;
+
+  public ArrayTypeAdapter(
+      Gson context, TypeAdapter<E> componentTypeAdapter, Class<E> componentType) {
+    this.componentTypeAdapter =
+        new TypeAdapterRuntimeTypeWrapper<>(context, componentTypeAdapter, componentType);
+    this.componentType = componentType;
+  }
+
+  @Override
+  public Object read(JsonReader in) throws IOException {
+    if (in.peek() == JsonToken.NULL) {
+      in.nextNull();
+      return null;
+    }
+
+    ArrayList<E> list = new ArrayList<>();
+    in.beginArray();
+    while (in.hasNext()) {
+      E instance = componentTypeAdapter.read(in);
+      list.add(instance);
+    }
+    in.endArray();
+
+    int size = list.size();
+    // Have to copy primitives one by one to primitive array
+    if (componentType.isPrimitive()) {
+      Object array = Array.newInstance(componentType, size);
+      for (int i = 0; i < size; i++) {
+        Array.set(array, i, list.get(i));
+      }
+      return array;
+    }
+    // But for Object[] can use ArrayList.toArray
+    else {
+      @SuppressWarnings("unchecked")
+      E[] array = (E[]) Array.newInstance(componentType, size);
+      return list.toArray(array);
+    }
+  }
+
+  @Override
+  public void write(JsonWriter out, Object array) throws IOException {
+    if (array == null) {
+      out.nullValue();
+      return;
+    }
+
+    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);
+    }
+    out.endArray();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java b/gson/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..1286d9beea3fa35d3ee927ebaf4051ecfaf82a41
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.ObjectConstructor;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+/** Adapt a homogeneous collection of objects. */
+public final class CollectionTypeAdapterFactory implements TypeAdapterFactory {
+  private final ConstructorConstructor constructorConstructor;
+
+  public CollectionTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
+    this.constructorConstructor = constructorConstructor;
+  }
+
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+    Type type = typeToken.getType();
+
+    Class<? super T> rawType = typeToken.getRawType();
+    if (!Collection.class.isAssignableFrom(rawType)) {
+      return null;
+    }
+
+    Type elementType = $Gson$Types.getCollectionElementType(type, rawType);
+    TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
+    ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);
+
+    @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter
+    TypeAdapter<T> result = new Adapter(gson, elementType, elementTypeAdapter, constructor);
+    return result;
+  }
+
+  private static final class Adapter<E> extends TypeAdapter<Collection<E>> {
+    private final TypeAdapter<E> elementTypeAdapter;
+    private final ObjectConstructor<? extends Collection<E>> constructor;
+
+    public Adapter(
+        Gson context,
+        Type elementType,
+        TypeAdapter<E> elementTypeAdapter,
+        ObjectConstructor<? extends Collection<E>> constructor) {
+      this.elementTypeAdapter =
+          new TypeAdapterRuntimeTypeWrapper<>(context, elementTypeAdapter, elementType);
+      this.constructor = constructor;
+    }
+
+    @Override
+    public Collection<E> read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+
+      Collection<E> collection = constructor.construct();
+      in.beginArray();
+      while (in.hasNext()) {
+        E instance = elementTypeAdapter.read(in);
+        collection.add(instance);
+      }
+      in.endArray();
+      return collection;
+    }
+
+    @Override
+    public void write(JsonWriter out, Collection<E> collection) throws IOException {
+      if (collection == null) {
+        out.nullValue();
+        return;
+      }
+
+      out.beginArray();
+      for (E element : collection) {
+        elementTypeAdapter.write(out, element);
+      }
+      out.endArray();
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2061c112d67900b3905a7f1876c23571d4020936
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java
@@ -0,0 +1,218 @@
+/*
+ * 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.internal.bind;
+
+import com.google.gson.Gson;
+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.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.TimeZone;
+
+/**
+ * This type adapter supports subclasses of date by defining a {@link
+ * DefaultDateTypeAdapter.DateType} and then using its {@code createAdapterFactory} methods.
+ *
+ * <p><b>Important:</b> Instances of this class (or rather the {@link SimpleDateFormat} they use)
+ * capture the current default {@link Locale} and {@link TimeZone} when they are created. Therefore
+ * avoid storing factories obtained from {@link DateType} in {@code static} fields, since they only
+ * create a single adapter instance and its behavior would then depend on when Gson classes are
+ * loaded first, and which default {@code Locale} and {@code TimeZone} was used at that point.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public final class DefaultDateTypeAdapter<T extends Date> extends TypeAdapter<T> {
+  private static final String SIMPLE_NAME = "DefaultDateTypeAdapter";
+
+  /** Factory for {@link Date} adapters which use {@link DateFormat#DEFAULT} as style. */
+  public static final TypeAdapterFactory DEFAULT_STYLE_FACTORY =
+      // Because SimpleDateFormat captures the default TimeZone when it was created, let the factory
+      // always create new DefaultDateTypeAdapter instances (which are then cached by the Gson
+      // instances) instead of having a single static DefaultDateTypeAdapter instance
+      // Otherwise the behavior would depend on when an application first loads Gson classes and
+      // which default TimeZone is set at that point, which would be quite brittle
+      new TypeAdapterFactory() {
+        @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+        @Override
+        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+          return typeToken.getRawType() == Date.class
+              ? (TypeAdapter<T>)
+                  new DefaultDateTypeAdapter<>(
+                      DateType.DATE, DateFormat.DEFAULT, DateFormat.DEFAULT)
+              : null;
+        }
+
+        @Override
+        public String toString() {
+          return "DefaultDateTypeAdapter#DEFAULT_STYLE_FACTORY";
+        }
+      };
+
+  public abstract static class DateType<T extends Date> {
+    public static final DateType<Date> DATE =
+        new DateType<Date>(Date.class) {
+          @Override
+          protected Date deserialize(Date date) {
+            return date;
+          }
+        };
+
+    private final Class<T> dateClass;
+
+    protected DateType(Class<T> dateClass) {
+      this.dateClass = dateClass;
+    }
+
+    protected abstract T deserialize(Date date);
+
+    private TypeAdapterFactory createFactory(DefaultDateTypeAdapter<T> adapter) {
+      return TypeAdapters.newFactory(dateClass, adapter);
+    }
+
+    public final TypeAdapterFactory createAdapterFactory(String datePattern) {
+      return createFactory(new DefaultDateTypeAdapter<>(this, datePattern));
+    }
+
+    public final TypeAdapterFactory createAdapterFactory(int style) {
+      return createFactory(new DefaultDateTypeAdapter<>(this, style));
+    }
+
+    public final TypeAdapterFactory createAdapterFactory(int dateStyle, int timeStyle) {
+      return createFactory(new DefaultDateTypeAdapter<>(this, dateStyle, timeStyle));
+    }
+
+    public final TypeAdapterFactory createDefaultsAdapterFactory() {
+      return createFactory(
+          new DefaultDateTypeAdapter<>(this, DateFormat.DEFAULT, DateFormat.DEFAULT));
+    }
+  }
+
+  private final DateType<T> dateType;
+
+  /**
+   * List of 1 or more different date formats used for de-serialization attempts. The first of them
+   * is used for serialization as well.
+   */
+  private final List<DateFormat> dateFormats = new ArrayList<>();
+
+  private DefaultDateTypeAdapter(DateType<T> dateType, String datePattern) {
+    this.dateType = Objects.requireNonNull(dateType);
+    dateFormats.add(new SimpleDateFormat(datePattern, Locale.US));
+    if (!Locale.getDefault().equals(Locale.US)) {
+      dateFormats.add(new SimpleDateFormat(datePattern));
+    }
+  }
+
+  private DefaultDateTypeAdapter(DateType<T> dateType, int style) {
+    this.dateType = Objects.requireNonNull(dateType);
+    dateFormats.add(DateFormat.getDateInstance(style, Locale.US));
+    if (!Locale.getDefault().equals(Locale.US)) {
+      dateFormats.add(DateFormat.getDateInstance(style));
+    }
+    if (JavaVersion.isJava9OrLater()) {
+      dateFormats.add(PreJava9DateFormatProvider.getUsDateFormat(style));
+    }
+  }
+
+  private DefaultDateTypeAdapter(DateType<T> dateType, int dateStyle, int timeStyle) {
+    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));
+    }
+    if (JavaVersion.isJava9OrLater()) {
+      dateFormats.add(PreJava9DateFormatProvider.getUsDateTimeFormat(dateStyle, timeStyle));
+    }
+  }
+
+  @Override
+  public void write(JsonWriter out, Date value) throws IOException {
+    if (value == null) {
+      out.nullValue();
+      return;
+    }
+
+    DateFormat dateFormat = dateFormats.get(0);
+    String dateFormatAsString;
+    // Needs to be synchronized since JDK DateFormat classes are not thread-safe
+    synchronized (dateFormats) {
+      dateFormatAsString = dateFormat.format(value);
+    }
+    out.value(dateFormatAsString);
+  }
+
+  @Override
+  public T read(JsonReader in) throws IOException {
+    if (in.peek() == JsonToken.NULL) {
+      in.nextNull();
+      return null;
+    }
+    Date date = deserializeToDate(in);
+    return dateType.deserialize(date);
+  }
+
+  private Date deserializeToDate(JsonReader in) throws IOException {
+    String s = in.nextString();
+    // Needs to be synchronized since JDK DateFormat classes are not thread-safe
+    synchronized (dateFormats) {
+      for (DateFormat dateFormat : dateFormats) {
+        TimeZone originalTimeZone = dateFormat.getTimeZone();
+        try {
+          return dateFormat.parse(s);
+        } catch (ParseException ignored) {
+          // OK: try the next format
+        } finally {
+          dateFormat.setTimeZone(originalTimeZone);
+        }
+      }
+    }
+
+    try {
+      return ISO8601Utils.parse(s, new ParsePosition(0));
+    } catch (ParseException e) {
+      throw new JsonSyntaxException(
+          "Failed parsing '" + s + "' as Date; at path " + in.getPreviousPath(), e);
+    }
+  }
+
+  @Override
+  public String toString() {
+    DateFormat defaultFormat = dateFormats.get(0);
+    if (defaultFormat instanceof SimpleDateFormat) {
+      return SIMPLE_NAME + '(' + ((SimpleDateFormat) defaultFormat).toPattern() + ')';
+    } else {
+      return SIMPLE_NAME + '(' + defaultFormat.getClass().getSimpleName() + ')';
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..822956742c208fde2e444a9553a88d608774852e
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonAdapterAnnotationTypeAdapterFactory.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2014 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.reflect.TypeToken;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Given a type T, looks for the annotation {@link JsonAdapter} and uses an instance of the
+ * specified class as the default type adapter.
+ *
+ * @since 2.3
+ */
+public final class JsonAdapterAnnotationTypeAdapterFactory implements TypeAdapterFactory {
+  private static class DummyTypeAdapterFactory implements TypeAdapterFactory {
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+      throw new AssertionError("Factory should not be used");
+    }
+  }
+
+  /** Factory used for {@link TreeTypeAdapter}s created for {@code @JsonAdapter} on a class. */
+  private static final TypeAdapterFactory TREE_TYPE_CLASS_DUMMY_FACTORY =
+      new DummyTypeAdapterFactory();
+
+  /** Factory used for {@link TreeTypeAdapter}s created for {@code @JsonAdapter} on a field. */
+  private static final TypeAdapterFactory TREE_TYPE_FIELD_DUMMY_FACTORY =
+      new DummyTypeAdapterFactory();
+
+  private final ConstructorConstructor constructorConstructor;
+
+  /**
+   * For a class, if it is annotated with {@code @JsonAdapter} and refers to a {@link
+   * TypeAdapterFactory}, stores the factory instance in case it has been requested already. Has to
+   * be a {@link ConcurrentMap} because {@link Gson} guarantees to be thread-safe.
+   */
+  // Note: In case these strong reference to TypeAdapterFactory instances are considered
+  // a memory leak in the future, could consider switching to WeakReference<TypeAdapterFactory>
+  private final ConcurrentMap<Class<?>, TypeAdapterFactory> adapterFactoryMap;
+
+  public JsonAdapterAnnotationTypeAdapterFactory(ConstructorConstructor constructorConstructor) {
+    this.constructorConstructor = constructorConstructor;
+    this.adapterFactoryMap = new ConcurrentHashMap<>();
+  }
+
+  // Separate helper method to make sure callers retrieve annotation in a consistent way
+  private static JsonAdapter getAnnotation(Class<?> rawType) {
+    return rawType.getAnnotation(JsonAdapter.class);
+  }
+
+  // this is not safe; requires that user has specified correct adapter class for @JsonAdapter
+  @SuppressWarnings("unchecked")
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> targetType) {
+    Class<? super T> rawType = targetType.getRawType();
+    JsonAdapter annotation = getAnnotation(rawType);
+    if (annotation == null) {
+      return null;
+    }
+    return (TypeAdapter<T>)
+        getTypeAdapter(constructorConstructor, gson, targetType, annotation, true);
+  }
+
+  // Separate helper method to make sure callers create adapter in a consistent way
+  private static Object createAdapter(
+      ConstructorConstructor constructorConstructor, Class<?> adapterClass) {
+    // TODO: The exception messages created by ConstructorConstructor are currently written in the
+    // context of deserialization and for example suggest usage of TypeAdapter, which would not work
+    // for @JsonAdapter usage
+    return constructorConstructor.get(TypeToken.get(adapterClass)).construct();
+  }
+
+  private TypeAdapterFactory putFactoryAndGetCurrent(Class<?> rawType, TypeAdapterFactory factory) {
+    // Uses putIfAbsent in case multiple threads concurrently create factory
+    TypeAdapterFactory existingFactory = adapterFactoryMap.putIfAbsent(rawType, factory);
+    return existingFactory != null ? existingFactory : factory;
+  }
+
+  TypeAdapter<?> getTypeAdapter(
+      ConstructorConstructor constructorConstructor,
+      Gson gson,
+      TypeToken<?> type,
+      JsonAdapter annotation,
+      boolean isClassAnnotation) {
+    Object instance = createAdapter(constructorConstructor, annotation.value());
+
+    TypeAdapter<?> typeAdapter;
+    boolean nullSafe = annotation.nullSafe();
+    if (instance instanceof TypeAdapter) {
+      typeAdapter = (TypeAdapter<?>) instance;
+    } else if (instance instanceof TypeAdapterFactory) {
+      TypeAdapterFactory factory = (TypeAdapterFactory) instance;
+
+      if (isClassAnnotation) {
+        factory = putFactoryAndGetCurrent(type.getRawType(), factory);
+      }
+
+      typeAdapter = factory.create(gson, type);
+    } else if (instance instanceof JsonSerializer || instance instanceof JsonDeserializer) {
+      JsonSerializer<?> serializer =
+          instance instanceof JsonSerializer ? (JsonSerializer<?>) instance : null;
+      JsonDeserializer<?> deserializer =
+          instance instanceof JsonDeserializer ? (JsonDeserializer<?>) instance : null;
+
+      // Uses dummy factory instances because TreeTypeAdapter needs a 'skipPast' factory for
+      // `Gson.getDelegateAdapter` call and has to differentiate there whether TreeTypeAdapter was
+      // created for @JsonAdapter on class or field
+      TypeAdapterFactory skipPast;
+      if (isClassAnnotation) {
+        skipPast = TREE_TYPE_CLASS_DUMMY_FACTORY;
+      } else {
+        skipPast = TREE_TYPE_FIELD_DUMMY_FACTORY;
+      }
+      @SuppressWarnings({"unchecked", "rawtypes"})
+      TypeAdapter<?> tempAdapter =
+          new TreeTypeAdapter(serializer, deserializer, gson, type, skipPast, nullSafe);
+      typeAdapter = tempAdapter;
+
+      // TreeTypeAdapter handles nullSafe; don't additionally call `nullSafe()`
+      nullSafe = false;
+    } else {
+      throw new IllegalArgumentException(
+          "Invalid attempt to bind an instance of "
+              + instance.getClass().getName()
+              + " as a @JsonAdapter for "
+              + type.toString()
+              + ". @JsonAdapter value must be a TypeAdapter, TypeAdapterFactory,"
+              + " JsonSerializer or JsonDeserializer.");
+    }
+
+    if (typeAdapter != null && nullSafe) {
+      typeAdapter = typeAdapter.nullSafe();
+    }
+
+    return typeAdapter;
+  }
+
+  /**
+   * Returns whether {@code factory} is a type adapter factory created for {@code @JsonAdapter}
+   * placed on {@code type}.
+   */
+  public boolean isClassJsonAdapterFactory(TypeToken<?> type, TypeAdapterFactory factory) {
+    Objects.requireNonNull(type);
+    Objects.requireNonNull(factory);
+
+    if (factory == TREE_TYPE_CLASS_DUMMY_FACTORY) {
+      return true;
+    }
+
+    // Using raw type to match behavior of `create(Gson, TypeToken<T>)` above
+    Class<?> rawType = type.getRawType();
+
+    TypeAdapterFactory existingFactory = adapterFactoryMap.get(rawType);
+    if (existingFactory != null) {
+      // Checks for reference equality, like it is done by `Gson.getDelegateAdapter`
+      return existingFactory == factory;
+    }
+
+    // If no factory has been created for the type yet check manually for a @JsonAdapter annotation
+    // which specifies a TypeAdapterFactory
+    // Otherwise behavior would not be consistent, depending on whether or not adapter had been
+    // requested before call to `isClassJsonAdapterFactory` was made
+    JsonAdapter annotation = getAnnotation(rawType);
+    if (annotation == null) {
+      return false;
+    }
+
+    Class<?> adapterClass = annotation.value();
+    if (!TypeAdapterFactory.class.isAssignableFrom(adapterClass)) {
+      return false;
+    }
+
+    Object adapter = createAdapter(constructorConstructor, adapterClass);
+    TypeAdapterFactory newFactory = (TypeAdapterFactory) adapter;
+
+    return putFactoryAndGetCurrent(rawType, newFactory) == factory;
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc23c2d53df1bb29ff71a10df076782aeb675d9d
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * This reader walks the elements of a JsonElement as if it was coming from a character stream.
+ *
+ * @author Jesse Wilson
+ */
+public final class JsonTreeReader extends JsonReader {
+  private static final Reader UNREADABLE_READER =
+      new Reader() {
+        @Override
+        public int read(char[] buffer, int offset, int count) {
+          throw new AssertionError();
+        }
+
+        @Override
+        public void close() {
+          throw new AssertionError();
+        }
+      };
+  private static final Object SENTINEL_CLOSED = new Object();
+
+  /*
+   * The nesting stack. Using a manual array rather than an ArrayList saves 20%.
+   */
+  private Object[] stack = new Object[32];
+  private int stackSize = 0;
+
+  /*
+   * The path members. It corresponds directly to stack: At indices where the
+   * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
+   * pathNames contains the name at this scope. Where it contains an array
+   * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
+   * that array. Otherwise the value is undefined, and we take advantage of that
+   * by incrementing pathIndices when doing so isn't useful.
+   */
+  private String[] pathNames = new String[32];
+  private int[] pathIndices = new int[32];
+
+  public JsonTreeReader(JsonElement element) {
+    super(UNREADABLE_READER);
+    push(element);
+  }
+
+  @Override
+  public void beginArray() throws IOException {
+    expect(JsonToken.BEGIN_ARRAY);
+    JsonArray array = (JsonArray) peekStack();
+    push(array.iterator());
+    pathIndices[stackSize - 1] = 0;
+  }
+
+  @Override
+  public void endArray() throws IOException {
+    expect(JsonToken.END_ARRAY);
+    popStack(); // empty iterator
+    popStack(); // array
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+  }
+
+  @Override
+  public void beginObject() throws IOException {
+    expect(JsonToken.BEGIN_OBJECT);
+    JsonObject object = (JsonObject) peekStack();
+    push(object.entrySet().iterator());
+  }
+
+  @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) {
+      pathIndices[stackSize - 1]++;
+    }
+  }
+
+  @Override
+  public boolean hasNext() throws IOException {
+    JsonToken token = peek();
+    return token != JsonToken.END_OBJECT
+        && token != JsonToken.END_ARRAY
+        && token != JsonToken.END_DOCUMENT;
+  }
+
+  @Override
+  public JsonToken peek() throws IOException {
+    if (stackSize == 0) {
+      return JsonToken.END_DOCUMENT;
+    }
+
+    Object o = peekStack();
+    if (o instanceof Iterator) {
+      boolean isObject = stack[stackSize - 2] instanceof JsonObject;
+      Iterator<?> iterator = (Iterator<?>) o;
+      if (iterator.hasNext()) {
+        if (isObject) {
+          return JsonToken.NAME;
+        } else {
+          push(iterator.next());
+          return peek();
+        }
+      } else {
+        return isObject ? JsonToken.END_OBJECT : JsonToken.END_ARRAY;
+      }
+    } else if (o instanceof JsonObject) {
+      return JsonToken.BEGIN_OBJECT;
+    } else if (o instanceof JsonArray) {
+      return JsonToken.BEGIN_ARRAY;
+    } else if (o instanceof JsonPrimitive) {
+      JsonPrimitive primitive = (JsonPrimitive) o;
+      if (primitive.isString()) {
+        return JsonToken.STRING;
+      } else if (primitive.isBoolean()) {
+        return JsonToken.BOOLEAN;
+      } else if (primitive.isNumber()) {
+        return JsonToken.NUMBER;
+      } else {
+        throw new AssertionError();
+      }
+    } else if (o instanceof JsonNull) {
+      return JsonToken.NULL;
+    } else if (o == SENTINEL_CLOSED) {
+      throw new IllegalStateException("JsonReader is closed");
+    } else {
+      throw new MalformedJsonException(
+          "Custom JsonElement subclass " + o.getClass().getName() + " is not supported");
+    }
+  }
+
+  private Object peekStack() {
+    return stack[stackSize - 1];
+  }
+
+  @CanIgnoreReturnValue
+  private Object popStack() {
+    Object result = stack[--stackSize];
+    stack[stackSize] = null;
+    return result;
+  }
+
+  private void expect(JsonToken expected) throws IOException {
+    if (peek() != expected) {
+      throw new IllegalStateException(
+          "Expected " + expected + " but was " + peek() + locationString());
+    }
+  }
+
+  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] = 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) {
+      throw new IllegalStateException(
+          "Expected " + JsonToken.STRING + " but was " + token + locationString());
+    }
+    String result = ((JsonPrimitive) popStack()).getAsString();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+    return result;
+  }
+
+  @Override
+  public boolean nextBoolean() throws IOException {
+    expect(JsonToken.BOOLEAN);
+    boolean result = ((JsonPrimitive) popStack()).getAsBoolean();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+    return result;
+  }
+
+  @Override
+  public void nextNull() throws IOException {
+    expect(JsonToken.NULL);
+    popStack();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+  }
+
+  @Override
+  public double nextDouble() throws IOException {
+    JsonToken token = peek();
+    if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
+      throw new IllegalStateException(
+          "Expected " + JsonToken.NUMBER + " but was " + token + locationString());
+    }
+    double result = ((JsonPrimitive) peekStack()).getAsDouble();
+    if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result))) {
+      throw new MalformedJsonException("JSON forbids NaN and infinities: " + result);
+    }
+    popStack();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+    return result;
+  }
+
+  @Override
+  public long nextLong() throws IOException {
+    JsonToken token = peek();
+    if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
+      throw new IllegalStateException(
+          "Expected " + JsonToken.NUMBER + " but was " + token + locationString());
+    }
+    long result = ((JsonPrimitive) peekStack()).getAsLong();
+    popStack();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+    return result;
+  }
+
+  @Override
+  public int nextInt() throws IOException {
+    JsonToken token = peek();
+    if (token != JsonToken.NUMBER && token != JsonToken.STRING) {
+      throw new IllegalStateException(
+          "Expected " + JsonToken.NUMBER + " but was " + token + locationString());
+    }
+    int result = ((JsonPrimitive) peekStack()).getAsInt();
+    popStack();
+    if (stackSize > 0) {
+      pathIndices[stackSize - 1]++;
+    }
+    return result;
+  }
+
+  JsonElement nextJsonElement() throws IOException {
+    final JsonToken peeked = peek();
+    if (peeked == JsonToken.NAME
+        || peeked == JsonToken.END_ARRAY
+        || peeked == JsonToken.END_OBJECT
+        || peeked == JsonToken.END_DOCUMENT) {
+      throw new IllegalStateException("Unexpected " + peeked + " when reading a JsonElement.");
+    }
+    final JsonElement element = (JsonElement) peekStack();
+    skipValue();
+    return element;
+  }
+
+  @Override
+  public void close() throws IOException {
+    stack = new Object[] {SENTINEL_CLOSED};
+    stackSize = 1;
+  }
+
+  @Override
+  public void skipValue() throws IOException {
+    JsonToken peeked = peek();
+    switch (peeked) {
+      case NAME:
+        @SuppressWarnings("unused")
+        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;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + locationString();
+  }
+
+  public void promoteNameToValue() throws IOException {
+    expect(JsonToken.NAME);
+    Iterator<?> i = (Iterator<?>) peekStack();
+    Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
+    push(entry.getValue());
+    push(new JsonPrimitive((String) entry.getKey()));
+  }
+
+  private void push(Object newTop) {
+    if (stackSize == stack.length) {
+      int newLength = stackSize * 2;
+      stack = Arrays.copyOf(stack, newLength);
+      pathIndices = Arrays.copyOf(pathIndices, newLength);
+      pathNames = Arrays.copyOf(pathNames, newLength);
+    }
+    stack[stackSize++] = newTop;
+  }
+
+  private String getPath(boolean usePreviousPath) {
+    StringBuilder result = new StringBuilder().append('$');
+    for (int i = 0; i < stackSize; i++) {
+      if (stack[i] instanceof JsonArray) {
+        if (++i < stackSize && stack[i] instanceof Iterator) {
+          int pathIndex = pathIndices[i];
+          // If index is last path element it points to next array element; have to decrement
+          // `- 1` covers case where iterator for next element is on stack
+          // `- 2` covers case where peek() already pushed next element onto stack
+          if (usePreviousPath && pathIndex > 0 && (i == stackSize - 1 || i == stackSize - 2)) {
+            pathIndex--;
+          }
+          result.append('[').append(pathIndex).append(']');
+        }
+      } else if (stack[i] instanceof JsonObject) {
+        if (++i < stackSize && stack[i] instanceof Iterator) {
+          result.append('.');
+          if (pathNames[i] != null) {
+            result.append(pathNames[i]);
+          }
+        }
+      }
+    }
+    return result.toString();
+  }
+
+  @Override
+  public String getPath() {
+    return getPath(false);
+  }
+
+  @Override
+  public String getPreviousPath() {
+    return getPath(true);
+  }
+
+  private String locationString() {
+    return " at path " + getPath();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..fda2cf131ca9abf089ecbe582b519f166584595a
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** This writer creates a JsonElement. */
+public final class JsonTreeWriter extends JsonWriter {
+  private static final Writer UNWRITABLE_WRITER =
+      new Writer() {
+        @Override
+        public void write(char[] buffer, int offset, int counter) {
+          throw new AssertionError();
+        }
+
+        @Override
+        public void flush() {
+          throw new AssertionError();
+        }
+
+        @Override
+        public void close() {
+          throw new AssertionError();
+        }
+      };
+
+  /** Added to the top of the stack when this writer is closed to cause following ops to fail. */
+  private static final JsonPrimitive SENTINEL_CLOSED = new JsonPrimitive("closed");
+
+  /** The JsonElements and JsonArrays under modification, outermost to innermost. */
+  private final List<JsonElement> stack = new ArrayList<>();
+
+  /** The name for the next JSON object value. If non-null, the top of the stack is a JsonObject. */
+  private String pendingName;
+
+  /** the JSON element constructed by this writer. */
+  private JsonElement product = JsonNull.INSTANCE; // TODO: is this really what we want?;
+
+  public JsonTreeWriter() {
+    super(UNWRITABLE_WRITER);
+  }
+
+  /** Returns the top level object produced by this writer. */
+  public JsonElement get() {
+    if (!stack.isEmpty()) {
+      throw new IllegalStateException("Expected one JSON element but was " + stack);
+    }
+    return product;
+  }
+
+  private JsonElement peek() {
+    return stack.get(stack.size() - 1);
+  }
+
+  private void put(JsonElement value) {
+    if (pendingName != null) {
+      if (!value.isJsonNull() || getSerializeNulls()) {
+        JsonObject object = (JsonObject) peek();
+        object.add(pendingName, value);
+      }
+      pendingName = null;
+    } else if (stack.isEmpty()) {
+      product = value;
+    } else {
+      JsonElement element = peek();
+      if (element instanceof JsonArray) {
+        ((JsonArray) element).add(value);
+      } else {
+        throw new IllegalStateException();
+      }
+    }
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter beginArray() throws IOException {
+    JsonArray array = new JsonArray();
+    put(array);
+    stack.add(array);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter endArray() throws IOException {
+    if (stack.isEmpty() || pendingName != null) {
+      throw new IllegalStateException();
+    }
+    JsonElement element = peek();
+    if (element instanceof JsonArray) {
+      stack.remove(stack.size() - 1);
+      return this;
+    }
+    throw new IllegalStateException();
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter beginObject() throws IOException {
+    JsonObject object = new JsonObject();
+    put(object);
+    stack.add(object);
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter endObject() throws IOException {
+    if (stack.isEmpty() || pendingName != null) {
+      throw new IllegalStateException();
+    }
+    JsonElement element = peek();
+    if (element instanceof JsonObject) {
+      stack.remove(stack.size() - 1);
+      return this;
+    }
+    throw new IllegalStateException();
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter name(String name) throws IOException {
+    Objects.requireNonNull(name, "name == null");
+    if (stack.isEmpty() || pendingName != null) {
+      throw new IllegalStateException("Did not expect a name");
+    }
+    JsonElement element = peek();
+    if (element instanceof JsonObject) {
+      pendingName = name;
+      return this;
+    }
+    throw new IllegalStateException("Please begin an object before writing a name.");
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(String value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(boolean value) throws IOException {
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(Boolean value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(float value) throws IOException {
+    if (!isLenient() && (Float.isNaN(value) || Float.isInfinite(value))) {
+      throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value);
+    }
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(double value) throws IOException {
+    if (!isLenient() && (Double.isNaN(value) || Double.isInfinite(value))) {
+      throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value);
+    }
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(long value) throws IOException {
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter value(Number value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+
+    if (!isLenient()) {
+      double d = value.doubleValue();
+      if (Double.isNaN(d) || Double.isInfinite(d)) {
+        throw new IllegalArgumentException("JSON forbids NaN and infinities: " + value);
+      }
+    }
+
+    put(new JsonPrimitive(value));
+    return this;
+  }
+
+  @CanIgnoreReturnValue
+  @Override
+  public JsonWriter nullValue() throws IOException {
+    put(JsonNull.INSTANCE);
+    return this;
+  }
+
+  @Override
+  public JsonWriter jsonValue(String value) throws IOException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void flush() throws IOException {}
+
+  @Override
+  public void close() throws IOException {
+    if (!stack.isEmpty()) {
+      throw new IOException("Incomplete document");
+    }
+    stack.add(SENTINEL_CLOSED);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..56de0ea3a86c37a643591b4c140ca9dde3a02c8f
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.JsonReaderInternalAccess;
+import com.google.gson.internal.ObjectConstructor;
+import com.google.gson.internal.Streams;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Adapts maps to either JSON objects or JSON arrays.
+ *
+ * <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 example, consider a map whose keys are points on a grid.
+ * The default JSON form encodes reasonably:
+ *
+ * <pre>{@code
+ * Map<Point, String> original = new LinkedHashMap<>();
+ * original.put(new Point(5, 6), "a");
+ * original.put(new Point(8, 8), "b");
+ * System.out.println(gson.toJson(original, type));
+ * }</pre>
+ *
+ * The above code prints this JSON object:
+ *
+ * <pre>{@code
+ * {
+ *   "(5,6)": "a",
+ *   "(8,8)": "b"
+ * }
+ * }</pre>
+ *
+ * But GSON is unable to deserialize this value because the JSON string name is just the {@link
+ * Object#toString() toString()} of the map key. Attempting to convert the above JSON to an object
+ * fails with a parse exception:
+ *
+ * <pre>com.google.gson.JsonParseException: Expecting object found: "(5,6)"
+ *   at com.google.gson.JsonObjectDeserializationVisitor.visitFieldUsingCustomHandler
+ *   at com.google.gson.ObjectNavigator.navigateClassFields
+ *   ...</pre>
+ *
+ * <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. This approach is more flexible because any type can
+ * be used as the map's key; not just strings. But it's also less portable because the receiver of
+ * such JSON must be aware of the map entry convention.
+ *
+ * <p>Register this adapter when you are creating your GSON instance.
+ *
+ * <pre>{@code
+ * Gson gson = new GsonBuilder()
+ *   .registerTypeAdapter(Map.class, new MapAsArrayTypeAdapter())
+ *   .create();
+ * }</pre>
+ *
+ * This will change the structure of the JSON emitted by the code above. Now we get an array. In
+ * this case the arrays elements are map entries:
+ *
+ * <pre>{@code
+ * [
+ *   [
+ *     {
+ *       "x": 5,
+ *       "y": 6
+ *     },
+ *     "a",
+ *   ],
+ *   [
+ *     {
+ *       "x": 8,
+ *       "y": 8
+ *     },
+ *     "b"
+ *   ]
+ * ]
+ * }</pre>
+ *
+ * This format will serialize and deserialize just fine as long as this adapter is registered.
+ */
+public final class MapTypeAdapterFactory implements TypeAdapterFactory {
+  private final ConstructorConstructor constructorConstructor;
+  final boolean complexMapKeySerialization;
+
+  public MapTypeAdapterFactory(
+      ConstructorConstructor constructorConstructor, boolean complexMapKeySerialization) {
+    this.constructorConstructor = constructorConstructor;
+    this.complexMapKeySerialization = complexMapKeySerialization;
+  }
+
+  @Override
+  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+    Type type = typeToken.getType();
+
+    Class<? super T> rawType = typeToken.getRawType();
+    if (!Map.class.isAssignableFrom(rawType)) {
+      return null;
+    }
+
+    Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawType);
+    TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]);
+    TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1]));
+    ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    // we don't define a type parameter for the key or value types
+    TypeAdapter<T> result =
+        new Adapter(
+            gson, keyAndValueTypes[0], keyAdapter, keyAndValueTypes[1], valueAdapter, constructor);
+    return result;
+  }
+
+  /** Returns a type adapter that writes the value as a string. */
+  private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) {
+    return (keyType == boolean.class || keyType == Boolean.class)
+        ? TypeAdapters.BOOLEAN_AS_STRING
+        : context.getAdapter(TypeToken.get(keyType));
+  }
+
+  private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> {
+    private final TypeAdapter<K> keyTypeAdapter;
+    private final TypeAdapter<V> valueTypeAdapter;
+    private final ObjectConstructor<? extends Map<K, V>> constructor;
+
+    public Adapter(
+        Gson context,
+        Type keyType,
+        TypeAdapter<K> keyTypeAdapter,
+        Type valueType,
+        TypeAdapter<V> valueTypeAdapter,
+        ObjectConstructor<? extends Map<K, V>> constructor) {
+      this.keyTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, keyTypeAdapter, keyType);
+      this.valueTypeAdapter =
+          new TypeAdapterRuntimeTypeWrapper<>(context, valueTypeAdapter, valueType);
+      this.constructor = constructor;
+    }
+
+    @Override
+    public Map<K, V> read(JsonReader in) throws IOException {
+      JsonToken peek = in.peek();
+      if (peek == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+
+      Map<K, V> map = constructor.construct();
+
+      if (peek == JsonToken.BEGIN_ARRAY) {
+        in.beginArray();
+        while (in.hasNext()) {
+          in.beginArray(); // entry array
+          K key = keyTypeAdapter.read(in);
+          V value = valueTypeAdapter.read(in);
+          V replaced = map.put(key, value);
+          if (replaced != null) {
+            throw new JsonSyntaxException("duplicate key: " + key);
+          }
+          in.endArray();
+        }
+        in.endArray();
+      } else {
+        in.beginObject();
+        while (in.hasNext()) {
+          JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in);
+          K key = keyTypeAdapter.read(in);
+          V value = valueTypeAdapter.read(in);
+          V replaced = map.put(key, value);
+          if (replaced != null) {
+            throw new JsonSyntaxException("duplicate key: " + key);
+          }
+        }
+        in.endObject();
+      }
+      return map;
+    }
+
+    @Override
+    public void write(JsonWriter out, Map<K, V> map) throws IOException {
+      if (map == null) {
+        out.nullValue();
+        return;
+      }
+
+      if (!complexMapKeySerialization) {
+        out.beginObject();
+        for (Map.Entry<K, V> entry : map.entrySet()) {
+          out.name(String.valueOf(entry.getKey()));
+          valueTypeAdapter.write(out, entry.getValue());
+        }
+        out.endObject();
+        return;
+      }
+
+      boolean hasComplexKeys = false;
+      List<JsonElement> keys = new ArrayList<>(map.size());
+
+      List<V> values = new ArrayList<>(map.size());
+      for (Map.Entry<K, V> entry : map.entrySet()) {
+        JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey());
+        keys.add(keyElement);
+        values.add(entry.getValue());
+        hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject();
+      }
+
+      if (hasComplexKeys) {
+        out.beginArray();
+        for (int i = 0, size = keys.size(); i < size; i++) {
+          out.beginArray(); // entry array
+          Streams.write(keys.get(i), out);
+          valueTypeAdapter.write(out, values.get(i));
+          out.endArray();
+        }
+        out.endArray();
+      } else {
+        out.beginObject();
+        for (int i = 0, size = keys.size(); i < size; i++) {
+          JsonElement keyElement = keys.get(i);
+          out.name(keyToString(keyElement));
+          valueTypeAdapter.write(out, values.get(i));
+        }
+        out.endObject();
+      }
+    }
+
+    private String keyToString(JsonElement keyElement) {
+      if (keyElement.isJsonPrimitive()) {
+        JsonPrimitive primitive = keyElement.getAsJsonPrimitive();
+        if (primitive.isNumber()) {
+          return String.valueOf(primitive.getAsNumber());
+        } else if (primitive.isBoolean()) {
+          return Boolean.toString(primitive.getAsBoolean());
+        } else if (primitive.isString()) {
+          return primitive.getAsString();
+        } else {
+          throw new AssertionError();
+        }
+      } else if (keyElement.isJsonNull()) {
+        return "null";
+      } else {
+        throw new AssertionError();
+      }
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..853f2a5fc0b55b17b403504788c1e1df1fc6f97a
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/NumberTypeAdapter.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.ToNumberStrategy;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+/** Type adapter for {@link Number}. */
+public final class NumberTypeAdapter extends TypeAdapter<Number> {
+  /** Gson default factory using {@link ToNumberPolicy#LAZILY_PARSED_NUMBER}. */
+  private static final TypeAdapterFactory LAZILY_PARSED_NUMBER_FACTORY =
+      newFactory(ToNumberPolicy.LAZILY_PARSED_NUMBER);
+
+  private final ToNumberStrategy toNumberStrategy;
+
+  private NumberTypeAdapter(ToNumberStrategy toNumberStrategy) {
+    this.toNumberStrategy = toNumberStrategy;
+  }
+
+  private static TypeAdapterFactory newFactory(ToNumberStrategy toNumberStrategy) {
+    final NumberTypeAdapter adapter = new NumberTypeAdapter(toNumberStrategy);
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        return type.getRawType() == Number.class ? (TypeAdapter<T>) adapter : null;
+      }
+    };
+  }
+
+  public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) {
+    if (toNumberStrategy == ToNumberPolicy.LAZILY_PARSED_NUMBER) {
+      return LAZILY_PARSED_NUMBER_FACTORY;
+    } else {
+      return newFactory(toNumberStrategy);
+    }
+  }
+
+  @Override
+  public Number read(JsonReader in) throws IOException {
+    JsonToken jsonToken = in.peek();
+    switch (jsonToken) {
+      case NULL:
+        in.nextNull();
+        return null;
+      case NUMBER:
+      case STRING:
+        return toNumberStrategy.readNumber(in);
+      default:
+        throw new JsonSyntaxException(
+            "Expecting number, got: " + jsonToken + "; at path " + in.getPath());
+    }
+  }
+
+  @Override
+  public void write(JsonWriter out, Number value) throws IOException {
+    out.value(value);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..20d16062915a6bd19f44e32e6a9c039555143109
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.ToNumberStrategy;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.LinkedTreeMap;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Adapts types whose static type is only 'Object'. Uses getClass() on serialization and a
+ * primitive/Map/List on deserialization.
+ */
+public final class ObjectTypeAdapter extends TypeAdapter<Object> {
+  /** Gson default factory using {@link ToNumberPolicy#DOUBLE}. */
+  private static final TypeAdapterFactory DOUBLE_FACTORY = newFactory(ToNumberPolicy.DOUBLE);
+
+  private final Gson gson;
+  private final ToNumberStrategy toNumberStrategy;
+
+  private ObjectTypeAdapter(Gson gson, ToNumberStrategy toNumberStrategy) {
+    this.gson = gson;
+    this.toNumberStrategy = toNumberStrategy;
+  }
+
+  private static TypeAdapterFactory newFactory(final ToNumberStrategy toNumberStrategy) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        if (type.getRawType() == Object.class) {
+          return (TypeAdapter<T>) new ObjectTypeAdapter(gson, toNumberStrategy);
+        }
+        return null;
+      }
+    };
+  }
+
+  public static TypeAdapterFactory getFactory(ToNumberStrategy toNumberStrategy) {
+    if (toNumberStrategy == ToNumberPolicy.DOUBLE) {
+      return DOUBLE_FACTORY;
+    } else {
+      return newFactory(toNumberStrategy);
+    }
+  }
+
+  /**
+   * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next element
+   * is neither of those.
+   */
+  private Object tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
+    switch (peeked) {
+      case BEGIN_ARRAY:
+        in.beginArray();
+        return new ArrayList<>();
+      case BEGIN_OBJECT:
+        in.beginObject();
+        return new LinkedTreeMap<>();
+      default:
+        return null;
+    }
+  }
+
+  /** Reads an {@code Object} which cannot have any nested elements */
+  private Object readTerminal(JsonReader in, JsonToken peeked) throws IOException {
+    switch (peeked) {
+      case STRING:
+        return in.nextString();
+      case NUMBER:
+        return toNumberStrategy.readNumber(in);
+      case BOOLEAN:
+        return in.nextBoolean();
+      case NULL:
+        in.nextNull();
+        return null;
+      default:
+        // When read(JsonReader) is called with JsonReader in invalid state
+        throw new IllegalStateException("Unexpected token: " + peeked);
+    }
+  }
+
+  @Override
+  public Object read(JsonReader in) throws IOException {
+    // Either List or Map
+    Object current;
+    JsonToken peeked = in.peek();
+
+    current = tryBeginNesting(in, peeked);
+    if (current == null) {
+      return readTerminal(in, peeked);
+    }
+
+    Deque<Object> stack = new ArrayDeque<>();
+
+    while (true) {
+      while (in.hasNext()) {
+        String name = null;
+        // Name is only used for JSON object members
+        if (current instanceof Map) {
+          name = in.nextName();
+        }
+
+        peeked = in.peek();
+        Object value = tryBeginNesting(in, peeked);
+        boolean isNesting = value != null;
+
+        if (value == null) {
+          value = readTerminal(in, peeked);
+        }
+
+        if (current instanceof List) {
+          @SuppressWarnings("unchecked")
+          List<Object> list = (List<Object>) current;
+          list.add(value);
+        } else {
+          @SuppressWarnings("unchecked")
+          Map<String, Object> map = (Map<String, Object>) current;
+          map.put(name, value);
+        }
+
+        if (isNesting) {
+          stack.addLast(current);
+          current = value;
+        }
+      }
+
+      // End current element
+      if (current instanceof List) {
+        in.endArray();
+      } else {
+        in.endObject();
+      }
+
+      if (stack.isEmpty()) {
+        return current;
+      } else {
+        // Continue with enclosing element
+        current = stack.removeLast();
+      }
+    }
+  }
+
+  @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();
+      out.endObject();
+      return;
+    }
+
+    typeAdapter.write(out, value);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..79e93bcc77093a5a7e08a585db5eaff9032f5002
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.FieldNamingStrategy;
+import com.google.gson.Gson;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.ReflectionAccessFilter;
+import com.google.gson.ReflectionAccessFilter.FilterResult;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.internal.Excluder;
+import com.google.gson.internal.ObjectConstructor;
+import com.google.gson.internal.Primitives;
+import com.google.gson.internal.ReflectionAccessFilterHelper;
+import com.google.gson.internal.TroubleshootingGuide;
+import com.google.gson.internal.reflect.ReflectionHelper;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.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;
+
+/** Type adapter that reflects over the fields and methods of a class. */
+public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
+  private final ConstructorConstructor constructorConstructor;
+  private final FieldNamingStrategy fieldNamingPolicy;
+  private final Excluder excluder;
+  private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
+  private final List<ReflectionAccessFilter> reflectionFilters;
+
+  public ReflectiveTypeAdapterFactory(
+      ConstructorConstructor constructorConstructor,
+      FieldNamingStrategy fieldNamingPolicy,
+      Excluder excluder,
+      JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory,
+      List<ReflectionAccessFilter> reflectionFilters) {
+    this.constructorConstructor = constructorConstructor;
+    this.fieldNamingPolicy = fieldNamingPolicy;
+    this.excluder = excluder;
+    this.jsonAdapterFactory = jsonAdapterFactory;
+    this.reflectionFilters = reflectionFilters;
+  }
+
+  private boolean includeField(Field f, boolean serialize) {
+    return !excluder.excludeField(f, serialize);
+  }
+
+  /** first element holds the default name */
+  @SuppressWarnings("MixedMutabilityReturnType")
+  private List<String> getFieldNames(Field f) {
+    SerializedName annotation = f.getAnnotation(SerializedName.class);
+    if (annotation == null) {
+      String name = fieldNamingPolicy.translateName(f);
+      return Collections.singletonList(name);
+    }
+
+    String serializedName = annotation.value();
+    String[] alternates = annotation.alternate();
+    if (alternates.length == 0) {
+      return Collections.singletonList(serializedName);
+    }
+
+    List<String> fieldNames = new ArrayList<>(alternates.length + 1);
+    fieldNames.add(serializedName);
+    Collections.addAll(fieldNames, alternates);
+    return fieldNames;
+  }
+
+  @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!
+    }
+
+    // Don't allow using reflection on anonymous and local classes because synthetic fields for
+    // captured enclosing values make this unreliable
+    if (ReflectionHelper.isAnonymousOrNonStaticLocal(raw)) {
+      // This adapter just serializes and deserializes null, ignoring the actual values
+      // This is done for backward compatibility; troubleshooting-wise it might be better to throw
+      // exceptions
+      return new TypeAdapter<T>() {
+        @Override
+        public T read(JsonReader in) throws IOException {
+          in.skipValue();
+          return null;
+        }
+
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+          out.nullValue();
+        }
+
+        @Override
+        public String toString() {
+          return "AnonymousOrNonStaticLocalClassAdapter";
+        }
+      };
+    }
+
+    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.");
+    }
+    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 FieldReflectionAdapter<>(
+        constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
+  }
+
+  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 BoundField createBoundField(
+      final Gson context,
+      final Field field,
+      final Method accessor,
+      final String serializedName,
+      final TypeToken<?> fieldType,
+      final boolean serialize,
+      final boolean blockInaccessible) {
+
+    final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
+
+    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, false);
+    }
+    final boolean jsonAdapterPresent = mapped != null;
+    if (mapped == null) {
+      mapped = context.getAdapter(fieldType);
+    }
+
+    @SuppressWarnings("unchecked")
+    final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
+    final TypeAdapter<Object> writeTypeAdapter;
+    if (serialize) {
+      writeTypeAdapter =
+          jsonAdapterPresent
+              ? typeAdapter
+              : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
+    } else {
+      // Will never actually be used, but we set it to avoid confusing nullness-analysis tools
+      writeTypeAdapter = typeAdapter;
+    }
+    return new BoundField(serializedName, field) {
+      @Override
+      void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException {
+        if (blockInaccessible) {
+          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;
+        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(serializedName);
+        writeTypeAdapter.write(writer, fieldValue);
+      }
+
+      @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(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(target, fieldValue);
+        }
+      }
+    };
+  }
+
+  private static class FieldsData {
+    public static final FieldsData EMPTY =
+        new FieldsData(
+            Collections.<String, BoundField>emptyMap(), Collections.<BoundField>emptyList());
+
+    /** Maps from JSON member name to field */
+    public final Map<String, BoundField> deserializedFields;
+
+    public final List<BoundField> serializedFields;
+
+    public FieldsData(
+        Map<String, BoundField> deserializedFields, List<BoundField> serializedFields) {
+      this.deserializedFields = deserializedFields;
+      this.serializedFields = serializedFields;
+    }
+  }
+
+  private static IllegalArgumentException createDuplicateFieldException(
+      Class<?> declaringType, String duplicateName, Field field1, Field field2) {
+    throw new IllegalArgumentException(
+        "Class "
+            + declaringType.getName()
+            + " declares multiple JSON fields named '"
+            + duplicateName
+            + "'; conflict is caused by fields "
+            + ReflectionHelper.fieldToString(field1)
+            + " and "
+            + ReflectionHelper.fieldToString(field2)
+            + "\nSee "
+            + TroubleshootingGuide.createUrl("duplicate-fields"));
+  }
+
+  private FieldsData getBoundFields(
+      Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible, boolean isRecord) {
+    if (raw.isInterface()) {
+      return FieldsData.EMPTY;
+    }
+
+    Map<String, BoundField> deserializedFields = new LinkedHashMap<>();
+    // For serialized fields use a Map to track duplicate field names; otherwise this could be a
+    // List<BoundField> instead
+    Map<String, BoundField> serializedFields = new LinkedHashMap<>();
+
+    Class<?> originalRaw = raw;
+    while (raw != Object.class) {
+      Field[] fields = raw.getDeclaredFields();
+
+      // For inherited fields, check if access to their declaring class is allowed
+      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.");
+        }
+        blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
+      }
+
+      for (Field field : fields) {
+        boolean serialize = includeField(field, true);
+        boolean deserialize = includeField(field, false);
+        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
+        // 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());
+        List<String> fieldNames = getFieldNames(field);
+        String serializedName = fieldNames.get(0);
+        BoundField boundField =
+            createBoundField(
+                context,
+                field,
+                accessor,
+                serializedName,
+                TypeToken.get(fieldType),
+                serialize,
+                blockInaccessible);
+
+        if (deserialize) {
+          for (String name : fieldNames) {
+            BoundField replaced = deserializedFields.put(name, boundField);
+
+            if (replaced != null) {
+              throw createDuplicateFieldException(originalRaw, name, replaced.field, field);
+            }
+          }
+        }
+
+        if (serialize) {
+          BoundField replaced = serializedFields.put(serializedName, boundField);
+          if (replaced != null) {
+            throw createDuplicateFieldException(originalRaw, serializedName, replaced.field, field);
+          }
+        }
+      }
+      type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
+      raw = type.getRawType();
+    }
+    return new FieldsData(deserializedFields, new ArrayList<>(serializedFields.values()));
+  }
+
+  abstract static class BoundField {
+    /** Name used for serialization (but not for deserialization) */
+    final String serializedName;
+
+    final Field field;
+
+    /** Name of the underlying field */
+    final String fieldName;
+
+    protected BoundField(String serializedName, Field field) {
+      this.serializedName = serializedName;
+      this.field = field;
+      this.fieldName = field.getName();
+    }
+
+    /** 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;
+  }
+
+  /**
+   * 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 abstract static class Adapter<T, A> extends TypeAdapter<T> {
+    private final FieldsData fieldsData;
+
+    Adapter(FieldsData fieldsData) {
+      this.fieldsData = fieldsData;
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+      if (value == null) {
+        out.nullValue();
+        return;
+      }
+
+      out.beginObject();
+      try {
+        for (BoundField boundField : fieldsData.serializedFields) {
+          boundField.write(out, value);
+        }
+      } catch (IllegalAccessException e) {
+        throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+      }
+      out.endObject();
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+
+      A accumulator = createAccumulator();
+      Map<String, BoundField> deserializedFields = fieldsData.deserializedFields;
+
+      try {
+        in.beginObject();
+        while (in.hasNext()) {
+          String name = in.nextName();
+          BoundField field = deserializedFields.get(name);
+          if (field == null) {
+            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, FieldsData fieldsData) {
+      super(fieldsData);
+      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, FieldsData fieldsData, boolean blockInaccessible) {
+      super(fieldsData);
+      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/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f0a7ba1fb287d23e939ca7d477711835eac0f30
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java
@@ -0,0 +1,28 @@
+/*
+ * 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.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/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ef1e40b9f390d9d4a3d4c46e4b7fede304399b6
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.$Gson$Preconditions;
+import com.google.gson.internal.Streams;
+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;
+
+/**
+ * Adapts a Gson 1.x tree-style adapter as a streaming TypeAdapter. Since the tree adapter may be
+ * serialization-only or deserialization-only, this class has a facility to look up a delegate type
+ * adapter on demand.
+ */
+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;
+
+  /**
+   * Only intended as {@code skipPast} for {@link Gson#getDelegateAdapter(TypeAdapterFactory,
+   * TypeToken)}, must not be used in any other way.
+   */
+  private final TypeAdapterFactory skipPastForGetDelegateAdapter;
+
+  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. Field
+   * has to be {@code volatile} because {@link Gson} guarantees to be thread-safe.
+   */
+  private volatile TypeAdapter<T> delegate;
+
+  public TreeTypeAdapter(
+      JsonSerializer<T> serializer,
+      JsonDeserializer<T> deserializer,
+      Gson gson,
+      TypeToken<T> typeToken,
+      TypeAdapterFactory skipPast,
+      boolean nullSafe) {
+    this.serializer = serializer;
+    this.deserializer = deserializer;
+    this.gson = gson;
+    this.typeToken = typeToken;
+    this.skipPastForGetDelegateAdapter = 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 {
+    if (deserializer == null) {
+      return delegate().read(in);
+    }
+    JsonElement value = Streams.parse(in);
+    if (nullSafe && value.isJsonNull()) {
+      return null;
+    }
+    return deserializer.deserialize(value, typeToken.getType(), context);
+  }
+
+  @Override
+  public void write(JsonWriter out, T value) throws IOException {
+    if (serializer == null) {
+      delegate().write(out, value);
+      return;
+    }
+    if (nullSafe && value == null) {
+      out.nullValue();
+      return;
+    }
+    JsonElement tree = serializer.serialize(value, typeToken.getType(), context);
+    Streams.write(tree, out);
+  }
+
+  private TypeAdapter<T> delegate() {
+    // A race might lead to `delegate` being assigned by multiple threads but the last assignment
+    // will stick
+    TypeAdapter<T> d = delegate;
+    return d != null
+        ? d
+        : (delegate = gson.getDelegateAdapter(skipPastForGetDelegateAdapter, typeToken));
+  }
+
+  /**
+   * 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) {
+    return new SingleTypeFactory(typeAdapter, exactType, false, null);
+  }
+
+  /** Returns a new factory that will match each type and its raw type against {@code exactType}. */
+  public static TypeAdapterFactory newFactoryWithMatchRawType(
+      TypeToken<?> exactType, Object typeAdapter) {
+    // only bother matching raw types if exact type is a raw type
+    boolean matchRawType = exactType.getType() == exactType.getRawType();
+    return new SingleTypeFactory(typeAdapter, exactType, matchRawType, null);
+  }
+
+  /**
+   * Returns a new factory that will match each type's raw type for assignability to {@code
+   * hierarchyType}.
+   */
+  public static TypeAdapterFactory newTypeHierarchyFactory(
+      Class<?> hierarchyType, Object typeAdapter) {
+    return new SingleTypeFactory(typeAdapter, null, false, hierarchyType);
+  }
+
+  private static final class SingleTypeFactory implements TypeAdapterFactory {
+    private final TypeToken<?> exactType;
+    private final boolean matchRawType;
+    private final Class<?> hierarchyType;
+    private final JsonSerializer<?> serializer;
+    private final JsonDeserializer<?> deserializer;
+
+    SingleTypeFactory(
+        Object typeAdapter, TypeToken<?> exactType, boolean matchRawType, Class<?> hierarchyType) {
+      serializer = typeAdapter instanceof JsonSerializer ? (JsonSerializer<?>) typeAdapter : null;
+      deserializer =
+          typeAdapter instanceof JsonDeserializer ? (JsonDeserializer<?>) typeAdapter : null;
+      $Gson$Preconditions.checkArgument(serializer != null || deserializer != null);
+      this.exactType = exactType;
+      this.matchRawType = matchRawType;
+      this.hierarchyType = hierarchyType;
+    }
+
+    @SuppressWarnings("unchecked") // guarded by typeToken.equals() call
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+      boolean matches =
+          exactType != null
+              ? exactType.equals(type) || (matchRawType && exactType.getType() == type.getRawType())
+              : hierarchyType.isAssignableFrom(type.getRawType());
+      return matches
+          ? new TreeTypeAdapter<>(
+              (JsonSerializer<T>) serializer, (JsonDeserializer<T>) deserializer, gson, type, this)
+          : null;
+    }
+  }
+
+  private final class GsonContextImpl
+      implements JsonSerializationContext, JsonDeserializationContext {
+    @Override
+    public JsonElement serialize(Object src) {
+      return gson.toJsonTree(src);
+    }
+
+    @Override
+    public JsonElement serialize(Object src, Type typeOfSrc) {
+      return gson.toJsonTree(src, typeOfSrc);
+    }
+
+    @Override
+    @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
+    public <R> R deserialize(JsonElement json, Type typeOfT) throws JsonParseException {
+      return gson.fromJson(json, typeOfT);
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..f64dbc656295fab3a9526ec29b762b99fcc0a718
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+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;
+  private final TypeAdapter<T> delegate;
+  private final Type type;
+
+  TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
+    this.context = context;
+    this.delegate = delegate;
+    this.type = type;
+  }
+
+  @Override
+  public T read(JsonReader in) throws IOException {
+    return delegate.read(in);
+  }
+
+  @Override
+  public void write(JsonWriter out, T value) throws IOException {
+    // Order of preference for choosing type adapters
+    // First preference: a type adapter registered for the runtime type
+    // Second preference: a type adapter registered for the declared type
+    // Third preference: reflective type adapter for the runtime type
+    //                   (if it is a subclass of the declared type)
+    // Fourth preference: reflective type adapter for the declared type
+
+    TypeAdapter<T> chosen = delegate;
+    Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value);
+    if (runtimeType != type) {
+      @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 (!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;
+      } else {
+        // Use the type adapter for runtime type
+        chosen = runtimeTypeAdapter;
+      }
+    }
+    chosen.write(out, value);
+  }
+
+  /**
+   * 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 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/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
new file mode 100644
index 0000000000000000000000000000000000000000..53803bea3256cd287ca91aa035a1462b5b1d59a5
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java
@@ -0,0 +1,1165 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.internal.NumberLimits;
+import com.google.gson.internal.TroubleshootingGuide;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Calendar;
+import java.util.Currency;
+import java.util.Deque;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicIntegerArray;
+
+/** Type adapters for basic types. */
+public final class TypeAdapters {
+  private TypeAdapters() {
+    throw new UnsupportedOperationException();
+  }
+
+  @SuppressWarnings("rawtypes")
+  public static final TypeAdapter<Class> CLASS =
+      new TypeAdapter<Class>() {
+        @Override
+        public void write(JsonWriter out, Class value) throws IOException {
+          throw new UnsupportedOperationException(
+              "Attempted to serialize java.lang.Class: "
+                  + value.getName()
+                  + ". Forgot to register a type adapter?"
+                  + "\nSee "
+                  + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
+        }
+
+        @Override
+        public Class read(JsonReader in) throws IOException {
+          throw new UnsupportedOperationException(
+              "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"
+                  + "\nSee "
+                  + TroubleshootingGuide.createUrl("java-lang-class-unsupported"));
+        }
+      }.nullSafe();
+
+  public static final TypeAdapterFactory CLASS_FACTORY = newFactory(Class.class, CLASS);
+
+  public static final TypeAdapter<BitSet> BIT_SET =
+      new TypeAdapter<BitSet>() {
+        @Override
+        public BitSet read(JsonReader in) throws IOException {
+          BitSet bitset = new BitSet();
+          in.beginArray();
+          int i = 0;
+          JsonToken tokenType = in.peek();
+          while (tokenType != JsonToken.END_ARRAY) {
+            boolean set;
+            switch (tokenType) {
+              case NUMBER:
+              case STRING:
+                int intValue = in.nextInt();
+                if (intValue == 0) {
+                  set = false;
+                } else if (intValue == 1) {
+                  set = true;
+                } else {
+                  throw new JsonSyntaxException(
+                      "Invalid bitset value "
+                          + intValue
+                          + ", expected 0 or 1; at path "
+                          + in.getPreviousPath());
+                }
+                break;
+              case BOOLEAN:
+                set = in.nextBoolean();
+                break;
+              default:
+                throw new JsonSyntaxException(
+                    "Invalid bitset value type: " + tokenType + "; at path " + in.getPath());
+            }
+            if (set) {
+              bitset.set(i);
+            }
+            ++i;
+            tokenType = in.peek();
+          }
+          in.endArray();
+          return bitset;
+        }
+
+        @Override
+        public void write(JsonWriter out, BitSet src) throws IOException {
+          out.beginArray();
+          for (int i = 0, length = src.length(); i < length; i++) {
+            int value = src.get(i) ? 1 : 0;
+            out.value(value);
+          }
+          out.endArray();
+        }
+      }.nullSafe();
+
+  public static final TypeAdapterFactory BIT_SET_FACTORY = newFactory(BitSet.class, BIT_SET);
+
+  public static final TypeAdapter<Boolean> BOOLEAN =
+      new TypeAdapter<Boolean>() {
+        @Override
+        public Boolean read(JsonReader in) throws IOException {
+          JsonToken peek = in.peek();
+          if (peek == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          } else if (peek == JsonToken.STRING) {
+            // support strings for compatibility with GSON 1.7
+            return Boolean.parseBoolean(in.nextString());
+          }
+          return in.nextBoolean();
+        }
+
+        @Override
+        public void write(JsonWriter out, Boolean value) throws IOException {
+          out.value(value);
+        }
+      };
+
+  /**
+   * Writes a boolean as a string. Useful for map keys, where booleans aren't otherwise permitted.
+   */
+  public static final TypeAdapter<Boolean> BOOLEAN_AS_STRING =
+      new TypeAdapter<Boolean>() {
+        @Override
+        public Boolean read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return Boolean.valueOf(in.nextString());
+        }
+
+        @Override
+        public void write(JsonWriter out, Boolean value) throws IOException {
+          out.value(value == null ? "null" : value.toString());
+        }
+      };
+
+  public static final TypeAdapterFactory BOOLEAN_FACTORY =
+      newFactory(boolean.class, Boolean.class, BOOLEAN);
+
+  public static final TypeAdapter<Number> BYTE =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+
+          int intValue;
+          try {
+            intValue = in.nextInt();
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+          }
+          // Allow up to 255 to support unsigned values
+          if (intValue > 255 || intValue < Byte.MIN_VALUE) {
+            throw new JsonSyntaxException(
+                "Lossy conversion from " + intValue + " to byte; at path " + in.getPreviousPath());
+          }
+          return (byte) intValue;
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+          } else {
+            out.value(value.byteValue());
+          }
+        }
+      };
+
+  public static final TypeAdapterFactory BYTE_FACTORY = newFactory(byte.class, Byte.class, BYTE);
+
+  public static final TypeAdapter<Number> SHORT =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+
+          int intValue;
+          try {
+            intValue = in.nextInt();
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+          }
+          // Allow up to 65535 to support unsigned values
+          if (intValue > 65535 || intValue < Short.MIN_VALUE) {
+            throw new JsonSyntaxException(
+                "Lossy conversion from " + intValue + " to short; at path " + in.getPreviousPath());
+          }
+          return (short) intValue;
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+          } else {
+            out.value(value.shortValue());
+          }
+        }
+      };
+
+  public static final TypeAdapterFactory SHORT_FACTORY =
+      newFactory(short.class, Short.class, SHORT);
+
+  public static final TypeAdapter<Number> INTEGER =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          try {
+            return in.nextInt();
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+          } else {
+            out.value(value.intValue());
+          }
+        }
+      };
+  public static final TypeAdapterFactory INTEGER_FACTORY =
+      newFactory(int.class, Integer.class, INTEGER);
+
+  public static final TypeAdapter<AtomicInteger> ATOMIC_INTEGER =
+      new TypeAdapter<AtomicInteger>() {
+        @Override
+        public AtomicInteger read(JsonReader in) throws IOException {
+          try {
+            return new AtomicInteger(in.nextInt());
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, AtomicInteger value) throws IOException {
+          out.value(value.get());
+        }
+      }.nullSafe();
+  public static final TypeAdapterFactory ATOMIC_INTEGER_FACTORY =
+      newFactory(AtomicInteger.class, TypeAdapters.ATOMIC_INTEGER);
+
+  public static final TypeAdapter<AtomicBoolean> ATOMIC_BOOLEAN =
+      new TypeAdapter<AtomicBoolean>() {
+        @Override
+        public AtomicBoolean read(JsonReader in) throws IOException {
+          return new AtomicBoolean(in.nextBoolean());
+        }
+
+        @Override
+        public void write(JsonWriter out, AtomicBoolean value) throws IOException {
+          out.value(value.get());
+        }
+      }.nullSafe();
+  public static final TypeAdapterFactory ATOMIC_BOOLEAN_FACTORY =
+      newFactory(AtomicBoolean.class, TypeAdapters.ATOMIC_BOOLEAN);
+
+  public static final TypeAdapter<AtomicIntegerArray> ATOMIC_INTEGER_ARRAY =
+      new TypeAdapter<AtomicIntegerArray>() {
+        @Override
+        public AtomicIntegerArray read(JsonReader in) throws IOException {
+          List<Integer> list = new ArrayList<>();
+          in.beginArray();
+          while (in.hasNext()) {
+            try {
+              int integer = in.nextInt();
+              list.add(integer);
+            } catch (NumberFormatException e) {
+              throw new JsonSyntaxException(e);
+            }
+          }
+          in.endArray();
+          int length = list.size();
+          AtomicIntegerArray array = new AtomicIntegerArray(length);
+          for (int i = 0; i < length; ++i) {
+            array.set(i, list.get(i));
+          }
+          return array;
+        }
+
+        @Override
+        public void write(JsonWriter out, AtomicIntegerArray value) throws IOException {
+          out.beginArray();
+          for (int i = 0, length = value.length(); i < length; i++) {
+            out.value(value.get(i));
+          }
+          out.endArray();
+        }
+      }.nullSafe();
+  public static final TypeAdapterFactory ATOMIC_INTEGER_ARRAY_FACTORY =
+      newFactory(AtomicIntegerArray.class, TypeAdapters.ATOMIC_INTEGER_ARRAY);
+
+  public static final TypeAdapter<Number> LONG =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          try {
+            return in.nextLong();
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+          } else {
+            out.value(value.longValue());
+          }
+        }
+      };
+
+  public static final TypeAdapter<Number> FLOAT =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return (float) in.nextDouble();
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          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);
+          }
+        }
+      };
+
+  public static final TypeAdapter<Number> DOUBLE =
+      new TypeAdapter<Number>() {
+        @Override
+        public Number read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return in.nextDouble();
+        }
+
+        @Override
+        public void write(JsonWriter out, Number value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+          } else {
+            out.value(value.doubleValue());
+          }
+        }
+      };
+
+  public static final TypeAdapter<Character> CHARACTER =
+      new TypeAdapter<Character>() {
+        @Override
+        public Character read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String str = in.nextString();
+          if (str.length() != 1) {
+            throw new JsonSyntaxException(
+                "Expecting character, got: " + str + "; at " + in.getPreviousPath());
+          }
+          return str.charAt(0);
+        }
+
+        @Override
+        public void write(JsonWriter out, Character value) throws IOException {
+          out.value(value == null ? null : String.valueOf(value));
+        }
+      };
+
+  public static final TypeAdapterFactory CHARACTER_FACTORY =
+      newFactory(char.class, Character.class, CHARACTER);
+
+  public static final TypeAdapter<String> STRING =
+      new TypeAdapter<String>() {
+        @Override
+        public String read(JsonReader in) throws IOException {
+          JsonToken peek = in.peek();
+          if (peek == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          /* coerce booleans to strings for backwards compatibility */
+          if (peek == JsonToken.BOOLEAN) {
+            return Boolean.toString(in.nextBoolean());
+          }
+          return in.nextString();
+        }
+
+        @Override
+        public void write(JsonWriter out, String value) throws IOException {
+          out.value(value);
+        }
+      };
+
+  public static final TypeAdapter<BigDecimal> BIG_DECIMAL =
+      new TypeAdapter<BigDecimal>() {
+        @Override
+        public BigDecimal read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String s = in.nextString();
+          try {
+            return NumberLimits.parseBigDecimal(s);
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(
+                "Failed parsing '" + s + "' as BigDecimal; at path " + in.getPreviousPath(), e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, BigDecimal value) throws IOException {
+          out.value(value);
+        }
+      };
+
+  public static final TypeAdapter<BigInteger> BIG_INTEGER =
+      new TypeAdapter<BigInteger>() {
+        @Override
+        public BigInteger read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String s = in.nextString();
+          try {
+            return NumberLimits.parseBigInteger(s);
+          } catch (NumberFormatException e) {
+            throw new JsonSyntaxException(
+                "Failed parsing '" + s + "' as BigInteger; at path " + in.getPreviousPath(), e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, BigInteger value) throws IOException {
+          out.value(value);
+        }
+      };
+
+  public static final TypeAdapter<LazilyParsedNumber> LAZILY_PARSED_NUMBER =
+      new TypeAdapter<LazilyParsedNumber>() {
+        // Normally users should not be able to access and deserialize LazilyParsedNumber because
+        // it is an internal type, but implement this nonetheless in case there are legit corner
+        // cases where this is possible
+        @Override
+        public LazilyParsedNumber read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return new LazilyParsedNumber(in.nextString());
+        }
+
+        @Override
+        public void write(JsonWriter out, LazilyParsedNumber value) throws IOException {
+          out.value(value);
+        }
+      };
+
+  public static final TypeAdapterFactory STRING_FACTORY = newFactory(String.class, STRING);
+
+  public static final TypeAdapter<StringBuilder> STRING_BUILDER =
+      new TypeAdapter<StringBuilder>() {
+        @Override
+        public StringBuilder read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return new StringBuilder(in.nextString());
+        }
+
+        @Override
+        public void write(JsonWriter out, StringBuilder value) throws IOException {
+          out.value(value == null ? null : value.toString());
+        }
+      };
+
+  public static final TypeAdapterFactory STRING_BUILDER_FACTORY =
+      newFactory(StringBuilder.class, STRING_BUILDER);
+
+  public static final TypeAdapter<StringBuffer> STRING_BUFFER =
+      new TypeAdapter<StringBuffer>() {
+        @Override
+        public StringBuffer read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          return new StringBuffer(in.nextString());
+        }
+
+        @Override
+        public void write(JsonWriter out, StringBuffer value) throws IOException {
+          out.value(value == null ? null : value.toString());
+        }
+      };
+
+  public static final TypeAdapterFactory STRING_BUFFER_FACTORY =
+      newFactory(StringBuffer.class, STRING_BUFFER);
+
+  public static final TypeAdapter<URL> URL =
+      new TypeAdapter<URL>() {
+        @Override
+        public URL read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String nextString = in.nextString();
+          return nextString.equals("null") ? null : new URL(nextString);
+        }
+
+        @Override
+        public void write(JsonWriter out, URL value) throws IOException {
+          out.value(value == null ? null : value.toExternalForm());
+        }
+      };
+
+  public static final TypeAdapterFactory URL_FACTORY = newFactory(URL.class, URL);
+
+  public static final TypeAdapter<URI> URI =
+      new TypeAdapter<URI>() {
+        @Override
+        public URI read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          try {
+            String nextString = in.nextString();
+            return nextString.equals("null") ? null : new URI(nextString);
+          } catch (URISyntaxException e) {
+            throw new JsonIOException(e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, URI value) throws IOException {
+          out.value(value == null ? null : value.toASCIIString());
+        }
+      };
+
+  public static final TypeAdapterFactory URI_FACTORY = newFactory(URI.class, URI);
+
+  public static final TypeAdapter<InetAddress> INET_ADDRESS =
+      new TypeAdapter<InetAddress>() {
+        @Override
+        public InetAddress read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          // regrettably, this should have included both the host name and the host address
+          // For compatibility, we use InetAddress.getByName rather than the possibly-better
+          // .getAllByName
+          @SuppressWarnings("AddressSelection")
+          InetAddress addr = InetAddress.getByName(in.nextString());
+          return addr;
+        }
+
+        @Override
+        public void write(JsonWriter out, InetAddress value) throws IOException {
+          out.value(value == null ? null : value.getHostAddress());
+        }
+      };
+
+  public static final TypeAdapterFactory INET_ADDRESS_FACTORY =
+      newTypeHierarchyFactory(InetAddress.class, INET_ADDRESS);
+
+  public static final TypeAdapter<UUID> UUID =
+      new TypeAdapter<UUID>() {
+        @Override
+        public UUID read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String s = in.nextString();
+          try {
+            return java.util.UUID.fromString(s);
+          } catch (IllegalArgumentException e) {
+            throw new JsonSyntaxException(
+                "Failed parsing '" + s + "' as UUID; at path " + in.getPreviousPath(), e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, UUID value) throws IOException {
+          out.value(value == null ? null : value.toString());
+        }
+      };
+
+  public static final TypeAdapterFactory UUID_FACTORY = newFactory(UUID.class, UUID);
+
+  public static final TypeAdapter<Currency> CURRENCY =
+      new TypeAdapter<Currency>() {
+        @Override
+        public Currency read(JsonReader in) throws IOException {
+          String s = in.nextString();
+          try {
+            return Currency.getInstance(s);
+          } catch (IllegalArgumentException e) {
+            throw new JsonSyntaxException(
+                "Failed parsing '" + s + "' as Currency; at path " + in.getPreviousPath(), e);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, Currency value) throws IOException {
+          out.value(value.getCurrencyCode());
+        }
+      }.nullSafe();
+  public static final TypeAdapterFactory CURRENCY_FACTORY = newFactory(Currency.class, CURRENCY);
+
+  public static final TypeAdapter<Calendar> CALENDAR =
+      new TypeAdapter<Calendar>() {
+        private static final String YEAR = "year";
+        private static final String MONTH = "month";
+        private static final String DAY_OF_MONTH = "dayOfMonth";
+        private static final String HOUR_OF_DAY = "hourOfDay";
+        private static final String MINUTE = "minute";
+        private static final String SECOND = "second";
+
+        @Override
+        public Calendar read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          in.beginObject();
+          int year = 0;
+          int month = 0;
+          int dayOfMonth = 0;
+          int hourOfDay = 0;
+          int minute = 0;
+          int second = 0;
+          while (in.peek() != JsonToken.END_OBJECT) {
+            String name = in.nextName();
+            int value = in.nextInt();
+            switch (name) {
+              case YEAR:
+                year = value;
+                break;
+              case MONTH:
+                month = value;
+                break;
+              case DAY_OF_MONTH:
+                dayOfMonth = value;
+                break;
+              case HOUR_OF_DAY:
+                hourOfDay = value;
+                break;
+              case MINUTE:
+                minute = value;
+                break;
+              case SECOND:
+                second = value;
+                break;
+              default:
+                // Ignore unknown JSON property
+            }
+          }
+          in.endObject();
+          return new GregorianCalendar(year, month, dayOfMonth, hourOfDay, minute, second);
+        }
+
+        @Override
+        public void write(JsonWriter out, Calendar value) throws IOException {
+          if (value == null) {
+            out.nullValue();
+            return;
+          }
+          out.beginObject();
+          out.name(YEAR);
+          out.value(value.get(Calendar.YEAR));
+          out.name(MONTH);
+          out.value(value.get(Calendar.MONTH));
+          out.name(DAY_OF_MONTH);
+          out.value(value.get(Calendar.DAY_OF_MONTH));
+          out.name(HOUR_OF_DAY);
+          out.value(value.get(Calendar.HOUR_OF_DAY));
+          out.name(MINUTE);
+          out.value(value.get(Calendar.MINUTE));
+          out.name(SECOND);
+          out.value(value.get(Calendar.SECOND));
+          out.endObject();
+        }
+      };
+
+  public static final TypeAdapterFactory CALENDAR_FACTORY =
+      newFactoryForMultipleTypes(Calendar.class, GregorianCalendar.class, CALENDAR);
+
+  public static final TypeAdapter<Locale> LOCALE =
+      new TypeAdapter<Locale>() {
+        @Override
+        public Locale read(JsonReader in) throws IOException {
+          if (in.peek() == JsonToken.NULL) {
+            in.nextNull();
+            return null;
+          }
+          String locale = in.nextString();
+          StringTokenizer tokenizer = new StringTokenizer(locale, "_");
+          String language = null;
+          String country = null;
+          String variant = null;
+          if (tokenizer.hasMoreElements()) {
+            language = tokenizer.nextToken();
+          }
+          if (tokenizer.hasMoreElements()) {
+            country = tokenizer.nextToken();
+          }
+          if (tokenizer.hasMoreElements()) {
+            variant = tokenizer.nextToken();
+          }
+          if (country == null && variant == null) {
+            return new Locale(language);
+          } else if (variant == null) {
+            return new Locale(language, country);
+          } else {
+            return new Locale(language, country, variant);
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, Locale value) throws IOException {
+          out.value(value == null ? null : value.toString());
+        }
+      };
+
+  public static final TypeAdapterFactory LOCALE_FACTORY = newFactory(Locale.class, LOCALE);
+
+  public static final TypeAdapter<JsonElement> JSON_ELEMENT =
+      new TypeAdapter<JsonElement>() {
+        /**
+         * Tries to begin reading a JSON array or JSON object, returning {@code null} if the next
+         * element is neither of those.
+         */
+        private JsonElement tryBeginNesting(JsonReader in, JsonToken peeked) throws IOException {
+          switch (peeked) {
+            case BEGIN_ARRAY:
+              in.beginArray();
+              return new JsonArray();
+            case BEGIN_OBJECT:
+              in.beginObject();
+              return new JsonObject();
+            default:
+              return null;
+          }
+        }
+
+        /** Reads a {@link JsonElement} which cannot have any nested elements */
+        private JsonElement readTerminal(JsonReader in, JsonToken peeked) throws IOException {
+          switch (peeked) {
+            case STRING:
+              return new JsonPrimitive(in.nextString());
+            case NUMBER:
+              String number = in.nextString();
+              return new JsonPrimitive(new LazilyParsedNumber(number));
+            case BOOLEAN:
+              return new JsonPrimitive(in.nextBoolean());
+            case NULL:
+              in.nextNull();
+              return JsonNull.INSTANCE;
+            default:
+              // When read(JsonReader) is called with JsonReader in invalid state
+              throw new IllegalStateException("Unexpected token: " + peeked);
+          }
+        }
+
+        @Override
+        public JsonElement read(JsonReader in) throws IOException {
+          if (in instanceof JsonTreeReader) {
+            return ((JsonTreeReader) in).nextJsonElement();
+          }
+
+          // Either JsonArray or JsonObject
+          JsonElement current;
+          JsonToken peeked = in.peek();
+
+          current = tryBeginNesting(in, peeked);
+          if (current == null) {
+            return readTerminal(in, peeked);
+          }
+
+          Deque<JsonElement> stack = new ArrayDeque<>();
+
+          while (true) {
+            while (in.hasNext()) {
+              String name = null;
+              // Name is only used for JSON object members
+              if (current instanceof JsonObject) {
+                name = in.nextName();
+              }
+
+              peeked = in.peek();
+              JsonElement value = tryBeginNesting(in, peeked);
+              boolean isNesting = value != null;
+
+              if (value == null) {
+                value = readTerminal(in, peeked);
+              }
+
+              if (current instanceof JsonArray) {
+                ((JsonArray) current).add(value);
+              } else {
+                ((JsonObject) current).add(name, value);
+              }
+
+              if (isNesting) {
+                stack.addLast(current);
+                current = value;
+              }
+            }
+
+            // End current element
+            if (current instanceof JsonArray) {
+              in.endArray();
+            } else {
+              in.endObject();
+            }
+
+            if (stack.isEmpty()) {
+              return current;
+            } else {
+              // Continue with enclosing element
+              current = stack.removeLast();
+            }
+          }
+        }
+
+        @Override
+        public void write(JsonWriter out, JsonElement value) throws IOException {
+          if (value == null || value.isJsonNull()) {
+            out.nullValue();
+          } else if (value.isJsonPrimitive()) {
+            JsonPrimitive primitive = value.getAsJsonPrimitive();
+            if (primitive.isNumber()) {
+              out.value(primitive.getAsNumber());
+            } else if (primitive.isBoolean()) {
+              out.value(primitive.getAsBoolean());
+            } else {
+              out.value(primitive.getAsString());
+            }
+
+          } else if (value.isJsonArray()) {
+            out.beginArray();
+            for (JsonElement e : value.getAsJsonArray()) {
+              write(out, e);
+            }
+            out.endArray();
+
+          } else if (value.isJsonObject()) {
+            out.beginObject();
+            for (Map.Entry<String, JsonElement> e : value.getAsJsonObject().entrySet()) {
+              out.name(e.getKey());
+              write(out, e.getValue());
+            }
+            out.endObject();
+
+          } else {
+            throw new IllegalArgumentException("Couldn't write " + value.getClass());
+          }
+        }
+      };
+
+  public static final TypeAdapterFactory JSON_ELEMENT_FACTORY =
+      newTypeHierarchyFactory(JsonElement.class, JSON_ELEMENT);
+
+  private static final class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> {
+    private final Map<String, T> nameToConstant = new HashMap<>();
+    private final Map<String, T> stringToConstant = new HashMap<>();
+    private final Map<T, String> constantToName = new HashMap<>();
+
+    public EnumTypeAdapter(final Class<T> classOfT) {
+      try {
+        // Uses reflection to find enum constants to work around name mismatches for obfuscated
+        // classes
+        // Reflection access might throw SecurityException, therefore run this in privileged
+        // context; should be acceptable because this only retrieves enum constants, but does not
+        // expose anything else
+        Field[] constantFields =
+            AccessController.doPrivileged(
+                new PrivilegedAction<Field[]>() {
+                  @Override
+                  public Field[] run() {
+                    Field[] fields = classOfT.getDeclaredFields();
+                    ArrayList<Field> constantFieldsList = new ArrayList<>(fields.length);
+                    for (Field f : fields) {
+                      if (f.isEnumConstant()) {
+                        constantFieldsList.add(f);
+                      }
+                    }
+
+                    Field[] constantFields = constantFieldsList.toArray(new Field[0]);
+                    AccessibleObject.setAccessible(constantFields, true);
+                    return constantFields;
+                  }
+                });
+        for (Field constantField : constantFields) {
+          @SuppressWarnings("unchecked")
+          T constant = (T) constantField.get(null);
+          String name = constant.name();
+          String toStringVal = constant.toString();
+
+          SerializedName annotation = constantField.getAnnotation(SerializedName.class);
+          if (annotation != null) {
+            name = annotation.value();
+            for (String alternate : annotation.alternate()) {
+              nameToConstant.put(alternate, constant);
+            }
+          }
+          nameToConstant.put(name, constant);
+          stringToConstant.put(toStringVal, constant);
+          constantToName.put(constant, name);
+        }
+      } catch (IllegalAccessException e) {
+        throw new AssertionError(e);
+      }
+    }
+
+    @Override
+    public T read(JsonReader in) throws IOException {
+      if (in.peek() == JsonToken.NULL) {
+        in.nextNull();
+        return null;
+      }
+      String key = in.nextString();
+      T constant = nameToConstant.get(key);
+      return (constant == null) ? stringToConstant.get(key) : constant;
+    }
+
+    @Override
+    public void write(JsonWriter out, T value) throws IOException {
+      out.value(value == null ? null : constantToName.get(value));
+    }
+  }
+
+  public static final TypeAdapterFactory ENUM_FACTORY =
+      new TypeAdapterFactory() {
+        @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) {
+            return null;
+          }
+          if (!rawType.isEnum()) {
+            rawType = rawType.getSuperclass(); // handle anonymous subclasses
+          }
+          @SuppressWarnings({"rawtypes", "unchecked"})
+          TypeAdapter<T> adapter = (TypeAdapter<T>) new EnumTypeAdapter(rawType);
+          return adapter;
+        }
+      };
+
+  @SuppressWarnings("TypeParameterNaming")
+  public static <TT> TypeAdapterFactory newFactory(
+      final TypeToken<TT> type, final TypeAdapter<TT> typeAdapter) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+        return typeToken.equals(type) ? (TypeAdapter<T>) typeAdapter : null;
+      }
+    };
+  }
+
+  @SuppressWarnings("TypeParameterNaming")
+  public static <TT> TypeAdapterFactory newFactory(
+      final Class<TT> type, final TypeAdapter<TT> typeAdapter) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+        return typeToken.getRawType() == type ? (TypeAdapter<T>) typeAdapter : null;
+      }
+
+      @Override
+      public String toString() {
+        return "Factory[type=" + type.getName() + ",adapter=" + typeAdapter + "]";
+      }
+    };
+  }
+
+  @SuppressWarnings("TypeParameterNaming")
+  public static <TT> TypeAdapterFactory newFactory(
+      final Class<TT> unboxed, final Class<TT> boxed, final TypeAdapter<? super TT> typeAdapter) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+        Class<? super T> rawType = typeToken.getRawType();
+        return (rawType == unboxed || rawType == boxed) ? (TypeAdapter<T>) typeAdapter : null;
+      }
+
+      @Override
+      public String toString() {
+        return "Factory[type="
+            + boxed.getName()
+            + "+"
+            + unboxed.getName()
+            + ",adapter="
+            + typeAdapter
+            + "]";
+      }
+    };
+  }
+
+  @SuppressWarnings("TypeParameterNaming")
+  public static <TT> TypeAdapterFactory newFactoryForMultipleTypes(
+      final Class<TT> base,
+      final Class<? extends TT> sub,
+      final TypeAdapter<? super TT> typeAdapter) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+        Class<? super T> rawType = typeToken.getRawType();
+        return (rawType == base || rawType == sub) ? (TypeAdapter<T>) typeAdapter : null;
+      }
+
+      @Override
+      public String toString() {
+        return "Factory[type="
+            + base.getName()
+            + "+"
+            + sub.getName()
+            + ",adapter="
+            + typeAdapter
+            + "]";
+      }
+    };
+  }
+
+  /**
+   * Returns a factory for all subtypes of {@code typeAdapter}. We do a runtime check to confirm
+   * that the deserialized type matches the type requested.
+   */
+  public static <T1> TypeAdapterFactory newTypeHierarchyFactory(
+      final Class<T1> clazz, final TypeAdapter<T1> typeAdapter) {
+    return new TypeAdapterFactory() {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T2> TypeAdapter<T2> create(Gson gson, TypeToken<T2> typeToken) {
+        final Class<? super T2> requestedType = typeToken.getRawType();
+        if (!clazz.isAssignableFrom(requestedType)) {
+          return null;
+        }
+        return (TypeAdapter<T2>)
+            new TypeAdapter<T1>() {
+              @Override
+              public void write(JsonWriter out, T1 value) throws IOException {
+                typeAdapter.write(out, value);
+              }
+
+              @Override
+              public T1 read(JsonReader in) throws IOException {
+                T1 result = typeAdapter.read(in);
+                if (result != null && !requestedType.isInstance(result)) {
+                  throw new JsonSyntaxException(
+                      "Expected a "
+                          + requestedType.getName()
+                          + " but was "
+                          + result.getClass().getName()
+                          + "; at path "
+                          + in.getPreviousPath());
+                }
+                return result;
+              }
+            };
+      }
+
+      @Override
+      public String toString() {
+        return "Factory[typeHierarchy=" + clazz.getName() + ",adapter=" + typeAdapter + "]";
+      }
+    };
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java b/gson/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java
new file mode 100644
index 0000000000000000000000000000000000000000..28074a10d1d79c4c9b02c9896c51f85da3be88b8
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2015 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.internal.bind.util;
+
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * Utilities methods for manipulating dates in iso8601 format. This is much faster and GC friendly
+ * than using SimpleDateFormat so highly suitable if you (un)serialize lots of date objects.
+ *
+ * <p>Supported parse format:
+ * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]
+ *
+ * @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
+ */
+// Date parsing code from Jackson databind ISO8601Utils.java
+// https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
+public class ISO8601Utils {
+  private ISO8601Utils() {}
+
+  /**
+   * ID to represent the 'UTC' string, default timezone since Jackson 2.7
+   *
+   * @since 2.7
+   */
+  private static final String UTC_ID = "UTC";
+
+  /**
+   * The UTC timezone, prefetched to avoid more lookups.
+   *
+   * @since 2.7
+   */
+  private static final TimeZone TIMEZONE_UTC = TimeZone.getTimeZone(UTC_ID);
+
+  /*
+  /**********************************************************
+  /* Formatting
+  /**********************************************************
+   */
+
+  /**
+   * Format a date into 'yyyy-MM-ddThh:mm:ssZ' (default timezone, no milliseconds precision)
+   *
+   * @param date the date to format
+   * @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
+   */
+  public static String format(Date date) {
+    return format(date, false, TIMEZONE_UTC);
+  }
+
+  /**
+   * Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone)
+   *
+   * @param date the date to format
+   * @param millis true to include millis precision otherwise false
+   * @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z'
+   */
+  public static String format(Date date, boolean millis) {
+    return format(date, millis, TIMEZONE_UTC);
+  }
+
+  /**
+   * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   *
+   * @param date the date to format
+   * @param millis true to include millis precision otherwise false
+   * @param tz timezone to use for the formatting (UTC will produce 'Z')
+   * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
+   */
+  public static String format(Date date, boolean millis, TimeZone tz) {
+    Calendar calendar = new GregorianCalendar(tz, Locale.US);
+    calendar.setTime(date);
+
+    // estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
+    int capacity = "yyyy-MM-ddThh:mm:ss".length();
+    capacity += millis ? ".sss".length() : 0;
+    capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length();
+    StringBuilder formatted = new StringBuilder(capacity);
+
+    padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
+    formatted.append('-');
+    padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
+    formatted.append('-');
+    padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
+    formatted.append('T');
+    padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
+    formatted.append(':');
+    padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
+    formatted.append(':');
+    padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
+    if (millis) {
+      formatted.append('.');
+      padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
+    }
+
+    int offset = tz.getOffset(calendar.getTimeInMillis());
+    if (offset != 0) {
+      int hours = Math.abs((offset / (60 * 1000)) / 60);
+      int minutes = Math.abs((offset / (60 * 1000)) % 60);
+      formatted.append(offset < 0 ? '-' : '+');
+      padInt(formatted, hours, "hh".length());
+      formatted.append(':');
+      padInt(formatted, minutes, "mm".length());
+    } else {
+      formatted.append('Z');
+    }
+
+    return formatted.toString();
+  }
+
+  /*
+  /**********************************************************
+  /* Parsing
+  /**********************************************************
+   */
+
+  /**
+   * Parse a date from ISO-8601 formatted string. It expects a format
+   * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:mm]]]
+   *
+   * @param date ISO string to parse in the appropriate format.
+   * @param pos The position to start parsing from, updated to where parsing stopped.
+   * @return the parsed date
+   * @throws ParseException if the date is not in the appropriate format
+   */
+  public static Date parse(String date, ParsePosition pos) throws ParseException {
+    Exception fail = null;
+    try {
+      int offset = pos.getIndex();
+
+      // extract year
+      int year = parseInt(date, offset, offset += 4);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract month
+      int month = parseInt(date, offset, offset += 2);
+      if (checkOffset(date, offset, '-')) {
+        offset += 1;
+      }
+
+      // extract day
+      int day = parseInt(date, offset, offset += 2);
+
+      // default time value
+      int hour = 0;
+      int minutes = 0;
+      int seconds = 0;
+
+      // always use 0 otherwise returned date will include millis of current time
+      int milliseconds = 0;
+
+      // if the value has no time component (and no time zone), we are done
+      boolean hasT = checkOffset(date, offset, 'T');
+
+      if (!hasT && (date.length() <= offset)) {
+        Calendar calendar = new GregorianCalendar(year, month - 1, day);
+        calendar.setLenient(false);
+
+        pos.setIndex(offset);
+        return calendar.getTime();
+      }
+
+      if (hasT) {
+
+        // extract hours, minutes, seconds and milliseconds
+        hour = parseInt(date, offset += 1, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+
+        minutes = parseInt(date, offset, offset += 2);
+        if (checkOffset(date, offset, ':')) {
+          offset += 1;
+        }
+        // second and milliseconds can be optional
+        if (date.length() > offset) {
+          char c = date.charAt(offset);
+          if (c != 'Z' && c != '+' && c != '-') {
+            seconds = parseInt(date, offset, offset += 2);
+            if (seconds > 59 && seconds < 63) {
+              seconds = 59; // truncate up to 3 leap seconds
+            }
+            // milliseconds can be optional in the format
+            if (checkOffset(date, offset, '.')) {
+              offset += 1;
+              int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
+              int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
+              int fraction = parseInt(date, offset, parseEndOffset);
+              // compensate for "missing" digits
+              switch (parseEndOffset - offset) { // number of digits parsed
+                case 2:
+                  milliseconds = fraction * 10;
+                  break;
+                case 1:
+                  milliseconds = fraction * 100;
+                  break;
+                default:
+                  milliseconds = fraction;
+              }
+              offset = endOffset;
+            }
+          }
+        }
+      }
+
+      // extract timezone
+      if (date.length() <= offset) {
+        throw new IllegalArgumentException("No time zone indicator");
+      }
+
+      TimeZone timezone = null;
+      char timezoneIndicator = date.charAt(offset);
+
+      if (timezoneIndicator == 'Z') {
+        timezone = TIMEZONE_UTC;
+        offset += 1;
+      } else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
+        String timezoneOffset = date.substring(offset);
+
+        // When timezone has no minutes, we should append it, valid timezones are, for example:
+        // +00:00, +0000 and +00
+        timezoneOffset = timezoneOffset.length() >= 5 ? timezoneOffset : timezoneOffset + "00";
+
+        offset += timezoneOffset.length();
+        // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
+        if (timezoneOffset.equals("+0000") || timezoneOffset.equals("+00:00")) {
+          timezone = TIMEZONE_UTC;
+        } else {
+          // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
+          //    not sure why, but that's the way it looks. Further, Javadocs for
+          //    `java.util.TimeZone` specifically instruct use of GMT as base for
+          //    custom timezones... odd.
+          String timezoneId = "GMT" + timezoneOffset;
+          // String timezoneId = "UTC" + timezoneOffset;
+
+          timezone = TimeZone.getTimeZone(timezoneId);
+
+          String act = timezone.getID();
+          if (!act.equals(timezoneId)) {
+            /* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
+             *    one without. If so, don't sweat.
+             *   Yes, very inefficient. Hopefully not hit often.
+             *   If it becomes a perf problem, add 'loose' comparison instead.
+             */
+            String cleaned = act.replace(":", "");
+            if (!cleaned.equals(timezoneId)) {
+              throw new IndexOutOfBoundsException(
+                  "Mismatching time zone indicator: "
+                      + timezoneId
+                      + " given, resolves to "
+                      + timezone.getID());
+            }
+          }
+        }
+      } else {
+        throw new IndexOutOfBoundsException(
+            "Invalid time zone indicator '" + timezoneIndicator + "'");
+      }
+
+      Calendar calendar = new GregorianCalendar(timezone);
+      calendar.setLenient(false);
+      calendar.set(Calendar.YEAR, year);
+      calendar.set(Calendar.MONTH, month - 1);
+      calendar.set(Calendar.DAY_OF_MONTH, day);
+      calendar.set(Calendar.HOUR_OF_DAY, hour);
+      calendar.set(Calendar.MINUTE, minutes);
+      calendar.set(Calendar.SECOND, seconds);
+      calendar.set(Calendar.MILLISECOND, milliseconds);
+
+      pos.setIndex(offset);
+      return calendar.getTime();
+      // If we get a ParseException it'll already have the right message/offset.
+      // Other exception types can convert here.
+    } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
+      fail = e;
+    }
+    String input = (date == null) ? null : ('"' + date + '"');
+    String msg = fail.getMessage();
+    if (msg == null || msg.isEmpty()) {
+      msg = "(" + fail.getClass().getName() + ")";
+    }
+    ParseException ex =
+        new ParseException("Failed to parse date [" + input + "]: " + msg, pos.getIndex());
+    ex.initCause(fail);
+    throw ex;
+  }
+
+  /**
+   * Check if the expected character exist at the given offset in the value.
+   *
+   * @param value the string to check at the specified offset
+   * @param offset the offset to look for the expected character
+   * @param expected the expected character
+   * @return true if the expected character exist at the given offset
+   */
+  private static boolean checkOffset(String value, int offset, char expected) {
+    return (offset < value.length()) && (value.charAt(offset) == expected);
+  }
+
+  /**
+   * Parse an integer located between 2 given offsets in a string
+   *
+   * @param value the string to parse
+   * @param beginIndex the start index for the integer in the string
+   * @param endIndex the end index for the integer in the string
+   * @return the int
+   * @throws NumberFormatException if the value is not a number
+   */
+  private static int parseInt(String value, int beginIndex, int endIndex)
+      throws NumberFormatException {
+    if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
+      throw new NumberFormatException(value);
+    }
+    // use same logic as in Integer.parseInt() but less generic we're not supporting negative values
+    int i = beginIndex;
+    int result = 0;
+    int digit;
+    if (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
+      }
+      result = -digit;
+    }
+    while (i < endIndex) {
+      digit = Character.digit(value.charAt(i++), 10);
+      if (digit < 0) {
+        throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
+      }
+      result *= 10;
+      result -= digit;
+    }
+    return -result;
+  }
+
+  /**
+   * Zero pad a number to a specified length
+   *
+   * @param buffer buffer to use for padding
+   * @param value the integer value to pad if necessary.
+   * @param length the length of the string we should zero pad
+   */
+  private static void padInt(StringBuilder buffer, int value, int length) {
+    String strValue = Integer.toString(value);
+    for (int i = length - strValue.length(); i > 0; i--) {
+      buffer.append('0');
+    }
+    buffer.append(strValue);
+  }
+
+  /**
+   * Returns the index of the first character in the string that is not a digit, starting at offset.
+   */
+  private static int indexOfNonDigit(String string, int offset) {
+    for (int i = offset; i < string.length(); i++) {
+      char c = string.charAt(i);
+      if (c < '0' || c > '9') {
+        return i;
+      }
+    }
+    return string.length();
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/package-info.java b/gson/gson/src/main/java/com/google/gson/internal/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..20b54fb8163df7acf5ac3fbd87393fbcaf90ab98
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+/**
+ * Do NOT use any class in this package as they are meant for internal use in Gson. These classes
+ * will very likely change incompatibly in future versions. You have been warned.
+ *
+ * @author Inderjeet Singh, Joel Leitch, Jesse Wilson
+ */
+package com.google.gson.internal;
diff --git a/gson/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ee75bde92a4d63f83b5ffbb002daa3170228630
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2021 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.internal.reflect;
+
+import com.google.gson.JsonIOException;
+import com.google.gson.internal.GsonBuildConfig;
+import com.google.gson.internal.TroubleshootingGuide;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+public class 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 (ReflectiveOperationException e) {
+      instance = new RecordNotSupportedHelper();
+    }
+    RECORD_HELPER = instance;
+  }
+
+  private ReflectionHelper() {}
+
+  private static String getInaccessibleTroubleshootingSuffix(Exception e) {
+    // Class was added in Java 9, therefore cannot use instanceof
+    if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) {
+      String message = e.getMessage();
+      String troubleshootingId =
+          message != null && message.contains("to module com.google.gson")
+              ? "reflection-inaccessible-to-module-gson"
+              : "reflection-inaccessible";
+      return "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId);
+    }
+    return "";
+  }
+
+  /**
+   * Internal implementation of making an {@link AccessibleObject} accessible.
+   *
+   * @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(AccessibleObject object) throws JsonIOException {
+    try {
+      object.setAccessible(true);
+    } catch (Exception 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."
+              + getInaccessibleTroubleshootingSuffix(exception),
+          exception);
+    }
+  }
+
+  /**
+   * Returns a short string describing the {@link AccessibleObject} in a human-readable way. The
+   * result is normally shorter than {@link AccessibleObject#toString()} because it omits modifiers
+   * (e.g. {@code final}) and uses simple names for constructor and method parameter types.
+   *
+   * @param object object to describe
+   * @param uppercaseFirstLetter whether the first letter of the description should be uppercased
+   */
+  public static String getAccessibleObjectDescription(
+      AccessibleObject object, boolean uppercaseFirstLetter) {
+    String description;
+
+    if (object instanceof Field) {
+      description = "field '" + fieldToString((Field) object) + "'";
+    } 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 field, omitting modifiers and the field type. */
+  public static String fieldToString(Field field) {
+    return field.getDeclaringClass().getName() + "#" + field.getName();
+  }
+
+  /**
+   * 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();
+  }
+
+  // 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(", ");
+      }
+      stringBuilder.append(parameters[i].getSimpleName());
+    }
+
+    stringBuilder.append(')');
+  }
+
+  public static boolean isStatic(Class<?> clazz) {
+    return Modifier.isStatic(clazz.getModifiers());
+  }
+
+  /** Returns whether the class is anonymous or a non-static local class. */
+  public static boolean isAnonymousOrNonStaticLocal(Class<?> clazz) {
+    return !isStatic(clazz) && (clazz.isAnonymousClass() || clazz.isLocalClass());
+  }
+
+  /**
+   * Tries making the constructor accessible, returning an exception message if this fails.
+   *
+   * @param constructor constructor to make accessible
+   * @return exception message; {@code null} if successful, non-{@code null} if unsuccessful
+   */
+  public static String tryMakeAccessible(Constructor<?> constructor) {
+    try {
+      constructor.setAccessible(true);
+      return null;
+    } catch (Exception exception) {
+      return "Failed making constructor '"
+          + constructorToString(constructor)
+          + "' accessible; either increase its visibility or write a custom InstanceCreator or"
+          + " TypeAdapter for its declaring type: "
+          // Include the message since it might contain more detailed information
+          + exception.getMessage()
+          + getInaccessibleTroubleshootingSuffix(exception);
+    }
+  }
+
+  /** 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, ClassNotFoundException {
+      isRecord = Class.class.getMethod("isRecord");
+      getRecordComponents = Class.class.getMethod("getRecordComponents");
+      Class<?> classRecordComponent = Class.forName("java.lang.reflect.RecordComponent");
+      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/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..1991daefb03a5e530acdcf60ee7469e6535cbde5
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlDateTypeAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 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.internal.sql;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Adapter for java.sql.Date. Although this class appears stateless, it is not. DateFormat captures
+ * its time zone and locale when it is created, which gives this class state. DateFormat isn't
+ * thread safe either, so this class has to synchronize its read and write methods.
+ */
+@SuppressWarnings("JavaUtilDate")
+final class SqlDateTypeAdapter extends TypeAdapter<java.sql.Date> {
+  static final TypeAdapterFactory FACTORY =
+      new TypeAdapterFactory() {
+        @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+        @Override
+        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+          return typeToken.getRawType() == java.sql.Date.class
+              ? (TypeAdapter<T>) new SqlDateTypeAdapter()
+              : null;
+        }
+      };
+
+  private final DateFormat format = new SimpleDateFormat("MMM d, yyyy");
+
+  private SqlDateTypeAdapter() {}
+
+  @Override
+  public java.sql.Date read(JsonReader in) throws IOException {
+    if (in.peek() == JsonToken.NULL) {
+      in.nextNull();
+      return null;
+    }
+    String s = in.nextString();
+    synchronized (this) {
+      TimeZone originalTimeZone = format.getTimeZone(); // Save the original time zone
+      try {
+        Date utilDate = format.parse(s);
+        return new java.sql.Date(utilDate.getTime());
+      } catch (ParseException e) {
+        throw new JsonSyntaxException(
+            "Failed parsing '" + s + "' as SQL Date; at path " + in.getPreviousPath(), e);
+      } finally {
+        format.setTimeZone(originalTimeZone); // Restore the original time zone after parsing
+      }
+    }
+  }
+
+  @Override
+  public void write(JsonWriter out, java.sql.Date value) throws IOException {
+    if (value == null) {
+      out.nullValue();
+      return;
+    }
+    String dateString;
+    synchronized (this) {
+      dateString = format.format(value);
+    }
+    out.value(dateString);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d63ae0677e91ce8e514ed2579a9d1a5a16464e98
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimeTypeAdapter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.internal.sql;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.sql.Time;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Adapter for java.sql.Time. Although this class appears stateless, it is not. DateFormat captures
+ * its time zone and locale when it is created, which gives this class state. DateFormat isn't
+ * thread safe either, so this class has to synchronize its read and write methods.
+ */
+@SuppressWarnings("JavaUtilDate")
+final class SqlTimeTypeAdapter extends TypeAdapter<Time> {
+  static final TypeAdapterFactory FACTORY =
+      new TypeAdapterFactory() {
+        @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+        @Override
+        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+          return typeToken.getRawType() == Time.class
+              ? (TypeAdapter<T>) new SqlTimeTypeAdapter()
+              : null;
+        }
+      };
+
+  private final DateFormat format = new SimpleDateFormat("hh:mm:ss a");
+
+  private SqlTimeTypeAdapter() {}
+
+  @Override
+  public Time read(JsonReader in) throws IOException {
+    if (in.peek() == JsonToken.NULL) {
+      in.nextNull();
+      return null;
+    }
+    String s = in.nextString();
+    synchronized (this) {
+      TimeZone originalTimeZone = format.getTimeZone(); // Save the original time zone
+      try {
+        Date date = format.parse(s);
+        return new Time(date.getTime());
+      } catch (ParseException e) {
+        throw new JsonSyntaxException(
+            "Failed parsing '" + s + "' as SQL Time; at path " + in.getPreviousPath(), e);
+      } finally {
+        format.setTimeZone(originalTimeZone); // Restore the original time zone
+      }
+    }
+  }
+
+  @Override
+  public void write(JsonWriter out, Time value) throws IOException {
+    if (value == null) {
+      out.nullValue();
+      return;
+    }
+    String timeString;
+    synchronized (this) {
+      timeString = format.format(value);
+    }
+    out.value(timeString);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimestampTypeAdapter.java b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimestampTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..63c4b86d5117ab6f0a7f19efbf81723b6c303443
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTimestampTypeAdapter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 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.internal.sql;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Date;
+
+@SuppressWarnings("JavaUtilDate")
+class SqlTimestampTypeAdapter extends TypeAdapter<Timestamp> {
+  static final TypeAdapterFactory FACTORY =
+      new TypeAdapterFactory() {
+        @SuppressWarnings("unchecked") // we use a runtime check to make sure the 'T's equal
+        @Override
+        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
+          if (typeToken.getRawType() == Timestamp.class) {
+            final TypeAdapter<Date> dateTypeAdapter = gson.getAdapter(Date.class);
+            return (TypeAdapter<T>) new SqlTimestampTypeAdapter(dateTypeAdapter);
+          } else {
+            return null;
+          }
+        }
+      };
+
+  private final TypeAdapter<Date> dateTypeAdapter;
+
+  private SqlTimestampTypeAdapter(TypeAdapter<Date> dateTypeAdapter) {
+    this.dateTypeAdapter = dateTypeAdapter;
+  }
+
+  @Override
+  public Timestamp read(JsonReader in) throws IOException {
+    Date date = dateTypeAdapter.read(in);
+    return date != null ? new Timestamp(date.getTime()) : null;
+  }
+
+  @Override
+  public void write(JsonWriter out, Timestamp value) throws IOException {
+    dateTypeAdapter.write(out, value);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTypesSupport.java b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTypesSupport.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1a384b0b91c9c487d4518bf60d9b281c29bb55f
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/internal/sql/SqlTypesSupport.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 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.internal.sql;
+
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.bind.DefaultDateTypeAdapter.DateType;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * Encapsulates access to {@code java.sql} types, to allow Gson to work without the {@code java.sql}
+ * module being present. No {@link ClassNotFoundException}s will be thrown in case the {@code
+ * java.sql} module is not present.
+ *
+ * <p>If {@link #SUPPORTS_SQL_TYPES} is {@code true}, all other constants of this class will be
+ * non-{@code null}. However, if it is {@code false} all other constants will be {@code null} and
+ * there will be no support for {@code java.sql} types.
+ */
+@SuppressWarnings("JavaUtilDate")
+public final class SqlTypesSupport {
+  /** {@code true} if {@code java.sql} types are supported, {@code false} otherwise */
+  public static final boolean SUPPORTS_SQL_TYPES;
+
+  public static final DateType<? extends Date> DATE_DATE_TYPE;
+  public static final DateType<? extends Date> TIMESTAMP_DATE_TYPE;
+
+  public static final TypeAdapterFactory DATE_FACTORY;
+  public static final TypeAdapterFactory TIME_FACTORY;
+  public static final TypeAdapterFactory TIMESTAMP_FACTORY;
+
+  static {
+    boolean sqlTypesSupport;
+    try {
+      Class.forName("java.sql.Date");
+      sqlTypesSupport = true;
+    } catch (ClassNotFoundException classNotFoundException) {
+      sqlTypesSupport = false;
+    }
+    SUPPORTS_SQL_TYPES = sqlTypesSupport;
+
+    if (SUPPORTS_SQL_TYPES) {
+      DATE_DATE_TYPE =
+          new DateType<java.sql.Date>(java.sql.Date.class) {
+            @Override
+            protected java.sql.Date deserialize(Date date) {
+              return new java.sql.Date(date.getTime());
+            }
+          };
+      TIMESTAMP_DATE_TYPE =
+          new DateType<Timestamp>(Timestamp.class) {
+            @Override
+            protected Timestamp deserialize(Date date) {
+              return new Timestamp(date.getTime());
+            }
+          };
+
+      DATE_FACTORY = SqlDateTypeAdapter.FACTORY;
+      TIME_FACTORY = SqlTimeTypeAdapter.FACTORY;
+      TIMESTAMP_FACTORY = SqlTimestampTypeAdapter.FACTORY;
+    } else {
+      DATE_DATE_TYPE = null;
+      TIMESTAMP_DATE_TYPE = null;
+
+      DATE_FACTORY = null;
+      TIME_FACTORY = null;
+      TIMESTAMP_FACTORY = null;
+    }
+  }
+
+  private SqlTypesSupport() {}
+}
diff --git a/gson/gson/src/main/java/com/google/gson/package-info.java b/gson/gson/src/main/java/com/google/gson/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..b7d3b9abe28557249dbe6102ba867d26d8a7bfa4
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package provides the {@link com.google.gson.Gson} class to convert Json to Java and
+ * vice-versa.
+ *
+ * <p>The primary class to use is {@link com.google.gson.Gson} which can be constructed with {@code
+ * new Gson()} (using default settings) or by using {@link com.google.gson.GsonBuilder} (to
+ * configure various options such as using versioning and so on).
+ *
+ * @author Inderjeet Singh, Joel Leitch
+ */
+package com.google.gson;
diff --git a/gson/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/gson/src/main/java/com/google/gson/reflect/TypeToken.java
new file mode 100644
index 0000000000000000000000000000000000000000..49de430039c7ab6e9d048ad7f4d384b774739abf
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/reflect/TypeToken.java
@@ -0,0 +1,452 @@
+/*
+ * 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.reflect;
+
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.internal.TroubleshootingGuide;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+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 represent generic types,
+ * so this class does. Forces clients to create a subclass of this class which enables retrieval the
+ * type information even at runtime.
+ *
+ * <p>For example, to create a type literal for {@code List<String>}, you can create an empty
+ * anonymous class:
+ *
+ * <p>{@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};}
+ *
+ * <p>Capturing a type variable as type argument of an anonymous {@code TypeToken} subclass is not
+ * allowed, for example {@code TypeToken<List<T>>}. Due to type erasure the runtime type of a type
+ * variable is not available to Gson and therefore it cannot provide the functionality one might
+ * expect. This would give a false sense of type-safety at compile time and could 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
+ */
+public class TypeToken<T> {
+  private final Class<? super T> rawType;
+  private final Type type;
+  private final int hashCode;
+
+  /**
+   * Constructs a new type literal. Derives represented class from type parameter.
+   *
+   * <p>Clients create an empty anonymous subclass. Doing so embeds the type parameter in the
+   * anonymous class's type hierarchy so we can reconstitute it at runtime despite erasure, for
+   * example:
+   *
+   * <p>{@code new TypeToken<List<String>>() {}}
+   *
+   * @throws IllegalArgumentException If the anonymous {@code TypeToken} subclass captures a type
+   *     variable, for example {@code TypeToken<List<T>>}. See the {@code TypeToken} class
+   *     documentation for more details.
+   */
+  @SuppressWarnings("unchecked")
+  protected TypeToken() {
+    this.type = getTypeTokenTypeArgument();
+    this.rawType = (Class<? super T>) $Gson$Types.getRawType(type);
+    this.hashCode = type.hashCode();
+  }
+
+  /** Unsafe. Constructs a type literal manually. */
+  @SuppressWarnings("unchecked")
+  private TypeToken(Type type) {
+    this.type = $Gson$Types.canonicalize(Objects.requireNonNull(type));
+    this.rawType = (Class<? super T>) $Gson$Types.getRawType(this.type);
+    this.hashCode = this.type.hashCode();
+  }
+
+  private static boolean isCapturingTypeVariablesForbidden() {
+    return !Objects.equals(System.getProperty("gson.allowCapturingTypeVariables"), "true");
+  }
+
+  /**
+   * Verifies that {@code this} is an instance of a direct subclass of TypeToken and returns the
+   * type argument for {@code T} in {@link $Gson$Types#canonicalize canonical form}.
+   */
+  private Type getTypeTokenTypeArgument() {
+    Type superclass = getClass().getGenericSuperclass();
+    if (superclass instanceof ParameterizedType) {
+      ParameterizedType parameterized = (ParameterizedType) superclass;
+      if (parameterized.getRawType() == TypeToken.class) {
+        Type typeArgument = $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
+
+        if (isCapturingTypeVariablesForbidden()) {
+          verifyNoTypeVariable(typeArgument);
+        }
+        return typeArgument;
+      }
+    }
+    // Check for raw TypeToken as superclass
+    else if (superclass == TypeToken.class) {
+      throw new IllegalStateException(
+          "TypeToken must be created with a type argument: new TypeToken<...>() {}; When using code"
+              + " shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved."
+              + "\nSee "
+              + TroubleshootingGuide.createUrl("type-token-raw"));
+    }
+
+    // User created subclass of subclass of TypeToken
+    throw new IllegalStateException("Must only create direct subclasses of TypeToken");
+  }
+
+  private static void verifyNoTypeVariable(Type type) {
+    if (type instanceof TypeVariable) {
+      TypeVariable<?> typeVariable = (TypeVariable<?>) type;
+      throw new IllegalArgumentException(
+          "TypeToken type argument must not contain a type variable; captured type variable "
+              + typeVariable.getName()
+              + " declared by "
+              + typeVariable.getGenericDeclaration()
+              + "\nSee "
+              + TroubleshootingGuide.createUrl("typetoken-type-variable"));
+    } else if (type instanceof GenericArrayType) {
+      verifyNoTypeVariable(((GenericArrayType) type).getGenericComponentType());
+    } else if (type instanceof ParameterizedType) {
+      ParameterizedType parameterizedType = (ParameterizedType) type;
+      Type ownerType = parameterizedType.getOwnerType();
+      if (ownerType != null) {
+        verifyNoTypeVariable(ownerType);
+      }
+
+      for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
+        verifyNoTypeVariable(typeArgument);
+      }
+    } else if (type instanceof WildcardType) {
+      WildcardType wildcardType = (WildcardType) type;
+      for (Type bound : wildcardType.getLowerBounds()) {
+        verifyNoTypeVariable(bound);
+      }
+      for (Type bound : wildcardType.getUpperBounds()) {
+        verifyNoTypeVariable(bound);
+      }
+    } else if (type == null) {
+      // Occurs in Eclipse IDE and certain Java versions (e.g. Java 11.0.18) when capturing type
+      // variable declared by method of local class, see
+      // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/975
+      throw new IllegalArgumentException(
+          "TypeToken captured `null` as type argument; probably a compiler / runtime bug");
+    }
+  }
+
+  /** Returns the raw (non-generic) type for this type. */
+  public final Class<? super T> getRawType() {
+    return rawType;
+  }
+
+  /** Gets underlying {@code Type} instance. */
+  public final Type getType() {
+    return type;
+  }
+
+  /**
+   * Check if this type is assignable from the given class object.
+   *
+   * @deprecated this implementation may be inconsistent with javac for types with wildcards.
+   */
+  @Deprecated
+  public boolean isAssignableFrom(Class<?> cls) {
+    return isAssignableFrom((Type) cls);
+  }
+
+  /**
+   * Check if this type is assignable from the given Type.
+   *
+   * @deprecated this implementation may be inconsistent with javac for types with wildcards.
+   */
+  @Deprecated
+  public boolean isAssignableFrom(Type from) {
+    if (from == null) {
+      return false;
+    }
+
+    if (type.equals(from)) {
+      return true;
+    }
+
+    if (type instanceof Class<?>) {
+      return rawType.isAssignableFrom($Gson$Types.getRawType(from));
+    } else if (type instanceof ParameterizedType) {
+      return isAssignableFrom(from, (ParameterizedType) type, new HashMap<String, Type>());
+    } else if (type instanceof GenericArrayType) {
+      return rawType.isAssignableFrom($Gson$Types.getRawType(from))
+          && isAssignableFrom(from, (GenericArrayType) type);
+    } else {
+      throw buildUnsupportedTypeException(
+          type, Class.class, ParameterizedType.class, GenericArrayType.class);
+    }
+  }
+
+  /**
+   * Check if this type is assignable from the given type token.
+   *
+   * @deprecated this implementation may be inconsistent with javac for types with wildcards.
+   */
+  @Deprecated
+  public boolean isAssignableFrom(TypeToken<?> token) {
+    return isAssignableFrom(token.getType());
+  }
+
+  /**
+   * Private helper function that performs some assignability checks for the provided
+   * GenericArrayType.
+   */
+  private static boolean isAssignableFrom(Type from, GenericArrayType to) {
+    Type toGenericComponentType = to.getGenericComponentType();
+    if (toGenericComponentType instanceof ParameterizedType) {
+      Type t = from;
+      if (from instanceof GenericArrayType) {
+        t = ((GenericArrayType) from).getGenericComponentType();
+      } else if (from instanceof Class<?>) {
+        Class<?> classType = (Class<?>) from;
+        while (classType.isArray()) {
+          classType = classType.getComponentType();
+        }
+        t = classType;
+      }
+      return isAssignableFrom(
+          t, (ParameterizedType) toGenericComponentType, new HashMap<String, Type>());
+    }
+    // No generic defined on "to"; therefore, return true and let other
+    // checks determine assignability
+    return true;
+  }
+
+  /** Private recursive helper function to actually do the type-safe checking of assignability. */
+  private static boolean isAssignableFrom(
+      Type from, ParameterizedType to, Map<String, Type> typeVarMap) {
+
+    if (from == null) {
+      return false;
+    }
+
+    if (to.equals(from)) {
+      return true;
+    }
+
+    // First figure out the class and any type information.
+    Class<?> clazz = $Gson$Types.getRawType(from);
+    ParameterizedType ptype = null;
+    if (from instanceof ParameterizedType) {
+      ptype = (ParameterizedType) from;
+    }
+
+    // Load up parameterized variable info if it was parameterized.
+    if (ptype != null) {
+      Type[] tArgs = ptype.getActualTypeArguments();
+      TypeVariable<?>[] tParams = clazz.getTypeParameters();
+      for (int i = 0; i < tArgs.length; i++) {
+        Type arg = tArgs[i];
+        TypeVariable<?> var = tParams[i];
+        while (arg instanceof TypeVariable<?>) {
+          TypeVariable<?> v = (TypeVariable<?>) arg;
+          arg = typeVarMap.get(v.getName());
+        }
+        typeVarMap.put(var.getName(), arg);
+      }
+
+      // check if they are equivalent under our current mapping.
+      if (typeEquals(ptype, to, typeVarMap)) {
+        return true;
+      }
+    }
+
+    for (Type itype : clazz.getGenericInterfaces()) {
+      if (isAssignableFrom(itype, to, new HashMap<>(typeVarMap))) {
+        return true;
+      }
+    }
+
+    // Interfaces didn't work, try the superclass.
+    Type sType = clazz.getGenericSuperclass();
+    return isAssignableFrom(sType, to, new HashMap<>(typeVarMap));
+  }
+
+  /**
+   * Checks if two parameterized types are exactly equal, under the variable replacement described
+   * in the typeVarMap.
+   */
+  private static boolean typeEquals(
+      ParameterizedType from, ParameterizedType to, Map<String, Type> typeVarMap) {
+    if (from.getRawType().equals(to.getRawType())) {
+      Type[] fromArgs = from.getActualTypeArguments();
+      Type[] toArgs = to.getActualTypeArguments();
+      for (int i = 0; i < fromArgs.length; i++) {
+        if (!matches(fromArgs[i], toArgs[i], typeVarMap)) {
+          return false;
+        }
+      }
+      return true;
+    }
+    return false;
+  }
+
+  private static IllegalArgumentException buildUnsupportedTypeException(
+      Type token, Class<?>... expected) {
+
+    // Build exception message
+    StringBuilder exceptionMessage = new StringBuilder("Unsupported type, expected one of: ");
+    for (Class<?> clazz : expected) {
+      exceptionMessage.append(clazz.getName()).append(", ");
+    }
+    exceptionMessage
+        .append("but got: ")
+        .append(token.getClass().getName())
+        .append(", for type token: ")
+        .append(token.toString());
+
+    return new IllegalArgumentException(exceptionMessage.toString());
+  }
+
+  /**
+   * Checks if two types are the same or are equivalent under a variable mapping given in the type
+   * map that was provided.
+   */
+  private static boolean matches(Type from, Type to, Map<String, Type> typeMap) {
+    return to.equals(from)
+        || (from instanceof TypeVariable
+            && to.equals(typeMap.get(((TypeVariable<?>) from).getName())));
+  }
+
+  @Override
+  public final int hashCode() {
+    return this.hashCode;
+  }
+
+  @Override
+  public final boolean equals(Object o) {
+    return o instanceof TypeToken<?> && $Gson$Types.equals(type, ((TypeToken<?>) o).type);
+  }
+
+  @Override
+  public final String toString() {
+    return $Gson$Types.typeToString(type);
+  }
+
+  /** Gets type literal for the given {@code Type} instance. */
+  public static TypeToken<?> get(Type type) {
+    return new TypeToken<>(type);
+  }
+
+  /** Gets type literal for the given {@code Class} instance. */
+  public static <T> TypeToken<T> get(Class<T> type) {
+    return new TypeToken<>(type);
+  }
+
+  /**
+   * 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.
+   *
+   * <p>If {@code rawType} is a non-generic class and no type arguments are provided, this method
+   * simply delegates to {@link #get(Class)} and creates a {@code TypeToken(Class)}.
+   *
+   * @throws IllegalArgumentException If {@code rawType} is not of type {@code Class}, 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 legacy reasons create a TypeToken(Class) if the type is not generic
+    if (typeArguments.length == 0) {
+      return get(rawClass);
+    }
+
+    // Check for this here to avoid misleading exception thrown by ParameterizedTypeImpl
+    if ($Gson$Types.requiresOwnerType(rawType)) {
+      throw new IllegalArgumentException(
+          "Raw type "
+              + rawClass.getName()
+              + " is not supported because it requires specifying an owner type");
+    }
+
+    for (int i = 0; i < expectedArgsCount; i++) {
+      Type typeArgument =
+          Objects.requireNonNull(typeArguments[i], "Type argument must not be null");
+      Class<?> rawTypeArgument = $Gson$Types.getRawType(typeArgument);
+      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));
+  }
+
+  /**
+   * Gets type literal for the array type whose elements are all instances of {@code componentType}.
+   */
+  public static TypeToken<?> getArray(Type componentType) {
+    return new TypeToken<>($Gson$Types.arrayOf(componentType));
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/reflect/package-info.java b/gson/gson/src/main/java/com/google/gson/reflect/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf51ffc7f695ab951cc36811ffacbd9a383f35be
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/reflect/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * This package provides utility classes for finding type information for generic types.
+ *
+ * @author Inderjeet Singh, Joel Leitch
+ */
+package com.google.gson.reflect;
diff --git a/gson/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/gson/src/main/java/com/google/gson/stream/JsonReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..2dc4e654024493d19e1b19e261021b604d68c4b1
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/JsonReader.java
@@ -0,0 +1,1806 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.Strictness;
+import com.google.gson.internal.JsonReaderInternalAccess;
+import com.google.gson.internal.TroubleshootingGuide;
+import com.google.gson.internal.bind.JsonTreeReader;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Reads a JSON (<a href="https://www.ietf.org/rfc/rfc8259.txt">RFC 8259</a>) encoded value as a
+ * stream of tokens. This stream includes both literal values (strings, numbers, booleans, and
+ * nulls) as well as the begin and end delimiters of objects and arrays. The tokens are traversed in
+ * 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.
+ *
+ * <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}.
+ *
+ * <p>Next, create handler methods for each structure in your JSON text. You'll need a method for
+ * each object type and for each array type.
+ *
+ * <ul>
+ *   <li>Within <strong>array handling</strong> methods, first call {@link #beginArray} to consume
+ *       the array's opening bracket. Then create a while loop that accumulates values, terminating
+ *       when {@link #hasNext} is false. Finally, read the array's closing bracket by calling {@link
+ *       #endArray}.
+ *   <li>Within <strong>object handling</strong> methods, first call {@link #beginObject} to consume
+ *       the object's opening brace. Then create a while loop that assigns values to local variables
+ *       based on their name. This loop should terminate when {@link #hasNext} is false. Finally,
+ *       read the object's closing brace by calling {@link #endObject}.
+ * </ul>
+ *
+ * <p>When a nested object or array is encountered, delegate to the corresponding handler method.
+ *
+ * <p>When an unknown name is encountered, strict parsers should fail with an exception. Lenient
+ * parsers should call {@link #skipValue()} to recursively skip the value's nested tokens, which may
+ * otherwise conflict.
+ *
+ * <p>If a value may be null, you should first check using {@link #peek()}. Null literals can be
+ * consumed using either {@link #nextNull()} or {@link #skipValue()}.
+ *
+ * <h2>Configuration</h2>
+ *
+ * The behavior of this reader can be customized with the following methods:
+ *
+ * <ul>
+ *   <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
+ * </ul>
+ *
+ * The default configuration of {@code JsonReader} instances used internally by the {@link Gson}
+ * class differs, and can be adjusted with the various {@link GsonBuilder} methods.
+ *
+ * <h2>Example</h2>
+ *
+ * Suppose we'd like to parse a stream of messages such as the following:
+ *
+ * <pre>{@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I read a JSON stream in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonReader!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]
+ * }</pre>
+ *
+ * This code implements the parser for the above structure:
+ *
+ * <pre>{@code
+ * public List<Message> readJsonStream(InputStream in) throws IOException {
+ *   JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ *   try {
+ *     return readMessagesArray(reader);
+ *   } finally {
+ *     reader.close();
+ *   }
+ * }
+ *
+ * public List<Message> readMessagesArray(JsonReader reader) throws IOException {
+ *   List<Message> messages = new ArrayList<>();
+ *
+ *   reader.beginArray();
+ *   while (reader.hasNext()) {
+ *     messages.add(readMessage(reader));
+ *   }
+ *   reader.endArray();
+ *   return messages;
+ * }
+ *
+ * public Message readMessage(JsonReader reader) throws IOException {
+ *   long id = -1;
+ *   String text = null;
+ *   User user = null;
+ *   List<Double> geo = null;
+ *
+ *   reader.beginObject();
+ *   while (reader.hasNext()) {
+ *     String name = reader.nextName();
+ *     if (name.equals("id")) {
+ *       id = reader.nextLong();
+ *     } else if (name.equals("text")) {
+ *       text = reader.nextString();
+ *     } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
+ *       geo = readDoublesArray(reader);
+ *     } else if (name.equals("user")) {
+ *       user = readUser(reader);
+ *     } else {
+ *       reader.skipValue();
+ *     }
+ *   }
+ *   reader.endObject();
+ *   return new Message(id, text, user, geo);
+ * }
+ *
+ * public List<Double> readDoublesArray(JsonReader reader) throws IOException {
+ *   List<Double> doubles = new ArrayList<>();
+ *
+ *   reader.beginArray();
+ *   while (reader.hasNext()) {
+ *     doubles.add(reader.nextDouble());
+ *   }
+ *   reader.endArray();
+ *   return doubles;
+ * }
+ *
+ * public User readUser(JsonReader reader) throws IOException {
+ *   String username = null;
+ *   int followersCount = -1;
+ *
+ *   reader.beginObject();
+ *   while (reader.hasNext()) {
+ *     String name = reader.nextName();
+ *     if (name.equals("name")) {
+ *       username = reader.nextString();
+ *     } else if (name.equals("followers_count")) {
+ *       followersCount = reader.nextInt();
+ *     } else {
+ *       reader.skipValue();
+ *     }
+ *   }
+ *   reader.endObject();
+ *   return new User(username, followersCount);
+ * }
+ * }</pre>
+ *
+ * <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}. This behavior is intended to prevent lossy numeric conversions:
+ * double is JavaScript's only numeric type and very large values like {@code 9007199254740993}
+ * cannot be represented exactly on that platform. To minimize precision loss, extremely large
+ * values should be written and read as strings in JSON.
+ *
+ * <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 to a private JSON file by
+ * executing it with an HTML {@code <script>} tag.
+ *
+ * <p>Prefixing JSON files with <code>")]}'\n"</code> makes them non-executable by {@code <script>}
+ * tags, disarming the attack. Since the prefix is malformed JSON, strict parsing fails when it is
+ * encountered. This class permits the non-execute prefix when {@linkplain
+ * #setStrictness(Strictness) lenient parsing} is enabled.
+ *
+ * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances of this class are
+ * not thread safe.
+ *
+ * @author Jesse Wilson
+ * @since 1.6
+ */
+public class JsonReader implements Closeable {
+  private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
+
+  private static final int PEEKED_NONE = 0;
+  private static final int PEEKED_BEGIN_OBJECT = 1;
+  private static final int PEEKED_END_OBJECT = 2;
+  private static final int PEEKED_BEGIN_ARRAY = 3;
+  private static final int PEEKED_END_ARRAY = 4;
+  private static final int PEEKED_TRUE = 5;
+  private static final int PEEKED_FALSE = 6;
+  private static final int PEEKED_NULL = 7;
+  private static final int PEEKED_SINGLE_QUOTED = 8;
+  private static final int PEEKED_DOUBLE_QUOTED = 9;
+  private static final int PEEKED_UNQUOTED = 10;
+
+  /** When this is returned, the string value is stored in peekedString. */
+  private static final int PEEKED_BUFFERED = 11;
+
+  private static final int PEEKED_SINGLE_QUOTED_NAME = 12;
+  private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;
+  private static final int PEEKED_UNQUOTED_NAME = 14;
+
+  /** When this is returned, the integer value is stored in peekedLong. */
+  private static final int PEEKED_LONG = 15;
+
+  private static final int PEEKED_NUMBER = 16;
+  private static final int PEEKED_EOF = 17;
+
+  /* State machine when parsing numbers */
+  private static final int NUMBER_CHAR_NONE = 0;
+  private static final int NUMBER_CHAR_SIGN = 1;
+  private static final int NUMBER_CHAR_DIGIT = 2;
+  private static final int NUMBER_CHAR_DECIMAL = 3;
+  private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;
+  private static final int NUMBER_CHAR_EXP_E = 5;
+  private static final int NUMBER_CHAR_EXP_SIGN = 6;
+  private static final int NUMBER_CHAR_EXP_DIGIT = 7;
+
+  /** The input JSON. */
+  private final Reader in;
+
+  private Strictness strictness = Strictness.LEGACY_STRICT;
+
+  static final int BUFFER_SIZE = 1024;
+
+  /**
+   * Use a manual buffer to easily read and unread upcoming characters, and also so we can create
+   * strings without an intermediate StringBuilder. We decode literals directly out of this buffer,
+   * so it must be at least as long as the longest token that can be reported as a number.
+   */
+  private final char[] buffer = new char[BUFFER_SIZE];
+
+  private int pos = 0;
+  private int limit = 0;
+
+  private int lineNumber = 0;
+  private int lineStart = 0;
+
+  int peeked = PEEKED_NONE;
+
+  /**
+   * A peeked value that was composed entirely of digits with an optional leading dash. Positive
+   * values may not have a leading 0.
+   */
+  private long peekedLong;
+
+  /**
+   * The number of characters in a peeked number literal. Increment 'pos' by this after reading a
+   * number.
+   */
+  private int peekedNumberLength;
+
+  /**
+   * A peeked string that should be parsed on the next double, long or string. This is populated
+   * before a numeric value is parsed and used if that parsing fails.
+   */
+  private String peekedString;
+
+  /*
+   * The nesting stack. Using a manual array rather than an ArrayList saves 20%.
+   */
+  private int[] stack = new int[32];
+  private int stackSize = 0;
+
+  {
+    stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
+  }
+
+  /*
+   * The path members. It corresponds directly to stack: At indices where the
+   * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
+   * pathNames contains the name at this scope. Where it contains an array
+   * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
+   * that array. Otherwise the value is undefined, and we take advantage of that
+   * by incrementing pathIndices when doing so isn't useful.
+   */
+  private String[] pathNames = new String[32];
+  private int[] pathIndices = new int[32];
+
+  /** Creates a new instance that reads a JSON-encoded stream from {@code in}. */
+  public JsonReader(Reader in) {
+    this.in = Objects.requireNonNull(in, "in == null");
+  }
+
+  /**
+   * Sets the strictness of this reader.
+   *
+   * @deprecated Please use {@link #setStrictness(Strictness)} instead. {@code
+   *     JsonReader.setLenient(true)} should be replaced by {@code
+   *     JsonReader.setStrictness(Strictness.LENIENT)} and {@code JsonReader.setLenient(false)}
+   *     should be replaced by {@code JsonReader.setStrictness(Strictness.LEGACY_STRICT)}.<br>
+   *     However, if you used {@code setLenient(false)} before, you might prefer {@link
+   *     Strictness#STRICT} now instead.
+   * @param lenient whether this reader should be lenient. If true, the strictness is set to {@link
+   *     Strictness#LENIENT}. If false, the strictness is set to {@link Strictness#LEGACY_STRICT}.
+   * @see #setStrictness(Strictness)
+   */
+  @Deprecated
+  // Don't specify @InlineMe, so caller with `setLenient(false)` becomes aware of new
+  // Strictness.STRICT
+  @SuppressWarnings("InlineMeSuggester")
+  public final void setLenient(boolean lenient) {
+    setStrictness(lenient ? Strictness.LENIENT : Strictness.LEGACY_STRICT);
+  }
+
+  /**
+   * Returns true if the {@link Strictness} of this reader is equal to {@link Strictness#LENIENT}.
+   *
+   * @see #setStrictness(Strictness)
+   */
+  public final boolean isLenient() {
+    return strictness == Strictness.LENIENT;
+  }
+
+  /**
+   * Configures how liberal this parser is in what it accepts.
+   *
+   * <p>In {@linkplain Strictness#STRICT strict} mode, the parser only accepts JSON in accordance
+   * with <a href="https://www.ietf.org/rfc/rfc8259.txt">RFC 8259</a>. In {@linkplain
+   * Strictness#LEGACY_STRICT legacy strict} mode (the default), only JSON in accordance with the
+   * RFC 8259 is accepted, with a few exceptions denoted below for backwards compatibility reasons.
+   * In {@linkplain Strictness#LENIENT lenient} mode, all sort of non-spec compliant JSON is
+   * accepted (see below).
+   *
+   * <dl>
+   *   <dt>{@link Strictness#STRICT}
+   *   <dd>In strict mode, only input compliant with RFC 8259 is accepted.
+   *   <dt>{@link Strictness#LEGACY_STRICT}
+   *   <dd>In legacy strict mode, the following departures from RFC 8259 are accepted:
+   *       <ul>
+   *         <li>JsonReader allows the literals {@code true}, {@code false} and {@code null} to have
+   *             any capitalization, for example {@code fAlSe} or {@code NULL}
+   *         <li>JsonReader supports the escape sequence {@code \'}, representing a {@code '}
+   *             (single-quote)
+   *         <li>JsonReader supports the escape sequence <code>\<i>LF</i></code> (with {@code LF}
+   *             being the Unicode character {@code U+000A}), resulting in a {@code LF} within the
+   *             read JSON string
+   *         <li>JsonReader allows unescaped control characters ({@code U+0000} through {@code
+   *             U+001F})
+   *       </ul>
+   *   <dt>{@link Strictness#LENIENT}
+   *   <dd>In lenient mode, all input that is accepted in legacy strict mode is accepted in addition
+   *       to the following departures from RFC 8259:
+   *       <ul>
+   *         <li>Streams that start with the <a href="#nonexecuteprefix">non-execute prefix</a>,
+   *             {@code ")]}'\n"}
+   *         <li>Streams that include multiple top-level values. With legacy strict or strict
+   *             parsing, each stream must contain exactly one top-level value.
+   *         <li>Numbers may be {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
+   *             infinities} represented by {@code NaN} and {@code (-)Infinity} respectively.
+   *         <li>End of line comments starting with {@code //} or {@code #} and ending with a
+   *             newline character.
+   *         <li>C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such
+   *             comments may not be nested.
+   *         <li>Names that are unquoted or {@code 'single quoted'}.
+   *         <li>Strings that are unquoted or {@code 'single quoted'}.
+   *         <li>Array elements separated by {@code ;} instead of {@code ,}.
+   *         <li>Unnecessary array separators. These are interpreted as if null was the omitted
+   *             value.
+   *         <li>Names and values separated by {@code =} or {@code =>} instead of {@code :}.
+   *         <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
+   *       </ul>
+   * </dl>
+   *
+   * @param strictness the new strictness value of this reader. May not be {@code null}.
+   * @since $next-version$
+   */
+  public final void setStrictness(Strictness strictness) {
+    Objects.requireNonNull(strictness);
+    this.strictness = strictness;
+  }
+
+  /**
+   * Returns the {@linkplain Strictness strictness} of this reader.
+   *
+   * @see #setStrictness(Strictness)
+   * @since $next-version$
+   */
+  public final Strictness getStrictness() {
+    return strictness;
+  }
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
+   * array.
+   */
+  public void beginArray() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_BEGIN_ARRAY) {
+      push(JsonScope.EMPTY_ARRAY);
+      pathIndices[stackSize - 1] = 0;
+      peeked = PEEKED_NONE;
+    } else {
+      throw unexpectedTokenError("BEGIN_ARRAY");
+    }
+  }
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the end of the current
+   * array.
+   */
+  public void endArray() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_END_ARRAY) {
+      stackSize--;
+      pathIndices[stackSize - 1]++;
+      peeked = PEEKED_NONE;
+    } else {
+      throw unexpectedTokenError("END_ARRAY");
+    }
+  }
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
+   * object.
+   */
+  public void beginObject() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_BEGIN_OBJECT) {
+      push(JsonScope.EMPTY_OBJECT);
+      peeked = PEEKED_NONE;
+    } else {
+      throw unexpectedTokenError("BEGIN_OBJECT");
+    }
+  }
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is the end of the current
+   * object.
+   */
+  public void endObject() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_END_OBJECT) {
+      stackSize--;
+      pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
+      pathIndices[stackSize - 1]++;
+      peeked = PEEKED_NONE;
+    } else {
+      throw unexpectedTokenError("END_OBJECT");
+    }
+  }
+
+  /** Returns true if the current array or object has another element. */
+  public boolean hasNext() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
+  }
+
+  /** Returns the type of the next token without consuming it. */
+  public JsonToken peek() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    switch (p) {
+      case PEEKED_BEGIN_OBJECT:
+        return JsonToken.BEGIN_OBJECT;
+      case PEEKED_END_OBJECT:
+        return JsonToken.END_OBJECT;
+      case PEEKED_BEGIN_ARRAY:
+        return JsonToken.BEGIN_ARRAY;
+      case PEEKED_END_ARRAY:
+        return JsonToken.END_ARRAY;
+      case PEEKED_SINGLE_QUOTED_NAME:
+      case PEEKED_DOUBLE_QUOTED_NAME:
+      case PEEKED_UNQUOTED_NAME:
+        return JsonToken.NAME;
+      case PEEKED_TRUE:
+      case PEEKED_FALSE:
+        return JsonToken.BOOLEAN;
+      case PEEKED_NULL:
+        return JsonToken.NULL;
+      case PEEKED_SINGLE_QUOTED:
+      case PEEKED_DOUBLE_QUOTED:
+      case PEEKED_UNQUOTED:
+      case PEEKED_BUFFERED:
+        return JsonToken.STRING;
+      case PEEKED_LONG:
+      case PEEKED_NUMBER:
+        return JsonToken.NUMBER;
+      case PEEKED_EOF:
+        return JsonToken.END_DOCUMENT;
+      default:
+        throw new AssertionError();
+    }
+  }
+
+  @SuppressWarnings("fallthrough")
+  int doPeek() throws IOException {
+    int peekStack = stack[stackSize - 1];
+    if (peekStack == JsonScope.EMPTY_ARRAY) {
+      stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
+    } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
+      // Look for a comma before the next element.
+      int c = nextNonWhitespace(true);
+      switch (c) {
+        case ']':
+          return peeked = PEEKED_END_ARRAY;
+        case ';':
+          checkLenient(); // fall-through
+        case ',':
+          break;
+        default:
+          throw syntaxError("Unterminated array");
+      }
+    } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
+      stack[stackSize - 1] = JsonScope.DANGLING_NAME;
+      // Look for a comma before the next element.
+      if (peekStack == JsonScope.NONEMPTY_OBJECT) {
+        int c = nextNonWhitespace(true);
+        switch (c) {
+          case '}':
+            return peeked = PEEKED_END_OBJECT;
+          case ';':
+            checkLenient(); // fall-through
+          case ',':
+            break;
+          default:
+            throw syntaxError("Unterminated object");
+        }
+      }
+      int c = nextNonWhitespace(true);
+      switch (c) {
+        case '"':
+          return peeked = PEEKED_DOUBLE_QUOTED_NAME;
+        case '\'':
+          checkLenient();
+          return peeked = PEEKED_SINGLE_QUOTED_NAME;
+        case '}':
+          if (peekStack != JsonScope.NONEMPTY_OBJECT) {
+            return peeked = PEEKED_END_OBJECT;
+          } else {
+            throw syntaxError("Expected name");
+          }
+        default:
+          checkLenient();
+          pos--; // Don't consume the first character in an unquoted string.
+          if (isLiteral((char) c)) {
+            return peeked = PEEKED_UNQUOTED_NAME;
+          } else {
+            throw syntaxError("Expected name");
+          }
+      }
+    } else if (peekStack == JsonScope.DANGLING_NAME) {
+      stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
+      // Look for a colon before the value.
+      int c = nextNonWhitespace(true);
+      switch (c) {
+        case ':':
+          break;
+        case '=':
+          checkLenient();
+          if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
+            pos++;
+          }
+          break;
+        default:
+          throw syntaxError("Expected ':'");
+      }
+    } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
+      if (strictness == Strictness.LENIENT) {
+        consumeNonExecutePrefix();
+      }
+      stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
+    } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
+      int c = nextNonWhitespace(false);
+      if (c == -1) {
+        return peeked = PEEKED_EOF;
+      } else {
+        checkLenient();
+        pos--;
+      }
+    } else if (peekStack == JsonScope.CLOSED) {
+      throw new IllegalStateException("JsonReader is closed");
+    }
+
+    int c = nextNonWhitespace(true);
+    switch (c) {
+      case ']':
+        if (peekStack == JsonScope.EMPTY_ARRAY) {
+          return peeked = PEEKED_END_ARRAY;
+        }
+        // fall-through to handle ",]"
+      case ';':
+      case ',':
+        // In lenient mode, a 0-length literal in an array means 'null'.
+        if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
+          checkLenient();
+          pos--;
+          return peeked = PEEKED_NULL;
+        } else {
+          throw syntaxError("Unexpected value");
+        }
+      case '\'':
+        checkLenient();
+        return peeked = PEEKED_SINGLE_QUOTED;
+      case '"':
+        return peeked = PEEKED_DOUBLE_QUOTED;
+      case '[':
+        return peeked = PEEKED_BEGIN_ARRAY;
+      case '{':
+        return peeked = PEEKED_BEGIN_OBJECT;
+      default:
+        pos--; // Don't consume the first character in a literal value.
+    }
+
+    int result = peekKeyword();
+    if (result != PEEKED_NONE) {
+      return result;
+    }
+
+    result = peekNumber();
+    if (result != PEEKED_NONE) {
+      return result;
+    }
+
+    if (!isLiteral(buffer[pos])) {
+      throw syntaxError("Expected value");
+    }
+
+    checkLenient();
+    return peeked = PEEKED_UNQUOTED;
+  }
+
+  private int peekKeyword() throws IOException {
+    // Figure out which keyword we're matching against by its first character.
+    char c = buffer[pos];
+    String keyword;
+    String keywordUpper;
+    int peeking;
+
+    // Look at the first letter to determine what keyword we are trying to match.
+    if (c == 't' || c == 'T') {
+      keyword = "true";
+      keywordUpper = "TRUE";
+      peeking = PEEKED_TRUE;
+    } else if (c == 'f' || c == 'F') {
+      keyword = "false";
+      keywordUpper = "FALSE";
+      peeking = PEEKED_FALSE;
+    } else if (c == 'n' || c == 'N') {
+      keyword = "null";
+      keywordUpper = "NULL";
+      peeking = PEEKED_NULL;
+    } else {
+      return PEEKED_NONE;
+    }
+
+    // Uppercased keywords are not allowed in STRICT mode
+    boolean allowsUpperCased = strictness != Strictness.STRICT;
+
+    // Confirm that chars [0..length) match the keyword.
+    int length = keyword.length();
+    for (int i = 0; i < length; i++) {
+      if (pos + i >= limit && !fillBuffer(i + 1)) {
+        return PEEKED_NONE;
+      }
+      c = buffer[pos + i];
+      boolean matched = c == keyword.charAt(i) || (allowsUpperCased && c == keywordUpper.charAt(i));
+      if (!matched) {
+        return PEEKED_NONE;
+      }
+    }
+
+    if ((pos + length < limit || fillBuffer(length + 1)) && isLiteral(buffer[pos + length])) {
+      return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
+    }
+
+    // We've found the keyword followed either by EOF or by a non-literal character.
+    pos += length;
+    return peeked = peeking;
+  }
+
+  private int peekNumber() throws IOException {
+    // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
+    char[] buffer = this.buffer;
+    int p = pos;
+    int l = limit;
+
+    long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
+    boolean negative = false;
+    boolean fitsInLong = true;
+    int last = NUMBER_CHAR_NONE;
+
+    int i = 0;
+
+    charactersOfNumber:
+    for (; true; i++) {
+      if (p + i == l) {
+        if (i == buffer.length) {
+          // Though this looks like a well-formed number, it's too long to continue reading. Give up
+          // and let the application handle this as an unquoted literal.
+          return PEEKED_NONE;
+        }
+        if (!fillBuffer(i + 1)) {
+          break;
+        }
+        p = pos;
+        l = limit;
+      }
+
+      char c = buffer[p + i];
+      switch (c) {
+        case '-':
+          if (last == NUMBER_CHAR_NONE) {
+            negative = true;
+            last = NUMBER_CHAR_SIGN;
+            continue;
+          } else if (last == NUMBER_CHAR_EXP_E) {
+            last = NUMBER_CHAR_EXP_SIGN;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case '+':
+          if (last == NUMBER_CHAR_EXP_E) {
+            last = NUMBER_CHAR_EXP_SIGN;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case 'e':
+        case 'E':
+          if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
+            last = NUMBER_CHAR_EXP_E;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        case '.':
+          if (last == NUMBER_CHAR_DIGIT) {
+            last = NUMBER_CHAR_DECIMAL;
+            continue;
+          }
+          return PEEKED_NONE;
+
+        default:
+          if (c < '0' || c > '9') {
+            if (!isLiteral(c)) {
+              break charactersOfNumber;
+            }
+            return PEEKED_NONE;
+          }
+          if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
+            value = -(c - '0');
+            last = NUMBER_CHAR_DIGIT;
+          } else if (last == NUMBER_CHAR_DIGIT) {
+            if (value == 0) {
+              return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
+            }
+            long newValue = value * 10 - (c - '0');
+            fitsInLong &=
+                value > MIN_INCOMPLETE_INTEGER
+                    || (value == MIN_INCOMPLETE_INTEGER && newValue < value);
+            value = newValue;
+          } else if (last == NUMBER_CHAR_DECIMAL) {
+            last = NUMBER_CHAR_FRACTION_DIGIT;
+          } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
+            last = NUMBER_CHAR_EXP_DIGIT;
+          }
+      }
+    }
+
+    // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
+    // Don't store -0 as long; user might want to read it as double -0.0
+    // Don't try to convert Long.MIN_VALUE to positive long; it would overflow MAX_VALUE
+    if (last == NUMBER_CHAR_DIGIT
+        && fitsInLong
+        && (value != Long.MIN_VALUE || negative)
+        && (value != 0 || !negative)) {
+      peekedLong = negative ? value : -value;
+      pos += i;
+      return peeked = PEEKED_LONG;
+    } else if (last == NUMBER_CHAR_DIGIT
+        || last == NUMBER_CHAR_FRACTION_DIGIT
+        || last == NUMBER_CHAR_EXP_DIGIT) {
+      peekedNumberLength = i;
+      return peeked = PEEKED_NUMBER;
+    } else {
+      return PEEKED_NONE;
+    }
+  }
+
+  @SuppressWarnings("fallthrough")
+  private boolean isLiteral(char c) throws IOException {
+    switch (c) {
+      case '/':
+      case '\\':
+      case ';':
+      case '#':
+      case '=':
+        checkLenient(); // fall-through
+      case '{':
+      case '}':
+      case '[':
+      case ']':
+      case ':':
+      case ',':
+      case ' ':
+      case '\t':
+      case '\f':
+      case '\r':
+      case '\n':
+        return false;
+      default:
+        return true;
+    }
+  }
+
+  /**
+   * Returns the next token, a {@link JsonToken#NAME property name}, and consumes it.
+   *
+   * @throws IOException if the next token in the stream is not a property name.
+   */
+  public String nextName() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    String result;
+    if (p == PEEKED_UNQUOTED_NAME) {
+      result = nextUnquotedValue();
+    } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
+      result = nextQuotedValue('\'');
+    } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
+      result = nextQuotedValue('"');
+    } else {
+      throw unexpectedTokenError("a name");
+    }
+    peeked = PEEKED_NONE;
+    pathNames[stackSize - 1] = result;
+    return result;
+  }
+
+  /**
+   * 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.
+   *
+   * @throws IllegalStateException if the next token is not a string or if this reader is closed.
+   */
+  public String nextString() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    String result;
+    if (p == PEEKED_UNQUOTED) {
+      result = nextUnquotedValue();
+    } else if (p == PEEKED_SINGLE_QUOTED) {
+      result = nextQuotedValue('\'');
+    } else if (p == PEEKED_DOUBLE_QUOTED) {
+      result = nextQuotedValue('"');
+    } else if (p == PEEKED_BUFFERED) {
+      result = peekedString;
+      peekedString = null;
+    } else if (p == PEEKED_LONG) {
+      result = Long.toString(peekedLong);
+    } else if (p == PEEKED_NUMBER) {
+      result = new String(buffer, pos, peekedNumberLength);
+      pos += peekedNumberLength;
+    } else {
+      throw unexpectedTokenError("a string");
+    }
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /**
+   * 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 this reader is closed.
+   */
+  public boolean nextBoolean() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_TRUE) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return true;
+    } else if (p == PEEKED_FALSE) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return false;
+    }
+    throw unexpectedTokenError("a boolean");
+  }
+
+  /**
+   * Consumes the next token from the JSON stream and asserts that it is a literal null.
+   *
+   * @throws IllegalStateException if the next token is not null or if this reader is closed.
+   */
+  public void nextNull() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+    if (p == PEEKED_NULL) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+    } else {
+      throw unexpectedTokenError("null");
+    }
+  }
+
+  /**
+   * 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.
+   * @throws MalformedJsonException if the next literal value is NaN or Infinity and this reader is
+   *     not {@link #setStrictness(Strictness) lenient}.
+   */
+  public double nextDouble() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    if (p == PEEKED_LONG) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return (double) peekedLong;
+    }
+
+    if (p == PEEKED_NUMBER) {
+      peekedString = new String(buffer, pos, peekedNumberLength);
+      pos += peekedNumberLength;
+    } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
+      peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
+    } else if (p == PEEKED_UNQUOTED) {
+      peekedString = nextUnquotedValue();
+    } else if (p != PEEKED_BUFFERED) {
+      throw unexpectedTokenError("a double");
+    }
+
+    peeked = PEEKED_BUFFERED;
+    double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
+    if (strictness != Strictness.LENIENT && (Double.isNaN(result) || Double.isInfinite(result))) {
+      throw syntaxError("JSON forbids NaN and infinities: " + result);
+    }
+    peekedString = null;
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /**
+   * 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.
+   *
+   * @throws IllegalStateException if the next token is not a literal value.
+   * @throws NumberFormatException if the next literal value cannot be parsed as a number, or
+   *     exactly represented as a long.
+   */
+  public long nextLong() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    if (p == PEEKED_LONG) {
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return peekedLong;
+    }
+
+    if (p == PEEKED_NUMBER) {
+      peekedString = new String(buffer, pos, peekedNumberLength);
+      pos += peekedNumberLength;
+    } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED || p == PEEKED_UNQUOTED) {
+      if (p == PEEKED_UNQUOTED) {
+        peekedString = nextUnquotedValue();
+      } else {
+        peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
+      }
+      try {
+        long result = Long.parseLong(peekedString);
+        peeked = PEEKED_NONE;
+        pathIndices[stackSize - 1]++;
+        return result;
+      } catch (NumberFormatException ignored) {
+        // Fall back to parse as a double below.
+      }
+    } else {
+      throw unexpectedTokenError("a long");
+    }
+
+    peeked = PEEKED_BUFFERED;
+    double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
+    long result = (long) asDouble;
+    if (result != asDouble) { // Make sure no precision was lost casting to 'long'.
+      throw new NumberFormatException("Expected a long but was " + peekedString + locationString());
+    }
+    peekedString = null;
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /**
+   * Returns the string up to but not including {@code quote}, unescaping any character escape
+   * sequences encountered along the way. The opening quote should have already been read. This
+   * consumes the closing quote, but does not include it in the returned string.
+   *
+   * @param quote either ' or ".
+   */
+  private String nextQuotedValue(char quote) throws IOException {
+    // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
+    char[] buffer = this.buffer;
+    StringBuilder builder = null;
+    while (true) {
+      int p = pos;
+      int l = limit;
+      /* the index of the first character not yet appended to the builder. */
+      int start = p;
+      while (p < l) {
+        int c = buffer[p++];
+
+        // In strict mode, throw an exception when meeting unescaped control characters (U+0000
+        // through U+001F)
+        if (strictness == Strictness.STRICT && c < 0x20) {
+          throw syntaxError(
+              "Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode");
+        } else if (c == quote) {
+          pos = p;
+          int len = p - start - 1;
+          if (builder == null) {
+            return new String(buffer, start, len);
+          } else {
+            builder.append(buffer, start, len);
+            return builder.toString();
+          }
+        } else if (c == '\\') {
+          pos = p;
+          int len = p - start - 1;
+          if (builder == null) {
+            int estimatedLength = (len + 1) * 2;
+            builder = new StringBuilder(Math.max(estimatedLength, 16));
+          }
+          builder.append(buffer, start, len);
+          builder.append(readEscapeCharacter());
+          p = pos;
+          l = limit;
+          start = p;
+        } else if (c == '\n') {
+          lineNumber++;
+          lineStart = p;
+        }
+      }
+
+      if (builder == null) {
+        int estimatedLength = (p - start) * 2;
+        builder = new StringBuilder(Math.max(estimatedLength, 16));
+      }
+      builder.append(buffer, start, p - start);
+      pos = p;
+      if (!fillBuffer(1)) {
+        throw syntaxError("Unterminated string");
+      }
+    }
+  }
+
+  /** Returns an unquoted value as a string. */
+  @SuppressWarnings("fallthrough")
+  private String nextUnquotedValue() throws IOException {
+    StringBuilder builder = null;
+    int i = 0;
+
+    findNonLiteralCharacter:
+    while (true) {
+      for (; pos + i < limit; i++) {
+        switch (buffer[pos + i]) {
+          case '/':
+          case '\\':
+          case ';':
+          case '#':
+          case '=':
+            checkLenient(); // fall-through
+          case '{':
+          case '}':
+          case '[':
+          case ']':
+          case ':':
+          case ',':
+          case ' ':
+          case '\t':
+          case '\f':
+          case '\r':
+          case '\n':
+            break findNonLiteralCharacter;
+          default:
+            // skip character to be included in string value
+        }
+      }
+
+      // Attempt to load the entire literal into the buffer at once.
+      if (i < buffer.length) {
+        if (fillBuffer(i + 1)) {
+          continue;
+        } else {
+          break;
+        }
+      }
+
+      // use a StringBuilder when the value is too long. This is too long to be a number!
+      if (builder == null) {
+        builder = new StringBuilder(Math.max(i, 16));
+      }
+      builder.append(buffer, pos, i);
+      pos += i;
+      i = 0;
+      if (!fillBuffer(1)) {
+        break;
+      }
+    }
+
+    String result =
+        (null == builder) ? new String(buffer, pos, i) : builder.append(buffer, pos, i).toString();
+    pos += i;
+    return result;
+  }
+
+  private void skipQuotedValue(char quote) throws IOException {
+    // Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
+    char[] buffer = this.buffer;
+    do {
+      int p = pos;
+      int l = limit;
+      /* the index of the first character not yet appended to the builder. */
+      while (p < l) {
+        int c = buffer[p++];
+        if (c == quote) {
+          pos = p;
+          return;
+        } else if (c == '\\') {
+          pos = p;
+          char unused = readEscapeCharacter();
+          p = pos;
+          l = limit;
+        } else if (c == '\n') {
+          lineNumber++;
+          lineStart = p;
+        }
+      }
+      pos = p;
+    } while (fillBuffer(1));
+    throw syntaxError("Unterminated string");
+  }
+
+  @SuppressWarnings("fallthrough")
+  private void skipUnquotedValue() throws IOException {
+    do {
+      int i = 0;
+      for (; pos + i < limit; i++) {
+        switch (buffer[pos + i]) {
+          case '/':
+          case '\\':
+          case ';':
+          case '#':
+          case '=':
+            checkLenient(); // fall-through
+          case '{':
+          case '}':
+          case '[':
+          case ']':
+          case ':':
+          case ',':
+          case ' ':
+          case '\t':
+          case '\f':
+          case '\r':
+          case '\n':
+            pos += i;
+            return;
+          default:
+            // skip the character
+        }
+      }
+      pos += i;
+    } while (fillBuffer(1));
+  }
+
+  /**
+   * 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.
+   *
+   * @throws IllegalStateException if the next token is not a literal value.
+   * @throws NumberFormatException if the next literal value cannot be parsed as a number, or
+   *     exactly represented as an int.
+   */
+  public int nextInt() throws IOException {
+    int p = peeked;
+    if (p == PEEKED_NONE) {
+      p = doPeek();
+    }
+
+    int result;
+    if (p == PEEKED_LONG) {
+      result = (int) peekedLong;
+      if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
+        throw new NumberFormatException("Expected an int but was " + peekedLong + locationString());
+      }
+      peeked = PEEKED_NONE;
+      pathIndices[stackSize - 1]++;
+      return result;
+    }
+
+    if (p == PEEKED_NUMBER) {
+      peekedString = new String(buffer, pos, peekedNumberLength);
+      pos += peekedNumberLength;
+    } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED || p == PEEKED_UNQUOTED) {
+      if (p == PEEKED_UNQUOTED) {
+        peekedString = nextUnquotedValue();
+      } else {
+        peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
+      }
+      try {
+        result = Integer.parseInt(peekedString);
+        peeked = PEEKED_NONE;
+        pathIndices[stackSize - 1]++;
+        return result;
+      } catch (NumberFormatException ignored) {
+        // Fall back to parse as a double below.
+      }
+    } else {
+      throw unexpectedTokenError("an int");
+    }
+
+    peeked = PEEKED_BUFFERED;
+    double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
+    result = (int) asDouble;
+    if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
+      throw new NumberFormatException("Expected an int but was " + peekedString + locationString());
+    }
+    peekedString = null;
+    peeked = PEEKED_NONE;
+    pathIndices[stackSize - 1]++;
+    return result;
+  }
+
+  /** Closes this JSON reader and the underlying {@link Reader}. */
+  @Override
+  public void close() throws IOException {
+    peeked = PEEKED_NONE;
+    stack[0] = JsonScope.CLOSED;
+    stackSize = 1;
+    in.close();
+  }
+
+  /**
+   * 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>Primitive value (for example a JSON number): The primitive value is skipped.
+   *   <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>End of a JSON array or object: Only this end token is skipped.
+   *   <li>End of JSON document: Skipping has no effect, the next token continues to be the end of
+   *       the document.
+   * </ul>
+   */
+  public void skipValue() throws IOException {
+    int count = 0;
+    do {
+      int p = peeked;
+      if (p == PEEKED_NONE) {
+        p = doPeek();
+      }
+
+      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) {
+            // Free the last path name so that it can be garbage collected
+            pathNames[stackSize - 1] = null;
+          }
+          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;
+        default:
+          // For all other tokens there is nothing to do; token has already been consumed from
+          // underlying reader
+      }
+      peeked = PEEKED_NONE;
+    } while (count > 0);
+
+    pathIndices[stackSize - 1]++;
+  }
+
+  private void push(int newTop) {
+    if (stackSize == stack.length) {
+      int newLength = stackSize * 2;
+      stack = Arrays.copyOf(stack, newLength);
+      pathIndices = Arrays.copyOf(pathIndices, newLength);
+      pathNames = Arrays.copyOf(pathNames, newLength);
+    }
+    stack[stackSize++] = newTop;
+  }
+
+  /**
+   * Returns true once {@code limit - pos >= minimum}. If the data is exhausted before that many
+   * characters are available, this returns false.
+   */
+  private boolean fillBuffer(int minimum) throws IOException {
+    char[] buffer = this.buffer;
+    lineStart -= pos;
+    if (limit != pos) {
+      limit -= pos;
+      System.arraycopy(buffer, pos, buffer, 0, limit);
+    } else {
+      limit = 0;
+    }
+
+    pos = 0;
+    int total;
+    while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
+      limit += total;
+
+      // if this is the first read, consume an optional byte order mark (BOM) if it exists
+      if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
+        pos++;
+        lineStart++;
+        minimum++;
+      }
+
+      if (limit >= minimum) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns the next character in the stream that is neither whitespace nor a part of a comment.
+   * When this returns, the returned character is always at {@code buffer[pos-1]}; this means the
+   * caller can always push back the returned character by decrementing {@code pos}.
+   */
+  private int nextNonWhitespace(boolean throwOnEof) throws IOException {
+    /*
+     * This code uses ugly local variables 'p' and 'l' representing the 'pos'
+     * and 'limit' fields respectively. Using locals rather than fields saves
+     * a few field reads for each whitespace character in a pretty-printed
+     * document, resulting in a 5% speedup. We need to flush 'p' to its field
+     * before any (potentially indirect) call to fillBuffer() and reread both
+     * 'p' and 'l' after any (potentially indirect) call to the same method.
+     */
+    char[] buffer = this.buffer;
+    int p = pos;
+    int l = limit;
+    while (true) {
+      if (p == l) {
+        pos = p;
+        if (!fillBuffer(1)) {
+          break;
+        }
+        p = pos;
+        l = limit;
+      }
+
+      int c = buffer[p++];
+      if (c == '\n') {
+        lineNumber++;
+        lineStart = p;
+        continue;
+      } else if (c == ' ' || c == '\r' || c == '\t') {
+        continue;
+      }
+
+      if (c == '/') {
+        pos = p;
+        if (p == l) {
+          pos--; // push back '/' so it's still in the buffer when this method returns
+          boolean charsLoaded = fillBuffer(2);
+          pos++; // consume the '/' again
+          if (!charsLoaded) {
+            return c;
+          }
+        }
+
+        checkLenient();
+        char peek = buffer[pos];
+        switch (peek) {
+          case '*':
+            // skip a /* c-style comment */
+            pos++;
+            if (!skipTo("*/")) {
+              throw syntaxError("Unterminated comment");
+            }
+            p = pos + 2;
+            l = limit;
+            continue;
+
+          case '/':
+            // skip a // end-of-line comment
+            pos++;
+            skipToEndOfLine();
+            p = pos;
+            l = limit;
+            continue;
+
+          default:
+            return c;
+        }
+      } else if (c == '#') {
+        pos = p;
+        /*
+         * Skip a # hash end-of-line comment. The JSON RFC doesn't
+         * specify this behaviour, but it's required to parse
+         * existing documents. See http://b/2571423.
+         */
+        checkLenient();
+        skipToEndOfLine();
+        p = pos;
+        l = limit;
+      } else {
+        pos = p;
+        return c;
+      }
+    }
+    if (throwOnEof) {
+      throw new EOFException("End of input" + locationString());
+    } else {
+      return -1;
+    }
+  }
+
+  private void checkLenient() throws MalformedJsonException {
+    if (strictness != Strictness.LENIENT) {
+      throw syntaxError(
+          "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON");
+    }
+  }
+
+  /**
+   * Advances the position until after the next newline character. If the line is terminated by
+   * "\r\n", the '\n' must be consumed as whitespace by the caller.
+   */
+  private void skipToEndOfLine() throws IOException {
+    while (pos < limit || fillBuffer(1)) {
+      char c = buffer[pos++];
+      if (c == '\n') {
+        lineNumber++;
+        lineStart = pos;
+        break;
+      } else if (c == '\r') {
+        break;
+      }
+    }
+  }
+
+  /**
+   * @param toFind a string to search for. Must not contain a newline.
+   */
+  private boolean skipTo(String toFind) throws IOException {
+    int length = toFind.length();
+    outer:
+    for (; pos + length <= limit || fillBuffer(length); pos++) {
+      if (buffer[pos] == '\n') {
+        lineNumber++;
+        lineStart = pos + 1;
+        continue;
+      }
+      for (int c = 0; c < length; c++) {
+        if (buffer[pos + c] != toFind.charAt(c)) {
+          continue outer;
+        }
+      }
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + locationString();
+  }
+
+  String locationString() {
+    int line = lineNumber + 1;
+    int column = pos - lineStart + 1;
+    return " at line " + line + " column " + column + " path " + getPath();
+  }
+
+  private String getPath(boolean usePreviousPath) {
+    StringBuilder result = new StringBuilder().append('$');
+    for (int i = 0; i < stackSize; i++) {
+      int scope = stack[i];
+      switch (scope) {
+        case JsonScope.EMPTY_ARRAY:
+        case JsonScope.NONEMPTY_ARRAY:
+          int pathIndex = pathIndices[i];
+          // If index is last path element it points to next array element; have to decrement
+          if (usePreviousPath && pathIndex > 0 && i == stackSize - 1) {
+            pathIndex--;
+          }
+          result.append('[').append(pathIndex).append(']');
+          break;
+        case JsonScope.EMPTY_OBJECT:
+        case JsonScope.DANGLING_NAME:
+        case JsonScope.NONEMPTY_OBJECT:
+          result.append('.');
+          if (pathNames[i] != null) {
+            result.append(pathNames[i]);
+          }
+          break;
+        case JsonScope.NONEMPTY_DOCUMENT:
+        case JsonScope.EMPTY_DOCUMENT:
+        case JsonScope.CLOSED:
+          break;
+        default:
+          throw new AssertionError("Unknown scope value: " + scope);
+      }
+    }
+    return result.toString();
+  }
+
+  /**
+   * Returns a <a href="https://goessner.net/articles/JsonPath/">JSONPath</a> in <i>dot-notation</i>
+   * to the next (or current) location in the JSON document. That means:
+   *
+   * <ul>
+   *   <li>For JSON arrays the path points to the index of the next element (even if there are no
+   *       further elements).
+   *   <li>For JSON objects the path points to the last property, or to the current property if its
+   *       name has already been consumed.
+   * </ul>
+   *
+   * <p>This method can be useful to add additional context to exception messages <i>before</i> a
+   * value is consumed, for example when the {@linkplain #peek() peeked} token is unexpected.
+   */
+  public String getPath() {
+    return getPath(false);
+  }
+
+  /**
+   * Returns a <a href="https://goessner.net/articles/JsonPath/">JSONPath</a> in <i>dot-notation</i>
+   * to the previous (or current) location in the JSON document. That means:
+   *
+   * <ul>
+   *   <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>For JSON objects the path points to the last property, or to the current property if its
+   *       name has already been consumed.
+   * </ul>
+   *
+   * <p>This method can be useful to add additional context to exception messages <i>after</i> a
+   * value has been consumed.
+   */
+  public String getPreviousPath() {
+    return getPath(true);
+  }
+
+  /**
+   * Unescapes the character identified by the character or characters that immediately follow a
+   * backslash. The backslash '\' should have already been read. This supports both Unicode escapes
+   * "u000A" and two-character escapes "\n".
+   *
+   * @throws MalformedJsonException if the escape sequence is malformed
+   */
+  @SuppressWarnings("fallthrough")
+  private char readEscapeCharacter() throws IOException {
+    if (pos == limit && !fillBuffer(1)) {
+      throw syntaxError("Unterminated escape sequence");
+    }
+
+    char escaped = buffer[pos++];
+    switch (escaped) {
+      case 'u':
+        if (pos + 4 > limit && !fillBuffer(4)) {
+          throw syntaxError("Unterminated escape sequence");
+        }
+        // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
+        int result = 0;
+        for (int i = pos, end = i + 4; i < end; i++) {
+          char c = buffer[i];
+          result <<= 4;
+          if (c >= '0' && c <= '9') {
+            result += (c - '0');
+          } else if (c >= 'a' && c <= 'f') {
+            result += (c - 'a' + 10);
+          } else if (c >= 'A' && c <= 'F') {
+            result += (c - 'A' + 10);
+          } else {
+            throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4));
+          }
+        }
+        pos += 4;
+        return (char) result;
+
+      case 't':
+        return '\t';
+
+      case 'b':
+        return '\b';
+
+      case 'n':
+        return '\n';
+
+      case 'r':
+        return '\r';
+
+      case 'f':
+        return '\f';
+
+      case '\n':
+        if (strictness == Strictness.STRICT) {
+          throw syntaxError("Cannot escape a newline character in strict mode");
+        }
+        lineNumber++;
+        lineStart = pos;
+        // fall-through
+
+      case '\'':
+        if (strictness == Strictness.STRICT) {
+          throw syntaxError("Invalid escaped character \"'\" in strict mode");
+        }
+      case '"':
+      case '\\':
+      case '/':
+        return escaped;
+      default:
+        // throw error when none of the above cases are matched
+        throw syntaxError("Invalid escape sequence");
+    }
+  }
+
+  /**
+   * Throws a new {@link MalformedJsonException} with the given message and information about the
+   * current location.
+   */
+  private MalformedJsonException syntaxError(String message) throws MalformedJsonException {
+    throw new MalformedJsonException(
+        message + locationString() + "\nSee " + TroubleshootingGuide.createUrl("malformed-json"));
+  }
+
+  private IllegalStateException unexpectedTokenError(String expected) throws IOException {
+    JsonToken peeked = peek();
+    String troubleshootingId =
+        peeked == JsonToken.NULL ? "adapter-not-null-safe" : "unexpected-json-structure";
+    return new IllegalStateException(
+        "Expected "
+            + expected
+            + " but was "
+            + peek()
+            + locationString()
+            + "\nSee "
+            + TroubleshootingGuide.createUrl(troubleshootingId));
+  }
+
+  /** Consumes the non-execute prefix if it exists. */
+  private void consumeNonExecutePrefix() throws IOException {
+    // fast-forward through the leading whitespace
+    int unused = nextNonWhitespace(true);
+    pos--;
+
+    if (pos + 5 > limit && !fillBuffer(5)) {
+      return;
+    }
+
+    int p = pos;
+    char[] buf = buffer;
+    if (buf[p] != ')'
+        || buf[p + 1] != ']'
+        || buf[p + 2] != '}'
+        || buf[p + 3] != '\''
+        || buf[p + 4] != '\n') {
+      return; // not a security token!
+    }
+
+    // we consumed a security token!
+    pos += 5;
+  }
+
+  static {
+    JsonReaderInternalAccess.INSTANCE =
+        new JsonReaderInternalAccess() {
+          @Override
+          public void promoteNameToValue(JsonReader reader) throws IOException {
+            if (reader instanceof JsonTreeReader) {
+              ((JsonTreeReader) reader).promoteNameToValue();
+              return;
+            }
+            int p = reader.peeked;
+            if (p == PEEKED_NONE) {
+              p = reader.doPeek();
+            }
+            if (p == PEEKED_DOUBLE_QUOTED_NAME) {
+              reader.peeked = PEEKED_DOUBLE_QUOTED;
+            } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
+              reader.peeked = PEEKED_SINGLE_QUOTED;
+            } else if (p == PEEKED_UNQUOTED_NAME) {
+              reader.peeked = PEEKED_UNQUOTED;
+            } else {
+              throw reader.unexpectedTokenError("a name");
+            }
+          }
+        };
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/stream/JsonScope.java b/gson/gson/src/main/java/com/google/gson/stream/JsonScope.java
new file mode 100644
index 0000000000000000000000000000000000000000..16259578c3fcab44b78f86be85a1c508e2a2958e
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/JsonScope.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+/**
+ * Lexical scoping elements within a JSON reader or writer.
+ *
+ * @author Jesse Wilson
+ * @since 1.6
+ */
+final class JsonScope {
+  private JsonScope() {}
+
+  /** An array with no elements requires no separator before the next element. */
+  static final int EMPTY_ARRAY = 1;
+
+  /** An array with at least one value requires a separator before the next element. */
+  static final int NONEMPTY_ARRAY = 2;
+
+  /** An object with no name/value pairs requires no separator before the next element. */
+  static final int EMPTY_OBJECT = 3;
+
+  /** An object whose most recent element is a key. The next element must be a value. */
+  static final int DANGLING_NAME = 4;
+
+  /** An object with at least one name/value pair requires a separator before the next element. */
+  static final int NONEMPTY_OBJECT = 5;
+
+  /** No top-level value has been started yet. */
+  static final int EMPTY_DOCUMENT = 6;
+
+  /** A top-level value has already been started. */
+  static final int NONEMPTY_DOCUMENT = 7;
+
+  /** A document that's been closed and cannot be accessed. */
+  static final int CLOSED = 8;
+}
diff --git a/gson/gson/src/main/java/com/google/gson/stream/JsonToken.java b/gson/gson/src/main/java/com/google/gson/stream/JsonToken.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8bfe48c537e5723f78da2e75b0e3d5d494f0e5a
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/JsonToken.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+/**
+ * A structure, name or value type in a JSON-encoded string.
+ *
+ * @author Jesse Wilson
+ * @since 1.6
+ */
+public enum JsonToken {
+
+  /**
+   * The opening of a JSON array. Written using {@link JsonWriter#beginArray} and read using {@link
+   * JsonReader#beginArray}.
+   */
+  BEGIN_ARRAY,
+
+  /**
+   * The closing of a JSON array. Written using {@link JsonWriter#endArray} and read using {@link
+   * JsonReader#endArray}.
+   */
+  END_ARRAY,
+
+  /**
+   * The opening of a JSON object. Written using {@link JsonWriter#beginObject} and read using
+   * {@link JsonReader#beginObject}.
+   */
+  BEGIN_OBJECT,
+
+  /**
+   * The closing of a JSON object. Written using {@link JsonWriter#endObject} and read using {@link
+   * JsonReader#endObject}.
+   */
+  END_OBJECT,
+
+  /**
+   * A JSON property name. Within objects, tokens alternate between names and their values. Written
+   * using {@link JsonWriter#name} and read using {@link JsonReader#nextName}
+   */
+  NAME,
+
+  /** A JSON string. */
+  STRING,
+
+  /**
+   * A JSON number represented in this API by a Java {@code double}, {@code long}, or {@code int}.
+   */
+  NUMBER,
+
+  /** A JSON {@code true} or {@code false}. */
+  BOOLEAN,
+
+  /** A JSON {@code null}. */
+  NULL,
+
+  /**
+   * The end of the JSON stream. This sentinel value is returned by {@link JsonReader#peek()} to
+   * signal that the JSON-encoded value has no more tokens.
+   */
+  END_DOCUMENT
+}
diff --git a/gson/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/gson/src/main/java/com/google/gson/stream/JsonWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..498a0ab4e1437e684428288d91f1a395206ee51f
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/JsonWriter.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+import static com.google.gson.stream.JsonScope.DANGLING_NAME;
+import static com.google.gson.stream.JsonScope.EMPTY_ARRAY;
+import static com.google.gson.stream.JsonScope.EMPTY_DOCUMENT;
+import static com.google.gson.stream.JsonScope.EMPTY_OBJECT;
+import static com.google.gson.stream.JsonScope.NONEMPTY_ARRAY;
+import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT;
+import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.FormattingStyle;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.Strictness;
+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="https://www.ietf.org/rfc/rfc8259.txt">RFC 8259</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.
+ *
+ * <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} methods or by nesting other arrays and
+ *       objects. Finally close the array using {@link #endArray()}.
+ *   <li>To write <strong>objects</strong>, first call {@link #beginObject()}. Write each of the
+ *       object's properties by alternating calls to {@link #name} with the property's value. Write
+ *       property values with the appropriate {@link #value} method or by nesting other objects or
+ *       arrays. Finally close the object using {@link #endObject()}.
+ * </ul>
+ *
+ * <h2>Configuration</h2>
+ *
+ * The behavior of this writer can be customized with the following methods:
+ *
+ * <ul>
+ *   <li>{@link #setFormattingStyle(FormattingStyle)}, the default is {@link
+ *       FormattingStyle#COMPACT}
+ *   <li>{@link #setHtmlSafe(boolean)}, by default HTML characters are not escaped in the JSON
+ *       output
+ *   <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
+ *   <li>{@link #setSerializeNulls(boolean)}, by default {@code null} is serialized
+ * </ul>
+ *
+ * The default configuration of {@code JsonWriter} instances used internally by the {@link Gson}
+ * class differs, and can be adjusted with the various {@link GsonBuilder} methods.
+ *
+ * <h2>Example</h2>
+ *
+ * Suppose we'd like to encode a stream of messages such as the following:
+ *
+ * <pre>{@code
+ * [
+ *   {
+ *     "id": 912345678901,
+ *     "text": "How do I stream JSON in Java?",
+ *     "geo": null,
+ *     "user": {
+ *       "name": "json_newb",
+ *       "followers_count": 41
+ *      }
+ *   },
+ *   {
+ *     "id": 912345678902,
+ *     "text": "@json_newb just use JsonWriter!",
+ *     "geo": [50.454722, -104.606667],
+ *     "user": {
+ *       "name": "jesse",
+ *       "followers_count": 2
+ *     }
+ *   }
+ * ]
+ * }</pre>
+ *
+ * This code encodes the above structure:
+ *
+ * <pre>{@code
+ * public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
+ *   JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ *   writer.setIndent("    ");
+ *   writeMessagesArray(writer, messages);
+ *   writer.close();
+ * }
+ *
+ * public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
+ *   writer.beginArray();
+ *   for (Message message : messages) {
+ *     writeMessage(writer, message);
+ *   }
+ *   writer.endArray();
+ * }
+ *
+ * public void writeMessage(JsonWriter writer, Message message) throws IOException {
+ *   writer.beginObject();
+ *   writer.name("id").value(message.getId());
+ *   writer.name("text").value(message.getText());
+ *   if (message.getGeo() != null) {
+ *     writer.name("geo");
+ *     writeDoublesArray(writer, message.getGeo());
+ *   } else {
+ *     writer.name("geo").nullValue();
+ *   }
+ *   writer.name("user");
+ *   writeUser(writer, message.getUser());
+ *   writer.endObject();
+ * }
+ *
+ * public void writeUser(JsonWriter writer, User user) throws IOException {
+ *   writer.beginObject();
+ *   writer.name("name").value(user.getName());
+ *   writer.name("followers_count").value(user.getFollowersCount());
+ *   writer.endObject();
+ * }
+ *
+ * public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
+ *   writer.beginArray();
+ *   for (Double value : doubles) {
+ *     writer.value(value);
+ *   }
+ *   writer.endArray();
+ * }
+ * }</pre>
+ *
+ * <p>Each {@code JsonWriter} may be used to write a single JSON stream. Instances of this class are
+ * not thread safe. Calls that would result in a malformed JSON string will fail with an {@link
+ * IllegalStateException}.
+ *
+ * @author Jesse Wilson
+ * @since 1.6
+ */
+public class JsonWriter implements Closeable, Flushable {
+
+  // Syntax as defined by https://datatracker.ietf.org/doc/html/rfc8259#section-6
+  private static final Pattern VALID_JSON_NUMBER_PATTERN =
+      Pattern.compile("-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?");
+
+  /*
+   * From RFC 8259, "All Unicode characters may be placed within the
+   * quotation marks except for the characters that must be escaped:
+   * quotation mark, reverse solidus, and the control characters
+   * (U+0000 through U+001F)."
+   *
+   * We also escape '\u2028' and '\u2029', which JavaScript interprets as
+   * newline characters. This prevents eval() from failing with a syntax
+   * error. http://code.google.com/p/google-gson/issues/detail?id=341
+   */
+  private static final String[] REPLACEMENT_CHARS;
+  private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
+
+  static {
+    REPLACEMENT_CHARS = new String[128];
+    for (int i = 0; i <= 0x1f; i++) {
+      REPLACEMENT_CHARS[i] = String.format("\\u%04x", i);
+    }
+    REPLACEMENT_CHARS['"'] = "\\\"";
+    REPLACEMENT_CHARS['\\'] = "\\\\";
+    REPLACEMENT_CHARS['\t'] = "\\t";
+    REPLACEMENT_CHARS['\b'] = "\\b";
+    REPLACEMENT_CHARS['\n'] = "\\n";
+    REPLACEMENT_CHARS['\r'] = "\\r";
+    REPLACEMENT_CHARS['\f'] = "\\f";
+    HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone();
+    HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c";
+    HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e";
+    HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026";
+    HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d";
+    HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
+  }
+
+  /** The JSON output destination */
+  private final Writer out;
+
+  private int[] stack = new int[32];
+  private int stackSize = 0;
+
+  {
+    push(EMPTY_DOCUMENT);
+  }
+
+  private FormattingStyle formattingStyle;
+  // These fields cache data derived from the formatting style, to avoid having to
+  // re-evaluate it every time something is written
+  private String formattedColon;
+  private String formattedComma;
+  private boolean usesEmptyNewlineAndIndent;
+
+  private Strictness strictness = Strictness.LEGACY_STRICT;
+
+  private boolean htmlSafe;
+
+  private String deferredName;
+
+  private boolean serializeNulls = true;
+
+  /**
+   * Creates a new instance that writes a JSON-encoded stream to {@code out}. For best performance,
+   * ensure {@link Writer} is buffered; wrapping in {@link java.io.BufferedWriter BufferedWriter} if
+   * necessary.
+   */
+  public JsonWriter(Writer out) {
+    this.out = Objects.requireNonNull(out, "out == null");
+    setFormattingStyle(FormattingStyle.COMPACT);
+  }
+
+  /**
+   * Sets the indentation string to be repeated for each level of indentation in the encoded
+   * document. If {@code indent.isEmpty()} the encoded document will be compact. Otherwise the
+   * encoded document will be more human-readable.
+   *
+   * <p>This is a convenience method which overwrites any previously {@linkplain
+   * #setFormattingStyle(FormattingStyle) set formatting style} with either {@link
+   * FormattingStyle#COMPACT} if the given indent string is empty, or {@link FormattingStyle#PRETTY}
+   * with the given indent if not empty.
+   *
+   * @param indent a string containing only whitespace.
+   */
+  public final void setIndent(String indent) {
+    if (indent.isEmpty()) {
+      setFormattingStyle(FormattingStyle.COMPACT);
+    } else {
+      setFormattingStyle(FormattingStyle.PRETTY.withIndent(indent));
+    }
+  }
+
+  /**
+   * Sets the formatting style to be used in the encoded document.
+   *
+   * <p>The formatting style specifies for example the indentation string to be repeated for each
+   * level of indentation, or the newline style, to accommodate various OS styles.
+   *
+   * @param formattingStyle the formatting style to use, must not be {@code null}.
+   * @since $next-version$
+   */
+  public final void setFormattingStyle(FormattingStyle formattingStyle) {
+    this.formattingStyle = Objects.requireNonNull(formattingStyle);
+
+    this.formattedComma = ",";
+    if (this.formattingStyle.usesSpaceAfterSeparators()) {
+      this.formattedColon = ": ";
+
+      // Only add space if no newline is written
+      if (this.formattingStyle.getNewline().isEmpty()) {
+        this.formattedComma = ", ";
+      }
+    } else {
+      this.formattedColon = ":";
+    }
+
+    this.usesEmptyNewlineAndIndent =
+        this.formattingStyle.getNewline().isEmpty() && this.formattingStyle.getIndent().isEmpty();
+  }
+
+  /**
+   * Returns the pretty printing style used by this writer.
+   *
+   * @return the {@code FormattingStyle} that will be used.
+   * @since $next-version$
+   */
+  public final FormattingStyle getFormattingStyle() {
+    return formattingStyle;
+  }
+
+  /**
+   * Sets the strictness of this writer.
+   *
+   * @deprecated Please use {@link #setStrictness(Strictness)} instead. {@code
+   *     JsonWriter.setLenient(true)} should be replaced by {@code
+   *     JsonWriter.setStrictness(Strictness.LENIENT)} and {@code JsonWriter.setLenient(false)}
+   *     should be replaced by {@code JsonWriter.setStrictness(Strictness.LEGACY_STRICT)}.<br>
+   *     However, if you used {@code setLenient(false)} before, you might prefer {@link
+   *     Strictness#STRICT} now instead.
+   * @param lenient whether this writer should be lenient. If true, the strictness is set to {@link
+   *     Strictness#LENIENT}. If false, the strictness is set to {@link Strictness#LEGACY_STRICT}.
+   * @see #setStrictness(Strictness)
+   */
+  @Deprecated
+  // Don't specify @InlineMe, so caller with `setLenient(false)` becomes aware of new
+  // Strictness.STRICT
+  @SuppressWarnings("InlineMeSuggester")
+  public final void setLenient(boolean lenient) {
+    setStrictness(lenient ? Strictness.LENIENT : Strictness.LEGACY_STRICT);
+  }
+
+  /**
+   * Returns true if the {@link Strictness} of this writer is equal to {@link Strictness#LENIENT}.
+   *
+   * @see JsonWriter#setStrictness(Strictness)
+   */
+  public boolean isLenient() {
+    return strictness == Strictness.LENIENT;
+  }
+
+  /**
+   * Configures how strict this writer is with regard to the syntax rules specified in <a
+   * href="https://www.ietf.org/rfc/rfc8259.txt">RFC 8259</a>. By default, {@link
+   * Strictness#LEGACY_STRICT} is used.
+   *
+   * <dl>
+   *   <dt>{@link Strictness#STRICT} &amp; {@link Strictness#LEGACY_STRICT}
+   *   <dd>The behavior of these is currently identical. In these strictness modes, the writer only
+   *       writes JSON in accordance with RFC 8259.
+   *   <dt>{@link Strictness#LENIENT}
+   *   <dd>This mode relaxes the behavior of the writer to allow the writing of {@link
+   *       Double#isNaN() NaNs} and {@link Double#isInfinite() infinities}. It also allows writing
+   *       multiple top level values.
+   * </dl>
+   *
+   * @param strictness the new strictness of this writer. May not be {@code null}.
+   * @since $next-version$
+   */
+  public final void setStrictness(Strictness strictness) {
+    this.strictness = Objects.requireNonNull(strictness);
+  }
+
+  /**
+   * Returns the {@linkplain Strictness strictness} of this writer.
+   *
+   * @see #setStrictness(Strictness)
+   * @since $next-version$
+   */
+  public final Strictness getStrictness() {
+    return strictness;
+  }
+
+  /**
+   * Configures this writer to emit JSON that's safe for direct inclusion in HTML and XML documents.
+   * This escapes the HTML characters {@code <}, {@code >}, {@code &}, {@code =} and {@code '}
+   * before writing them to the stream. Without this setting, your XML/HTML encoder should replace
+   * these characters with the corresponding escape sequences.
+   */
+  public final void setHtmlSafe(boolean htmlSafe) {
+    this.htmlSafe = htmlSafe;
+  }
+
+  /**
+   * Returns true if this writer writes JSON that's safe for inclusion in HTML and XML documents.
+   */
+  public final boolean isHtmlSafe() {
+    return htmlSafe;
+  }
+
+  /**
+   * Sets whether object members are serialized when their value is null. This has no impact on
+   * array elements. The default is true.
+   */
+  public final void setSerializeNulls(boolean serializeNulls) {
+    this.serializeNulls = serializeNulls;
+  }
+
+  /**
+   * Returns true if object members are serialized when their value is null. This has no impact on
+   * array elements. The default is true.
+   */
+  public final boolean getSerializeNulls() {
+    return serializeNulls;
+  }
+
+  /**
+   * Begins encoding a new array. Each call to this method must be paired with a call to {@link
+   * #endArray}.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter beginArray() throws IOException {
+    writeDeferredName();
+    return openScope(EMPTY_ARRAY, '[');
+  }
+
+  /**
+   * Ends encoding the current array.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter endArray() throws IOException {
+    return closeScope(EMPTY_ARRAY, NONEMPTY_ARRAY, ']');
+  }
+
+  /**
+   * Begins encoding a new object. Each call to this method must be paired with a call to {@link
+   * #endObject}.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter beginObject() throws IOException {
+    writeDeferredName();
+    return openScope(EMPTY_OBJECT, '{');
+  }
+
+  /**
+   * Ends encoding the current object.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter endObject() throws IOException {
+    return closeScope(EMPTY_OBJECT, NONEMPTY_OBJECT, '}');
+  }
+
+  /** Enters a new scope by appending any necessary whitespace and the given bracket. */
+  @CanIgnoreReturnValue
+  private JsonWriter openScope(int empty, char openBracket) throws IOException {
+    beforeValue();
+    push(empty);
+    out.write(openBracket);
+    return this;
+  }
+
+  /** Closes the current scope by appending any necessary whitespace and the given bracket. */
+  @CanIgnoreReturnValue
+  private JsonWriter closeScope(int empty, int nonempty, char closeBracket) throws IOException {
+    int context = peek();
+    if (context != nonempty && context != empty) {
+      throw new IllegalStateException("Nesting problem.");
+    }
+    if (deferredName != null) {
+      throw new IllegalStateException("Dangling name: " + deferredName);
+    }
+
+    stackSize--;
+    if (context == nonempty) {
+      newline();
+    }
+    out.write(closeBracket);
+    return this;
+  }
+
+  private void push(int newTop) {
+    if (stackSize == stack.length) {
+      stack = Arrays.copyOf(stack, stackSize * 2);
+    }
+    stack[stackSize++] = newTop;
+  }
+
+  /** Returns the value on the top of the stack. */
+  private int peek() {
+    if (stackSize == 0) {
+      throw new IllegalStateException("JsonWriter is closed.");
+    }
+    return stack[stackSize - 1];
+  }
+
+  /** Replace the value on the top of the stack with the given value. */
+  private void replaceTop(int topOfStack) {
+    stack[stackSize - 1] = topOfStack;
+  }
+
+  /**
+   * Encodes the property name.
+   *
+   * @param name the name of the forthcoming value. May not be {@code null}.
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter name(String name) throws IOException {
+    Objects.requireNonNull(name, "name == null");
+    if (deferredName != null) {
+      throw new IllegalStateException("Already wrote a name, expecting a value.");
+    }
+    int context = peek();
+    if (context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) {
+      throw new IllegalStateException("Please begin an object before writing a name.");
+    }
+    deferredName = name;
+    return this;
+  }
+
+  private void writeDeferredName() throws IOException {
+    if (deferredName != null) {
+      beforeName();
+      string(deferredName);
+      deferredName = null;
+    }
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @param value the literal string value, or null to encode a null literal.
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(String value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+    writeDeferredName();
+    beforeValue();
+    string(value);
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(boolean value) throws IOException {
+    writeDeferredName();
+    beforeValue();
+    out.write(value ? "true" : "false");
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @return this writer.
+   * @since 2.7
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(Boolean value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+    writeDeferredName();
+    beforeValue();
+    out.write(value ? "true" : "false");
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @param value a finite value, or if {@link #setStrictness(Strictness) lenient}, also {@link
+   *     Float#isNaN() NaN} or {@link Float#isInfinite() infinity}.
+   * @return this writer.
+   * @throws IllegalArgumentException if the value is NaN or Infinity and this writer is not {@link
+   *     #setStrictness(Strictness) lenient}.
+   * @since 2.9.1
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(float value) throws IOException {
+    writeDeferredName();
+    if (strictness != Strictness.LENIENT && (Float.isNaN(value) || Float.isInfinite(value))) {
+      throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+    }
+    beforeValue();
+    out.append(Float.toString(value));
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @param value a finite value, or if {@link #setStrictness(Strictness) lenient}, also {@link
+   *     Double#isNaN() NaN} or {@link Double#isInfinite() infinity}.
+   * @return this writer.
+   * @throws IllegalArgumentException if the value is NaN or Infinity and this writer is not {@link
+   *     #setStrictness(Strictness) lenient}.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(double value) throws IOException {
+    writeDeferredName();
+    if (strictness != Strictness.LENIENT && (Double.isNaN(value) || Double.isInfinite(value))) {
+      throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+    }
+    beforeValue();
+    out.append(Double.toString(value));
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(long value) throws IOException {
+    writeDeferredName();
+    beforeValue();
+    out.write(Long.toString(value));
+    return this;
+  }
+
+  /**
+   * Encodes {@code value}. The value is written by directly writing the {@link Number#toString()}
+   * result to JSON. Implementations must make sure that the result represents a valid JSON number.
+   *
+   * @param value a finite value, or if {@link #setStrictness(Strictness) lenient}, also {@link
+   *     Double#isNaN() NaN} or {@link Double#isInfinite() infinity}.
+   * @return this writer.
+   * @throws IllegalArgumentException if the value is NaN or Infinity and this writer is not {@link
+   *     #setStrictness(Strictness) lenient}; or if the {@code toString()} result is not a valid
+   *     JSON number.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter value(Number value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+
+    writeDeferredName();
+    String string = value.toString();
+    if (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN")) {
+      if (strictness != Strictness.LENIENT) {
+        throw new IllegalArgumentException("Numeric values must be finite, but was " + string);
+      }
+    } else {
+      Class<? extends Number> numberClass = value.getClass();
+      // Validate that string is valid before writing it directly to JSON output
+      if (!isTrustedNumberType(numberClass)
+          && !VALID_JSON_NUMBER_PATTERN.matcher(string).matches()) {
+        throw new IllegalArgumentException(
+            "String created by " + numberClass + " is not a valid JSON number: " + string);
+      }
+    }
+
+    beforeValue();
+    out.append(string);
+    return this;
+  }
+
+  /**
+   * Encodes {@code null}.
+   *
+   * @return this writer.
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter nullValue() throws IOException {
+    if (deferredName != null) {
+      if (serializeNulls) {
+        writeDeferredName();
+      } else {
+        deferredName = null;
+        return this; // skip the name and the value
+      }
+    }
+    beforeValue();
+    out.write("null");
+    return this;
+  }
+
+  /**
+   * Writes {@code value} directly to the writer without quoting or 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
+   */
+  @CanIgnoreReturnValue
+  public JsonWriter jsonValue(String value) throws IOException {
+    if (value == null) {
+      return nullValue();
+    }
+    writeDeferredName();
+    beforeValue();
+    out.append(value);
+    return this;
+  }
+
+  /**
+   * Ensures all buffered data is written to the underlying {@link Writer} and flushes that writer.
+   */
+  @Override
+  public void flush() throws IOException {
+    if (stackSize == 0) {
+      throw new IllegalStateException("JsonWriter is closed.");
+    }
+    out.flush();
+  }
+
+  /**
+   * Flushes and closes this writer and the underlying {@link Writer}.
+   *
+   * @throws IOException if the JSON document is incomplete.
+   */
+  @Override
+  public void close() throws IOException {
+    out.close();
+
+    int size = stackSize;
+    if (size > 1 || (size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT)) {
+      throw new IOException("Incomplete document");
+    }
+    stackSize = 0;
+  }
+
+  /**
+   * Returns whether the {@code toString()} of {@code c} can be trusted to return a valid JSON
+   * number.
+   */
+  private static boolean isTrustedNumberType(Class<? extends Number> c) {
+    // Note: Don't consider LazilyParsedNumber trusted because it could contain
+    // an arbitrary malformed string
+    return c == Integer.class
+        || c == Long.class
+        || c == Double.class
+        || c == Float.class
+        || c == Byte.class
+        || c == Short.class
+        || c == BigDecimal.class
+        || c == BigInteger.class
+        || c == AtomicInteger.class
+        || c == AtomicLong.class;
+  }
+
+  private void string(String value) throws IOException {
+    String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS;
+    out.write('\"');
+    int last = 0;
+    int length = value.length();
+    for (int i = 0; i < length; i++) {
+      char c = value.charAt(i);
+      String replacement;
+      if (c < 128) {
+        replacement = replacements[c];
+        if (replacement == null) {
+          continue;
+        }
+      } else if (c == '\u2028') {
+        replacement = "\\u2028";
+      } else if (c == '\u2029') {
+        replacement = "\\u2029";
+      } else {
+        continue;
+      }
+      if (last < i) {
+        out.write(value, last, i - last);
+      }
+      out.write(replacement);
+      last = i + 1;
+    }
+    if (last < length) {
+      out.write(value, last, length - last);
+    }
+    out.write('\"');
+  }
+
+  private void newline() throws IOException {
+    if (usesEmptyNewlineAndIndent) {
+      return;
+    }
+
+    out.write(formattingStyle.getNewline());
+    for (int i = 1, size = stackSize; i < size; i++) {
+      out.write(formattingStyle.getIndent());
+    }
+  }
+
+  /**
+   * Inserts any necessary separators and whitespace before a name. Also adjusts the stack to expect
+   * the name's value.
+   */
+  private void beforeName() throws IOException {
+    int context = peek();
+    if (context == NONEMPTY_OBJECT) { // first in object
+      out.write(formattedComma);
+    } else if (context != EMPTY_OBJECT) { // not in an object!
+      throw new IllegalStateException("Nesting problem.");
+    }
+    newline();
+    replaceTop(DANGLING_NAME);
+  }
+
+  /**
+   * Inserts any necessary separators and whitespace before a literal value, inline array, or inline
+   * object. Also adjusts the stack to expect either a closing bracket or another element.
+   */
+  @SuppressWarnings("fallthrough")
+  private void beforeValue() throws IOException {
+    switch (peek()) {
+      case NONEMPTY_DOCUMENT:
+        if (strictness != Strictness.LENIENT) {
+          throw new IllegalStateException("JSON must have only one top-level value.");
+        }
+        // fall-through
+      case EMPTY_DOCUMENT: // first in document
+        replaceTop(NONEMPTY_DOCUMENT);
+        break;
+
+      case EMPTY_ARRAY: // first in array
+        replaceTop(NONEMPTY_ARRAY);
+        newline();
+        break;
+
+      case NONEMPTY_ARRAY: // another in array
+        out.append(formattedComma);
+        newline();
+        break;
+
+      case DANGLING_NAME: // value for name
+        out.append(formattedColon);
+        replaceTop(NONEMPTY_OBJECT);
+        break;
+
+      default:
+        throw new IllegalStateException("Nesting problem.");
+    }
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java b/gson/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0c08769e120a9a5778a7be7b82889fbab8c7bf2
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/MalformedJsonException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+import com.google.gson.Strictness;
+import java.io.IOException;
+
+/**
+ * Thrown when a reader encounters malformed JSON. Some syntax errors can be ignored by using {@link
+ * Strictness#LENIENT} for {@link JsonReader#setStrictness(Strictness)}.
+ */
+public final class MalformedJsonException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  public MalformedJsonException(String msg) {
+    super(msg);
+  }
+
+  public MalformedJsonException(String msg, Throwable throwable) {
+    super(msg, throwable);
+  }
+
+  public MalformedJsonException(Throwable throwable) {
+    super(throwable);
+  }
+}
diff --git a/gson/gson/src/main/java/com/google/gson/stream/package-info.java b/gson/gson/src/main/java/com/google/gson/stream/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..4cb50e69003f9d421c090fbc01854ce02e00af90
--- /dev/null
+++ b/gson/gson/src/main/java/com/google/gson/stream/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+/** This package provides classes for processing JSON in an efficient streaming way. */
+package com.google.gson.stream;
diff --git a/gson/gson/src/main/java/module-info.java b/gson/gson/src/main/java/module-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b951ceafa96bc5e4cdcb596489fd8516e3b94a2
--- /dev/null
+++ b/gson/gson/src/main/java/module-info.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+/**
+ * Defines the Gson serialization/deserialization API.
+ *
+ * @since 2.8.6
+ */
+module com.google.gson {
+  exports com.google.gson;
+  exports com.google.gson.annotations;
+  exports com.google.gson.reflect;
+  exports com.google.gson.stream;
+
+  // Dependency on Error Prone Annotations
+  requires static com.google.errorprone.annotations;
+
+  // Optional dependency on java.sql
+  requires static java.sql;
+
+  // Optional dependency on jdk.unsupported for JDK's sun.misc.Unsafe
+  requires static jdk.unsupported;
+}
diff --git a/gson/gson/src/main/resources/META-INF/proguard/gson.pro b/gson/gson/src/main/resources/META-INF/proguard/gson.pro
new file mode 100644
index 0000000000000000000000000000000000000000..8f5a69b30f8033b5067351c6108fc654b4b8a59d
--- /dev/null
+++ b/gson/gson/src/main/resources/META-INF/proguard/gson.pro
@@ -0,0 +1,72 @@
+### Gson ProGuard and R8 rules which are relevant for all users
+### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files
+###
+### IMPORTANT:
+### - These rules are additive; don't include anything here which is not specific to Gson (such as completely
+###   disabling obfuscation for all classes); the user would be unable to disable that then
+### - These rules are not complete; users will most likely have to add additional rules for their specific
+###   classes, for example to disable obfuscation for certain fields or to keep no-args constructors
+###
+
+# Keep generic signatures; needed for correct type resolution
+-keepattributes Signature
+
+# Keep Gson annotations
+# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093
+-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
+
+### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if
+### the corresponding class or field is matches by a `-keep` rule as well, see
+### https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode
+
+# Keep class TypeToken (respectively its generic signature) if present
+-if class com.google.gson.reflect.TypeToken
+-keep,allowobfuscation class com.google.gson.reflect.TypeToken
+
+# Keep any (anonymous) classes extending TypeToken
+-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken
+
+# Keep classes with @JsonAdapter annotation
+-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class *
+
+# Keep fields with any other Gson annotation
+# Also allow obfuscation, assuming that users will additionally use @SerializedName or
+# other means to preserve the field names
+-keepclassmembers,allowobfuscation class * {
+  @com.google.gson.annotations.Expose <fields>;
+  @com.google.gson.annotations.JsonAdapter <fields>;
+  @com.google.gson.annotations.Since <fields>;
+  @com.google.gson.annotations.Until <fields>;
+}
+
+# Keep no-args constructor of classes which can be used with @JsonAdapter
+# By default their no-args constructor is invoked to create an adapter instance
+-keepclassmembers class * extends com.google.gson.TypeAdapter {
+  <init>();
+}
+-keepclassmembers class * implements com.google.gson.TypeAdapterFactory {
+  <init>();
+}
+-keepclassmembers class * implements com.google.gson.JsonSerializer {
+  <init>();
+}
+-keepclassmembers class * implements com.google.gson.JsonDeserializer {
+  <init>();
+}
+
+# Keep fields annotated with @SerializedName for classes which are referenced.
+# If classes with fields annotated with @SerializedName have a no-args
+# constructor keep that as well. Based on
+# https://issuetracker.google.com/issues/150189783#comment11.
+# See also https://github.com/google/gson/pull/2420#discussion_r1241813541
+# for a more detailed explanation.
+-if class *
+-keepclasseswithmembers,allowobfuscation class <1> {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
+-if class * {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
+-keepclassmembers,allowobfuscation,allowoptimization class <1> {
+  <init>();
+}
diff --git a/gson/gson/src/test/java/com/google/gson/CommentsTest.java b/gson/gson/src/test/java/com/google/gson/CommentsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a08bc67371ebb93a49e1a01d50f7f3ff43ee98f7
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/CommentsTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.reflect.TypeToken;
+import java.util.List;
+import org.junit.Test;
+
+/**
+ * Tests that by default Gson accepts several forms of comments.
+ *
+ * @author Jesse Wilson
+ */
+public final class CommentsTest {
+
+  /** Test for issue 212. */
+  @Test
+  public void testParseComments() {
+    String json =
+        "[\n"
+            + "  // this is a comment\n"
+            + "  \"a\",\n"
+            + "  /* this is another comment */\n"
+            + "  \"b\",\n"
+            + "  # this is yet another comment\n"
+            + "  \"c\"\n"
+            + "]";
+
+    List<String> abc = new Gson().fromJson(json, new TypeToken<List<String>>() {}.getType());
+    assertThat(abc).containsExactly("a", "b", "c").inOrder();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f05c557638b611e80e827079248feab294994388
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/DefaultInetAddressTypeAdapterTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.net.InetAddress;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for the default serializer/deserializer for the {@code InetAddress} type.
+ *
+ * @author Joel Leitch
+ */
+public class DefaultInetAddressTypeAdapterTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testInetAddressSerializationAndDeserialization() throws Exception {
+    @SuppressWarnings("AddressSelection") // we really do want this method
+    InetAddress address = InetAddress.getByName("8.8.8.8");
+    String jsonAddress = gson.toJson(address);
+    assertThat(jsonAddress).isEqualTo("\"8.8.8.8\"");
+
+    InetAddress value = gson.fromJson(jsonAddress, InetAddress.class);
+    assertThat(address).isEqualTo(value);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.java b/gson/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1fd210223405603a7357aea9baa34bcfa2beee38
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/DefaultMapJsonSerializerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+
+/**
+ * Unit test for the default JSON map serialization object located in the {@link
+ * DefaultTypeAdapters} class.
+ *
+ * @author Joel Leitch
+ */
+public class DefaultMapJsonSerializerTest {
+  private Gson gson = new Gson();
+
+  @Test
+  public void testEmptyMapNoTypeSerialization() {
+    Map<String, String> emptyMap = new HashMap<>();
+    JsonElement element = gson.toJsonTree(emptyMap, emptyMap.getClass());
+    assertThat(element).isInstanceOf(JsonObject.class);
+    JsonObject emptyMapJsonObject = (JsonObject) element;
+    assertThat(emptyMapJsonObject.entrySet()).isEmpty();
+  }
+
+  @Test
+  public void testEmptyMapSerialization() {
+    Type mapType = new TypeToken<Map<String, String>>() {}.getType();
+    Map<String, String> emptyMap = new HashMap<>();
+    JsonElement element = gson.toJsonTree(emptyMap, mapType);
+
+    assertThat(element).isInstanceOf(JsonObject.class);
+    JsonObject emptyMapJsonObject = (JsonObject) element;
+    assertThat(emptyMapJsonObject.entrySet()).isEmpty();
+  }
+
+  @Test
+  public void testNonEmptyMapSerialization() {
+    Type mapType = new TypeToken<Map<String, String>>() {}.getType();
+    Map<String, String> myMap = new HashMap<>();
+    String key = "key1";
+    myMap.put(key, "value1");
+    Gson gson = new Gson();
+    JsonElement element = gson.toJsonTree(myMap, mapType);
+
+    assertThat(element.isJsonObject()).isTrue();
+    JsonObject mapJsonObject = element.getAsJsonObject();
+    assertThat(mapJsonObject.has(key)).isTrue();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java b/gson/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8738e5b98ca63ad093ccb6e4eddb7577d7c36871
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ExposeAnnotationExclusionStrategyTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.internal.Excluder;
+import java.lang.reflect.Field;
+import org.junit.Test;
+
+/**
+ * Unit tests for GsonBuilder.REQUIRE_EXPOSE_DESERIALIZE.
+ *
+ * @author Joel Leitch
+ */
+public class ExposeAnnotationExclusionStrategyTest {
+  private Excluder excluder = Excluder.DEFAULT.excludeFieldsWithoutExposeAnnotation();
+
+  private void assertIncludesClass(Class<?> c) {
+    assertThat(excluder.excludeClass(c, true)).isFalse();
+    assertThat(excluder.excludeClass(c, false)).isFalse();
+  }
+
+  private void assertIncludesField(Field f) {
+    assertThat(excluder.excludeField(f, true)).isFalse();
+    assertThat(excluder.excludeField(f, false)).isFalse();
+  }
+
+  private void assertExcludesField(Field f) {
+    assertThat(excluder.excludeField(f, true)).isTrue();
+    assertThat(excluder.excludeField(f, false)).isTrue();
+  }
+
+  @Test
+  public void testNeverSkipClasses() {
+    assertIncludesClass(MockObject.class);
+  }
+
+  @Test
+  public void testSkipNonAnnotatedFields() throws Exception {
+    Field f = createFieldAttributes("hiddenField");
+    assertExcludesField(f);
+  }
+
+  @Test
+  public void testSkipExplicitlySkippedFields() throws Exception {
+    Field f = createFieldAttributes("explicitlyHiddenField");
+    assertExcludesField(f);
+  }
+
+  @Test
+  public void testNeverSkipExposedAnnotatedFields() throws Exception {
+    Field f = createFieldAttributes("exposedField");
+    assertIncludesField(f);
+  }
+
+  @Test
+  public void testNeverSkipExplicitlyExposedAnnotatedFields() throws Exception {
+    Field f = createFieldAttributes("explicitlyExposedField");
+    assertIncludesField(f);
+  }
+
+  @Test
+  public void testDifferentSerializeAndDeserializeField() throws Exception {
+    Field f = createFieldAttributes("explicitlyDifferentModeField");
+    assertThat(excluder.excludeField(f, true)).isFalse();
+    assertThat(excluder.excludeField(f, false)).isTrue();
+  }
+
+  private static Field createFieldAttributes(String fieldName) throws Exception {
+    return MockObject.class.getField(fieldName);
+  }
+
+  @SuppressWarnings("unused")
+  private static class MockObject {
+    @Expose public final int exposedField = 0;
+
+    @Expose(serialize = true, deserialize = true)
+    public final int explicitlyExposedField = 0;
+
+    @Expose(serialize = false, deserialize = false)
+    public final int explicitlyHiddenField = 0;
+
+    @Expose(serialize = true, deserialize = false)
+    public final int explicitlyDifferentModeField = 0;
+
+    public final int hiddenField = 0;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/FieldAttributesTest.java b/gson/gson/src/test/java/com/google/gson/FieldAttributesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..17a4d70f6ee7aabba2c11e3b3e2626039f71db30
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/FieldAttributesTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2009 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for the {@link FieldAttributes} class.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class FieldAttributesTest {
+  private FieldAttributes fieldAttributes;
+
+  @Before
+  public void setUp() throws Exception {
+    fieldAttributes = new FieldAttributes(Foo.class.getField("bar"));
+  }
+
+  @SuppressWarnings("unused")
+  @Test
+  public void testNullField() {
+    try {
+      new FieldAttributes(null);
+      fail("Field parameter can not be null");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testDeclaringClass() {
+    assertThat(fieldAttributes.getDeclaringClass()).isAssignableTo(Foo.class);
+  }
+
+  @Test
+  public void testModifiers() {
+    assertThat(fieldAttributes.hasModifier(Modifier.STATIC)).isFalse();
+    assertThat(fieldAttributes.hasModifier(Modifier.FINAL)).isFalse();
+    assertThat(fieldAttributes.hasModifier(Modifier.ABSTRACT)).isFalse();
+    assertThat(fieldAttributes.hasModifier(Modifier.VOLATILE)).isFalse();
+    assertThat(fieldAttributes.hasModifier(Modifier.PROTECTED)).isFalse();
+
+    assertThat(fieldAttributes.hasModifier(Modifier.PUBLIC)).isTrue();
+    assertThat(fieldAttributes.hasModifier(Modifier.TRANSIENT)).isTrue();
+  }
+
+  @Test
+  public void testName() {
+    assertThat(fieldAttributes.getName()).isEqualTo("bar");
+  }
+
+  @Test
+  public void testDeclaredTypeAndClass() {
+    Type expectedType = new TypeToken<List<String>>() {}.getType();
+    assertThat(fieldAttributes.getDeclaredType()).isEqualTo(expectedType);
+    assertThat(fieldAttributes.getDeclaredClass()).isAssignableTo(List.class);
+  }
+
+  private static class Foo {
+    @SuppressWarnings("unused")
+    public transient List<String> bar;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/FieldNamingPolicyTest.java b/gson/gson/src/test/java/com/google/gson/FieldNamingPolicyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d085269f8a268599f0e41f4005abbb8496a2625
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/FieldNamingPolicyTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2021 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.gson.functional.FieldNamingTest;
+import java.lang.reflect.Field;
+import java.util.Locale;
+import org.junit.Test;
+
+/**
+ * Performs tests directly against {@link FieldNamingPolicy}; for integration tests see {@link
+ * FieldNamingTest}.
+ */
+public class FieldNamingPolicyTest {
+  @Test
+  public void testSeparateCamelCase() {
+    // Map from original -> expected
+    String[][] argumentPairs = {
+      {"a", "a"},
+      {"ab", "ab"},
+      {"Ab", "Ab"},
+      {"aB", "a_B"},
+      {"AB", "A_B"},
+      {"A_B", "A__B"},
+      {"firstSecondThird", "first_Second_Third"},
+      {"__", "__"},
+      {"_123", "_123"}
+    };
+
+    for (String[] pair : argumentPairs) {
+      assertThat(FieldNamingPolicy.separateCamelCase(pair[0], '_')).isEqualTo(pair[1]);
+    }
+  }
+
+  @Test
+  public void testUpperCaseFirstLetter() {
+    // Map from original -> expected
+    String[][] argumentPairs = {
+      {"a", "A"},
+      {"ab", "Ab"},
+      {"AB", "AB"},
+      {"_a", "_A"},
+      {"_ab", "_Ab"},
+      {"__", "__"},
+      {"_1", "_1"},
+      // Not a letter, but has uppercase variant (should not be uppercased)
+      // See https://github.com/google/gson/issues/1965
+      {"\u2170", "\u2170"},
+      {"_\u2170", "_\u2170"},
+      {"\u2170a", "\u2170A"},
+    };
+
+    for (String[] pair : argumentPairs) {
+      assertThat(FieldNamingPolicy.upperCaseFirstLetter(pair[0])).isEqualTo(pair[1]);
+    }
+  }
+
+  /** Upper-casing policies should be unaffected by default Locale. */
+  @Test
+  public void testUpperCasingLocaleIndependent() throws Exception {
+    class Dummy {
+      @SuppressWarnings("unused")
+      int i;
+    }
+
+    FieldNamingPolicy[] policies = {
+      FieldNamingPolicy.UPPER_CAMEL_CASE,
+      FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES,
+      FieldNamingPolicy.UPPER_CASE_WITH_UNDERSCORES,
+    };
+
+    Field field = Dummy.class.getDeclaredField("i");
+    String name = field.getName();
+    String expected = name.toUpperCase(Locale.ROOT);
+
+    Locale oldLocale = Locale.getDefault();
+    // Set Turkish as Locale which has special case conversion rules
+    Locale.setDefault(new Locale("tr"));
+
+    try {
+      // Verify that default Locale has different case conversion rules
+      assertWithMessage("Test setup is broken")
+          .that(name.toUpperCase(Locale.getDefault()))
+          .doesNotMatch(expected);
+
+      for (FieldNamingPolicy policy : policies) {
+        // Should ignore default Locale
+        assertWithMessage("Unexpected conversion for %s", policy)
+            .that(policy.translateName(field))
+            .matches(expected);
+      }
+    } finally {
+      Locale.setDefault(oldLocale);
+    }
+  }
+
+  /** Lower casing policies should be unaffected by default Locale. */
+  @Test
+  public void testLowerCasingLocaleIndependent() throws Exception {
+    class Dummy {
+      @SuppressWarnings({"unused", "ConstantField"})
+      int I;
+    }
+
+    FieldNamingPolicy[] policies = {
+      FieldNamingPolicy.LOWER_CASE_WITH_DASHES,
+      FieldNamingPolicy.LOWER_CASE_WITH_DOTS,
+      FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES,
+    };
+
+    Field field = Dummy.class.getDeclaredField("I");
+    String name = field.getName();
+    String expected = name.toLowerCase(Locale.ROOT);
+
+    Locale oldLocale = Locale.getDefault();
+    // Set Turkish as Locale which has special case conversion rules
+    Locale.setDefault(new Locale("tr"));
+
+    try {
+      // Verify that default Locale has different case conversion rules
+      assertWithMessage("Test setup is broken")
+          .that(name.toLowerCase(Locale.getDefault()))
+          .doesNotMatch(expected);
+
+      for (FieldNamingPolicy policy : policies) {
+        // Should ignore default Locale
+        assertWithMessage("Unexpected conversion for %s", policy)
+            .that(policy.translateName(field))
+            .matches(expected);
+      }
+    } finally {
+      Locale.setDefault(oldLocale);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java b/gson/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f17f29c72182c45588249507832dc86cdc001846
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/GenericArrayTypeTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for the {@code GenericArrayType}s created by the {@link $Gson$Types} class.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class GenericArrayTypeTest {
+  private GenericArrayType ourType;
+
+  @Before
+  public void setUp() throws Exception {
+    ourType =
+        $Gson$Types.arrayOf(
+            $Gson$Types.newParameterizedTypeWithOwner(null, List.class, String.class));
+  }
+
+  @Test
+  public void testOurTypeFunctionality() throws Exception {
+    Type parameterizedType = new TypeToken<List<String>>() {}.getType();
+    Type genericArrayType = new TypeToken<List<String>[]>() {}.getType();
+
+    assertThat(ourType.getGenericComponentType()).isEqualTo(parameterizedType);
+    assertThat(ourType).isEqualTo(genericArrayType);
+    assertThat(ourType.hashCode()).isEqualTo(genericArrayType.hashCode());
+  }
+
+  @Test
+  public void testNotEquals() throws Exception {
+    Type differentGenericArrayType = new TypeToken<List<String>[][]>() {}.getType();
+    assertThat(differentGenericArrayType.equals(ourType)).isFalse();
+    assertThat(ourType.equals(differentGenericArrayType)).isFalse();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/GsonBuilderTest.java b/gson/gson/src/test/java/com/google/gson/GsonBuilderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..202c672914424a9ff68deefcfab49e7f3300497c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/GsonBuilderTest.java
@@ -0,0 +1,394 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.util.Date;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link GsonBuilder}.
+ *
+ * @author Inderjeet Singh
+ */
+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();
+        }
+
+        @Override
+        public Object read(JsonReader in) {
+          throw new AssertionError();
+        }
+      };
+
+  @Test
+  public void testCreatingMoreThanOnce() {
+    GsonBuilder builder = new GsonBuilder();
+    Gson gson = builder.create();
+    assertThat(gson).isNotNull();
+    assertThat(builder.create()).isNotNull();
+
+    builder.setFieldNamingStrategy(
+        new FieldNamingStrategy() {
+          @Override
+          public String translateName(Field f) {
+            return "test";
+          }
+        });
+
+    Gson otherGson = builder.create();
+    assertThat(otherGson).isNotNull();
+    // Should be different instances because builder has been modified in the meantime
+    assertThat(gson).isNotSameInstanceAs(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) {
+            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());
+    assertThat(json1).isEqualTo("{}");
+
+    // Should use default reflective adapter
+    String json2 = gson.toJson(new CustomClass2());
+    assertThat(json2).isEqualTo("{}");
+
+    // Should use default instance creator
+    CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+    assertThat(customClass3.s).isEqualTo(CustomClass3.NO_ARG_CONSTRUCTOR_VALUE);
+  }
+
+  private static void assertCustomGson(Gson gson) {
+    String json1 = gson.toJson(new CustomClass1());
+    assertThat(json1).isEqualTo("\"custom-adapter\"");
+
+    String json2 = gson.toJson(new CustomClass2());
+    assertThat(json2).isEqualTo("\"custom-hierarchy-adapter\"");
+
+    CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+    assertThat(customClass3.s).isEqualTo("custom-instance");
+  }
+
+  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).create();
+    assertThat(gson.toJson(new HasModifiers())).isEqualTo("{\"d\":\"d\"}");
+  }
+
+  @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();
+    assertThat(gson.toJson(new HasTransients())).isEqualTo("{\"a\":\"a\"}");
+  }
+
+  static class HasTransients {
+    transient String a = "a";
+  }
+
+  @Test
+  public void testRegisterTypeAdapterForCoreType() {
+    Type[] types = {
+      byte.class, int.class, double.class, Short.class, Long.class, String.class,
+    };
+    for (Type type : types) {
+      new GsonBuilder().registerTypeAdapter(type, NULL_TYPE_ADAPTER);
+    }
+  }
+
+  @Test
+  public void testDisableJdkUnsafe() {
+    Gson gson = new GsonBuilder().disableJdkUnsafe().create();
+    try {
+      gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
+      fail("Expected exception");
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unable to create instance of class"
+                  + " com.google.gson.GsonBuilderTest$ClassWithoutNoArgsConstructor; usage of JDK"
+                  + " Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this"
+                  + " type, adding a no-args constructor, or enabling usage of JDK Unsafe may fix"
+                  + " this problem.");
+    }
+  }
+
+  private static class ClassWithoutNoArgsConstructor {
+    @SuppressWarnings("unused")
+    public ClassWithoutNoArgsConstructor(String s) {}
+  }
+
+  @Test
+  public void testSetVersionInvalid() {
+    GsonBuilder builder = new GsonBuilder();
+    try {
+      builder.setVersion(Double.NaN);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Invalid version: NaN");
+    }
+
+    try {
+      builder.setVersion(-0.1);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Invalid version: -0.1");
+    }
+  }
+
+  @Test
+  public void testDefaultStrictness() throws IOException {
+    GsonBuilder builder = new GsonBuilder();
+    Gson gson = builder.create();
+    assertThat(gson.newJsonReader(new StringReader("{}")).getStrictness())
+        .isEqualTo(Strictness.LEGACY_STRICT);
+    assertThat(gson.newJsonWriter(new StringWriter()).getStrictness())
+        .isEqualTo(Strictness.LEGACY_STRICT);
+  }
+
+  @SuppressWarnings({"deprecation", "InlineMeInliner"}) // for GsonBuilder.setLenient
+  @Test
+  public void testSetLenient() throws IOException {
+    GsonBuilder builder = new GsonBuilder();
+    builder.setLenient();
+    Gson gson = builder.create();
+    assertThat(gson.newJsonReader(new StringReader("{}")).getStrictness())
+        .isEqualTo(Strictness.LENIENT);
+    assertThat(gson.newJsonWriter(new StringWriter()).getStrictness())
+        .isEqualTo(Strictness.LENIENT);
+  }
+
+  @Test
+  public void testSetStrictness() throws IOException {
+    final Strictness STRICTNESS = Strictness.STRICT;
+    GsonBuilder builder = new GsonBuilder();
+    builder.setStrictness(STRICTNESS);
+    Gson gson = builder.create();
+    assertThat(gson.newJsonReader(new StringReader("{}")).getStrictness()).isEqualTo(STRICTNESS);
+    assertThat(gson.newJsonWriter(new StringWriter()).getStrictness()).isEqualTo(STRICTNESS);
+  }
+
+  @Test
+  public void testRegisterTypeAdapterForObjectAndJsonElements() {
+    final String ERROR_MESSAGE = "Cannot override built-in adapter for ";
+    Type[] types = {
+      Object.class, JsonElement.class, JsonArray.class,
+    };
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    for (Type type : types) {
+      IllegalArgumentException e =
+          assertThrows(
+              IllegalArgumentException.class,
+              () -> gsonBuilder.registerTypeAdapter(type, NULL_TYPE_ADAPTER));
+      assertThat(e).hasMessageThat().isEqualTo(ERROR_MESSAGE + type);
+    }
+  }
+
+  @Test
+  public void testRegisterTypeHierarchyAdapterJsonElements() {
+    final String ERROR_MESSAGE = "Cannot override built-in adapter for ";
+    Class<?>[] types = {
+      JsonElement.class, JsonArray.class,
+    };
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    for (Class<?> type : types) {
+      IllegalArgumentException e =
+          assertThrows(
+              IllegalArgumentException.class,
+              () -> gsonBuilder.registerTypeHierarchyAdapter(type, NULL_TYPE_ADAPTER));
+
+      assertThat(e).hasMessageThat().isEqualTo(ERROR_MESSAGE + type);
+    }
+    // But registering type hierarchy adapter for Object should be allowed
+    gsonBuilder.registerTypeHierarchyAdapter(Object.class, NULL_TYPE_ADAPTER);
+  }
+
+  @Test
+  public void testSetDateFormatWithInvalidPattern() {
+    GsonBuilder builder = new GsonBuilder();
+    String invalidPattern = "This is an invalid Pattern";
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> builder.setDateFormat(invalidPattern));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("The date pattern '" + invalidPattern + "' is not valid");
+  }
+
+  @Test
+  public void testSetDateFormatWithValidPattern() {
+    GsonBuilder builder = new GsonBuilder();
+    String validPattern = "yyyy-MM-dd";
+    // Should not throw an exception
+    builder.setDateFormat(validPattern);
+  }
+
+  @Test
+  public void testSetDateFormatNullPattern() {
+    GsonBuilder builder = new GsonBuilder();
+    @SuppressWarnings("JavaUtilDate")
+    Date date = new Date(0);
+    String originalFormatted = builder.create().toJson(date);
+
+    String customFormatted = builder.setDateFormat("yyyy-MM-dd").create().toJson(date);
+    assertThat(customFormatted).isNotEqualTo(originalFormatted);
+
+    // `null` should reset the format to the default
+    String resetFormatted = builder.setDateFormat(null).create().toJson(date);
+    assertThat(resetFormatted).isEqualTo(originalFormatted);
+  }
+
+  /**
+   * Tests behavior for an empty date pattern; this behavior is not publicly documented at the
+   * moment.
+   */
+  @Test
+  public void testSetDateFormatEmptyPattern() {
+    GsonBuilder builder = new GsonBuilder();
+    @SuppressWarnings("JavaUtilDate")
+    Date date = new Date(0);
+    String originalFormatted = builder.create().toJson(date);
+
+    String emptyFormatted = builder.setDateFormat("    ").create().toJson(date);
+    // Empty pattern was ignored
+    assertThat(emptyFormatted).isEqualTo(originalFormatted);
+  }
+
+  @Test
+  public void testSetDateFormatValidStyle() {
+    GsonBuilder builder = new GsonBuilder();
+    int[] validStyles = {DateFormat.FULL, DateFormat.LONG, DateFormat.MEDIUM, DateFormat.SHORT};
+
+    for (int style : validStyles) {
+      // Should not throw an exception
+      builder.setDateFormat(style);
+      builder.setDateFormat(style, style);
+    }
+  }
+
+  @Test
+  public void testSetDateFormatInvalidStyle() {
+    GsonBuilder builder = new GsonBuilder();
+
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> builder.setDateFormat(-1));
+    assertThat(e).hasMessageThat().isEqualTo("Invalid style: -1");
+
+    e = assertThrows(IllegalArgumentException.class, () -> builder.setDateFormat(4));
+    assertThat(e).hasMessageThat().isEqualTo("Invalid style: 4");
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class, () -> builder.setDateFormat(-1, DateFormat.FULL));
+    assertThat(e).hasMessageThat().isEqualTo("Invalid style: -1");
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class, () -> builder.setDateFormat(DateFormat.FULL, -1));
+    assertThat(e).hasMessageThat().isEqualTo("Invalid style: -1");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/GsonTest.java b/gson/gson/src/test/java/com/google/gson/GsonTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e51259e781a0fc8d084e646f7b25550a1b1a0d03
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/GsonTest.java
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2016 The Gson Authors
+ *
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson.FutureTypeAdapter;
+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;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link Gson}.
+ *
+ * @author Ryan Harter
+ */
+public final class GsonTest {
+
+  private static final Excluder CUSTOM_EXCLUDER =
+      Excluder.DEFAULT.excludeFieldsWithoutExposeAnnotation().disableInnerClassSerialization();
+
+  private static final FieldNamingStrategy CUSTOM_FIELD_NAMING_STRATEGY =
+      new FieldNamingStrategy() {
+        @Override
+        public String translateName(Field f) {
+          return "foo";
+        }
+      };
+
+  private static final ToNumberStrategy CUSTOM_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE;
+  private static final ToNumberStrategy CUSTOM_NUMBER_TO_NUMBER_STRATEGY =
+      ToNumberPolicy.LAZILY_PARSED_NUMBER;
+
+  @Test
+  public void testStrictnessDefault() {
+    assertThat(new Gson().strictness).isNull();
+  }
+
+  @Test
+  public void testOverridesDefaultExcluder() {
+    Gson gson =
+        new Gson(
+            CUSTOM_EXCLUDER,
+            CUSTOM_FIELD_NAMING_STRATEGY,
+            new HashMap<Type, InstanceCreator<?>>(),
+            true,
+            false,
+            true,
+            false,
+            FormattingStyle.PRETTY,
+            Strictness.LENIENT,
+            false,
+            true,
+            LongSerializationPolicy.DEFAULT,
+            null,
+            DateFormat.DEFAULT,
+            DateFormat.DEFAULT,
+            new ArrayList<TypeAdapterFactory>(),
+            new ArrayList<TypeAdapterFactory>(),
+            new ArrayList<TypeAdapterFactory>(),
+            CUSTOM_OBJECT_TO_NUMBER_STRATEGY,
+            CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
+            Collections.<ReflectionAccessFilter>emptyList());
+
+    assertThat(gson.excluder).isEqualTo(CUSTOM_EXCLUDER);
+    assertThat(gson.fieldNamingStrategy()).isEqualTo(CUSTOM_FIELD_NAMING_STRATEGY);
+    assertThat(gson.serializeNulls()).isTrue();
+    assertThat(gson.htmlSafe()).isFalse();
+  }
+
+  @Test
+  public void testClonedTypeAdapterFactoryListsAreIndependent() {
+    Gson original =
+        new Gson(
+            CUSTOM_EXCLUDER,
+            CUSTOM_FIELD_NAMING_STRATEGY,
+            new HashMap<Type, InstanceCreator<?>>(),
+            true,
+            false,
+            true,
+            false,
+            FormattingStyle.PRETTY,
+            Strictness.LENIENT,
+            false,
+            true,
+            LongSerializationPolicy.DEFAULT,
+            null,
+            DateFormat.DEFAULT,
+            DateFormat.DEFAULT,
+            new ArrayList<TypeAdapterFactory>(),
+            new ArrayList<TypeAdapterFactory>(),
+            new ArrayList<TypeAdapterFactory>(),
+            CUSTOM_OBJECT_TO_NUMBER_STRATEGY,
+            CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
+            Collections.<ReflectionAccessFilter>emptyList());
+
+    Gson clone =
+        original.newBuilder().registerTypeAdapter(int.class, new TestTypeAdapter()).create();
+
+    assertThat(clone.factories).hasSize(original.factories.size() + 1);
+  }
+
+  private static final class TestTypeAdapter extends TypeAdapter<Object> {
+    @Override
+    public void write(JsonWriter out, Object value) {
+      // Test stub.
+    }
+
+    @Override
+    public Object read(JsonReader in) {
+      return null;
+    }
+  }
+
+  @Test
+  public void testGetAdapter_Null() {
+    Gson gson = new Gson();
+    NullPointerException e =
+        assertThrows(NullPointerException.class, () -> gson.getAdapter((TypeToken<?>) null));
+    assertThat(e).hasMessageThat().isEqualTo("type must not be null");
+  }
+
+  @Test
+  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 this test");
+      }
+
+      @Override
+      public T read(JsonReader in) throws IOException {
+        throw new AssertionError("not needed for this 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);
+    assertThat(adapterInstancesCreated.get()).isEqualTo(2);
+    assertThat(adapter).isInstanceOf(DummyAdapter.class);
+    assertThat(threadAdapter.get()).isInstanceOf(DummyAdapter.class);
+  }
+
+  /**
+   * Verifies that two threads calling {@link Gson#getAdapter(TypeToken)} do not see the same
+   * unresolved {@link FutureTypeAdapter} instance, since that would not be thread-safe.
+   *
+   * <p>This test constructs the cyclic dependency {@literal CustomClass1 -> CustomClass2 ->
+   * CustomClass1} and lets one thread wait after the adapter for CustomClass2 has been obtained
+   * (which still refers to the nested unresolved FutureTypeAdapter for CustomClass1).
+   */
+  @Test
+  public void testGetAdapter_FutureAdapterConcurrency() throws Exception {
+    /**
+     * Adapter which wraps another adapter. Can be imagined as a simplified version of the {@code
+     * ReflectiveTypeAdapterFactory$Adapter}.
+     */
+    class WrappingAdapter<T> extends TypeAdapter<T> {
+      final TypeAdapter<?> wrapped;
+      boolean isFirstCall = true;
+
+      WrappingAdapter(TypeAdapter<?> wrapped) {
+        this.wrapped = wrapped;
+      }
+
+      @Override
+      public void write(JsonWriter out, T value) throws IOException {
+        // Due to how this test is set up there is infinite recursion, therefore
+        // need to track how deeply nested this call is
+        if (isFirstCall) {
+          isFirstCall = false;
+          out.beginArray();
+          wrapped.write(out, null);
+          out.endArray();
+          isFirstCall = true;
+        } else {
+          out.value("wrapped-nested");
+        }
+      }
+
+      @Override
+      public T read(JsonReader in) throws IOException {
+        throw new AssertionError("not needed for this test");
+      }
+    }
+
+    final CountDownLatch isThreadWaiting = new CountDownLatch(1);
+    final CountDownLatch canThreadProceed = new CountDownLatch(1);
+
+    final Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapterFactory(
+                new TypeAdapterFactory() {
+                  // volatile instead of AtomicBoolean is safe here because CountDownLatch prevents
+                  // "true" concurrency
+                  volatile boolean isFirstCaller = true;
+
+                  @Override
+                  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+                    Class<?> raw = type.getRawType();
+
+                    if (raw == CustomClass1.class) {
+                      // Retrieves a WrappingAdapter containing a nested FutureAdapter for
+                      // CustomClass1
+                      TypeAdapter<?> adapter = gson.getAdapter(CustomClass2.class);
+
+                      // Let thread wait so the FutureAdapter for CustomClass1 nested in the adapter
+                      // for CustomClass2 is not resolved yet
+                      if (isFirstCaller) {
+                        isFirstCaller = false;
+                        isThreadWaiting.countDown();
+
+                        try {
+                          canThreadProceed.await();
+                        } catch (InterruptedException e) {
+                          throw new RuntimeException(e);
+                        }
+                      }
+
+                      return new WrappingAdapter<>(adapter);
+                    } else if (raw == CustomClass2.class) {
+                      TypeAdapter<?> adapter = gson.getAdapter(CustomClass1.class);
+                      assertThat(adapter).isInstanceOf(FutureTypeAdapter.class);
+                      return new WrappingAdapter<>(adapter);
+                    } else {
+                      throw new AssertionError("Adapter for unexpected type requested: " + raw);
+                    }
+                  }
+                })
+            .create();
+
+    final AtomicReference<TypeAdapter<?>> otherThreadAdapter = new AtomicReference<>();
+    Thread thread =
+        new Thread() {
+          @Override
+          public void run() {
+            otherThreadAdapter.set(gson.getAdapter(CustomClass1.class));
+          }
+        };
+    thread.start();
+
+    // Wait until other thread has obtained FutureAdapter
+    isThreadWaiting.await();
+    TypeAdapter<?> adapter = gson.getAdapter(CustomClass1.class);
+    // Should not fail due to referring to unresolved FutureTypeAdapter
+    assertThat(adapter.toJson(null)).isEqualTo("[[\"wrapped-nested\"]]");
+
+    // Let other thread proceed and have it resolve its FutureTypeAdapter
+    canThreadProceed.countDown();
+    thread.join();
+    assertThat(otherThreadAdapter.get().toJson(null)).isEqualTo("[[\"wrapped-nested\"]]");
+  }
+
+  @Test
+  public void testGetDelegateAdapter() {
+    class DummyAdapter extends TypeAdapter<Number> {
+      private final int number;
+
+      DummyAdapter(int number) {
+        this.number = number;
+      }
+
+      @Override
+      public Number read(JsonReader in) throws IOException {
+        throw new AssertionError("not needed for test");
+      }
+
+      @Override
+      public void write(JsonWriter out, Number value) throws IOException {
+        throw new AssertionError("not needed for test");
+      }
+
+      // Override toString() for better assertion error messages
+      @Override
+      public String toString() {
+        return "adapter-" + number;
+      }
+    }
+
+    class DummyFactory implements TypeAdapterFactory {
+      private final DummyAdapter adapter;
+
+      DummyFactory(DummyAdapter adapter) {
+        this.adapter = adapter;
+      }
+
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        return (TypeAdapter<T>) adapter;
+      }
+
+      // Override equals to verify that reference equality check is performed by Gson,
+      // and this method is ignored
+      @Override
+      public boolean equals(Object obj) {
+        return obj instanceof DummyFactory && ((DummyFactory) obj).adapter.equals(adapter);
+      }
+
+      @Override
+      public int hashCode() {
+        return adapter.hashCode();
+      }
+    }
+
+    DummyAdapter adapter1 = new DummyAdapter(1);
+    DummyFactory factory1 = new DummyFactory(adapter1);
+    DummyAdapter adapter2 = new DummyAdapter(2);
+    DummyFactory factory2 = new DummyFactory(adapter2);
+
+    Gson gson =
+        new GsonBuilder()
+            // Note: This is 'last in, first out' order; Gson will first use factory2, then factory1
+            .registerTypeAdapterFactory(factory1)
+            .registerTypeAdapterFactory(factory2)
+            .create();
+
+    TypeToken<?> type = TypeToken.get(Number.class);
+
+    assertThrows(NullPointerException.class, () -> gson.getDelegateAdapter(null, type));
+    assertThrows(NullPointerException.class, () -> gson.getDelegateAdapter(factory1, null));
+
+    // For unknown factory the first adapter for that type should be returned
+    assertThat(gson.getDelegateAdapter(new DummyFactory(new DummyAdapter(0)), type))
+        .isEqualTo(adapter2);
+
+    assertThat(gson.getDelegateAdapter(factory2, type)).isEqualTo(adapter1);
+    // Default Gson adapter should be returned
+    assertThat(gson.getDelegateAdapter(factory1, type)).isNotInstanceOf(DummyAdapter.class);
+
+    DummyFactory factory1Eq = new DummyFactory(adapter1);
+    // Verify that test setup is correct
+    assertThat(factory1.equals(factory1Eq)).isTrue();
+    // Should only consider reference equality and ignore that custom `equals` method considers
+    // factories to be equal, therefore returning `adapter2` which came from `factory2` instead
+    // of skipping past `factory1`
+    assertThat(gson.getDelegateAdapter(factory1Eq, type)).isEqualTo(adapter2);
+  }
+
+  @Test
+  public void testNewJsonWriter_Default() throws IOException {
+    StringWriter writer = new StringWriter();
+    JsonWriter jsonWriter = new Gson().newJsonWriter(writer);
+    jsonWriter.beginObject();
+    jsonWriter.name("test");
+    jsonWriter.nullValue();
+    jsonWriter.name("<test2");
+    jsonWriter.value(true);
+    jsonWriter.endObject();
+
+    // Additional top-level value
+    IllegalStateException e = assertThrows(IllegalStateException.class, () -> jsonWriter.value(1));
+    assertThat(e).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
+
+    jsonWriter.close();
+    assertThat(writer.toString()).isEqualTo("{\"\\u003ctest2\":true}");
+  }
+
+  @SuppressWarnings({"deprecation", "InlineMeInliner"}) // for GsonBuilder.setLenient
+  @Test
+  public void testNewJsonWriter_Custom() throws IOException {
+    StringWriter writer = new StringWriter();
+    JsonWriter jsonWriter =
+        new GsonBuilder()
+            .disableHtmlEscaping()
+            .generateNonExecutableJson()
+            .setPrettyPrinting()
+            .serializeNulls()
+            .setLenient()
+            .create()
+            .newJsonWriter(writer);
+    jsonWriter.beginObject();
+    jsonWriter.name("test");
+    jsonWriter.nullValue();
+    jsonWriter.name("<test2");
+    jsonWriter.value(true);
+    jsonWriter.endObject();
+
+    // Additional top-level value
+    jsonWriter.value(1);
+
+    jsonWriter.close();
+    assertThat(writer.toString()).isEqualTo(")]}'\n{\n  \"test\": null,\n  \"<test2\": true\n}1");
+  }
+
+  @Test
+  public void testNewJsonReader_Default() throws IOException {
+    String json = "test"; // String without quotes
+    JsonReader jsonReader = new Gson().newJsonReader(new StringReader(json));
+    assertThrows(MalformedJsonException.class, jsonReader::nextString);
+    jsonReader.close();
+  }
+
+  @SuppressWarnings({"deprecation", "InlineMeInliner"}) // for GsonBuilder.setLenient
+  @Test
+  public void testNewJsonReader_Custom() throws IOException {
+    String json = "test"; // String without quotes
+    JsonReader jsonReader =
+        new GsonBuilder().setLenient().create().newJsonReader(new StringReader(json));
+    assertThat(jsonReader.nextString()).isEqualTo("test");
+    jsonReader.close();
+  }
+
+  /**
+   * Modifying a GsonBuilder obtained from {@link Gson#newBuilder()} of a {@code new Gson()} should
+   * not affect the Gson instance it came from.
+   */
+  @Test
+  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());
+    assertThat(json1).isEqualTo("{}");
+
+    // Should use default reflective adapter
+    String json2 = gson.toJson(new CustomClass2());
+    assertThat(json2).isEqualTo("{}");
+
+    // Should use default instance creator
+    CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+    assertThat(customClass3.s).isEqualTo(CustomClass3.NO_ARG_CONSTRUCTOR_VALUE);
+  }
+
+  /**
+   * 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.
+   */
+  @Test
+  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());
+    assertThat(json1).isEqualTo("\"overwritten custom-adapter\"");
+
+    String json2 = otherGson.toJson(new CustomClass2());
+    assertThat(json2).isEqualTo("\"overwritten custom-hierarchy-adapter\"");
+
+    CustomClass3 customClass3 = otherGson.fromJson("{}", CustomClass3.class);
+    assertThat(customClass3.s).isEqualTo("overwritten custom-instance");
+  }
+
+  private static void assertCustomGson(Gson gson) {
+    String json1 = gson.toJson(new CustomClass1());
+    assertThat(json1).isEqualTo("\"custom-adapter\"");
+
+    String json2 = gson.toJson(new CustomClass2());
+    assertThat(json2).isEqualTo("\"custom-hierarchy-adapter\"");
+
+    CustomClass3 customClass3 = gson.fromJson("{}", CustomClass3.class);
+    assertThat(customClass3.s).isEqualTo("custom-instance");
+  }
+
+  private static class CustomClass1 {}
+
+  private static class CustomClass2 {}
+
+  private static class CustomClass3 {
+    static final String NO_ARG_CONSTRUCTOR_VALUE = "default instance";
+
+    final String s;
+
+    public CustomClass3(String s) {
+      this.s = s;
+    }
+
+    @SuppressWarnings("unused") // called by Gson
+    public CustomClass3() {
+      this(NO_ARG_CONSTRUCTOR_VALUE);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6422bacc420a3f2a8f3b1a2600c455266c2452a6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/GsonTypeAdapterTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Type;
+import java.math.BigInteger;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Contains numerous tests involving registered type converters with a Gson instance.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class GsonTypeAdapterTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(AtomicLong.class, new ExceptionTypeAdapter())
+            .registerTypeAdapter(AtomicInteger.class, new AtomicIntegerTypeAdapter())
+            .create();
+  }
+
+  @Test
+  public void testDefaultTypeAdapterThrowsParseException() throws Exception {
+    try {
+      gson.fromJson("{\"abc\":123}", BigInteger.class);
+      fail("Should have thrown a JsonParseException");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testTypeAdapterThrowsException() throws Exception {
+    try {
+      gson.toJson(new AtomicLong(0));
+      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
+    assertThat(gson.toJson(null, AtomicLong.class)).isEqualTo("null");
+
+    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
+    assertThat(gson.fromJson(JsonNull.INSTANCE, AtomicLong.class)).isNull();
+  }
+
+  @Test
+  public void testTypeAdapterProperlyConvertsTypes() {
+    int intialValue = 1;
+    AtomicInteger atomicInt = new AtomicInteger(intialValue);
+    String json = gson.toJson(atomicInt);
+    assertThat(Integer.parseInt(json)).isEqualTo(intialValue + 1);
+
+    atomicInt = gson.fromJson(json, AtomicInteger.class);
+    assertThat(atomicInt.get()).isEqualTo(intialValue);
+  }
+
+  @Test
+  public void testTypeAdapterDoesNotAffectNonAdaptedTypes() {
+    String expected = "blah";
+    String actual = gson.toJson(expected);
+    assertThat(actual).isEqualTo("\"" + expected + "\"");
+
+    actual = gson.fromJson(actual, String.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  private static class ExceptionTypeAdapter
+      implements JsonSerializer<AtomicLong>, JsonDeserializer<AtomicLong> {
+    @Override
+    public JsonElement serialize(AtomicLong src, Type typeOfSrc, JsonSerializationContext context) {
+      throw new IllegalStateException();
+    }
+
+    @Override
+    public AtomicLong deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      throw new IllegalStateException();
+    }
+  }
+
+  private static class AtomicIntegerTypeAdapter
+      implements JsonSerializer<AtomicInteger>, JsonDeserializer<AtomicInteger> {
+    @Override
+    public JsonElement serialize(
+        AtomicInteger src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive(src.incrementAndGet());
+    }
+
+    @Override
+    public AtomicInteger deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      int intValue = json.getAsInt();
+      return new AtomicInteger(--intValue);
+    }
+  }
+
+  abstract static class Abstract {
+    String a;
+  }
+
+  static class Concrete extends Abstract {
+    String b;
+  }
+
+  // https://groups.google.com/d/topic/google-gson/EBmOCa8kJPE/discussion
+  @Test
+  public void testDeserializerForAbstractClass() {
+    Concrete instance = new Concrete();
+    instance.a = "android";
+    instance.b = "beep";
+    assertSerialized("{\"a\":\"android\"}", Abstract.class, true, true, instance);
+    assertSerialized("{\"a\":\"android\"}", Abstract.class, true, false, instance);
+    assertSerialized("{\"a\":\"android\"}", Abstract.class, false, true, instance);
+    assertSerialized("{\"a\":\"android\"}", Abstract.class, false, false, instance);
+    assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, true, true, instance);
+    assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, true, false, instance);
+    assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, false, true, instance);
+    assertSerialized("{\"b\":\"beep\",\"a\":\"android\"}", Concrete.class, false, false, instance);
+  }
+
+  private static void assertSerialized(
+      String expected,
+      Class<?> instanceType,
+      boolean registerAbstractDeserializer,
+      boolean registerAbstractHierarchyDeserializer,
+      Object instance) {
+    JsonDeserializer<Abstract> deserializer =
+        new JsonDeserializer<Abstract>() {
+          @Override
+          public Abstract deserialize(
+              JsonElement json, Type typeOfT, JsonDeserializationContext context)
+              throws JsonParseException {
+            throw new AssertionError();
+          }
+        };
+    GsonBuilder builder = new GsonBuilder();
+    if (registerAbstractDeserializer) {
+      builder.registerTypeAdapter(Abstract.class, deserializer);
+    }
+    if (registerAbstractHierarchyDeserializer) {
+      builder.registerTypeHierarchyAdapter(Abstract.class, deserializer);
+    }
+    Gson gson = builder.create();
+    assertThat(gson.toJson(instance, instanceType)).isEqualTo(expected);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java b/gson/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f768c5012c5bcaef32cfe6d8dd7f47982989f321
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/InnerClassExclusionStrategyTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.internal.Excluder;
+import java.lang.reflect.Field;
+import org.junit.Test;
+
+/**
+ * Unit test for GsonBuilder.EXCLUDE_INNER_CLASSES.
+ *
+ * @author Joel Leitch
+ */
+public class InnerClassExclusionStrategyTest {
+  public InnerClass innerClass = new InnerClass();
+  public StaticNestedClass staticNestedClass = new StaticNestedClass();
+  private Excluder excluder = Excluder.DEFAULT.disableInnerClassSerialization();
+
+  private void assertIncludesClass(Class<?> c) {
+    assertThat(excluder.excludeClass(c, true)).isFalse();
+    assertThat(excluder.excludeClass(c, false)).isFalse();
+  }
+
+  private void assertExcludesClass(Class<?> c) {
+    assertThat(excluder.excludeClass(c, true)).isTrue();
+    assertThat(excluder.excludeClass(c, false)).isTrue();
+  }
+
+  private void assertIncludesField(Field f) {
+    assertThat(excluder.excludeField(f, true)).isFalse();
+    assertThat(excluder.excludeField(f, false)).isFalse();
+  }
+
+  private void assertExcludesField(Field f) {
+    assertThat(excluder.excludeField(f, true)).isTrue();
+    assertThat(excluder.excludeField(f, false)).isTrue();
+  }
+
+  @Test
+  public void testExcludeInnerClassObject() {
+    Class<?> clazz = innerClass.getClass();
+    assertExcludesClass(clazz);
+  }
+
+  @Test
+  public void testExcludeInnerClassField() throws Exception {
+    Field f = getClass().getField("innerClass");
+    assertExcludesField(f);
+  }
+
+  @Test
+  public void testIncludeStaticNestedClassObject() {
+    Class<?> clazz = staticNestedClass.getClass();
+    assertIncludesClass(clazz);
+  }
+
+  @Test
+  public void testIncludeStaticNestedClassField() throws Exception {
+    Field f = getClass().getField("staticNestedClass");
+    assertIncludesField(f);
+  }
+
+  @SuppressWarnings("ClassCanBeStatic")
+  class InnerClass {}
+
+  static class StaticNestedClass {}
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JavaSerializationTest.java b/gson/gson/src/test/java/com/google/gson/JavaSerializationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..219e16bb56f560d928270b7dfa770b6defd9828a
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JavaSerializationTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2012 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.reflect.TypeToken;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+/**
+ * Check that Gson doesn't return non-serializable data types.
+ *
+ * @author Jesse Wilson
+ */
+public final class JavaSerializationTest {
+  private final Gson gson = new Gson();
+
+  @Test
+  public void testMapIsSerializable() throws Exception {
+    Type type = new TypeToken<Map<String, Integer>>() {}.getType();
+    Map<String, Integer> map = gson.fromJson("{\"b\":1,\"c\":2,\"a\":3}", type);
+    Map<String, Integer> serialized = serializedCopy(map);
+    assertThat(serialized).isEqualTo(map);
+    // Also check that the iteration order is retained.
+    assertThat(serialized.keySet()).containsExactly("b", "c", "a").inOrder();
+  }
+
+  @Test
+  public void testListIsSerializable() throws Exception {
+    Type type = new TypeToken<List<String>>() {}.getType();
+    List<String> list = gson.fromJson("[\"a\",\"b\",\"c\"]", type);
+    List<String> serialized = serializedCopy(list);
+    assertThat(serialized).isEqualTo(list);
+  }
+
+  @Test
+  public void testNumberIsSerializable() throws Exception {
+    Type type = new TypeToken<List<Number>>() {}.getType();
+    List<Number> list = gson.fromJson("[1,3.14,6.673e-11]", type);
+    List<Number> serialized = serializedCopy(list);
+    assertThat(serialized.get(0).doubleValue()).isEqualTo(1.0);
+    assertThat(serialized.get(1).doubleValue()).isEqualTo(3.14);
+    assertThat(serialized.get(2).doubleValue()).isEqualTo(6.673e-11);
+  }
+
+  @SuppressWarnings("unchecked") // Serialization promises to return the same type.
+  private static <T> T serializedCopy(T object) throws IOException, ClassNotFoundException {
+    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+    ObjectOutputStream out = new ObjectOutputStream(bytesOut);
+    out.writeObject(object);
+    out.close();
+    ByteArrayInputStream bytesIn = new ByteArrayInputStream(bytesOut.toByteArray());
+    ObjectInputStream in = new ObjectInputStream(bytesIn);
+    return (T) in.readObject();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f2577ce6c851a0efae2fb0553f1a3b866af8d48
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+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();
+    assertThat(list.get(0)).isEqualTo(new JsonPrimitive(1));
+
+    assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1));
+    assertThrows(IndexOutOfBoundsException.class, () -> list.get(2));
+
+    a.add((JsonElement) null);
+    assertThat(list.get(1)).isEqualTo(JsonNull.INSTANCE);
+  }
+
+  @Test
+  public void testSize() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    assertThat(list).hasSize(1);
+    list.add(new JsonPrimitive(2));
+    assertThat(list).hasSize(2);
+  }
+
+  @Test
+  public void testSet() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    JsonElement old = list.set(0, new JsonPrimitive(2));
+    assertThat(old).isEqualTo(new JsonPrimitive(1));
+    assertThat(list.get(0)).isEqualTo(new JsonPrimitive(2));
+    assertThat(a.get(0)).isEqualTo(new JsonPrimitive(2));
+
+    assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1)));
+    assertThrows(IndexOutOfBoundsException.class, () -> list.set(2, new JsonPrimitive(1)));
+
+    NullPointerException e = assertThrows(NullPointerException.class, () -> list.set(0, null));
+    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
+  }
+
+  @Test
+  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));
+    assertThat(list.add(new JsonPrimitive(4))).isTrue();
+    assertThat(list.add(JsonNull.INSTANCE)).isTrue();
+
+    List<JsonElement> expectedList =
+        Arrays.<JsonElement>asList(
+            new JsonPrimitive(2),
+            new JsonPrimitive(3),
+            new JsonPrimitive(1),
+            new JsonPrimitive(4),
+            JsonNull.INSTANCE);
+    assertThat(list).isEqualTo(expectedList);
+
+    assertThrows(IndexOutOfBoundsException.class, () -> list.set(-1, new JsonPrimitive(1)));
+    assertThrows(
+        IndexOutOfBoundsException.class, () -> list.set(list.size(), new JsonPrimitive(1)));
+
+    NullPointerException e = assertThrows(NullPointerException.class, () -> list.add(0, null));
+    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
+
+    e = assertThrows(NullPointerException.class, () -> list.add(null));
+    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
+  }
+
+  @Test
+  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));
+    assertThat(list).isEqualTo(expectedList);
+    assertThat(list).isEqualTo(expectedList);
+
+    NullPointerException e =
+        assertThrows(
+            NullPointerException.class,
+            () -> list.addAll(0, Collections.<JsonElement>singletonList(null)));
+    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
+
+    e =
+        assertThrows(
+            NullPointerException.class,
+            () -> list.addAll(Collections.<JsonElement>singletonList(null)));
+    assertThat(e).hasMessageThat().isEqualTo("Element must be non-null");
+  }
+
+  @Test
+  public void testRemoveIndex() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    assertThat(list.remove(0)).isEqualTo(new JsonPrimitive(1));
+    assertThat(list).hasSize(0);
+    assertThat(a).hasSize(0);
+
+    assertThrows(IndexOutOfBoundsException.class, () -> list.remove(0));
+  }
+
+  @Test
+  public void testRemoveElement() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    assertThat(list.remove(new JsonPrimitive(1))).isTrue();
+    assertThat(list).hasSize(0);
+    assertThat(a).hasSize(0);
+
+    assertThat(list.remove(new JsonPrimitive(1))).isFalse();
+    assertThat(list.remove(null)).isFalse();
+  }
+
+  @Test
+  public void testClear() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    list.clear();
+    assertThat(list).hasSize(0);
+    assertThat(a).hasSize(0);
+  }
+
+  @Test
+  public void testContains() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    assertThat(list).contains(new JsonPrimitive(1));
+    assertThat(list).doesNotContain(new JsonPrimitive(2));
+    assertThat(list).doesNotContain(null);
+
+    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+    boolean containsInt = list.contains(1); // should only contain JsonPrimitive(1)
+    assertThat(containsInt).isFalse();
+  }
+
+  @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();
+    assertThat(list.indexOf(new JsonPrimitive(1))).isEqualTo(0);
+    assertThat(list.indexOf(new JsonPrimitive(2))).isEqualTo(-1);
+    assertThat(list.indexOf(null)).isEqualTo(-1);
+
+    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+    int indexOfInt = list.indexOf(1); // should only contain JsonPrimitive(1)
+    assertThat(indexOfInt).isEqualTo(-1);
+
+    assertThat(list.lastIndexOf(new JsonPrimitive(1))).isEqualTo(1);
+    assertThat(list.lastIndexOf(new JsonPrimitive(2))).isEqualTo(-1);
+    assertThat(list.lastIndexOf(null)).isEqualTo(-1);
+  }
+
+  @Test
+  public void testToArray() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    assertThat(list.toArray()).isEqualTo(new Object[] {new JsonPrimitive(1)});
+
+    JsonElement[] array = list.toArray(new JsonElement[0]);
+    assertThat(array).isEqualTo(new Object[] {new JsonPrimitive(1)});
+
+    array = new JsonElement[1];
+    assertThat(list.toArray(array)).isEqualTo(array);
+    assertThat(array).isEqualTo(new Object[] {new JsonPrimitive(1)});
+
+    array = new JsonElement[] {null, new JsonPrimitive(2)};
+    assertThat(list.toArray(array)).isEqualTo(array);
+    // Should have set existing array element to null
+    assertThat(array).isEqualTo(new Object[] {new JsonPrimitive(1), null});
+  }
+
+  @Test
+  public void testEqualsHashCode() {
+    JsonArray a = new JsonArray();
+    a.add(1);
+
+    List<JsonElement> list = a.asList();
+    MoreAsserts.assertEqualsAndHashCode(list, Collections.singletonList(new JsonPrimitive(1)));
+    assertThat(list.equals(Collections.emptyList())).isFalse();
+    assertThat(list.equals(Collections.singletonList(new JsonPrimitive(2)))).isFalse();
+  }
+
+  /** 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);
+    assertThat(list).hasSize(1);
+    assertThat(list.get(0)).isEqualTo(new JsonPrimitive(1));
+
+    list.add(new JsonPrimitive(2));
+    assertThat(a).hasSize(2);
+    assertThat(a.get(1)).isEqualTo(new JsonPrimitive(2));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/gson/src/test/java/com/google/gson/JsonArrayTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..30942513fa150d307bf05d2e8a34b191ede25db4
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonArrayTest.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2011 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.testing.EqualsTester;
+import com.google.gson.common.MoreAsserts;
+import java.math.BigInteger;
+import org.junit.Test;
+
+/**
+ * Tests handling of JSON arrays.
+ *
+ * @author Jesse Wilson
+ */
+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();
+
+    new EqualsTester().addEqualityGroup(a).testEquals();
+
+    a.add(new JsonObject());
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+
+    b.add(new JsonObject());
+    MoreAsserts.assertEqualsAndHashCode(a, b);
+
+    a.add(new JsonObject());
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+
+    b.add(JsonNull.INSTANCE);
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+  }
+
+  @Test
+  public void testRemove() {
+    JsonArray array = new JsonArray();
+    try {
+      array.remove(0);
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+    JsonPrimitive a = new JsonPrimitive("a");
+    array.add(a);
+    assertThat(array.remove(a)).isTrue();
+    assertThat(array).doesNotContain(a);
+    array.add(a);
+    array.add(new JsonPrimitive("b"));
+    assertThat(array.remove(1).getAsString()).isEqualTo("b");
+    assertThat(array).hasSize(1);
+    assertThat(array).contains(a);
+  }
+
+  @Test
+  public void testSet() {
+    JsonArray array = new JsonArray();
+    try {
+      array.set(0, new JsonPrimitive(1));
+      fail();
+    } catch (IndexOutOfBoundsException expected) {
+    }
+    JsonPrimitive a = new JsonPrimitive("a");
+    array.add(a);
+
+    JsonPrimitive b = new JsonPrimitive("b");
+    JsonElement oldValue = array.set(0, b);
+    assertThat(oldValue).isEqualTo(a);
+    assertThat(array.get(0).getAsString()).isEqualTo("b");
+
+    oldValue = array.set(0, null);
+    assertThat(oldValue).isEqualTo(b);
+    assertThat(array.get(0)).isEqualTo(JsonNull.INSTANCE);
+
+    oldValue = array.set(0, new JsonPrimitive("c"));
+    assertThat(oldValue).isEqualTo(JsonNull.INSTANCE);
+    assertThat(array.get(0).getAsString()).isEqualTo("c");
+    assertThat(array).hasSize(1);
+  }
+
+  @Test
+  public void testDeepCopy() {
+    JsonArray original = new JsonArray();
+    JsonArray firstEntry = new JsonArray();
+    original.add(firstEntry);
+
+    JsonArray copy = original.deepCopy();
+    original.add(new JsonPrimitive("y"));
+
+    assertThat(copy).hasSize(1);
+    firstEntry.add(new JsonPrimitive("z"));
+
+    assertThat(original.get(0).getAsJsonArray()).hasSize(1);
+    assertThat(copy.get(0).getAsJsonArray()).hasSize(0);
+  }
+
+  @Test
+  public void testIsEmpty() {
+    JsonArray array = new JsonArray();
+    assertThat(array).isEmpty();
+
+    JsonPrimitive a = new JsonPrimitive("a");
+    array.add(a);
+    assertThat(array).isNotEmpty();
+
+    array.remove(0);
+    assertThat(array).isEmpty();
+  }
+
+  @Test
+  public void testFailedGetArrayValues() {
+    JsonArray jsonArray = new JsonArray();
+    jsonArray.add(
+        JsonParser.parseString(
+            "{"
+                + "\"key1\":\"value1\","
+                + "\"key2\":\"value2\","
+                + "\"key3\":\"value3\","
+                + "\"key4\":\"value4\""
+                + "}"));
+    try {
+      jsonArray.getAsBoolean();
+      fail("expected getBoolean to fail");
+    } catch (UnsupportedOperationException e) {
+      assertThat(e).hasMessageThat().isEqualTo("JsonObject");
+    }
+    try {
+      jsonArray.get(-1);
+      fail("expected get to fail");
+    } catch (IndexOutOfBoundsException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1");
+    }
+    try {
+      jsonArray.getAsString();
+      fail("expected getString to fail");
+    } catch (UnsupportedOperationException e) {
+      assertThat(e).hasMessageThat().isEqualTo("JsonObject");
+    }
+
+    jsonArray.remove(0);
+    jsonArray.add("hello");
+    try {
+      jsonArray.getAsDouble();
+      fail("expected getDouble to fail");
+    } catch (NumberFormatException e) {
+      assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
+    }
+    try {
+      jsonArray.getAsInt();
+      fail("expected getInt to fail");
+    } catch (NumberFormatException e) {
+      assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
+    }
+    try {
+      jsonArray.get(0).getAsJsonArray();
+      fail("expected getJSONArray to fail");
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\"");
+    }
+    try {
+      jsonArray.getAsJsonObject();
+      fail("expected getJSONObject to fail");
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Not a JSON Object: [\"hello\"]");
+    }
+    try {
+      jsonArray.getAsLong();
+      fail("expected getLong to fail");
+    } catch (NumberFormatException e) {
+      assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\"");
+    }
+  }
+
+  @Test
+  public void testGetAs_WrongArraySize() {
+    JsonArray jsonArray = new JsonArray();
+    try {
+      jsonArray.getAsByte();
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Array must have size 1, but has size 0");
+    }
+
+    jsonArray.add(true);
+    jsonArray.add(false);
+    try {
+      jsonArray.getAsByte();
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Array must have size 1, but has size 2");
+    }
+  }
+
+  @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");
+
+    assertThat(jsonArray.toString())
+        .isEqualTo("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]");
+  }
+
+  @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);
+
+    assertThat(jsonArray.toString()).isEqualTo("[1,2,-3,null,4,0]");
+  }
+
+  @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);
+
+    assertThat(jsonArray.toString()).isEqualTo("[1.0,2.13232,0.121,null,-0.00234,null]");
+  }
+
+  @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);
+
+    assertThat(jsonArray.toString()).isEqualTo("[true,true,false,false,null,true]");
+  }
+
+  @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");
+
+    assertThat(jsonArray.toString())
+        .isEqualTo("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]");
+  }
+
+  @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);
+    assertThat(jsonArray.get(jsonArray.size() - 1)).isEqualTo(JsonNull.INSTANCE);
+
+    jsonArray.add((Character) null);
+    assertThat(jsonArray.get(jsonArray.size() - 1)).isEqualTo(JsonNull.INSTANCE);
+
+    jsonArray.add(12.232);
+    jsonArray.add(BigInteger.valueOf(2323));
+
+    assertThat(jsonArray.toString())
+        .isEqualTo("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]");
+  }
+
+  @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);
+
+    assertThat(jsonArray.toString()).isEqualTo("[null,null,null,null,null,null,null,null,null]");
+    for (int i = 0; i < jsonArray.size(); i++) {
+      // Verify that they are actually a JsonNull and not a Java null
+      assertThat(jsonArray.get(i)).isEqualTo(JsonNull.INSTANCE);
+    }
+  }
+
+  @Test
+  public void testNullJsonElementAddition() {
+    JsonArray jsonArray = new JsonArray();
+    jsonArray.add((JsonElement) null);
+    assertThat(jsonArray.get(0)).isEqualTo(JsonNull.INSTANCE);
+  }
+
+  @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);
+
+    assertThat(jsonArray.toString())
+        .isEqualTo("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonNullTest.java b/gson/gson/src/test/java/com/google/gson/JsonNullTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca995b925b14aabe8cc99649bb9a23a8b28c62e3
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonNullTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.common.MoreAsserts;
+import org.junit.Test;
+
+/**
+ * Tests handling of JSON nulls.
+ *
+ * @author Jesse Wilson
+ */
+public final class JsonNullTest {
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testEqualsAndHashcode() {
+    MoreAsserts.assertEqualsAndHashCode(new JsonNull(), new JsonNull());
+    MoreAsserts.assertEqualsAndHashCode(new JsonNull(), JsonNull.INSTANCE);
+    MoreAsserts.assertEqualsAndHashCode(JsonNull.INSTANCE, JsonNull.INSTANCE);
+  }
+
+  @Test
+  public void testDeepCopy() {
+    @SuppressWarnings("deprecation")
+    JsonNull a = new JsonNull();
+    assertThat(a.deepCopy()).isSameInstanceAs(JsonNull.INSTANCE);
+    assertThat(JsonNull.INSTANCE.deepCopy()).isSameInstanceAs(JsonNull.INSTANCE);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java b/gson/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae8d2ea7ddabb36003aa2ca06fbe12610ba59b92
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+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();
+    assertThat(o.asMap().size()).isEqualTo(0);
+
+    o.addProperty("a", 1);
+    Map<String, JsonElement> map = o.asMap();
+    assertThat(map).hasSize(1);
+
+    map.clear();
+    assertThat(map).hasSize(0);
+    assertThat(o.size()).isEqualTo(0);
+  }
+
+  @Test
+  public void testContainsKey() {
+    JsonObject o = new JsonObject();
+    o.addProperty("a", 1);
+
+    Map<String, JsonElement> map = o.asMap();
+    assertThat(map.containsKey("a")).isTrue();
+    assertThat(map.containsKey("b")).isFalse();
+    assertThat(map.containsKey(null)).isFalse();
+  }
+
+  @Test
+  public void testContainsValue() {
+    JsonObject o = new JsonObject();
+    o.addProperty("a", 1);
+    o.add("b", JsonNull.INSTANCE);
+
+    Map<String, JsonElement> map = o.asMap();
+    assertThat(map.containsValue(new JsonPrimitive(1))).isTrue();
+    assertThat(map.containsValue(new JsonPrimitive(2))).isFalse();
+    assertThat(map.containsValue(null)).isFalse();
+
+    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
+    boolean containsInt = map.containsValue(1); // should only contain JsonPrimitive(1)
+    assertThat(containsInt).isFalse();
+  }
+
+  @Test
+  public void testGet() {
+    JsonObject o = new JsonObject();
+    o.addProperty("a", 1);
+
+    Map<String, JsonElement> map = o.asMap();
+    assertThat(map.get("a")).isEqualTo(new JsonPrimitive(1));
+    assertThat(map.get("b")).isNull();
+    assertThat(map.get(null)).isNull();
+  }
+
+  @Test
+  public void testPut() {
+    JsonObject o = new JsonObject();
+    Map<String, JsonElement> map = o.asMap();
+
+    assertThat(map.put("a", new JsonPrimitive(1))).isNull();
+    assertThat(map.get("a")).isEqualTo(new JsonPrimitive(1));
+
+    JsonElement old = map.put("a", new JsonPrimitive(2));
+    assertThat(old).isEqualTo(new JsonPrimitive(1));
+    assertThat(map).hasSize(1);
+    assertThat(map.get("a")).isEqualTo(new JsonPrimitive(2));
+    assertThat(o.get("a")).isEqualTo(new JsonPrimitive(2));
+
+    assertThat(map.put("b", JsonNull.INSTANCE)).isNull();
+    assertThat(map.get("b")).isEqualTo(JsonNull.INSTANCE);
+
+    try {
+      map.put(null, new JsonPrimitive(1));
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("key == null");
+    }
+
+    try {
+      map.put("a", null);
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+  }
+
+  @Test
+  public void testRemove() {
+    JsonObject o = new JsonObject();
+    o.addProperty("a", 1);
+
+    Map<String, JsonElement> map = o.asMap();
+    assertThat(map.remove("b")).isNull();
+    assertThat(map).hasSize(1);
+
+    JsonElement old = map.remove("a");
+    assertThat(old).isEqualTo(new JsonPrimitive(1));
+    assertThat(map).hasSize(0);
+
+    assertThat(map.remove("a")).isNull();
+    assertThat(map).hasSize(0);
+    assertThat(o.size()).isEqualTo(0);
+
+    assertThat(map.remove(null)).isNull();
+  }
+
+  @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);
+    assertThat(map).hasSize(2);
+    assertThat(map.get("a")).isEqualTo(new JsonPrimitive(2));
+    assertThat(map.get("b")).isEqualTo(new JsonPrimitive(3));
+
+    try {
+      map.putAll(Collections.<String, JsonElement>singletonMap(null, new JsonPrimitive(1)));
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("key == null");
+    }
+
+    try {
+      map.putAll(Collections.<String, JsonElement>singletonMap("a", null));
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+  }
+
+  @Test
+  public void testClear() {
+    JsonObject o = new JsonObject();
+    o.addProperty("a", 1);
+
+    Map<String, JsonElement> map = o.asMap();
+    map.clear();
+    assertThat(map).hasSize(0);
+    assertThat(o.size()).isEqualTo(0);
+  }
+
+  @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
+    assertThat(keySet).containsExactly("b", "a").inOrder();
+
+    // Key set doesn't support insertions
+    try {
+      keySet.add("c");
+      fail();
+    } catch (UnsupportedOperationException e) {
+    }
+
+    assertThat(keySet.remove("a")).isTrue();
+    assertThat(map.keySet()).isEqualTo(Collections.singleton("b"));
+    assertThat(o.keySet()).isEqualTo(Collections.singleton("b"));
+  }
+
+  @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
+    assertThat(values).containsExactly(new JsonPrimitive(2), new JsonPrimitive(1)).inOrder();
+
+    // Values collection doesn't support insertions
+    try {
+      values.add(new JsonPrimitive(3));
+      fail();
+    } catch (UnsupportedOperationException e) {
+    }
+
+    assertThat(values.remove(new JsonPrimitive(2))).isTrue();
+    assertThat(new ArrayList<>(map.values()))
+        .isEqualTo(Collections.singletonList(new JsonPrimitive(1)));
+    assertThat(o.size()).isEqualTo(1);
+    assertThat(o.get("b")).isEqualTo(new JsonPrimitive(1));
+  }
+
+  @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
+    assertThat(new ArrayList<>(entrySet)).isEqualTo(expectedEntrySet);
+
+    try {
+      entrySet.add(new SimpleEntry<String, JsonElement>("c", new JsonPrimitive(3)));
+      fail();
+    } catch (UnsupportedOperationException e) {
+    }
+
+    assertThat(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1)))).isTrue();
+    assertThat(map.entrySet())
+        .isEqualTo(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))));
+    assertThat(o.entrySet())
+        .isEqualTo(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))));
+
+    // Should return false because entry has already been removed
+    assertThat(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1)))).isFalse();
+
+    Entry<String, JsonElement> entry = entrySet.iterator().next();
+    JsonElement old = entry.setValue(new JsonPrimitive(3));
+    assertThat(old).isEqualTo(new JsonPrimitive(2));
+    assertThat(map.entrySet())
+        .isEqualTo(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))));
+    assertThat(o.entrySet())
+        .isEqualTo(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))));
+
+    try {
+      entry.setValue(null);
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+  }
+
+  @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)));
+    assertThat(map.equals(Collections.emptyMap())).isFalse();
+    assertThat(map.equals(Collections.singletonMap("a", new JsonPrimitive(2)))).isFalse();
+  }
+
+  /** 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);
+    assertThat(map).hasSize(1);
+    assertThat(map.get("a")).isEqualTo(new JsonPrimitive(1));
+
+    map.put("b", new JsonPrimitive(2));
+    assertThat(o.size()).isEqualTo(2);
+    assertThat(map.get("b")).isEqualTo(new JsonPrimitive(2));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonObjectTest.java b/gson/gson/src/test/java/com/google/gson/JsonObjectTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..353fa102d266983e1f8b00c89492f865522c999e
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonObjectTest.java
@@ -0,0 +1,347 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.testing.EqualsTester;
+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 {
+
+  @Test
+  public void testAddingAndRemovingObjectProperties() {
+    JsonObject jsonObj = new JsonObject();
+    String propertyName = "property";
+    assertThat(jsonObj.has(propertyName)).isFalse();
+    assertThat(jsonObj.get(propertyName)).isNull();
+
+    JsonPrimitive value = new JsonPrimitive("blah");
+    jsonObj.add(propertyName, value);
+    assertThat(jsonObj.get(propertyName)).isEqualTo(value);
+
+    JsonElement removedElement = jsonObj.remove(propertyName);
+    assertThat(removedElement).isEqualTo(value);
+    assertThat(jsonObj.has(propertyName)).isFalse();
+    assertThat(jsonObj.get(propertyName)).isNull();
+
+    assertThat(jsonObj.remove(propertyName)).isNull();
+  }
+
+  @Test
+  public void testAddingNullPropertyValue() {
+    String propertyName = "property";
+    JsonObject jsonObj = new JsonObject();
+    jsonObj.add(propertyName, null);
+
+    assertThat(jsonObj.has(propertyName)).isTrue();
+
+    JsonElement jsonElement = jsonObj.get(propertyName);
+    assertThat(jsonElement).isNotNull();
+    assertThat(jsonElement.isJsonNull()).isTrue();
+  }
+
+  @Test
+  public void testAddingNullOrEmptyPropertyName() {
+    JsonObject jsonObj = new JsonObject();
+    try {
+      jsonObj.add(null, JsonNull.INSTANCE);
+      fail("Should not allow null property names.");
+    } catch (NullPointerException expected) {
+    }
+
+    jsonObj.add("", JsonNull.INSTANCE);
+    jsonObj.add("   \t", JsonNull.INSTANCE);
+  }
+
+  @Test
+  public void testAddingBooleanProperties() {
+    String propertyName = "property";
+    JsonObject jsonObj = new JsonObject();
+    jsonObj.addProperty(propertyName, true);
+
+    assertThat(jsonObj.has(propertyName)).isTrue();
+
+    JsonElement jsonElement = jsonObj.get(propertyName);
+    assertThat(jsonElement).isNotNull();
+    assertThat(jsonElement.getAsBoolean()).isTrue();
+  }
+
+  @Test
+  public void testAddingStringProperties() {
+    String propertyName = "property";
+    String value = "blah";
+
+    JsonObject jsonObj = new JsonObject();
+    jsonObj.addProperty(propertyName, value);
+
+    assertThat(jsonObj.has(propertyName)).isTrue();
+
+    JsonElement jsonElement = jsonObj.get(propertyName);
+    assertThat(jsonElement).isNotNull();
+    assertThat(jsonElement.getAsString()).isEqualTo(value);
+  }
+
+  @Test
+  public void testAddingCharacterProperties() {
+    String propertyName = "property";
+    char value = 'a';
+
+    JsonObject jsonObj = new JsonObject();
+    jsonObj.addProperty(propertyName, value);
+
+    assertThat(jsonObj.has(propertyName)).isTrue();
+
+    JsonElement jsonElement = jsonObj.get(propertyName);
+    assertThat(jsonElement).isNotNull();
+    assertThat(jsonElement.getAsString()).isEqualTo(String.valueOf(value));
+
+    @SuppressWarnings("deprecation")
+    char character = jsonElement.getAsCharacter();
+    assertThat(character).isEqualTo(value);
+  }
+
+  /** 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"));
+    String json = new Gson().toJson(jsonObj);
+    assertThat(json).isEqualTo("{\"a\\\"b\":\"c\\\"d\"}");
+  }
+
+  /** From issue 227. */
+  @Test
+  public void testWritePropertyWithEmptyStringName() {
+    JsonObject jsonObj = new JsonObject();
+    jsonObj.add("", new JsonPrimitive(true));
+    assertThat(new Gson().toJson(jsonObj)).isEqualTo("{\"\":true}");
+  }
+
+  @Test
+  public void testReadPropertyWithEmptyStringName() {
+    JsonObject jsonObj = JsonParser.parseString("{\"\":true}").getAsJsonObject();
+    assertThat(jsonObj.get("").getAsBoolean()).isTrue();
+  }
+
+  @Test
+  public void testEqualsOnEmptyObject() {
+    MoreAsserts.assertEqualsAndHashCode(new JsonObject(), new JsonObject());
+  }
+
+  @Test
+  public void testEqualsNonEmptyObject() {
+    JsonObject a = new JsonObject();
+    JsonObject b = new JsonObject();
+
+    new EqualsTester().addEqualityGroup(a).testEquals();
+
+    a.add("foo", new JsonObject());
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+
+    b.add("foo", new JsonObject());
+    MoreAsserts.assertEqualsAndHashCode(a, b);
+
+    a.add("bar", new JsonObject());
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+
+    b.add("bar", JsonNull.INSTANCE);
+    assertThat(a.equals(b)).isFalse();
+    assertThat(b.equals(a)).isFalse();
+  }
+
+  @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);
+
+    assertThat(new ArrayList<>(a.keySet())).containsExactly("1", "2").inOrder();
+    assertThat(new ArrayList<>(b.keySet())).containsExactly("2", "1").inOrder();
+
+    MoreAsserts.assertEqualsAndHashCode(a, b);
+  }
+
+  @Test
+  public void testSize() {
+    JsonObject o = new JsonObject();
+    assertThat(o.size()).isEqualTo(0);
+
+    o.add("Hello", new JsonPrimitive(1));
+    assertThat(o.size()).isEqualTo(1);
+
+    o.add("Hi", new JsonPrimitive(1));
+    assertThat(o.size()).isEqualTo(2);
+
+    o.remove("Hello");
+    assertThat(o.size()).isEqualTo(1);
+  }
+
+  @Test
+  public void testIsEmpty() {
+    JsonObject o = new JsonObject();
+    assertThat(o.isEmpty()).isTrue();
+
+    o.add("Hello", new JsonPrimitive(1));
+    assertThat(o.isEmpty()).isFalse();
+
+    o.remove("Hello");
+    assertThat(o.isEmpty()).isTrue();
+  }
+
+  @Test
+  public void testDeepCopy() {
+    JsonObject original = new JsonObject();
+    JsonArray firstEntry = new JsonArray();
+    original.add("key", firstEntry);
+
+    JsonObject copy = original.deepCopy();
+    firstEntry.add(new JsonPrimitive("z"));
+
+    assertThat(original.get("key").getAsJsonArray()).hasSize(1);
+    assertThat(copy.get("key").getAsJsonArray()).hasSize(0);
+  }
+
+  /** From issue 941 */
+  @Test
+  public void testKeySet() {
+    JsonObject a = new JsonObject();
+    assertThat(a.keySet()).hasSize(0);
+
+    a.add("foo", new JsonArray());
+    a.add("bar", new JsonObject());
+
+    assertThat(a.size()).isEqualTo(2);
+    assertThat(a.keySet()).hasSize(2);
+    assertThat(a.keySet()).containsExactly("foo", "bar").inOrder();
+
+    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`
+    assertThat(new ArrayList<>(a.keySet())).isEqualTo(new ArrayList<>(expectedKeys));
+    Iterator<String> iterator = a.keySet().iterator();
+
+    // Remove keys one by one
+    for (int i = a.size(); i >= 1; i--) {
+      assertThat(iterator.hasNext()).isTrue();
+      assertThat(iterator.next()).isEqualTo(expectedKeys.getFirst());
+      iterator.remove();
+      expectedKeys.removeFirst();
+
+      assertThat(a.size()).isEqualTo(i - 1);
+      assertThat(new ArrayList<>(a.keySet())).isEqualTo(new ArrayList<>(expectedKeys));
+    }
+  }
+
+  @Test
+  public void testEntrySet() {
+    JsonObject o = new JsonObject();
+    assertThat(o.entrySet()).hasSize(0);
+
+    o.addProperty("b", true);
+    Set<?> expectedEntries = Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(true)));
+    assertThat(o.entrySet()).isEqualTo(expectedEntries);
+    assertThat(o.entrySet()).hasSize(1);
+
+    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)));
+    assertThat(new ArrayList<>(o.entrySet())).isEqualTo(expectedEntriesList);
+
+    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));
+
+      assertThat(entry.getValue()).isEqualTo(new JsonPrimitive(i));
+    }
+
+    expectedEntriesList =
+        Arrays.asList(
+            new SimpleEntry<>("b", new JsonPrimitive(0)),
+            new SimpleEntry<>("a", new JsonPrimitive(1)));
+    assertThat(new ArrayList<>(o.entrySet())).isEqualTo(expectedEntriesList);
+
+    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) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+    assertThat(entry.getValue()).isNotNull();
+
+    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`
+    assertThat(new ArrayList<>(o.entrySet())).isEqualTo(new ArrayList<>(expectedEntriesQueue));
+    iterator = o.entrySet().iterator();
+
+    // Remove entries one by one
+    for (int i = o.size(); i >= 1; i--) {
+      assertThat(iterator.hasNext()).isTrue();
+      assertThat(iterator.next()).isEqualTo(expectedEntriesQueue.getFirst());
+      iterator.remove();
+      expectedEntriesQueue.removeFirst();
+
+      assertThat(o.size()).isEqualTo(i - 1);
+      assertThat(new ArrayList<>(o.entrySet())).isEqualTo(new ArrayList<>(expectedEntriesQueue));
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonParserParameterizedTest.java b/gson/gson/src/test/java/com/google/gson/JsonParserParameterizedTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ec42b258bbf33e4d52050febc5afd13429ec4f1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonParserParameterizedTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class JsonParserParameterizedTest {
+  @Parameters
+  public static Iterable<String> data() {
+    return Arrays.asList(
+        "[]",
+        "{}",
+        "null",
+        "1.0",
+        "true",
+        "\"string\"",
+        "[true,1.0,null,{},2.0,{\"a\":[false]},[3.0,\"test\"],4.0]",
+        "{\"\":1.0,\"a\":true,\"b\":null,\"c\":[],\"d\":{\"a1\":2.0,\"b2\":[true,{\"a3\":3.0}]},\"e\":[{\"f\":4.0},\"test\"]}");
+  }
+
+  private final TypeAdapter<JsonElement> adapter = new Gson().getAdapter(JsonElement.class);
+  @Parameter public String json;
+
+  @Test
+  public void testParse() {
+    JsonElement deserialized = JsonParser.parseString(json);
+    String actualSerialized = adapter.toJson(deserialized);
+
+    // Serialized JsonElement should be the same as original JSON
+    assertThat(actualSerialized).isEqualTo(json);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonParserTest.java b/gson/gson/src/test/java/com/google/gson/JsonParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ba36712d0ac4f1d6098c00fb8b2dcbe7fe5be24
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonParserTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2009 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonReader;
+import java.io.CharArrayReader;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link JsonParser}
+ *
+ * @author Inderjeet Singh
+ */
+public class JsonParserTest {
+
+  @Test
+  public void testParseInvalidJson() {
+    assertThrows(JsonSyntaxException.class, () -> JsonParser.parseString("[[]"));
+  }
+
+  @Test
+  public void testParseUnquotedStringArrayFails() {
+    JsonElement element = JsonParser.parseString("[a,b,c]");
+    assertThat(element.getAsJsonArray().get(0).getAsString()).isEqualTo("a");
+    assertThat(element.getAsJsonArray().get(1).getAsString()).isEqualTo("b");
+    assertThat(element.getAsJsonArray().get(2).getAsString()).isEqualTo("c");
+    assertThat(element.getAsJsonArray()).hasSize(3);
+  }
+
+  @Test
+  public void testParseString() {
+    String json = "{a:10,b:'c'}";
+    JsonElement e = JsonParser.parseString(json);
+    assertThat(e.isJsonObject()).isTrue();
+    assertThat(e.getAsJsonObject().get("a").getAsInt()).isEqualTo(10);
+    assertThat(e.getAsJsonObject().get("b").getAsString()).isEqualTo("c");
+  }
+
+  @Test
+  public void testParseEmptyString() {
+    JsonElement e = JsonParser.parseString("\"   \"");
+    assertThat(e.isJsonPrimitive()).isTrue();
+    assertThat(e.getAsString()).isEqualTo("   ");
+  }
+
+  @Test
+  public void testParseEmptyWhitespaceInput() {
+    JsonElement e = JsonParser.parseString("     ");
+    assertThat(e.isJsonNull()).isTrue();
+  }
+
+  @Test
+  public void testParseUnquotedSingleWordStringFails() {
+    assertThat(JsonParser.parseString("Test").getAsString()).isEqualTo("Test");
+  }
+
+  @Test
+  public void testParseUnquotedMultiWordStringFails() {
+    assertThrows(
+        JsonSyntaxException.class, () -> JsonParser.parseString("Test is a test..blah blah"));
+  }
+
+  @Test
+  public void testParseMixedArray() {
+    String json = "[{},13,\"stringValue\"]";
+    JsonElement e = JsonParser.parseString(json);
+    assertThat(e.isJsonArray()).isTrue();
+
+    JsonArray array = e.getAsJsonArray();
+    assertThat(array.get(0).toString()).isEqualTo("{}");
+    assertThat(array.get(1).getAsInt()).isEqualTo(13);
+    assertThat(array.get(2).getAsString()).isEqualTo("stringValue");
+  }
+
+  private static String repeat(String s, int times) {
+    StringBuilder stringBuilder = new StringBuilder(s.length() * times);
+    for (int i = 0; i < times; i++) {
+      stringBuilder.append(s);
+    }
+    return stringBuilder.toString();
+  }
+
+  /** Deeply nested JSON arrays should not cause {@link StackOverflowError} */
+  @Test
+  public void testParseDeeplyNestedArrays() throws IOException {
+    int times = 10000;
+    // [[[ ... ]]]
+    String json = repeat("[", times) + repeat("]", times);
+
+    int actualTimes = 0;
+    JsonArray current = JsonParser.parseString(json).getAsJsonArray();
+    while (true) {
+      actualTimes++;
+      if (current.isEmpty()) {
+        break;
+      }
+      assertThat(current.size()).isEqualTo(1);
+      current = current.get(0).getAsJsonArray();
+    }
+    assertThat(actualTimes).isEqualTo(times);
+  }
+
+  /** Deeply nested JSON objects should not cause {@link StackOverflowError} */
+  @Test
+  public void testParseDeeplyNestedObjects() throws IOException {
+    int times = 10000;
+    // {"a":{"a": ... {"a":null} ... }}
+    String json = repeat("{\"a\":", times) + "null" + repeat("}", times);
+
+    int actualTimes = 0;
+    JsonObject current = JsonParser.parseString(json).getAsJsonObject();
+    while (true) {
+      assertThat(current.size()).isEqualTo(1);
+      actualTimes++;
+      JsonElement next = current.get("a");
+      if (next.isJsonNull()) {
+        break;
+      } else {
+        current = next.getAsJsonObject();
+      }
+    }
+    assertThat(actualTimes).isEqualTo(times);
+  }
+
+  @Test
+  public void testParseReader() {
+    StringReader reader = new StringReader("{a:10,b:'c'}");
+    JsonElement e = JsonParser.parseReader(reader);
+    assertThat(e.isJsonObject()).isTrue();
+    assertThat(e.getAsJsonObject().get("a").getAsInt()).isEqualTo(10);
+    assertThat(e.getAsJsonObject().get("b").getAsString()).isEqualTo("c");
+  }
+
+  @Test
+  public void testReadWriteTwoObjects() throws Exception {
+    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");
+    writer.write(gson.toJson(expectedTwo).toCharArray());
+    CharArrayReader reader = new CharArrayReader(writer.toCharArray());
+
+    JsonReader parser = new JsonReader(reader);
+    parser.setStrictness(Strictness.LENIENT);
+    JsonElement element1 = Streams.parse(parser);
+    JsonElement element2 = Streams.parse(parser);
+    BagOfPrimitives actualOne = gson.fromJson(element1, BagOfPrimitives.class);
+    assertThat(actualOne.stringValue).isEqualTo("one");
+    BagOfPrimitives actualTwo = gson.fromJson(element2, BagOfPrimitives.class);
+    assertThat(actualTwo.stringValue).isEqualTo("two");
+  }
+
+  @Test
+  public void testStrict() {
+    JsonReader reader = new JsonReader(new StringReader("faLsE"));
+    Strictness strictness = Strictness.STRICT;
+    // Strictness is ignored by JsonParser later; always parses in lenient mode
+    reader.setStrictness(strictness);
+
+    assertThat(JsonParser.parseReader(reader)).isEqualTo(new JsonPrimitive(false));
+    // Original strictness was restored
+    assertThat(reader.getStrictness()).isEqualTo(strictness);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java b/gson/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..36a5da44392556b29eb347efdcd920c4cc8bdc57
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonPrimitiveTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.gson.common.MoreAsserts;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.junit.Test;
+
+/**
+ * Unit test for the {@link JsonPrimitive} class.
+ *
+ * @author Joel Leitch
+ */
+public class JsonPrimitiveTest {
+
+  @SuppressWarnings("unused")
+  @Test
+  public void testNulls() {
+    try {
+      new JsonPrimitive((Boolean) null);
+      fail();
+    } catch (NullPointerException ignored) {
+    }
+    try {
+      new JsonPrimitive((Number) null);
+      fail();
+    } catch (NullPointerException ignored) {
+    }
+    try {
+      new JsonPrimitive((String) null);
+      fail();
+    } catch (NullPointerException ignored) {
+    }
+    try {
+      new JsonPrimitive((Character) null);
+      fail();
+    } catch (NullPointerException ignored) {
+    }
+  }
+
+  @Test
+  public void testBoolean() {
+    JsonPrimitive json = new JsonPrimitive(Boolean.TRUE);
+
+    assertThat(json.isBoolean()).isTrue();
+    assertThat(json.getAsBoolean()).isTrue();
+
+    // Extra support for booleans
+    json = new JsonPrimitive(1);
+    assertThat(json.getAsBoolean()).isFalse();
+
+    json = new JsonPrimitive("1");
+    assertThat(json.getAsBoolean()).isFalse();
+
+    json = new JsonPrimitive("true");
+    assertThat(json.getAsBoolean()).isTrue();
+
+    json = new JsonPrimitive("TrUe");
+    assertThat(json.getAsBoolean()).isTrue();
+
+    json = new JsonPrimitive("1.3");
+    assertThat(json.getAsBoolean()).isFalse();
+  }
+
+  @Test
+  public void testParsingStringAsBoolean() {
+    JsonPrimitive json = new JsonPrimitive("true");
+
+    assertThat(json.isBoolean()).isFalse();
+    assertThat(json.getAsBoolean()).isTrue();
+  }
+
+  @Test
+  public void testParsingStringAsNumber() {
+    JsonPrimitive json = new JsonPrimitive("1");
+
+    assertThat(json.isNumber()).isFalse();
+    assertThat(json.getAsDouble()).isEqualTo(1.0);
+    assertThat(json.getAsFloat()).isEqualTo(1F);
+    assertThat(json.getAsInt()).isEqualTo(1);
+    assertThat(json.getAsLong()).isEqualTo(1L);
+    assertThat(json.getAsShort()).isEqualTo((short) 1);
+    assertThat(json.getAsByte()).isEqualTo((byte) 1);
+    assertThat(json.getAsBigInteger()).isEqualTo(new BigInteger("1"));
+    assertThat(json.getAsBigDecimal()).isEqualTo(new BigDecimal("1"));
+  }
+
+  @Test
+  public void testAsNumber_Boolean() {
+    JsonPrimitive json = new JsonPrimitive(true);
+    try {
+      json.getAsNumber();
+      fail();
+    } catch (UnsupportedOperationException e) {
+      assertThat(e).hasMessageThat().isEqualTo("Primitive is neither a number nor a string");
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testStringsAndChar() {
+    JsonPrimitive json = new JsonPrimitive("abc");
+    assertThat(json.isString()).isTrue();
+    assertThat(json.getAsCharacter()).isEqualTo('a');
+    assertThat(json.getAsString()).isEqualTo("abc");
+
+    json = new JsonPrimitive('z');
+    assertThat(json.isString()).isTrue();
+    assertThat(json.getAsCharacter()).isEqualTo('z');
+    assertThat(json.getAsString()).isEqualTo("z");
+
+    json = new JsonPrimitive(true);
+    assertThat(json.getAsString()).isEqualTo("true");
+
+    json = new JsonPrimitive("");
+    assertThat(json.getAsString()).isEqualTo("");
+    try {
+      json.getAsCharacter();
+      fail();
+    } catch (UnsupportedOperationException e) {
+      assertThat(e).hasMessageThat().isEqualTo("String value is empty");
+    }
+  }
+
+  @Test
+  public void testExponential() {
+    JsonPrimitive json = new JsonPrimitive("1E+7");
+
+    assertThat(json.getAsBigDecimal()).isEqualTo(new BigDecimal("1E+7"));
+    assertThat(json.getAsDouble()).isEqualTo(1E+7);
+
+    try {
+      json.getAsInt();
+      fail("Integers can not handle exponents like this.");
+    } catch (NumberFormatException expected) {
+    }
+  }
+
+  @Test
+  public void testByteEqualsShort() {
+    JsonPrimitive p1 = new JsonPrimitive((byte) 10);
+    JsonPrimitive p2 = new JsonPrimitive((short) 10);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testByteEqualsInteger() {
+    JsonPrimitive p1 = new JsonPrimitive((byte) 10);
+    JsonPrimitive p2 = new JsonPrimitive(10);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testByteEqualsLong() {
+    JsonPrimitive p1 = new JsonPrimitive((byte) 10);
+    JsonPrimitive p2 = new JsonPrimitive(10L);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testByteEqualsBigInteger() {
+    JsonPrimitive p1 = new JsonPrimitive((byte) 10);
+    JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testShortEqualsInteger() {
+    JsonPrimitive p1 = new JsonPrimitive((short) 10);
+    JsonPrimitive p2 = new JsonPrimitive(10);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testShortEqualsLong() {
+    JsonPrimitive p1 = new JsonPrimitive((short) 10);
+    JsonPrimitive p2 = new JsonPrimitive(10L);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testShortEqualsBigInteger() {
+    JsonPrimitive p1 = new JsonPrimitive((short) 10);
+    JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testIntegerEqualsLong() {
+    JsonPrimitive p1 = new JsonPrimitive(10);
+    JsonPrimitive p2 = new JsonPrimitive(10L);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testIntegerEqualsBigInteger() {
+    JsonPrimitive p1 = new JsonPrimitive(10);
+    JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testLongEqualsBigInteger() {
+    JsonPrimitive p1 = new JsonPrimitive(10L);
+    JsonPrimitive p2 = new JsonPrimitive(new BigInteger("10"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testFloatEqualsDouble() {
+    JsonPrimitive p1 = new JsonPrimitive(10.25F);
+    JsonPrimitive p2 = new JsonPrimitive(10.25D);
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testFloatEqualsBigDecimal() {
+    JsonPrimitive p1 = new JsonPrimitive(10.25F);
+    JsonPrimitive p2 = new JsonPrimitive(new BigDecimal("10.25"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testDoubleEqualsBigDecimal() {
+    JsonPrimitive p1 = new JsonPrimitive(10.25D);
+    JsonPrimitive p2 = new JsonPrimitive(new BigDecimal("10.25"));
+    assertThat(p1).isEqualTo(p2);
+    assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
+  }
+
+  @Test
+  public void testValidJsonOnToString() {
+    JsonPrimitive json = new JsonPrimitive("Some\nEscaped\nValue");
+    assertThat(json.toString()).isEqualTo("\"Some\\nEscaped\\nValue\"");
+
+    json = new JsonPrimitive(new BigDecimal("1.333"));
+    assertThat(json.toString()).isEqualTo("1.333");
+  }
+
+  @Test
+  public void testEquals() {
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive("A"), new JsonPrimitive("A"));
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(true), new JsonPrimitive(true));
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(5L), new JsonPrimitive(5L));
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive('a'), new JsonPrimitive('a'));
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(Float.NaN), new JsonPrimitive(Float.NaN));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Float.NEGATIVE_INFINITY), new JsonPrimitive(Float.NEGATIVE_INFINITY));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Float.POSITIVE_INFINITY), new JsonPrimitive(Float.POSITIVE_INFINITY));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Double.NaN), new JsonPrimitive(Double.NaN));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Double.NEGATIVE_INFINITY), new JsonPrimitive(Double.NEGATIVE_INFINITY));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Double.POSITIVE_INFINITY), new JsonPrimitive(Double.POSITIVE_INFINITY));
+    assertThat(new JsonPrimitive("a").equals(new JsonPrimitive("b"))).isFalse();
+    assertThat(new JsonPrimitive(true).equals(new JsonPrimitive(false))).isFalse();
+    assertThat(new JsonPrimitive(0).equals(new JsonPrimitive(1))).isFalse();
+  }
+
+  @Test
+  public void testEqualsAcrossTypes() {
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive("a"), new JsonPrimitive('a'));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(new BigInteger("0")), new JsonPrimitive(0));
+    MoreAsserts.assertEqualsAndHashCode(new JsonPrimitive(0), new JsonPrimitive(0L));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(new BigDecimal("0")), new JsonPrimitive(0));
+    MoreAsserts.assertEqualsAndHashCode(
+        new JsonPrimitive(Float.NaN), new JsonPrimitive(Double.NaN));
+  }
+
+  @Test
+  public void testEqualsIntegerAndBigInteger() {
+    JsonPrimitive a = new JsonPrimitive(5L);
+    JsonPrimitive b = new JsonPrimitive(new BigInteger("18446744073709551621"));
+    assertWithMessage("%s not equals %s", a, b).that(a.equals(b)).isFalse();
+  }
+
+  @Test
+  public void testEqualsDoesNotEquateStringAndNonStringTypes() {
+    assertThat(new JsonPrimitive("true").equals(new JsonPrimitive(true))).isFalse();
+    assertThat(new JsonPrimitive("0").equals(new JsonPrimitive(0))).isFalse();
+    assertThat(new JsonPrimitive("NaN").equals(new JsonPrimitive(Float.NaN))).isFalse();
+  }
+
+  @Test
+  public void testDeepCopy() {
+    JsonPrimitive a = new JsonPrimitive("a");
+    assertThat(a).isSameInstanceAs(a.deepCopy()); // Primitives are immutable!
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/JsonStreamParserTest.java b/gson/gson/src/test/java/com/google/gson/JsonStreamParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..807b93f89d830f32751a6d2e233f872e5d89bf46
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/JsonStreamParserTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2009 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.io.EOFException;
+import java.util.NoSuchElementException;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link JsonStreamParser}
+ *
+ * @author Inderjeet Singh
+ */
+public class JsonStreamParserTest {
+  private JsonStreamParser parser;
+
+  @Before
+  public void setUp() throws Exception {
+    parser = new JsonStreamParser("'one' 'two'");
+  }
+
+  @Test
+  public void testParseTwoStrings() {
+    String actualOne = parser.next().getAsString();
+    assertThat(actualOne).isEqualTo("one");
+    String actualTwo = parser.next().getAsString();
+    assertThat(actualTwo).isEqualTo("two");
+  }
+
+  @Test
+  public void testIterator() {
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.next().getAsString()).isEqualTo("one");
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.next().getAsString()).isEqualTo("two");
+    assertThat(parser.hasNext()).isFalse();
+  }
+
+  @Test
+  public void testNoSideEffectForHasNext() {
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.next().getAsString()).isEqualTo("one");
+
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.hasNext()).isTrue();
+    assertThat(parser.next().getAsString()).isEqualTo("two");
+
+    assertThat(parser.hasNext()).isFalse();
+    assertThat(parser.hasNext()).isFalse();
+  }
+
+  @Test
+  public void testCallingNextBeyondAvailableInput() {
+    JsonElement unused1 = parser.next();
+    JsonElement unused2 = parser.next();
+    // Parser should not go beyond available input
+    assertThrows(NoSuchElementException.class, parser::next);
+  }
+
+  @Test
+  public void testEmptyInput() {
+    JsonStreamParser parser = new JsonStreamParser("");
+    JsonIOException e = assertThrows(JsonIOException.class, parser::next);
+    assertThat(e).hasCauseThat().isInstanceOf(EOFException.class);
+
+    parser = new JsonStreamParser("");
+    e = assertThrows(JsonIOException.class, parser::hasNext);
+    assertThat(e).hasCauseThat().isInstanceOf(EOFException.class);
+  }
+
+  @Test
+  public void testIncompleteInput() {
+    JsonStreamParser parser = new JsonStreamParser("[");
+    assertThat(parser.hasNext()).isTrue();
+    assertThrows(JsonSyntaxException.class, parser::next);
+  }
+
+  @Test
+  public void testMalformedInput() {
+    JsonStreamParser parser = new JsonStreamParser(":");
+    assertThrows(JsonSyntaxException.class, parser::hasNext);
+
+    parser = new JsonStreamParser(":");
+    assertThrows(JsonSyntaxException.class, parser::next);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java b/gson/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..39a57feae410d56ce850ce3196f4a00491833dc1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/LongSerializationPolicyTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2009 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit test for the {@link LongSerializationPolicy} class.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class LongSerializationPolicyTest {
+
+  @Test
+  public void testDefaultLongSerialization() throws Exception {
+    JsonElement element = LongSerializationPolicy.DEFAULT.serialize(1556L);
+    assertThat(element.isJsonPrimitive()).isTrue();
+
+    JsonPrimitive jsonPrimitive = element.getAsJsonPrimitive();
+    assertThat(jsonPrimitive.isString()).isFalse();
+    assertThat(jsonPrimitive.isNumber()).isTrue();
+    assertThat(element.getAsLong()).isEqualTo(1556L);
+  }
+
+  @Test
+  public void testDefaultLongSerializationIntegration() {
+    Gson gson =
+        new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.DEFAULT).create();
+    assertThat(gson.toJson(new long[] {1L}, long[].class)).isEqualTo("[1]");
+    assertThat(gson.toJson(new Long[] {1L}, Long[].class)).isEqualTo("[1]");
+  }
+
+  @Test
+  public void testDefaultLongSerializationNull() {
+    LongSerializationPolicy policy = LongSerializationPolicy.DEFAULT;
+    assertThat(policy.serialize(null).isJsonNull()).isTrue();
+
+    Gson gson = new GsonBuilder().setLongSerializationPolicy(policy).create();
+    assertThat(gson.toJson(null, Long.class)).isEqualTo("null");
+  }
+
+  @Test
+  public void testStringLongSerialization() throws Exception {
+    JsonElement element = LongSerializationPolicy.STRING.serialize(1556L);
+    assertThat(element.isJsonPrimitive()).isTrue();
+
+    JsonPrimitive jsonPrimitive = element.getAsJsonPrimitive();
+    assertThat(jsonPrimitive.isNumber()).isFalse();
+    assertThat(jsonPrimitive.isString()).isTrue();
+    assertThat(element.getAsString()).isEqualTo("1556");
+  }
+
+  @Test
+  public void testStringLongSerializationIntegration() {
+    Gson gson =
+        new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create();
+    assertThat(gson.toJson(new long[] {1L}, long[].class)).isEqualTo("[\"1\"]");
+    assertThat(gson.toJson(new Long[] {1L}, long[].class)).isEqualTo("[\"1\"]");
+  }
+
+  @Test
+  public void testStringLongSerializationNull() {
+    LongSerializationPolicy policy = LongSerializationPolicy.STRING;
+    assertThat(policy.serialize(null).isJsonNull()).isTrue();
+
+    Gson gson = new GsonBuilder().setLongSerializationPolicy(policy).create();
+    assertThat(gson.toJson(null, Long.class)).isEqualTo("null");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/MixedStreamTest.java b/gson/gson/src/test/java/com/google/gson/MixedStreamTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..42fed496342e41f2901c460d1911174534890c9d
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/MixedStreamTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2010 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+
+public final class MixedStreamTest {
+
+  private static final Car BLUE_MUSTANG = new Car("mustang", 0x0000FF);
+  private static final Car BLACK_BMW = new Car("bmw", 0x000000);
+  private static final Car RED_MIATA = new Car("miata", 0xFF0000);
+  private static final String CARS_JSON =
+      "[\n"
+          + "  {\n"
+          + "    \"name\": \"mustang\",\n"
+          + "    \"color\": 255\n"
+          + "  },\n"
+          + "  {\n"
+          + "    \"name\": \"bmw\",\n"
+          + "    \"color\": 0\n"
+          + "  },\n"
+          + "  {\n"
+          + "    \"name\": \"miata\",\n"
+          + "    \"color\": 16711680\n"
+          + "  }\n"
+          + "]";
+
+  @Test
+  public void testWriteMixedStreamed() throws IOException {
+    Gson gson = new Gson();
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+
+    jsonWriter.beginArray();
+    jsonWriter.setIndent("  ");
+    gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter);
+    gson.toJson(BLACK_BMW, Car.class, jsonWriter);
+    gson.toJson(RED_MIATA, Car.class, jsonWriter);
+    jsonWriter.endArray();
+
+    assertThat(stringWriter.toString()).isEqualTo(CARS_JSON);
+  }
+
+  @Test
+  public void testReadMixedStreamed() throws IOException {
+    Gson gson = new Gson();
+    StringReader stringReader = new StringReader(CARS_JSON);
+    JsonReader jsonReader = new JsonReader(stringReader);
+
+    jsonReader.beginArray();
+    assertThat(gson.<Car>fromJson(jsonReader, Car.class)).isEqualTo(BLUE_MUSTANG);
+    assertThat(gson.<Car>fromJson(jsonReader, Car.class)).isEqualTo(BLACK_BMW);
+    assertThat(gson.<Car>fromJson(jsonReader, Car.class)).isEqualTo(RED_MIATA);
+    jsonReader.endArray();
+  }
+
+  @SuppressWarnings("deprecation") // for JsonReader.setLenient
+  @Test
+  public void testReaderDoesNotMutateState() throws IOException {
+    Gson gson = new Gson();
+    JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON));
+    jsonReader.beginArray();
+
+    jsonReader.setLenient(false);
+    Car unused1 = gson.fromJson(jsonReader, Car.class);
+    assertThat(jsonReader.isLenient()).isFalse();
+
+    jsonReader.setLenient(true);
+    Car unused2 = gson.fromJson(jsonReader, Car.class);
+    assertThat(jsonReader.isLenient()).isTrue();
+  }
+
+  @SuppressWarnings("deprecation") // for JsonWriter.setLenient
+  @Test
+  public void testWriteDoesNotMutateState() throws IOException {
+    Gson gson = new Gson();
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.beginArray();
+
+    jsonWriter.setHtmlSafe(true);
+    jsonWriter.setLenient(true);
+    gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter);
+    assertThat(jsonWriter.isHtmlSafe()).isTrue();
+    assertThat(jsonWriter.isLenient()).isTrue();
+
+    jsonWriter.setHtmlSafe(false);
+    jsonWriter.setLenient(false);
+    gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter);
+    assertThat(jsonWriter.isHtmlSafe()).isFalse();
+    assertThat(jsonWriter.isLenient()).isFalse();
+  }
+
+  @Test
+  public void testReadInvalidState() throws IOException {
+    Gson gson = new Gson();
+    JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON));
+    jsonReader.beginArray();
+    jsonReader.beginObject();
+    try {
+      gson.fromJson(jsonReader, String.class);
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testReadClosed() throws IOException {
+    Gson gson = new Gson();
+    JsonReader jsonReader = new JsonReader(new StringReader(CARS_JSON));
+    jsonReader.close();
+    try {
+      gson.fromJson(jsonReader, new TypeToken<List<Car>>() {}.getType());
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteInvalidState() throws IOException {
+    Gson gson = new Gson();
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.beginObject();
+    try {
+      gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteClosed() throws IOException {
+    Gson gson = new Gson();
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.beginArray();
+    jsonWriter.endArray();
+    jsonWriter.close();
+    try {
+      gson.toJson(BLUE_MUSTANG, Car.class, jsonWriter);
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteNulls() {
+    Gson gson = new Gson();
+    try {
+      gson.toJson(new JsonPrimitive("hello"), (JsonWriter) null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+
+    StringWriter stringWriter = new StringWriter();
+    gson.toJson(null, new JsonWriter(stringWriter));
+    assertThat(stringWriter.toString()).isEqualTo("null");
+  }
+
+  @Test
+  public void testReadNulls() {
+    Gson gson = new Gson();
+    try {
+      gson.fromJson((JsonReader) null, Integer.class);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+    try {
+      gson.fromJson(new JsonReader(new StringReader("true")), (Type) null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testWriteHtmlSafe() {
+    List<String> contents = Arrays.asList("<", ">", "&", "=", "'");
+    Type type = new TypeToken<List<String>>() {}.getType();
+
+    StringWriter writer = new StringWriter();
+    new Gson().toJson(contents, type, new JsonWriter(writer));
+    assertThat(writer.toString())
+        .isEqualTo("[\"\\u003c\",\"\\u003e\",\"\\u0026\",\"\\u003d\",\"\\u0027\"]");
+
+    writer = new StringWriter();
+    new GsonBuilder().disableHtmlEscaping().create().toJson(contents, type, new JsonWriter(writer));
+    assertThat(writer.toString()).isEqualTo("[\"<\",\">\",\"&\",\"=\",\"'\"]");
+  }
+
+  @Test
+  public void testWriteLenient() {
+    List<Double> doubles =
+        Arrays.asList(
+            Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, -0.0d, 0.5d, 0.0d);
+    Type type = new TypeToken<List<Double>>() {}.getType();
+
+    StringWriter writer = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(writer);
+    new GsonBuilder()
+        .serializeSpecialFloatingPointValues()
+        .create()
+        .toJson(doubles, type, jsonWriter);
+    assertThat(writer.toString()).isEqualTo("[NaN,-Infinity,Infinity,-0.0,0.5,0.0]");
+
+    try {
+      new Gson().toJson(doubles, type, new JsonWriter(new StringWriter()));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  static final class Car {
+    String name;
+    int color;
+
+    Car(String name, int color) {
+      this.name = name;
+      this.color = color;
+    }
+
+    // used by Gson
+    Car() {}
+
+    @Override
+    public int hashCode() {
+      return name.hashCode() ^ color;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Car && ((Car) o).name.equals(name) && ((Car) o).color == color;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterParameterizedTest.java b/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterParameterizedTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..612c628b164aba54e6f0c511c6bf975ada372e2b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterParameterizedTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ObjectTypeAdapterParameterizedTest {
+  @Parameters
+  public static Iterable<String> data() {
+    return Arrays.asList(
+        "[]",
+        "{}",
+        "null",
+        "1.0",
+        "true",
+        "\"string\"",
+        "[true,1.0,null,{},2.0,{\"a\":[false]},[3.0,\"test\"],4.0]",
+        "{\"\":1.0,\"a\":true,\"b\":null,\"c\":[],\"d\":{\"a1\":2.0,\"b2\":[true,{\"a3\":3.0}]},\"e\":[{\"f\":4.0},\"test\"]}");
+  }
+
+  private final TypeAdapter<Object> adapter = new Gson().getAdapter(Object.class);
+  @Parameter public String json;
+
+  @Test
+  public void testReadWrite() throws IOException {
+    Object deserialized = adapter.fromJson(json);
+    String actualSerialized = adapter.toJson(deserialized);
+
+    // Serialized Object should be the same as original JSON
+    assertThat(actualSerialized).isEqualTo(json);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..274de8fad5523d777c3e591bcc39e1856c438c24
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ObjectTypeAdapterTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2011 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public final class ObjectTypeAdapterTest {
+  private final Gson gson = new GsonBuilder().create();
+  private final TypeAdapter<Object> adapter = gson.getAdapter(Object.class);
+
+  @Test
+  public void testDeserialize() throws Exception {
+    Map<?, ?> map = (Map<?, ?>) adapter.fromJson("{\"a\":5,\"b\":[1,2,null],\"c\":{\"x\":\"y\"}}");
+    assertThat(map.get("a")).isEqualTo(5.0);
+    assertThat(map.get("b")).isEqualTo(Arrays.asList(1.0, 2.0, null));
+    assertThat(map.get("c")).isEqualTo(Collections.singletonMap("x", "y"));
+    assertThat(map).hasSize(3);
+  }
+
+  @Test
+  public void testSerialize() {
+    Object object = new RuntimeType();
+    assertThat(adapter.toJson(object).replace("\"", "'")).isEqualTo("{'a':5,'b':[1,2,null]}");
+  }
+
+  @Test
+  public void testSerializeNullValue() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("a", null);
+    assertThat(adapter.toJson(map).replace('"', '\'')).isEqualTo("{'a':null}");
+  }
+
+  @Test
+  public void testDeserializeNullValue() throws Exception {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("a", null);
+    assertThat(adapter.fromJson("{\"a\":null}")).isEqualTo(map);
+  }
+
+  @Test
+  public void testSerializeObject() {
+    assertThat(adapter.toJson(new Object())).isEqualTo("{}");
+  }
+
+  private static String repeat(String s, int times) {
+    StringBuilder stringBuilder = new StringBuilder(s.length() * times);
+    for (int i = 0; i < times; i++) {
+      stringBuilder.append(s);
+    }
+    return stringBuilder.toString();
+  }
+
+  /** Deeply nested JSON arrays should not cause {@link StackOverflowError} */
+  @SuppressWarnings("unchecked")
+  @Test
+  public void testDeserializeDeeplyNestedArrays() throws IOException {
+    int times = 10000;
+    // [[[ ... ]]]
+    String json = repeat("[", times) + repeat("]", times);
+
+    int actualTimes = 0;
+    List<List<?>> current = (List<List<?>>) adapter.fromJson(json);
+    while (true) {
+      actualTimes++;
+      if (current.isEmpty()) {
+        break;
+      }
+      assertThat(current).hasSize(1);
+      current = (List<List<?>>) current.get(0);
+    }
+    assertThat(actualTimes).isEqualTo(times);
+  }
+
+  /** Deeply nested JSON objects should not cause {@link StackOverflowError} */
+  @SuppressWarnings("unchecked")
+  @Test
+  public void testDeserializeDeeplyNestedObjects() throws IOException {
+    int times = 10000;
+    // {"a":{"a": ... {"a":null} ... }}
+    String json = repeat("{\"a\":", times) + "null" + repeat("}", times);
+
+    int actualTimes = 0;
+    Map<String, Map<?, ?>> current = (Map<String, Map<?, ?>>) adapter.fromJson(json);
+    while (current != null) {
+      assertThat(current).hasSize(1);
+      actualTimes++;
+      current = (Map<String, Map<?, ?>>) current.get("a");
+    }
+    assertThat(actualTimes).isEqualTo(times);
+  }
+
+  @SuppressWarnings({"unused", "ClassCanBeStatic"})
+  private class RuntimeType {
+    Object a = 5;
+    Object b = Arrays.asList(1, 2, null);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java b/gson/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a34f35f0cabb5a0bf19ebaa95f1ae3421f6c08a4
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/OverrideCoreTypeAdaptersTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2012 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.Locale;
+import org.junit.Test;
+
+/**
+ * Tests handling of Core Type Adapters
+ *
+ * @author Jesse Wilson
+ */
+public class OverrideCoreTypeAdaptersTest {
+  private static final TypeAdapter<Boolean> booleanAsIntAdapter =
+      new TypeAdapter<Boolean>() {
+        @Override
+        public void write(JsonWriter out, Boolean value) throws IOException {
+          out.value(value ? 1 : 0);
+        }
+
+        @Override
+        public Boolean read(JsonReader in) throws IOException {
+          int value = in.nextInt();
+          return value != 0;
+        }
+      };
+
+  private static final TypeAdapter<String> swapCaseStringAdapter =
+      new TypeAdapter<String>() {
+        @Override
+        public void write(JsonWriter out, String value) throws IOException {
+          out.value(value.toUpperCase(Locale.US));
+        }
+
+        @Override
+        public String read(JsonReader in) throws IOException {
+          return in.nextString().toLowerCase(Locale.US);
+        }
+      };
+
+  @Test
+  public void testOverrideWrapperBooleanAdapter() {
+    Gson gson = new GsonBuilder().registerTypeAdapter(Boolean.class, booleanAsIntAdapter).create();
+    assertThat(gson.toJson(true, boolean.class)).isEqualTo("true");
+    assertThat(gson.toJson(true, Boolean.class)).isEqualTo("1");
+    assertThat(gson.fromJson("true", boolean.class)).isEqualTo(Boolean.TRUE);
+    assertThat(gson.fromJson("1", Boolean.class)).isEqualTo(Boolean.TRUE);
+    assertThat(gson.fromJson("0", Boolean.class)).isEqualTo(Boolean.FALSE);
+  }
+
+  @Test
+  public void testOverridePrimitiveBooleanAdapter() {
+    Gson gson = new GsonBuilder().registerTypeAdapter(boolean.class, booleanAsIntAdapter).create();
+    assertThat(gson.toJson(true, boolean.class)).isEqualTo("1");
+    assertThat(gson.toJson(true, Boolean.class)).isEqualTo("true");
+    assertThat(gson.fromJson("1", boolean.class)).isEqualTo(Boolean.TRUE);
+    assertThat(gson.fromJson("true", Boolean.class)).isEqualTo(Boolean.TRUE);
+    assertThat(gson.toJson(false, boolean.class)).isEqualTo("0");
+  }
+
+  @Test
+  public void testOverrideStringAdapter() {
+    Gson gson = new GsonBuilder().registerTypeAdapter(String.class, swapCaseStringAdapter).create();
+    assertThat(gson.toJson("Hello", String.class)).isEqualTo("\"HELLO\"");
+    assertThat(gson.fromJson("\"Hello\"", String.class)).isEqualTo("hello");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java b/gson/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java
new file mode 100644
index 0000000000000000000000000000000000000000..34b7adc70cc292972391c01a2a417046e2af17e2
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ParameterizedTypeFixtures.java
@@ -0,0 +1,162 @@
+/*
+ * 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;
+
+import com.google.common.base.Objects;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.internal.Primitives;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+/**
+ * This class contains some test fixtures for Parameterized types. These classes should ideally
+ * belong either in the common or functional package, but they are placed here because they need
+ * access to package protected elements of com.google.gson.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ParameterizedTypeFixtures {
+  private ParameterizedTypeFixtures() {}
+
+  public static final class MyParameterizedType<T> {
+    public final T value;
+
+    public MyParameterizedType(T value) {
+      this.value = value;
+    }
+
+    public T getValue() {
+      return value;
+    }
+
+    public String getExpectedJson() {
+      String valueAsJson = getExpectedJson(value);
+      return String.format("{\"value\":%s}", valueAsJson);
+    }
+
+    private static String getExpectedJson(Object obj) {
+      Class<?> clazz = obj.getClass();
+      if (Primitives.isWrapperType(Primitives.wrap(clazz))) {
+        return obj.toString();
+      } else if (obj.getClass().equals(String.class)) {
+        return "\"" + obj.toString() + "\"";
+      } else {
+        // Try invoking a getExpectedJson() method if it exists
+        try {
+          Method method = clazz.getMethod("getExpectedJson");
+          Object results = method.invoke(obj);
+          return (String) results;
+        } catch (ReflectiveOperationException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+
+    @Override
+    public int hashCode() {
+      return value == null ? 0 : value.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof MyParameterizedType<?>)) {
+        return false;
+      }
+      MyParameterizedType<?> that = (MyParameterizedType<?>) obj;
+      return Objects.equal(getValue(), that.getValue());
+    }
+  }
+
+  public static class MyParameterizedTypeInstanceCreator<T>
+      implements InstanceCreator<MyParameterizedType<T>> {
+    private final T instanceOfT;
+
+    /**
+     * Caution the specified instance is reused by the instance creator for each call. This means
+     * that the fields of the same objects will be overwritten by Gson. This is usually fine in
+     * tests since there we deserialize just once, but quite dangerous in practice.
+     */
+    public MyParameterizedTypeInstanceCreator(T instanceOfT) {
+      this.instanceOfT = instanceOfT;
+    }
+
+    @Override
+    public MyParameterizedType<T> createInstance(Type type) {
+      return new MyParameterizedType<>(instanceOfT);
+    }
+  }
+
+  public static final class MyParameterizedTypeAdapter<T>
+      implements JsonSerializer<MyParameterizedType<T>>, JsonDeserializer<MyParameterizedType<T>> {
+    @SuppressWarnings("unchecked")
+    public static <T> String getExpectedJson(MyParameterizedType<T> obj) {
+      Class<T> clazz = (Class<T>) obj.value.getClass();
+      boolean addQuotes = !clazz.isArray() && !Primitives.unwrap(clazz).isPrimitive();
+      StringBuilder sb = new StringBuilder("{\"");
+      sb.append(obj.value.getClass().getSimpleName()).append("\":");
+      if (addQuotes) {
+        sb.append("\"");
+      }
+      sb.append(obj.value.toString());
+      if (addQuotes) {
+        sb.append("\"");
+      }
+      sb.append("}");
+      return sb.toString();
+    }
+
+    @Override
+    public JsonElement serialize(
+        MyParameterizedType<T> src, Type classOfSrc, JsonSerializationContext context) {
+      JsonObject json = new JsonObject();
+      T value = src.getValue();
+      json.add(value.getClass().getSimpleName(), context.serialize(value));
+      return json;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public MyParameterizedType<T> deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      Type genericClass = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];
+      Class<?> rawType = $Gson$Types.getRawType(genericClass);
+      String className = rawType.getSimpleName();
+      JsonElement jsonElement = json.getAsJsonObject().get(className);
+
+      T value;
+      if (genericClass == Integer.class) {
+        value = (T) Integer.valueOf(jsonElement.getAsInt());
+      } else if (genericClass == String.class) {
+        value = (T) jsonElement.getAsString();
+      } else {
+        value = (T) jsonElement;
+      }
+
+      if (Primitives.isPrimitive(genericClass)) {
+        PrimitiveTypeAdapter typeAdapter = new PrimitiveTypeAdapter();
+        value = (T) typeAdapter.adaptType(value, rawType);
+      }
+      return new MyParameterizedType<>(value);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java b/gson/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca837c540ead01ed20acbc06c9660f1f3ad167b1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ParameterizedTypeTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@code ParameterizedType}s created by the {@link $Gson$Types} class.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ParameterizedTypeTest {
+  private ParameterizedType ourType;
+
+  @Before
+  public void setUp() throws Exception {
+    ourType = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, String.class);
+  }
+
+  @Test
+  public void testOurTypeFunctionality() {
+    Type parameterizedType = new TypeToken<List<String>>() {}.getType();
+    assertThat(ourType.getOwnerType()).isNull();
+    assertThat(ourType.getActualTypeArguments()[0]).isSameInstanceAs(String.class);
+    assertThat(ourType.getRawType()).isSameInstanceAs(List.class);
+    assertThat(ourType).isEqualTo(parameterizedType);
+    assertThat(ourType.hashCode()).isEqualTo(parameterizedType.hashCode());
+  }
+
+  @Test
+  public void testNotEquals() {
+    Type differentParameterizedType = new TypeToken<List<Integer>>() {}.getType();
+    assertThat(differentParameterizedType.equals(ourType)).isFalse();
+    assertThat(ourType.equals(differentParameterizedType)).isFalse();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java b/gson/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b9ae415457a4b6fb56dfa143e78270976f9bbe1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/PrimitiveTypeAdapter.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import com.google.gson.internal.Primitives;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/**
+ * Handles type conversion from some object to some primitive (or primitive wrapper instance).
+ *
+ * <p>Used by {@link ParameterizedTypeFixtures.MyParameterizedTypeAdapter}.
+ *
+ * @author Joel Leitch
+ */
+final class PrimitiveTypeAdapter {
+
+  @SuppressWarnings("unchecked")
+  public <T> T adaptType(Object from, Class<T> to) {
+    Class<?> aClass = Primitives.wrap(to);
+    if (Primitives.isWrapperType(aClass)) {
+      if (aClass == Character.class) {
+        String value = from.toString();
+        if (value.length() == 1) {
+          return (T) (Character) from.toString().charAt(0);
+        }
+        throw new JsonParseException("The value: " + value + " contains more than a character.");
+      }
+
+      try {
+        Constructor<?> constructor = aClass.getConstructor(String.class);
+        return (T) constructor.newInstance(from.toString());
+      } catch (ReflectiveOperationException e) {
+        throw new JsonParseException(e);
+      }
+    } else if (Enum.class.isAssignableFrom(to)) {
+      // Case where the type being adapted to is an Enum
+      // We will try to convert from.toString() to the enum
+      try {
+        Method valuesMethod = to.getMethod("valueOf", String.class);
+        return (T) valuesMethod.invoke(null, from.toString());
+      } catch (ReflectiveOperationException e) {
+        throw new RuntimeException(e);
+      }
+    } else {
+      throw new JsonParseException("Can not adapt type " + from.getClass() + " to " + to);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d6ce1f7888a20b8cc0eb4a07802adff13c7dbdb9
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2021 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.IOException;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import org.junit.Test;
+
+public class ToNumberPolicyTest {
+  @Test
+  public void testDouble() throws IOException {
+    ToNumberStrategy strategy = ToNumberPolicy.DOUBLE;
+    assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1);
+    assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279")))
+        .isEqualTo(3.141592653589793D);
+
+    MalformedJsonException e =
+        assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+
+    assertThrows(
+        NumberFormatException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
+  }
+
+  @Test
+  public void testLazilyParsedNumber() throws IOException {
+    ToNumberStrategy strategy = ToNumberPolicy.LAZILY_PARSED_NUMBER;
+    assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(new LazilyParsedNumber("10.1"));
+    assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279")))
+        .isEqualTo(new LazilyParsedNumber("3.141592653589793238462643383279"));
+    assertThat(strategy.readNumber(fromString("1e400"))).isEqualTo(new LazilyParsedNumber("1e400"));
+  }
+
+  @Test
+  public void testLongOrDouble() throws IOException {
+    ToNumberStrategy strategy = ToNumberPolicy.LONG_OR_DOUBLE;
+    assertThat(strategy.readNumber(fromString("10"))).isEqualTo(10L);
+    assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(10.1);
+    assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279")))
+        .isEqualTo(3.141592653589793D);
+
+    Exception e =
+        assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("1e400")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("JSON forbids NaN and infinities: Infinity; at path $");
+
+    e =
+        assertThrows(
+            JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
+    assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
+
+    assertThat(strategy.readNumber(fromStringLenient("NaN"))).isEqualTo(Double.NaN);
+    assertThat(strategy.readNumber(fromStringLenient("Infinity")))
+        .isEqualTo(Double.POSITIVE_INFINITY);
+    assertThat(strategy.readNumber(fromStringLenient("-Infinity")))
+        .isEqualTo(Double.NEGATIVE_INFINITY);
+
+    e = assertThrows(MalformedJsonException.class, () -> strategy.readNumber(fromString("NaN")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1"
+                + " column 1 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+
+    e =
+        assertThrows(
+            MalformedJsonException.class, () -> strategy.readNumber(fromString("Infinity")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1"
+                + " column 1 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+
+    e =
+        assertThrows(
+            MalformedJsonException.class, () -> strategy.readNumber(fromString("-Infinity")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at line 1"
+                + " column 1 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+  }
+
+  @Test
+  public void testBigDecimal() throws IOException {
+    ToNumberStrategy strategy = ToNumberPolicy.BIG_DECIMAL;
+    assertThat(strategy.readNumber(fromString("10.1"))).isEqualTo(new BigDecimal("10.1"));
+    assertThat(strategy.readNumber(fromString("3.141592653589793238462643383279")))
+        .isEqualTo(new BigDecimal("3.141592653589793238462643383279"));
+    assertThat(strategy.readNumber(fromString("1e400"))).isEqualTo(new BigDecimal("1e400"));
+
+    JsonParseException e =
+        assertThrows(
+            JsonParseException.class, () -> strategy.readNumber(fromString("\"not-a-number\"")));
+    assertThat(e).hasMessageThat().isEqualTo("Cannot parse not-a-number; at path $");
+  }
+
+  @Test
+  public void testNullsAreNeverExpected() throws IOException {
+    IllegalStateException e =
+        assertThrows(
+            IllegalStateException.class,
+            () -> ToNumberPolicy.DOUBLE.readNumber(fromString("null")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected a double but was NULL at line 1 column 5 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
+
+    e =
+        assertThrows(
+            IllegalStateException.class,
+            () -> ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected a string but was NULL at line 1 column 5 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
+
+    e =
+        assertThrows(
+            IllegalStateException.class,
+            () -> ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected a string but was NULL at line 1 column 5 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
+
+    e =
+        assertThrows(
+            IllegalStateException.class,
+            () -> ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected a string but was NULL at line 1 column 5 path $\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
+  }
+
+  private static JsonReader fromString(String json) {
+    return new JsonReader(new StringReader(json));
+  }
+
+  private static JsonReader fromStringLenient(String json) {
+    JsonReader jsonReader = fromString(json);
+    jsonReader.setStrictness(Strictness.LENIENT);
+    return jsonReader;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/TypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/TypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a551adb8595112ada7c0ccbf3367f91870a311b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/TypeAdapterTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import org.junit.Test;
+
+public class TypeAdapterTest {
+  @Test
+  public void testNullSafe() throws IOException {
+    TypeAdapter<String> adapter =
+        new TypeAdapter<String>() {
+          @Override
+          public void write(JsonWriter out, String value) {
+            throw new AssertionError("unexpected call");
+          }
+
+          @Override
+          public String read(JsonReader in) {
+            throw new AssertionError("unexpected call");
+          }
+        }.nullSafe();
+
+    assertThat(adapter.toJson(null)).isEqualTo("null");
+    assertThat(adapter.fromJson("null")).isNull();
+  }
+
+  /**
+   * 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) {
+            throw new AssertionError("not needed by this test");
+          }
+        };
+
+    JsonIOException e = assertThrows(JsonIOException.class, () -> adapter.toJson(1));
+    assertThat(e).hasCauseThat().isEqualTo(exception);
+
+    e = assertThrows(JsonIOException.class, () -> adapter.toJsonTree(1));
+    assertThat(e).hasCauseThat().isEqualTo(exception);
+  }
+
+  private static final TypeAdapter<String> adapter =
+      new TypeAdapter<String>() {
+        @Override
+        public void write(JsonWriter out, String value) throws IOException {
+          out.value(value);
+        }
+
+        @Override
+        public String read(JsonReader in) throws IOException {
+          return in.nextString();
+        }
+      };
+
+  // Note: This test just verifies the current behavior; it is a bit questionable
+  // whether that behavior is actually desired
+  @Test
+  public void testFromJson_Reader_TrailingData() throws IOException {
+    assertThat(adapter.fromJson(new StringReader("\"a\"1"))).isEqualTo("a");
+  }
+
+  // Note: This test just verifies the current behavior; it is a bit questionable
+  // whether that behavior is actually desired
+  @Test
+  public void testFromJson_String_TrailingData() throws IOException {
+    assertThat(adapter.fromJson("\"a\"1")).isEqualTo("a");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c2b22ca6cf2c68190d642a4910c5f99d3fb313f
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.Keep;
+import com.google.gson.annotations.Since;
+import com.google.gson.annotations.Until;
+import com.google.gson.internal.Excluder;
+import java.lang.reflect.Field;
+import org.junit.Test;
+
+/**
+ * Unit tests for the {@link Excluder} class.
+ *
+ * @author Joel Leitch
+ */
+public class VersionExclusionStrategyTest {
+  private static final double VERSION = 5.0D;
+
+  private static void assertIncludesClass(Excluder excluder, Class<?> c) {
+    assertThat(excluder.excludeClass(c, true)).isFalse();
+    assertThat(excluder.excludeClass(c, false)).isFalse();
+  }
+
+  private static void assertExcludesClass(Excluder excluder, Class<?> c) {
+    assertThat(excluder.excludeClass(c, true)).isTrue();
+    assertThat(excluder.excludeClass(c, false)).isTrue();
+  }
+
+  private static void assertIncludesField(Excluder excluder, Field f) {
+    assertThat(excluder.excludeField(f, true)).isFalse();
+    assertThat(excluder.excludeField(f, false)).isFalse();
+  }
+
+  private static void assertExcludesField(Excluder excluder, Field f) {
+    assertThat(excluder.excludeField(f, true)).isTrue();
+    assertThat(excluder.excludeField(f, false)).isTrue();
+  }
+
+  @Test
+  public void testSameVersion() throws Exception {
+    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
+    assertIncludesClass(excluder, MockClassSince.class);
+    assertIncludesField(excluder, MockClassSince.class.getField("someField"));
+
+    // Until version is exclusive
+    assertExcludesClass(excluder, MockClassUntil.class);
+    assertExcludesField(excluder, MockClassUntil.class.getField("someField"));
+
+    assertIncludesClass(excluder, MockClassBoth.class);
+    assertIncludesField(excluder, MockClassBoth.class.getField("someField"));
+  }
+
+  @Test
+  public void testNewerVersion() throws Exception {
+    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
+    assertIncludesClass(excluder, MockClassSince.class);
+    assertIncludesField(excluder, MockClassSince.class.getField("someField"));
+
+    assertExcludesClass(excluder, MockClassUntil.class);
+    assertExcludesField(excluder, MockClassUntil.class.getField("someField"));
+
+    assertExcludesClass(excluder, MockClassBoth.class);
+    assertExcludesField(excluder, MockClassBoth.class.getField("someField"));
+  }
+
+  @Test
+  public void testOlderVersion() throws Exception {
+    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
+    assertExcludesClass(excluder, MockClassSince.class);
+    assertExcludesField(excluder, MockClassSince.class.getField("someField"));
+
+    assertIncludesClass(excluder, MockClassUntil.class);
+    assertIncludesField(excluder, MockClassUntil.class.getField("someField"));
+
+    assertExcludesClass(excluder, MockClassBoth.class);
+    assertExcludesField(excluder, MockClassBoth.class.getField("someField"));
+  }
+
+  @Since(VERSION)
+  private static class MockClassSince {
+
+    @Since(VERSION)
+    @Keep
+    public final int someField = 0;
+  }
+
+  @Until(VERSION)
+  private static class MockClassUntil {
+
+    @Until(VERSION)
+    @Keep
+    public final int someField = 0;
+  }
+
+  @Since(VERSION)
+  @Until(VERSION + 2)
+  private static class MockClassBoth {
+
+    @Since(VERSION)
+    @Until(VERSION + 2)
+    @Keep
+    public final int someField = 0;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/common/MoreAsserts.java b/gson/gson/src/test/java/com/google/gson/common/MoreAsserts.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b74056dcc7e704af1037b92e3dcc31795fe4099
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/common/MoreAsserts.java
@@ -0,0 +1,109 @@
+/*
+ * 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.common;
+
+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} so that we didn't have to write them.
+ *
+ * @author Inderjeet Singh
+ */
+public class MoreAsserts {
+  private MoreAsserts() {}
+
+  /**
+   * Asserts that the specified {@code value} is not present in {@code collection}
+   *
+   * @param collection the collection to look into
+   * @param value the value that needs to be checked for presence
+   */
+  public static <T> void assertContains(Collection<T> collection, T value) {
+    for (T entry : collection) {
+      if (entry.equals(value)) {
+        return;
+      }
+    }
+    Assert.fail(value + " not present in " + collection);
+  }
+
+  public static void assertEqualsAndHashCode(Object a, Object b) {
+    Assert.assertTrue(a.equals(b));
+    Assert.assertTrue(b.equals(a));
+    Assert.assertEquals(a.hashCode(), b.hashCode());
+    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/gson/src/test/java/com/google/gson/common/TestTypes.java b/gson/gson/src/test/java/com/google/gson/common/TestTypes.java
new file mode 100644
index 0000000000000000000000000000000000000000..b41f4b36da670df0f2e096ff1fbba0e3790aee9d
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/common/TestTypes.java
@@ -0,0 +1,434 @@
+/*
+ * 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.common;
+
+import com.google.common.base.Objects;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.annotations.SerializedName;
+import java.lang.reflect.Type;
+import java.util.Collection;
+
+/**
+ * Types used for testing JSON serialization and deserialization
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class TestTypes {
+  private TestTypes() {}
+
+  public static class Base {
+    public static final String BASE_NAME = Base.class.getSimpleName();
+    public static final String BASE_FIELD_KEY = "baseName";
+    public static final String SERIALIZER_KEY = "serializerName";
+    public String baseName = BASE_NAME;
+    public String serializerName;
+  }
+
+  public static class Sub extends Base {
+    public static final String SUB_NAME = Sub.class.getSimpleName();
+    public static final String SUB_FIELD_KEY = "subName";
+    public final String subName = SUB_NAME;
+  }
+
+  public static class ClassWithBaseField {
+    public static final String FIELD_KEY = "base";
+    public final Base base;
+
+    public ClassWithBaseField(Base base) {
+      this.base = base;
+    }
+  }
+
+  public static class ClassWithBaseArrayField {
+    public static final String FIELD_KEY = "base";
+    public final Base[] base;
+
+    public ClassWithBaseArrayField(Base[] base) {
+      this.base = base;
+    }
+  }
+
+  public static class ClassWithBaseCollectionField {
+    public static final String FIELD_KEY = "base";
+    public final Collection<Base> base;
+
+    public ClassWithBaseCollectionField(Collection<Base> base) {
+      this.base = base;
+    }
+  }
+
+  public static class BaseSerializer implements JsonSerializer<Base> {
+    public static final String NAME = BaseSerializer.class.getSimpleName();
+
+    @Override
+    public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) {
+      JsonObject obj = new JsonObject();
+      obj.addProperty(Base.SERIALIZER_KEY, NAME);
+      return obj;
+    }
+  }
+
+  public static class SubSerializer implements JsonSerializer<Sub> {
+    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 {
+    public final String someConstantStringInstanceField;
+
+    public StringWrapper(String value) {
+      someConstantStringInstanceField = value;
+    }
+  }
+
+  public static class BagOfPrimitives {
+    public static final long DEFAULT_VALUE = 0;
+    public long longValue;
+    public int intValue;
+    public boolean booleanValue;
+    public String stringValue;
+
+    public BagOfPrimitives() {
+      this(DEFAULT_VALUE, 0, false, "");
+    }
+
+    public BagOfPrimitives(long longValue, int intValue, boolean booleanValue, String stringValue) {
+      this.longValue = longValue;
+      this.intValue = intValue;
+      this.booleanValue = booleanValue;
+      this.stringValue = stringValue;
+    }
+
+    public int getIntValue() {
+      return intValue;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      sb.append("\"longValue\":").append(longValue).append(",");
+      sb.append("\"intValue\":").append(intValue).append(",");
+      sb.append("\"booleanValue\":").append(booleanValue).append(",");
+      sb.append("\"stringValue\":\"").append(stringValue).append("\"");
+      sb.append("}");
+      return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + (booleanValue ? 1231 : 1237);
+      result = prime * result + intValue;
+      result = prime * result + (int) (longValue ^ (longValue >>> 32));
+      result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode());
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof BagOfPrimitives)) {
+        return false;
+      }
+      BagOfPrimitives that = (BagOfPrimitives) o;
+      return longValue == that.longValue
+          && getIntValue() == that.getIntValue()
+          && booleanValue == that.booleanValue
+          && Objects.equal(stringValue, that.stringValue);
+    }
+
+    @Override
+    public String toString() {
+      return String.format(
+          "(longValue=%d,intValue=%d,booleanValue=%b,stringValue=%s)",
+          longValue, intValue, booleanValue, stringValue);
+    }
+  }
+
+  public static class BagOfPrimitiveWrappers {
+    private final Long longValue;
+    private final Integer intValue;
+    private final Boolean booleanValue;
+
+    public BagOfPrimitiveWrappers(Long longValue, Integer intValue, Boolean booleanValue) {
+      this.longValue = longValue;
+      this.intValue = intValue;
+      this.booleanValue = booleanValue;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      sb.append("\"longValue\":").append(longValue).append(",");
+      sb.append("\"intValue\":").append(intValue).append(",");
+      sb.append("\"booleanValue\":").append(booleanValue);
+      sb.append("}");
+      return sb.toString();
+    }
+  }
+
+  public static class PrimitiveArray {
+    private final long[] longArray;
+
+    public PrimitiveArray() {
+      this(new long[0]);
+    }
+
+    public PrimitiveArray(long[] longArray) {
+      this.longArray = longArray;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{\"longArray\":[");
+
+      boolean first = true;
+      for (long l : longArray) {
+        if (!first) {
+          sb.append(",");
+        } else {
+          first = false;
+        }
+        sb.append(l);
+      }
+
+      sb.append("]}");
+      return sb.toString();
+    }
+  }
+
+  // for missing hashCode() override
+  @SuppressWarnings({"overrides", "EqualsHashCode"})
+  public static class ClassWithNoFields {
+    // Nothing here..
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof ClassWithNoFields;
+    }
+  }
+
+  public static class Nested {
+    private final BagOfPrimitives primitive1;
+    private final BagOfPrimitives primitive2;
+
+    public Nested() {
+      this(null, null);
+    }
+
+    public Nested(BagOfPrimitives primitive1, BagOfPrimitives primitive2) {
+      this.primitive1 = primitive1;
+      this.primitive2 = primitive2;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      appendFields(sb);
+      sb.append("}");
+      return sb.toString();
+    }
+
+    public void appendFields(StringBuilder sb) {
+      if (primitive1 != null) {
+        sb.append("\"primitive1\":").append(primitive1.getExpectedJson());
+      }
+      if (primitive1 != null && primitive2 != null) {
+        sb.append(",");
+      }
+      if (primitive2 != null) {
+        sb.append("\"primitive2\":").append(primitive2.getExpectedJson());
+      }
+    }
+  }
+
+  public static class ClassWithTransientFields<T> {
+    public transient T transientT;
+    public final transient long transientLongValue;
+    private final long[] longValue;
+
+    public ClassWithTransientFields() {
+      this(0L);
+    }
+
+    public ClassWithTransientFields(long value) {
+      longValue = new long[] {value};
+      transientLongValue = value + 1;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      sb.append("\"longValue\":[").append(longValue[0]).append("]");
+      sb.append("}");
+      return sb.toString();
+    }
+  }
+
+  public static class ClassWithCustomTypeConverter {
+    private final BagOfPrimitives bag;
+    private final int value;
+
+    public ClassWithCustomTypeConverter() {
+      this(new BagOfPrimitives(), 10);
+    }
+
+    public ClassWithCustomTypeConverter(int value) {
+      this(new BagOfPrimitives(value, value, false, ""), value);
+    }
+
+    public ClassWithCustomTypeConverter(BagOfPrimitives bag, int value) {
+      this.bag = bag;
+      this.value = value;
+    }
+
+    public BagOfPrimitives getBag() {
+      return bag;
+    }
+
+    public String getExpectedJson() {
+      return "{\"url\":\"" + bag.getExpectedJson() + "\",\"value\":" + value + "}";
+    }
+
+    public int getValue() {
+      return value;
+    }
+  }
+
+  public static class ArrayOfObjects {
+    private final BagOfPrimitives[] elements;
+
+    public ArrayOfObjects() {
+      elements = new BagOfPrimitives[3];
+      for (int i = 0; i < elements.length; ++i) {
+        elements[i] = new BagOfPrimitives(i, i + 2, false, "i" + i);
+      }
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder("{\"elements\":[");
+      boolean first = true;
+      for (BagOfPrimitives element : elements) {
+        if (first) {
+          first = false;
+        } else {
+          sb.append(",");
+        }
+        sb.append(element.getExpectedJson());
+      }
+      sb.append("]}");
+      return sb.toString();
+    }
+  }
+
+  public static class ClassOverridingEquals {
+    public ClassOverridingEquals ref;
+
+    public String getExpectedJson() {
+      if (ref == null) {
+        return "{}";
+      }
+      return "{\"ref\":" + ref.getExpectedJson() + "}";
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return true;
+    }
+
+    @Override
+    public int hashCode() {
+      return 1;
+    }
+  }
+
+  public static class ClassWithArray {
+    public final Object[] array;
+
+    public ClassWithArray() {
+      array = null;
+    }
+
+    public ClassWithArray(Object[] array) {
+      this.array = array;
+    }
+  }
+
+  public static class ClassWithObjects {
+    public final BagOfPrimitives bag;
+
+    public ClassWithObjects() {
+      this(new BagOfPrimitives());
+    }
+
+    public ClassWithObjects(BagOfPrimitives bag) {
+      this.bag = bag;
+    }
+  }
+
+  public static class ClassWithSerializedNameFields {
+    @SerializedName("fooBar")
+    public final int f;
+
+    @SerializedName("Another Foo")
+    public final int g;
+
+    public ClassWithSerializedNameFields() {
+      this(1, 4);
+    }
+
+    public ClassWithSerializedNameFields(int f, int g) {
+      this.f = f;
+      this.g = g;
+    }
+
+    public String getExpectedJson() {
+      return '{' + "\"fooBar\":" + f + ",\"Another Foo\":" + g + '}';
+    }
+  }
+
+  public static class CrazyLongTypeAdapter implements JsonSerializer<Long>, JsonDeserializer<Long> {
+    public static final long DIFFERENCE = 5L;
+
+    @Override
+    public JsonElement serialize(Long src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive(src + DIFFERENCE);
+    }
+
+    @Override
+    public Long deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return json.getAsLong() - DIFFERENCE;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ArrayTest.java b/gson/gson/src/test/java/com/google/gson/functional/ArrayTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a88ad9f4716f96dbeb01c427b5aca19401a8592f
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ArrayTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+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 java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json serialization and deserialization of arrays.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ArrayTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testTopLevelArrayOfIntsSerialization() {
+    int[] target = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+    assertThat(gson.toJson(target)).isEqualTo("[1,2,3,4,5,6,7,8,9]");
+  }
+
+  @Test
+  public void testTopLevelArrayOfIntsDeserialization() {
+    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);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testInvalidArrayDeserialization() {
+    String json = "[1, 2 3, 4, 5]";
+    try {
+      gson.fromJson(json, int[].class);
+      fail("Gson should not deserialize array elements with missing ,");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testEmptyArraySerialization() {
+    int[] target = {};
+    assertThat(gson.toJson(target)).isEqualTo("[]");
+  }
+
+  @Test
+  public void testEmptyArrayDeserialization() {
+    int[] actualObject = gson.fromJson("[]", int[].class);
+    assertThat(actualObject).hasLength(0);
+
+    Integer[] actualObject2 = gson.fromJson("[]", Integer[].class);
+    assertThat(actualObject2).hasLength(0);
+
+    actualObject = gson.fromJson("[ ]", int[].class);
+    assertThat(actualObject).hasLength(0);
+  }
+
+  @Test
+  public void testNullsInArraySerialization() {
+    String[] array = {"foo", null, "bar"};
+    String expected = "[\"foo\",null,\"bar\"]";
+    String json = gson.toJson(array);
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNullsInArrayDeserialization() {
+    String json = "[\"foo\",null,\"bar\"]";
+    String[] expected = {"foo", null, "bar"};
+    String[] target = gson.fromJson(json, expected.getClass());
+    assertThat(target).asList().containsAnyIn(expected);
+  }
+
+  @Test
+  public void testSingleNullInArraySerialization() {
+    BagOfPrimitives[] array = new BagOfPrimitives[1];
+    array[0] = null;
+    String json = gson.toJson(array);
+    assertThat(json).isEqualTo("[null]");
+  }
+
+  @Test
+  public void testSingleNullInArrayDeserialization() {
+    BagOfPrimitives[] array = gson.fromJson("[null]", BagOfPrimitives[].class);
+    assertThat(array).asList().containsExactly((Object) null);
+  }
+
+  @Test
+  public void testNullsInArrayWithSerializeNullPropertySetSerialization() {
+    gson = new GsonBuilder().serializeNulls().create();
+    String[] array = {"foo", null, "bar"};
+    String expected = "[\"foo\",null,\"bar\"]";
+    String json = gson.toJson(array);
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testArrayOfStringsSerialization() {
+    String[] target = {"Hello", "World"};
+    assertThat(gson.toJson(target)).isEqualTo("[\"Hello\",\"World\"]");
+  }
+
+  @Test
+  public void testArrayOfStringsDeserialization() {
+    String json = "[\"Hello\",\"World\"]";
+    String[] target = gson.fromJson(json, String[].class);
+    assertThat(target).asList().containsExactly("Hello", "World");
+  }
+
+  @Test
+  public void testSingleStringArraySerialization() {
+    String[] s = {"hello"};
+    String output = gson.toJson(s);
+    assertThat(output).isEqualTo("[\"hello\"]");
+  }
+
+  @Test
+  public void testSingleStringArrayDeserialization() {
+    String json = "[\"hello\"]";
+    String[] arrayType = gson.fromJson(json, String[].class);
+    assertThat(arrayType).asList().containsExactly("hello");
+  }
+
+  @Test
+  public void testArrayOfCollectionSerialization() {
+    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;
+      sb.append('[').append(startValue).append(',').append(startValue + 1).append(']');
+      ArrayList<Integer> tmpList = new ArrayList<>();
+      tmpList.add(startValue);
+      tmpList.add(startValue + 1);
+      arrayOfCollection[i] = tmpList;
+
+      if (i < arraySize - 1) {
+        sb.append(',');
+      }
+    }
+    sb.append(']');
+
+    String json = gson.toJson(arrayOfCollection, typeToSerialize);
+    assertThat(json).isEqualTo(sb.toString());
+  }
+
+  @Test
+  public void testArrayOfCollectionDeserialization() {
+    String json = "[[1,2],[3,4]]";
+    Type type = new TypeToken<Collection<Integer>[]>() {}.getType();
+    Collection<Integer>[] target = gson.fromJson(json, type);
+
+    assertThat(target.length).isEqualTo(2);
+    assertThat(target[0].toArray(new Integer[0])).isEqualTo(new Integer[] {1, 2});
+    assertThat(target[1].toArray(new Integer[0])).isEqualTo(new Integer[] {3, 4});
+  }
+
+  @Test
+  public void testArrayOfPrimitivesAsObjectsSerialization() {
+    Object[] objs = new Object[] {1, "abc", 0.3f, 5L};
+    String json = gson.toJson(objs);
+    assertThat(json).contains("abc");
+    assertThat(json).contains("0.3");
+    assertThat(json).contains("5");
+  }
+
+  @Test
+  public void testArrayOfPrimitivesAsObjectsDeserialization() {
+    String json = "[1,'abc',0.3,1.1,5]";
+    Object[] objs = gson.fromJson(json, Object[].class);
+    assertThat(((Number) objs[0]).intValue()).isEqualTo(1);
+    assertThat(objs[1]).isEqualTo("abc");
+    assertThat(((Number) objs[2]).doubleValue()).isEqualTo(0.3);
+    assertThat(new BigDecimal(objs[3].toString())).isEqualTo(new BigDecimal("1.1"));
+    assertThat(((Number) objs[4]).shortValue()).isEqualTo(5);
+  }
+
+  @Test
+  public void testObjectArrayWithNonPrimitivesSerialization() {
+    ClassWithObjects classWithObjects = new ClassWithObjects();
+    BagOfPrimitives bagOfPrimitives = new BagOfPrimitives();
+    String classWithObjectsJson = gson.toJson(classWithObjects);
+    String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives);
+
+    Object[] objects = {classWithObjects, bagOfPrimitives};
+    String json = gson.toJson(objects);
+
+    assertThat(json).contains(classWithObjectsJson);
+    assertThat(json).contains(bagOfPrimitivesJson);
+  }
+
+  @Test
+  public void testArrayOfNullSerialization() {
+    Object[] array = {null};
+    String json = gson.toJson(array);
+    assertThat(json).isEqualTo("[null]");
+  }
+
+  @Test
+  public void testArrayOfNullDeserialization() {
+    String[] values = gson.fromJson("[null]", String[].class);
+    assertThat(values[0]).isNull();
+  }
+
+  /** Regression tests for Issue 272 */
+  @Test
+  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"}
+    };
+    String json = gson.toJson(items);
+    assertThat(json).contains("[[\"3m Co");
+    assertThat(json).contains("Manufacturing\"]]");
+  }
+
+  @Test
+  public void testMultidimensionalObjectArraysSerialization() {
+    Object[][] array = {new Object[] {1, 2}};
+    assertThat(gson.toJson(array)).isEqualTo("[[1,2]]");
+  }
+
+  @Test
+  public void testMultidimensionalPrimitiveArraysSerialization() {
+    int[][] array = {{1, 2}, {3, 4}};
+    assertThat(gson.toJson(array)).isEqualTo("[[1,2],[3,4]]");
+  }
+
+  /** Regression test for Issue 205 */
+  @Test
+  public void testMixingTypesInObjectArraySerialization() {
+    Object[] array = {1, 2, new Object[] {"one", "two", 3}};
+    assertThat(gson.toJson(array)).isEqualTo("[1,2,[\"one\",\"two\",3]]");
+  }
+
+  /** Regression tests for Issue 272 */
+  @Test
+  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);
+    assertThat(items[0][0]).isEqualTo("3m Co");
+    assertThat(items[1][5]).isEqualTo("Manufacturing");
+  }
+
+  @Test
+  public void testMultidimensionalPrimitiveArraysDeserialization() {
+    String json = "[[1,2],[3,4]]";
+    int[][] expected = {{1, 2}, {3, 4}};
+    assertThat(gson.fromJson(json, int[][].class)).isEqualTo(expected);
+  }
+
+  /** http://code.google.com/p/google-gson/issues/detail?id=342 */
+  @Test
+  public void testArrayElementsAreArrays() {
+    Object[] stringArrays = {
+      new String[] {"test1", "test2"},
+      new String[] {"test3", "test4"}
+    };
+    assertThat(new Gson().toJson(stringArrays))
+        .isEqualTo("[[\"test1\",\"test2\"],[\"test3\",\"test4\"]]");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java b/gson/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3cd110f73675c4f82545954838ec17783c85e93b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/CircularReferenceTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.ClassOverridingEquals;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests related to circular reference detection and error reporting.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class CircularReferenceTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testCircularSerialization() {
+    ContainsReferenceToSelfType a = new ContainsReferenceToSelfType();
+    ContainsReferenceToSelfType b = new ContainsReferenceToSelfType();
+    a.children.add(b);
+    b.children.add(a);
+    try {
+      gson.toJson(a);
+      fail("Circular types should not get printed!");
+    } catch (StackOverflowError expected) {
+    }
+  }
+
+  @Test
+  public void testSelfReferenceIgnoredInSerialization() {
+    ClassOverridingEquals objA = new ClassOverridingEquals();
+    objA.ref = objA;
+
+    String json = gson.toJson(objA);
+    assertThat(json).doesNotContain("ref"); // self-reference is ignored
+  }
+
+  @Test
+  public void testSelfReferenceArrayFieldSerialization() {
+    ClassWithSelfReferenceArray objA = new ClassWithSelfReferenceArray();
+    objA.children = new ClassWithSelfReferenceArray[] {objA};
+
+    try {
+      gson.toJson(objA);
+      fail("Circular reference to self can not be serialized!");
+    } catch (StackOverflowError expected) {
+    }
+  }
+
+  @Test
+  public void testSelfReferenceCustomHandlerSerialization() {
+    ClassWithSelfReference obj = new ClassWithSelfReference();
+    obj.child = obj;
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ClassWithSelfReference.class,
+                new JsonSerializer<ClassWithSelfReference>() {
+                  @Override
+                  public JsonElement serialize(
+                      ClassWithSelfReference src,
+                      Type typeOfSrc,
+                      JsonSerializationContext context) {
+                    JsonObject obj = new JsonObject();
+                    obj.addProperty("property", "value");
+                    obj.add("child", context.serialize(src.child));
+                    return obj;
+                  }
+                })
+            .create();
+    try {
+      gson.toJson(obj);
+      fail("Circular reference to self can not be serialized!");
+    } catch (StackOverflowError expected) {
+    }
+  }
+
+  @Test
+  public void testDirectedAcyclicGraphSerialization() {
+    ContainsReferenceToSelfType a = new ContainsReferenceToSelfType();
+    ContainsReferenceToSelfType b = new ContainsReferenceToSelfType();
+    ContainsReferenceToSelfType c = new ContainsReferenceToSelfType();
+    a.children.add(b);
+    a.children.add(c);
+    b.children.add(c);
+    assertThat(gson.toJson(a)).isNotNull();
+  }
+
+  @Test
+  public void testDirectedAcyclicGraphDeserialization() {
+    String json = "{\"children\":[{\"children\":[{\"children\":[]}]},{\"children\":[]}]}";
+    ContainsReferenceToSelfType target = gson.fromJson(json, ContainsReferenceToSelfType.class);
+    assertThat(target).isNotNull();
+    assertThat(target.children).hasSize(2);
+  }
+
+  private static class ContainsReferenceToSelfType {
+    Collection<ContainsReferenceToSelfType> children = new ArrayList<>();
+  }
+
+  private static class ClassWithSelfReference {
+    ClassWithSelfReference child;
+  }
+
+  private static class ClassWithSelfReferenceArray {
+    @SuppressWarnings("unused")
+    ClassWithSelfReferenceArray[] children;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/CollectionTest.java b/gson/gson/src/test/java/com/google/gson/functional/CollectionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..54bf68c2ef78ceeb879519f2d6b5e2184110e8a4
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/CollectionTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+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;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.PriorityQueue;
+import java.util.Queue;
+import java.util.Set;
+import java.util.Stack;
+import java.util.Vector;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json serialization and deserialization of collections.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class CollectionTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testTopLevelCollectionOfIntegersSerialization() {
+    Collection<Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
+    Type targetType = new TypeToken<Collection<Integer>>() {}.getType();
+    String json = gson.toJson(target, targetType);
+    assertThat(json).isEqualTo("[1,2,3,4,5,6,7,8,9]");
+  }
+
+  @Test
+  public void testTopLevelCollectionOfIntegersDeserialization() {
+    String json = "[0,1,2,3,4,5,6,7,8,9]";
+    Type collectionType = new TypeToken<Collection<Integer>>() {}.getType();
+    Collection<Integer> target = gson.fromJson(json, collectionType);
+    int[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+    assertThat(toIntArray(target)).isEqualTo(expected);
+  }
+
+  @Test
+  public void testTopLevelListOfIntegerCollectionsDeserialization() {
+    String json = "[[1,2,3],[4,5,6],[7,8,9]]";
+    Type collectionType = new TypeToken<Collection<Collection<Integer>>>() {}.getType();
+    List<Collection<Integer>> target = gson.fromJson(json, collectionType);
+    int[][] expected = new int[3][3];
+    for (int i = 0; i < 3; ++i) {
+      int start = (3 * i) + 1;
+      for (int j = 0; j < 3; ++j) {
+        expected[i][j] = start + j;
+      }
+    }
+
+    for (int i = 0; i < 3; i++) {
+      assertThat(toIntArray(target.get(i))).isEqualTo(expected[i]);
+    }
+  }
+
+  @Test
+  @SuppressWarnings("JdkObsolete")
+  public void testLinkedListSerialization() {
+    List<String> list = new LinkedList<>();
+    list.add("a1");
+    list.add("a2");
+    Type linkedListType = new TypeToken<LinkedList<String>>() {}.getType();
+    String json = gson.toJson(list, linkedListType);
+    assertThat(json).contains("a1");
+    assertThat(json).contains("a2");
+  }
+
+  @Test
+  public void testLinkedListDeserialization() {
+    String json = "['a1','a2']";
+    Type linkedListType = new TypeToken<LinkedList<String>>() {}.getType();
+    List<String> list = gson.fromJson(json, linkedListType);
+    assertThat(list.get(0)).isEqualTo("a1");
+    assertThat(list.get(1)).isEqualTo("a2");
+  }
+
+  @Test
+  @SuppressWarnings("JdkObsolete")
+  public void testQueueSerialization() {
+    Queue<String> queue = new LinkedList<>();
+    queue.add("a1");
+    queue.add("a2");
+    Type queueType = new TypeToken<Queue<String>>() {}.getType();
+    String json = gson.toJson(queue, queueType);
+    assertThat(json).contains("a1");
+    assertThat(json).contains("a2");
+  }
+
+  @Test
+  public void testQueueDeserialization() {
+    String json = "['a1','a2']";
+    Type queueType = new TypeToken<Queue<String>>() {}.getType();
+    Queue<String> queue = gson.fromJson(json, queueType);
+    assertThat(queue.element()).isEqualTo("a1");
+    queue.remove();
+    assertThat(queue.element()).isEqualTo("a2");
+  }
+
+  @Test
+  public void testPriorityQueue() {
+    Type type = new TypeToken<PriorityQueue<Integer>>() {}.getType();
+    PriorityQueue<Integer> queue = gson.fromJson("[10, 20, 22]", type);
+    assertThat(queue.size()).isEqualTo(3);
+    String json = gson.toJson(queue);
+    assertThat(queue.remove()).isEqualTo(10);
+    assertThat(queue.remove()).isEqualTo(20);
+    assertThat(queue.remove()).isEqualTo(22);
+    assertThat(json).isEqualTo("[10,20,22]");
+  }
+
+  @Test
+  public void testVector() {
+    Type type = new TypeToken<Vector<Integer>>() {}.getType();
+    Vector<Integer> target = gson.fromJson("[10, 20, 31]", type);
+    assertThat(target.size()).isEqualTo(3);
+    assertThat(target.get(0)).isEqualTo(10);
+    assertThat(target.get(1)).isEqualTo(20);
+    assertThat(target.get(2)).isEqualTo(31);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("[10,20,31]");
+  }
+
+  @Test
+  public void testStack() {
+    Type type = new TypeToken<Stack<Integer>>() {}.getType();
+    Stack<Integer> target = gson.fromJson("[11, 13, 17]", type);
+    assertThat(target.size()).isEqualTo(3);
+    String json = gson.toJson(target);
+    assertThat(target.pop()).isEqualTo(17);
+    assertThat(target.pop()).isEqualTo(13);
+    assertThat(target.pop()).isEqualTo(11);
+    assertThat(json).isEqualTo("[11,13,17]");
+  }
+
+  @Test
+  public void testNullsInListSerialization() {
+    List<String> list = new ArrayList<>();
+    list.add("foo");
+    list.add(null);
+    list.add("bar");
+    String expected = "[\"foo\",null,\"bar\"]";
+    Type typeOfList = new TypeToken<List<String>>() {}.getType();
+    String json = gson.toJson(list, typeOfList);
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNullsInListDeserialization() {
+    List<String> expected = new ArrayList<>();
+    expected.add("foo");
+    expected.add(null);
+    expected.add("bar");
+    String json = "[\"foo\",null,\"bar\"]";
+    Type expectedType = new TypeToken<List<String>>() {}.getType();
+    List<String> target = gson.fromJson(json, expectedType);
+    for (int i = 0; i < expected.size(); ++i) {
+      assertThat(target.get(i)).isEqualTo(expected.get(i));
+    }
+  }
+
+  @Test
+  public void testCollectionOfObjectSerialization() {
+    List<Object> target = new ArrayList<>();
+    target.add("Hello");
+    target.add("World");
+    assertThat(gson.toJson(target)).isEqualTo("[\"Hello\",\"World\"]");
+
+    Type type = new TypeToken<List<Object>>() {}.getType();
+    assertThat(gson.toJson(target, type)).isEqualTo("[\"Hello\",\"World\"]");
+  }
+
+  @Test
+  public void testCollectionOfObjectWithNullSerialization() {
+    List<Object> target = new ArrayList<>();
+    target.add("Hello");
+    target.add(null);
+    target.add("World");
+    assertThat(gson.toJson(target)).isEqualTo("[\"Hello\",null,\"World\"]");
+
+    Type type = new TypeToken<List<Object>>() {}.getType();
+    assertThat(gson.toJson(target, type)).isEqualTo("[\"Hello\",null,\"World\"]");
+  }
+
+  @Test
+  public void testCollectionOfStringsSerialization() {
+    List<String> target = new ArrayList<>();
+    target.add("Hello");
+    target.add("World");
+    assertThat(gson.toJson(target)).isEqualTo("[\"Hello\",\"World\"]");
+  }
+
+  @Test
+  public void testCollectionOfBagOfPrimitivesSerialization() {
+    List<BagOfPrimitives> target = new ArrayList<>();
+    BagOfPrimitives objA = new BagOfPrimitives(3L, 1, true, "blah");
+    BagOfPrimitives objB = new BagOfPrimitives(2L, 6, false, "blahB");
+    target.add(objA);
+    target.add(objB);
+
+    String result = gson.toJson(target);
+    assertThat(result).startsWith("[");
+    assertThat(result).endsWith("]");
+    for (BagOfPrimitives obj : target) {
+      assertThat(result).contains(obj.getExpectedJson());
+    }
+  }
+
+  @Test
+  public void testCollectionOfStringsDeserialization() {
+    String json = "[\"Hello\",\"World\"]";
+    Type collectionType = new TypeToken<Collection<String>>() {}.getType();
+    Collection<String> target = gson.fromJson(json, collectionType);
+
+    assertThat(target).containsExactly("Hello", "World").inOrder();
+  }
+
+  @Test
+  public void testRawCollectionOfIntegersSerialization() {
+    Collection<Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
+    assertThat(gson.toJson(target)).isEqualTo("[1,2,3,4,5,6,7,8,9]");
+  }
+
+  @Test
+  public void testObjectCollectionSerialization() {
+    BagOfPrimitives bag1 = new BagOfPrimitives();
+    Collection<?> target = Arrays.asList(bag1, bag1, "test");
+    String json = gson.toJson(target);
+    assertThat(json).contains(bag1.getExpectedJson());
+  }
+
+  @Test
+  public void testRawCollectionDeserializationNotAllowed() {
+    String json = "[0,1,2,3,4,5,6,7,8,9]";
+    Collection<?> integers = gson.fromJson(json, Collection.class);
+    // JsonReader converts numbers to double by default so we need a floating point comparison
+    assertThat(integers)
+        .containsExactly(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0)
+        .inOrder();
+
+    json = "[\"Hello\", \"World\"]";
+    Collection<?> strings = gson.fromJson(json, Collection.class);
+    assertThat(strings).containsExactly("Hello", "World").inOrder();
+  }
+
+  @Test
+  public void testRawCollectionOfBagOfPrimitivesNotAllowed() {
+    BagOfPrimitives bag = new BagOfPrimitives(10, 20, false, "stringValue");
+    String json = '[' + bag.getExpectedJson() + ',' + bag.getExpectedJson() + ']';
+    Collection<?> target = gson.fromJson(json, Collection.class);
+    assertThat(target.size()).isEqualTo(2);
+    for (Object bag1 : target) {
+      // Gson 2.0 converts raw objects into maps
+      @SuppressWarnings("unchecked")
+      Map<String, Object> map = (Map<String, Object>) bag1;
+      assertThat(map.values()).containsExactly(10.0, 20.0, false, "stringValue");
+    }
+  }
+
+  @Test
+  public void testWildcardPrimitiveCollectionSerilaization() {
+    Collection<? extends Integer> target = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
+    Type collectionType = new TypeToken<Collection<? extends Integer>>() {}.getType();
+    String json = gson.toJson(target, collectionType);
+    assertThat(json).isEqualTo("[1,2,3,4,5,6,7,8,9]");
+
+    json = gson.toJson(target);
+    assertThat(json).isEqualTo("[1,2,3,4,5,6,7,8,9]");
+  }
+
+  @Test
+  public void testWildcardPrimitiveCollectionDeserilaization() {
+    String json = "[1,2,3,4,5,6,7,8,9]";
+    Type collectionType = new TypeToken<Collection<? extends Integer>>() {}.getType();
+    Collection<? extends Integer> target = gson.fromJson(json, collectionType);
+    assertThat(target.size()).isEqualTo(9);
+    assertThat(target).contains(1);
+    assertThat(target).contains(2);
+  }
+
+  @Test
+  public void testWildcardCollectionField() {
+    Collection<BagOfPrimitives> collection = new ArrayList<>();
+    BagOfPrimitives objA = new BagOfPrimitives(3L, 1, true, "blah");
+    BagOfPrimitives objB = new BagOfPrimitives(2L, 6, false, "blahB");
+    collection.add(objA);
+    collection.add(objB);
+
+    ObjectWithWildcardCollection target = new ObjectWithWildcardCollection(collection);
+    String json = gson.toJson(target);
+    assertThat(json).contains(objA.getExpectedJson());
+    assertThat(json).contains(objB.getExpectedJson());
+
+    target = gson.fromJson(json, ObjectWithWildcardCollection.class);
+    Collection<? extends BagOfPrimitives> deserializedCollection = target.getCollection();
+    assertThat(deserializedCollection.size()).isEqualTo(2);
+    assertThat(deserializedCollection).contains(objA);
+    assertThat(deserializedCollection).contains(objB);
+  }
+
+  @Test
+  public void testFieldIsArrayList() {
+    HasArrayListField object = new HasArrayListField();
+    object.longs.add(1L);
+    object.longs.add(3L);
+    String json = gson.toJson(object, HasArrayListField.class);
+    assertThat(json).isEqualTo("{\"longs\":[1,3]}");
+    HasArrayListField copy = gson.fromJson("{\"longs\":[1,3]}", HasArrayListField.class);
+    assertThat(copy.longs).isEqualTo(Arrays.asList(1L, 3L));
+  }
+
+  @Test
+  public void testUserCollectionTypeAdapter() {
+    Type listOfString = new TypeToken<List<String>>() {}.getType();
+    Object stringListSerializer =
+        new JsonSerializer<List<String>>() {
+          @Override
+          public JsonElement serialize(
+              List<String> src, Type typeOfSrc, JsonSerializationContext context) {
+            return new JsonPrimitive(src.get(0) + ";" + src.get(1));
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(listOfString, stringListSerializer).create();
+    assertThat(gson.toJson(Arrays.asList("ab", "cd"), listOfString)).isEqualTo("\"ab;cd\"");
+  }
+
+  static class HasArrayListField {
+    ArrayList<Long> longs = new ArrayList<>();
+  }
+
+  private static int[] toIntArray(Collection<?> collection) {
+    int[] ints = new int[collection.size()];
+    int i = 0;
+    for (Iterator<?> iterator = collection.iterator(); iterator.hasNext(); ++i) {
+      Object obj = iterator.next();
+      if (obj instanceof Integer) {
+        ints[i] = (Integer) obj;
+      } else if (obj instanceof Long) {
+        ints[i] = ((Long) obj).intValue();
+      }
+    }
+    return ints;
+  }
+
+  private static class ObjectWithWildcardCollection {
+    private final Collection<? extends BagOfPrimitives> collection;
+
+    public ObjectWithWildcardCollection(Collection<? extends BagOfPrimitives> collection) {
+      this.collection = collection;
+    }
+
+    public Collection<? extends BagOfPrimitives> getCollection() {
+      return collection;
+    }
+  }
+
+  private static class Entry {
+    int value;
+
+    Entry(int value) {
+      this.value = value;
+    }
+  }
+
+  @Test
+  public void testSetSerialization() {
+    Set<Entry> set = new HashSet<>();
+    set.add(new Entry(1));
+    set.add(new Entry(2));
+    String json = gson.toJson(set);
+    assertThat(json).contains("1");
+    assertThat(json).contains("2");
+  }
+
+  @Test
+  public void testSetDeserialization() {
+    String json = "[{value:1},{value:2}]";
+    Type type = new TypeToken<Set<Entry>>() {}.getType();
+    Set<Entry> set = gson.fromJson(json, type);
+    assertThat(set.size()).isEqualTo(2);
+    for (Entry entry : set) {
+      assertThat(entry.value).isAnyOf(1, 2);
+    }
+  }
+
+  private static class BigClass {
+    private Map<String, ? extends List<SmallClass>> inBig;
+  }
+
+  private static class SmallClass {
+    private String inSmall;
+  }
+
+  @Test
+  public void testIssue1107() {
+    String json =
+        "{\n"
+            + "  \"inBig\": {\n"
+            + "    \"key\": [\n"
+            + "      { \"inSmall\": \"hello\" }\n"
+            + "    ]\n"
+            + "  }\n"
+            + "}";
+    BigClass bigClass = new Gson().fromJson(json, BigClass.class);
+    SmallClass small = bigClass.inBig.get("key").get(0);
+    assertThat(small).isNotNull();
+    assertThat(small.inSmall).isEqualTo("hello");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java b/gson/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c5d5e5f4513e3475554338fbeedea4540f24125
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ConcurrencyTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for ensuring Gson thread-safety.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ConcurrencyTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  /**
+   * Source-code based on
+   * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+   */
+  @Test
+  public void testSingleThreadSerialization() {
+    MyObject myObj = new MyObject();
+    for (int i = 0; i < 10; i++) {
+      String unused = gson.toJson(myObj);
+    }
+  }
+
+  /**
+   * Source-code based on
+   * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+   */
+  @Test
+  public void testSingleThreadDeserialization() {
+    for (int i = 0; i < 10; i++) {
+      MyObject unused = gson.fromJson("{'a':'hello','b':'world','i':1}", MyObject.class);
+    }
+  }
+
+  /**
+   * Source-code based on
+   * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+   */
+  @Test
+  public void testMultiThreadSerialization() throws InterruptedException {
+    final CountDownLatch startLatch = new CountDownLatch(1);
+    final CountDownLatch finishedLatch = new CountDownLatch(10);
+    final AtomicBoolean failed = new AtomicBoolean(false);
+    ExecutorService executor = Executors.newFixedThreadPool(10);
+    for (int taskCount = 0; taskCount < 10; taskCount++) {
+      executor.execute(
+          new Runnable() {
+            @Override
+            public void run() {
+              MyObject myObj = new MyObject();
+              try {
+                startLatch.await();
+                for (int i = 0; i < 10; i++) {
+                  String unused = gson.toJson(myObj);
+                }
+              } catch (Throwable t) {
+                failed.set(true);
+              } finally {
+                finishedLatch.countDown();
+              }
+            }
+          });
+    }
+    startLatch.countDown();
+    finishedLatch.await();
+    assertThat(failed.get()).isFalse();
+  }
+
+  /**
+   * Source-code based on
+   * http://groups.google.com/group/google-gson/browse_thread/thread/563bb51ee2495081
+   */
+  @Test
+  public void testMultiThreadDeserialization() throws InterruptedException {
+    final CountDownLatch startLatch = new CountDownLatch(1);
+    final CountDownLatch finishedLatch = new CountDownLatch(10);
+    final AtomicBoolean failed = new AtomicBoolean(false);
+    ExecutorService executor = Executors.newFixedThreadPool(10);
+    for (int taskCount = 0; taskCount < 10; taskCount++) {
+      executor.execute(
+          new Runnable() {
+            @Override
+            public void run() {
+              try {
+                startLatch.await();
+                for (int i = 0; i < 10; i++) {
+                  MyObject unused =
+                      gson.fromJson("{'a':'hello','b':'world','i':1}", MyObject.class);
+                }
+              } catch (Throwable t) {
+                failed.set(true);
+              } finally {
+                finishedLatch.countDown();
+              }
+            }
+          });
+    }
+    startLatch.countDown();
+    finishedLatch.await();
+    assertThat(failed.get()).isFalse();
+  }
+
+  @SuppressWarnings("unused")
+  private static class MyObject {
+    String a;
+    String b;
+    int i;
+
+    MyObject() {
+      this("hello", "world", 42);
+    }
+
+    public MyObject(String a, String b, int i) {
+      this.a = a;
+      this.b = b;
+      this.i = i;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java b/gson/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..732f1a3cb7cbb69cbc5c6c7b454ddaa54fc91247
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/CustomDeserializerTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+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.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.common.TestTypes.Base;
+import com.google.gson.common.TestTypes.ClassWithBaseField;
+import java.lang.reflect.Type;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional Test exercising custom deserialization only. When test applies to both serialization
+ * and deserialization then add it to CustomTypeAdapterTest.
+ *
+ * @author Joel Leitch
+ */
+public class CustomDeserializerTest {
+  private static final String DEFAULT_VALUE = "test123";
+  private static final String SUFFIX = "blah";
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(DataHolder.class, new DataHolderDeserializer())
+            .create();
+  }
+
+  @Test
+  public void testDefaultConstructorNotCalledOnObject() {
+    DataHolder data = new DataHolder(DEFAULT_VALUE);
+    String json = gson.toJson(data);
+
+    DataHolder actual = gson.fromJson(json, DataHolder.class);
+    assertThat(actual.getData()).isEqualTo(DEFAULT_VALUE + SUFFIX);
+  }
+
+  @Test
+  public void testDefaultConstructorNotCalledOnField() {
+    DataHolderWrapper dataWrapper = new DataHolderWrapper(new DataHolder(DEFAULT_VALUE));
+    String json = gson.toJson(dataWrapper);
+
+    DataHolderWrapper actual = gson.fromJson(json, DataHolderWrapper.class);
+    assertThat(actual.getWrappedData().getData()).isEqualTo(DEFAULT_VALUE + SUFFIX);
+  }
+
+  private static class DataHolder {
+    private final String data;
+
+    // For use by Gson
+    @SuppressWarnings("unused")
+    private DataHolder() {
+      throw new IllegalStateException();
+    }
+
+    public DataHolder(String data) {
+      this.data = data;
+    }
+
+    public String getData() {
+      return data;
+    }
+  }
+
+  private static class DataHolderWrapper {
+    private final DataHolder wrappedData;
+
+    // For use by Gson
+    @SuppressWarnings("unused")
+    private DataHolderWrapper() {
+      this(new DataHolder(DEFAULT_VALUE));
+    }
+
+    public DataHolderWrapper(DataHolder data) {
+      this.wrappedData = data;
+    }
+
+    public DataHolder getWrappedData() {
+      return wrappedData;
+    }
+  }
+
+  private static class DataHolderDeserializer implements JsonDeserializer<DataHolder> {
+    @Override
+    public DataHolder deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      JsonObject jsonObj = json.getAsJsonObject();
+      String dataString = jsonObj.get("data").getAsString();
+      return new DataHolder(dataString + SUFFIX);
+    }
+  }
+
+  @Test
+  public void testJsonTypeFieldBasedDeserialization() {
+    String json = "{field1:'abc',field2:'def',__type__:'SUB_TYPE1'}";
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                MyBase.class,
+                new JsonDeserializer<MyBase>() {
+                  @Override
+                  public MyBase deserialize(
+                      JsonElement json, Type pojoType, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    String type = json.getAsJsonObject().get(MyBase.TYPE_ACCESS).getAsString();
+                    return context.deserialize(json, SubTypes.valueOf(type).getSubclass());
+                  }
+                })
+            .create();
+    SubType1 target = (SubType1) gson.fromJson(json, MyBase.class);
+    assertThat(target.field1).isEqualTo("abc");
+  }
+
+  private static class MyBase {
+    static final String TYPE_ACCESS = "__type__";
+  }
+
+  @SuppressWarnings("ImmutableEnumChecker")
+  private enum SubTypes {
+    SUB_TYPE1(SubType1.class),
+    SUB_TYPE2(SubType2.class);
+    private final Type subClass;
+
+    private SubTypes(Type subClass) {
+      this.subClass = subClass;
+    }
+
+    public Type getSubclass() {
+      return subClass;
+    }
+  }
+
+  private static class SubType1 extends MyBase {
+    String field1;
+  }
+
+  private static class SubType2 extends MyBase {
+    @SuppressWarnings("unused")
+    String field2;
+  }
+
+  @Test
+  public void testCustomDeserializerReturnsNullForTopLevelObject() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonDeserializer<Base>() {
+                  @Override
+                  public Base deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    return null;
+                  }
+                })
+            .create();
+    String json = "{baseName:'Base',subName:'SubRevised'}";
+    Base target = gson.fromJson(json, Base.class);
+    assertThat(target).isNull();
+  }
+
+  @Test
+  public void testCustomDeserializerReturnsNull() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonDeserializer<Base>() {
+                  @Override
+                  public Base deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    return null;
+                  }
+                })
+            .create();
+    String json = "{base:{baseName:'Base',subName:'SubRevised'}}";
+    ClassWithBaseField target = gson.fromJson(json, ClassWithBaseField.class);
+    assertThat(target.base).isNull();
+  }
+
+  @Test
+  public void testCustomDeserializerReturnsNullForArrayElements() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonDeserializer<Base>() {
+                  @Override
+                  public Base deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    return null;
+                  }
+                })
+            .create();
+    String json = "[{baseName:'Base'},{baseName:'Base'}]";
+    Base[] target = gson.fromJson(json, Base[].class);
+    assertThat(target[0]).isNull();
+    assertThat(target[1]).isNull();
+  }
+
+  @Test
+  public void testCustomDeserializerReturnsNullForArrayElementsForArrayField() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonDeserializer<Base>() {
+                  @Override
+                  public Base deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    return null;
+                  }
+                })
+            .create();
+    String json = "{bases:[{baseName:'Base'},{baseName:'Base'}]}";
+    ClassWithBaseArray target = gson.fromJson(json, ClassWithBaseArray.class);
+    assertThat(target.bases[0]).isNull();
+    assertThat(target.bases[1]).isNull();
+  }
+
+  private static final class ClassWithBaseArray {
+    Base[] bases;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java b/gson/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b8a9b8865f6320ab8276ca4119964037201f4f8
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/CustomSerializerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.Base;
+import com.google.gson.common.TestTypes.BaseSerializer;
+import com.google.gson.common.TestTypes.ClassWithBaseArrayField;
+import com.google.gson.common.TestTypes.ClassWithBaseField;
+import com.google.gson.common.TestTypes.Sub;
+import com.google.gson.common.TestTypes.SubSerializer;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+/**
+ * Functional Test exercising custom serialization only. When test applies to both serialization and
+ * deserialization then add it to CustomTypeAdapterTest.
+ *
+ * @author Inderjeet Singh
+ */
+public class CustomSerializerTest {
+
+  @Test
+  public void testBaseClassSerializerInvokedForBaseClassFields() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Base.class, new BaseSerializer())
+            .registerTypeAdapter(Sub.class, new SubSerializer())
+            .create();
+    ClassWithBaseField target = new ClassWithBaseField(new Base());
+    JsonObject json = (JsonObject) gson.toJsonTree(target);
+    JsonObject base = json.get("base").getAsJsonObject();
+    assertThat(base.get(Base.SERIALIZER_KEY).getAsString()).isEqualTo(BaseSerializer.NAME);
+  }
+
+  @Test
+  public void testSubClassSerializerInvokedForBaseClassFieldsHoldingSubClassInstances() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Base.class, new BaseSerializer())
+            .registerTypeAdapter(Sub.class, new SubSerializer())
+            .create();
+    ClassWithBaseField target = new ClassWithBaseField(new Sub());
+    JsonObject json = (JsonObject) gson.toJsonTree(target);
+    JsonObject base = json.get("base").getAsJsonObject();
+    assertThat(base.get(Base.SERIALIZER_KEY).getAsString()).isEqualTo(SubSerializer.NAME);
+  }
+
+  @Test
+  public void testSubClassSerializerInvokedForBaseClassFieldsHoldingArrayOfSubClassInstances() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Base.class, new BaseSerializer())
+            .registerTypeAdapter(Sub.class, new SubSerializer())
+            .create();
+    ClassWithBaseArrayField target = new ClassWithBaseArrayField(new Base[] {new Sub(), new Sub()});
+    JsonObject json = (JsonObject) gson.toJsonTree(target);
+    JsonArray array = json.get("base").getAsJsonArray();
+    for (JsonElement element : array) {
+      JsonElement serializerKey = element.getAsJsonObject().get(Base.SERIALIZER_KEY);
+      assertThat(serializerKey.getAsString()).isEqualTo(SubSerializer.NAME);
+    }
+  }
+
+  @Test
+  public void testBaseClassSerializerInvokedForBaseClassFieldsHoldingSubClassInstances() {
+    Gson gson = new GsonBuilder().registerTypeAdapter(Base.class, new BaseSerializer()).create();
+    ClassWithBaseField target = new ClassWithBaseField(new Sub());
+    JsonObject json = (JsonObject) gson.toJsonTree(target);
+    JsonObject base = json.get("base").getAsJsonObject();
+    assertThat(base.get(Base.SERIALIZER_KEY).getAsString()).isEqualTo(BaseSerializer.NAME);
+  }
+
+  @Test
+  public void testSerializerReturnsNull() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonSerializer<Base>() {
+                  @Override
+                  public JsonElement serialize(
+                      Base src, Type typeOfSrc, JsonSerializationContext context) {
+                    return null;
+                  }
+                })
+            .create();
+    JsonElement json = gson.toJsonTree(new Base());
+    assertThat(json.isJsonNull()).isTrue();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java b/gson/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7499aa8a42cdc2194945e0829c740d820cef29a6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/CustomTypeAdaptersTest.java
@@ -0,0 +1,560 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+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.common.TestTypes.ClassWithCustomTypeConverter;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Functional tests for the support of custom serializer and deserializers.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class CustomTypeAdaptersTest {
+  private GsonBuilder builder;
+
+  @Before
+  public void setUp() throws Exception {
+    builder = new GsonBuilder();
+  }
+
+  @Test
+  public void testCustomSerializers() {
+    Gson gson =
+        builder
+            .registerTypeAdapter(
+                ClassWithCustomTypeConverter.class,
+                new JsonSerializer<ClassWithCustomTypeConverter>() {
+                  @Override
+                  public JsonElement serialize(
+                      ClassWithCustomTypeConverter src,
+                      Type typeOfSrc,
+                      JsonSerializationContext context) {
+                    JsonObject json = new JsonObject();
+                    json.addProperty("bag", 5);
+                    json.addProperty("value", 25);
+                    return json;
+                  }
+                })
+            .create();
+    ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter();
+    assertThat(gson.toJson(target)).isEqualTo("{\"bag\":5,\"value\":25}");
+  }
+
+  @Test
+  public void testCustomDeserializers() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ClassWithCustomTypeConverter.class,
+                new JsonDeserializer<ClassWithCustomTypeConverter>() {
+                  @Override
+                  public ClassWithCustomTypeConverter deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+                    JsonObject jsonObject = json.getAsJsonObject();
+                    int value = jsonObject.get("bag").getAsInt();
+                    return new ClassWithCustomTypeConverter(
+                        new BagOfPrimitives(value, value, false, ""), value);
+                  }
+                })
+            .create();
+    String json = "{\"bag\":5,\"value\":25}";
+    ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class);
+    assertThat(target.getBag().getIntValue()).isEqualTo(5);
+  }
+
+  @Test
+  @Ignore
+  public void disable_testCustomSerializersOfSelf() {
+    Gson gson = createGsonObjectWithFooTypeAdapter();
+    Gson basicGson = new Gson();
+    Foo newFooObject = new Foo(1, 2L);
+    String jsonFromCustomSerializer = gson.toJson(newFooObject);
+    String jsonFromGson = basicGson.toJson(newFooObject);
+
+    assertThat(jsonFromCustomSerializer).isEqualTo(jsonFromGson);
+  }
+
+  @Test
+  @Ignore
+  public void disable_testCustomDeserializersOfSelf() {
+    Gson gson = createGsonObjectWithFooTypeAdapter();
+    Gson basicGson = new Gson();
+    Foo expectedFoo = new Foo(1, 2L);
+    String json = basicGson.toJson(expectedFoo);
+    Foo newFooObject = gson.fromJson(json, Foo.class);
+
+    assertThat(newFooObject.key).isEqualTo(expectedFoo.key);
+    assertThat(newFooObject.value).isEqualTo(expectedFoo.value);
+  }
+
+  @Test
+  public void testCustomNestedSerializers() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                BagOfPrimitives.class,
+                new JsonSerializer<BagOfPrimitives>() {
+                  @Override
+                  public JsonElement serialize(
+                      BagOfPrimitives src, Type typeOfSrc, JsonSerializationContext context) {
+                    return new JsonPrimitive(6);
+                  }
+                })
+            .create();
+    ClassWithCustomTypeConverter target = new ClassWithCustomTypeConverter();
+    assertThat(gson.toJson(target)).isEqualTo("{\"bag\":6,\"value\":10}");
+  }
+
+  @Test
+  public void testCustomNestedDeserializers() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                BagOfPrimitives.class,
+                new JsonDeserializer<BagOfPrimitives>() {
+                  @Override
+                  public BagOfPrimitives deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    int value = json.getAsInt();
+                    return new BagOfPrimitives(value, value, false, "");
+                  }
+                })
+            .create();
+    String json = "{\"bag\":7,\"value\":25}";
+    ClassWithCustomTypeConverter target = gson.fromJson(json, ClassWithCustomTypeConverter.class);
+    assertThat(target.getBag().getIntValue()).isEqualTo(7);
+  }
+
+  @Test
+  public void testCustomTypeAdapterDoesNotAppliesToSubClasses() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonSerializer<Base>() {
+                  @Override
+                  public JsonElement serialize(
+                      Base src, Type typeOfSrc, JsonSerializationContext context) {
+                    JsonObject json = new JsonObject();
+                    json.addProperty("value", src.baseValue);
+                    return json;
+                  }
+                })
+            .create();
+    Base b = new Base();
+    String json = gson.toJson(b);
+    assertThat(json).contains("value");
+    b = new Derived();
+    json = gson.toJson(b);
+    assertThat(json).contains("derivedValue");
+  }
+
+  @Test
+  public void testCustomTypeAdapterAppliesToSubClassesSerializedAsBaseClass() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new JsonSerializer<Base>() {
+                  @Override
+                  public JsonElement serialize(
+                      Base src, Type typeOfSrc, JsonSerializationContext context) {
+                    JsonObject json = new JsonObject();
+                    json.addProperty("value", src.baseValue);
+                    return json;
+                  }
+                })
+            .create();
+    Base b = new Base();
+    String json = gson.toJson(b);
+    assertThat(json).contains("value");
+    b = new Derived();
+    json = gson.toJson(b, Base.class);
+    assertThat(json).contains("value");
+    assertThat(json).doesNotContain("derivedValue");
+  }
+
+  private static class Base {
+    int baseValue = 2;
+  }
+
+  private static class Derived extends Base {
+    @SuppressWarnings("unused")
+    int derivedValue = 3;
+  }
+
+  private Gson createGsonObjectWithFooTypeAdapter() {
+    return new GsonBuilder().registerTypeAdapter(Foo.class, new FooTypeAdapter()).create();
+  }
+
+  public static class Foo {
+    private final int key;
+    private final long value;
+
+    public Foo() {
+      this(0, 0L);
+    }
+
+    public Foo(int key, long value) {
+      this.key = key;
+      this.value = value;
+    }
+  }
+
+  public static final class FooTypeAdapter implements JsonSerializer<Foo>, JsonDeserializer<Foo> {
+    @Override
+    public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return context.deserialize(json, typeOfT);
+    }
+
+    @Override
+    public JsonElement serialize(Foo src, Type typeOfSrc, JsonSerializationContext context) {
+      return context.serialize(src, typeOfSrc);
+    }
+  }
+
+  @Test
+  public void testCustomSerializerInvokedForPrimitives() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                boolean.class,
+                new JsonSerializer<Boolean>() {
+                  @Override
+                  public JsonElement serialize(Boolean s, Type t, JsonSerializationContext c) {
+                    return new JsonPrimitive(s ? 1 : 0);
+                  }
+                })
+            .create();
+    assertThat(gson.toJson(true, boolean.class)).isEqualTo("1");
+    assertThat(gson.toJson(true, Boolean.class)).isEqualTo("true");
+  }
+
+  @Test
+  public void testCustomDeserializerInvokedForPrimitives() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                boolean.class,
+                new JsonDeserializer<Boolean>() {
+                  @Override
+                  public Boolean deserialize(
+                      JsonElement json, Type t, JsonDeserializationContext context) {
+                    return json.getAsInt() != 0;
+                  }
+                })
+            .create();
+    assertThat(gson.fromJson("1", boolean.class)).isEqualTo(Boolean.TRUE);
+    assertThat(gson.fromJson("true", Boolean.class)).isEqualTo(Boolean.TRUE);
+  }
+
+  @Test
+  public void testCustomByteArraySerializer() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                byte[].class,
+                new JsonSerializer<byte[]>() {
+                  @Override
+                  public JsonElement serialize(
+                      byte[] src, Type typeOfSrc, JsonSerializationContext context) {
+                    StringBuilder sb = new StringBuilder(src.length);
+                    for (byte b : src) {
+                      sb.append(b);
+                    }
+                    return new JsonPrimitive(sb.toString());
+                  }
+                })
+            .create();
+    byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+    String json = gson.toJson(data);
+    assertThat(json).isEqualTo("\"0123456789\"");
+  }
+
+  @Test
+  public void testCustomByteArrayDeserializerAndInstanceCreator() {
+    GsonBuilder gsonBuilder =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                byte[].class,
+                new JsonDeserializer<byte[]>() {
+                  @Override
+                  public byte[] deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    String str = json.getAsString();
+                    byte[] data = new byte[str.length()];
+                    for (int i = 0; i < data.length; ++i) {
+                      data[i] = Byte.parseByte("" + str.charAt(i));
+                    }
+                    return data;
+                  }
+                });
+    Gson gson = gsonBuilder.create();
+    String json = "'0123456789'";
+    byte[] actual = gson.fromJson(json, byte[].class);
+    byte[] expected = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+    for (int i = 0; i < actual.length; ++i) {
+      assertThat(actual[i]).isEqualTo(expected[i]);
+    }
+  }
+
+  private static final class StringHolder {
+    String part1;
+    String part2;
+
+    public StringHolder(String string) {
+      List<String> parts = Splitter.on(':').splitToList(string);
+      part1 = parts.get(0);
+      part2 = parts.get(1);
+    }
+
+    public StringHolder(String part1, String part2) {
+      this.part1 = part1;
+      this.part2 = part2;
+    }
+  }
+
+  private static class StringHolderTypeAdapter
+      implements JsonSerializer<StringHolder>,
+          JsonDeserializer<StringHolder>,
+          InstanceCreator<StringHolder> {
+
+    @Override
+    public StringHolder createInstance(Type type) {
+      // Fill up with objects that will be thrown away
+      return new StringHolder("unknown:thing");
+    }
+
+    @Override
+    public StringHolder deserialize(
+        JsonElement src, Type type, JsonDeserializationContext context) {
+      return new StringHolder(src.getAsString());
+    }
+
+    @Override
+    public JsonElement serialize(
+        StringHolder src, Type typeOfSrc, JsonSerializationContext context) {
+      String contents = src.part1 + ':' + src.part2;
+      return new JsonPrimitive(contents);
+    }
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForCollectionElementSerializationWithType() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    Type setType = new TypeToken<Set<StringHolder>>() {}.getType();
+    StringHolder holder = new StringHolder("Jacob", "Tomaw");
+    Set<StringHolder> setOfHolders = new HashSet<>();
+    setOfHolders.add(holder);
+    String json = gson.toJson(setOfHolders, setType);
+    assertThat(json).contains("Jacob:Tomaw");
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForCollectionElementSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    StringHolder holder = new StringHolder("Jacob", "Tomaw");
+    Set<StringHolder> setOfHolders = new HashSet<>();
+    setOfHolders.add(holder);
+    String json = gson.toJson(setOfHolders);
+    assertThat(json).contains("Jacob:Tomaw");
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForCollectionElementDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    Type setType = new TypeToken<Set<StringHolder>>() {}.getType();
+    Set<StringHolder> setOfHolders = gson.fromJson("['Jacob:Tomaw']", setType);
+    assertThat(setOfHolders.size()).isEqualTo(1);
+    StringHolder foo = setOfHolders.iterator().next();
+    assertThat(foo.part1).isEqualTo("Jacob");
+    assertThat(foo.part2).isEqualTo("Tomaw");
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForMapElementSerializationWithType() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    Type mapType = new TypeToken<Map<String, StringHolder>>() {}.getType();
+    StringHolder holder = new StringHolder("Jacob", "Tomaw");
+    Map<String, StringHolder> mapOfHolders = new HashMap<>();
+    mapOfHolders.put("foo", holder);
+    String json = gson.toJson(mapOfHolders, mapType);
+    assertThat(json).contains("\"foo\":\"Jacob:Tomaw\"");
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForMapElementSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    StringHolder holder = new StringHolder("Jacob", "Tomaw");
+    Map<String, StringHolder> mapOfHolders = new HashMap<>();
+    mapOfHolders.put("foo", holder);
+    String json = gson.toJson(mapOfHolders);
+    assertThat(json).contains("\"foo\":\"Jacob:Tomaw\"");
+  }
+
+  // Test created from Issue 70
+  @Test
+  public void testCustomAdapterInvokedForMapElementDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(StringHolder.class, new StringHolderTypeAdapter())
+            .create();
+    Type mapType = new TypeToken<Map<String, StringHolder>>() {}.getType();
+    Map<String, StringHolder> mapOfFoo = gson.fromJson("{'foo':'Jacob:Tomaw'}", mapType);
+    assertThat(mapOfFoo.size()).isEqualTo(1);
+    StringHolder foo = mapOfFoo.get("foo");
+    assertThat(foo.part1).isEqualTo("Jacob");
+    assertThat(foo.part2).isEqualTo("Tomaw");
+  }
+
+  @Test
+  public void testEnsureCustomSerializerNotInvokedForNullValues() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(DataHolder.class, new DataHolderSerializer())
+            .create();
+    DataHolderWrapper target = new DataHolderWrapper(new DataHolder("abc"));
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("{\"wrappedData\":{\"myData\":\"abc\"}}");
+  }
+
+  @Test
+  public void testEnsureCustomDeserializerNotInvokedForNullValues() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(DataHolder.class, new DataHolderDeserializer())
+            .create();
+    String json = "{wrappedData:null}";
+    DataHolderWrapper actual = gson.fromJson(json, DataHolderWrapper.class);
+    assertThat(actual.wrappedData).isNull();
+  }
+
+  // Test created from Issue 352
+  @Test
+  @SuppressWarnings({"JavaUtilDate", "UndefinedEquals"})
+  public void testRegisterHierarchyAdapterForDate() {
+    Gson gson =
+        new GsonBuilder().registerTypeHierarchyAdapter(Date.class, new DateTypeAdapter()).create();
+    assertThat(gson.toJson(new Date(0))).isEqualTo("0");
+    assertThat(gson.toJson(new java.sql.Date(0))).isEqualTo("0");
+    assertThat(gson.fromJson("0", Date.class)).isEqualTo(new Date(0));
+    assertThat(gson.fromJson("0", java.sql.Date.class)).isEqualTo(new java.sql.Date(0));
+  }
+
+  private static class DataHolder {
+    final String data;
+
+    public DataHolder(String data) {
+      this.data = data;
+    }
+  }
+
+  private static class DataHolderWrapper {
+    final DataHolder wrappedData;
+
+    public DataHolderWrapper(DataHolder data) {
+      this.wrappedData = data;
+    }
+  }
+
+  private static class DataHolderSerializer implements JsonSerializer<DataHolder> {
+    @Override
+    public JsonElement serialize(DataHolder src, Type typeOfSrc, JsonSerializationContext context) {
+      JsonObject obj = new JsonObject();
+      obj.addProperty("myData", src.data);
+      return obj;
+    }
+  }
+
+  private static class DataHolderDeserializer implements JsonDeserializer<DataHolder> {
+    @Override
+    public DataHolder deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      JsonObject jsonObj = json.getAsJsonObject();
+      JsonElement jsonElement = jsonObj.get("data");
+      if (jsonElement == null || jsonElement.isJsonNull()) {
+        return new DataHolder(null);
+      }
+      return new DataHolder(jsonElement.getAsString());
+    }
+  }
+
+  @SuppressWarnings("JavaUtilDate")
+  private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
+    @Override
+    public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+      return typeOfT == Date.class
+          ? new Date(json.getAsLong())
+          : new java.sql.Date(json.getAsLong());
+    }
+
+    @Override
+    public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive(src.getTime());
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..af3ffe11ca457935579de487530c42ae1a79fd4d
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java
@@ -0,0 +1,797 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+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.Constructor;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URL;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeSet;
+import java.util.UUID;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional test for Json serialization and deserialization for common classes for which default
+ * support is provided in Gson. The tests for Map types are available in {@link MapTest}.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+@SuppressWarnings("JavaUtilDate")
+public class DefaultTypeAdaptersTest {
+  private Gson gson;
+  private TimeZone oldTimeZone;
+  private Locale oldLocale;
+
+  @Before
+  public void setUp() throws Exception {
+    this.oldTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+    this.oldLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    gson = new Gson();
+  }
+
+  @After
+  public void tearDown() {
+    TimeZone.setDefault(oldTimeZone);
+    Locale.setDefault(oldLocale);
+  }
+
+  @Test
+  public void testClassSerialization() {
+    var exception =
+        assertThrows(UnsupportedOperationException.class, () -> gson.toJson(String.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Attempted to serialize java.lang.Class: java.lang.String. Forgot to register a type"
+                + " adapter?\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#java-lang-class-unsupported");
+
+    // Override with a custom type adapter for class.
+    gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create();
+    assertThat(gson.toJson(String.class)).isEqualTo("\"java.lang.String\"");
+  }
+
+  @Test
+  public void testClassDeserialization() {
+    var exception =
+        assertThrows(
+            UnsupportedOperationException.class, () -> gson.fromJson("String.class", Class.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#java-lang-class-unsupported");
+
+    // Override with a custom type adapter for class.
+    gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create();
+    assertThat(gson.fromJson("java.lang.String", Class.class)).isAssignableTo(String.class);
+  }
+
+  @Test
+  public void testUrlSerialization() throws Exception {
+    String urlValue = "http://google.com/";
+    URL url = new URL(urlValue);
+    assertThat(gson.toJson(url)).isEqualTo("\"http://google.com/\"");
+  }
+
+  @Test
+  public void testUrlDeserialization() {
+    String urlValue = "http://google.com/";
+    String json = "'http:\\/\\/google.com\\/'";
+    URL target1 = gson.fromJson(json, URL.class);
+    assertThat(target1.toExternalForm()).isEqualTo(urlValue);
+
+    URL target2 = gson.fromJson('"' + urlValue + '"', URL.class);
+    assertThat(target2.toExternalForm()).isEqualTo(urlValue);
+  }
+
+  @Test
+  public void testUrlNullSerialization() {
+    ClassWithUrlField target = new ClassWithUrlField();
+    assertThat(gson.toJson(target)).isEqualTo("{}");
+  }
+
+  @Test
+  public void testUrlNullDeserialization() {
+    String json = "{}";
+    ClassWithUrlField target = gson.fromJson(json, ClassWithUrlField.class);
+    assertThat(target.url).isNull();
+  }
+
+  private static class ClassWithUrlField {
+    URL url;
+  }
+
+  @Test
+  public void testUriSerialization() throws Exception {
+    String uriValue = "http://google.com/";
+    URI uri = new URI(uriValue);
+    assertThat(gson.toJson(uri)).isEqualTo("\"http://google.com/\"");
+  }
+
+  @Test
+  public void testUriDeserialization() {
+    String uriValue = "http://google.com/";
+    String json = '"' + uriValue + '"';
+    URI target = gson.fromJson(json, URI.class);
+    assertThat(target.toASCIIString()).isEqualTo(uriValue);
+  }
+
+  @Test
+  public void testNullSerialization() {
+    testNullSerializationAndDeserialization(Boolean.class);
+    testNullSerializationAndDeserialization(Byte.class);
+    testNullSerializationAndDeserialization(Short.class);
+    testNullSerializationAndDeserialization(Integer.class);
+    testNullSerializationAndDeserialization(Long.class);
+    testNullSerializationAndDeserialization(Double.class);
+    testNullSerializationAndDeserialization(Float.class);
+    testNullSerializationAndDeserialization(Number.class);
+    testNullSerializationAndDeserialization(Character.class);
+    testNullSerializationAndDeserialization(String.class);
+    testNullSerializationAndDeserialization(StringBuilder.class);
+    testNullSerializationAndDeserialization(StringBuffer.class);
+    testNullSerializationAndDeserialization(BigDecimal.class);
+    testNullSerializationAndDeserialization(BigInteger.class);
+    testNullSerializationAndDeserialization(TreeSet.class);
+    testNullSerializationAndDeserialization(ArrayList.class);
+    testNullSerializationAndDeserialization(HashSet.class);
+    testNullSerializationAndDeserialization(Properties.class);
+    testNullSerializationAndDeserialization(URL.class);
+    testNullSerializationAndDeserialization(URI.class);
+    testNullSerializationAndDeserialization(UUID.class);
+    testNullSerializationAndDeserialization(Locale.class);
+    testNullSerializationAndDeserialization(InetAddress.class);
+    testNullSerializationAndDeserialization(BitSet.class);
+    testNullSerializationAndDeserialization(Date.class);
+    testNullSerializationAndDeserialization(GregorianCalendar.class);
+    testNullSerializationAndDeserialization(Calendar.class);
+    testNullSerializationAndDeserialization(Class.class);
+  }
+
+  private void testNullSerializationAndDeserialization(Class<?> c) {
+    testNullSerializationAndDeserialization(gson, c);
+  }
+
+  public static void testNullSerializationAndDeserialization(Gson gson, Class<?> c) {
+    assertThat(gson.toJson(null, c)).isEqualTo("null");
+    assertThat(gson.fromJson("null", c)).isEqualTo(null);
+  }
+
+  @Test
+  public void testUuidSerialization() {
+    String uuidValue = "c237bec1-19ef-4858-a98e-521cf0aad4c0";
+    UUID uuid = UUID.fromString(uuidValue);
+    assertThat(gson.toJson(uuid)).isEqualTo('"' + uuidValue + '"');
+  }
+
+  @Test
+  public void testUuidDeserialization() {
+    String uuidValue = "c237bec1-19ef-4858-a98e-521cf0aad4c0";
+    String json = '"' + uuidValue + '"';
+    UUID target = gson.fromJson(json, UUID.class);
+    assertThat(target.toString()).isEqualTo(uuidValue);
+  }
+
+  @Test
+  public void testLocaleSerializationWithLanguage() {
+    Locale target = new Locale("en");
+    assertThat(gson.toJson(target)).isEqualTo("\"en\"");
+  }
+
+  @Test
+  public void testLocaleDeserializationWithLanguage() {
+    String json = "\"en\"";
+    Locale locale = gson.fromJson(json, Locale.class);
+    assertThat(locale.getLanguage()).isEqualTo("en");
+  }
+
+  @Test
+  public void testLocaleSerializationWithLanguageCountry() {
+    Locale target = Locale.CANADA_FRENCH;
+    assertThat(gson.toJson(target)).isEqualTo("\"fr_CA\"");
+  }
+
+  @Test
+  public void testLocaleDeserializationWithLanguageCountry() {
+    String json = "\"fr_CA\"";
+    Locale locale = gson.fromJson(json, Locale.class);
+    assertThat(locale).isEqualTo(Locale.CANADA_FRENCH);
+  }
+
+  @Test
+  public void testLocaleSerializationWithLanguageCountryVariant() {
+    Locale target = new Locale("de", "DE", "EURO");
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("\"de_DE_EURO\"");
+  }
+
+  @Test
+  public void testLocaleDeserializationWithLanguageCountryVariant() {
+    String json = "\"de_DE_EURO\"";
+    Locale locale = gson.fromJson(json, Locale.class);
+    assertThat(locale.getLanguage()).isEqualTo("de");
+    assertThat(locale.getCountry()).isEqualTo("DE");
+    assertThat(locale.getVariant()).isEqualTo("EURO");
+  }
+
+  @Test
+  public void testBigDecimalFieldSerialization() {
+    ClassWithBigDecimal target = new ClassWithBigDecimal("-122.01e-21");
+    String json = gson.toJson(target);
+    String actual = json.substring(json.indexOf(':') + 1, json.indexOf('}'));
+    assertThat(new BigDecimal(actual)).isEqualTo(target.value);
+  }
+
+  @Test
+  public void testBigDecimalFieldDeserialization() {
+    ClassWithBigDecimal expected = new ClassWithBigDecimal("-122.01e-21");
+    String json = expected.getExpectedJson();
+    ClassWithBigDecimal actual = gson.fromJson(json, ClassWithBigDecimal.class);
+    assertThat(actual.value).isEqualTo(expected.value);
+  }
+
+  @Test
+  public void testBadValueForBigDecimalDeserialization() {
+    // Exponent of a BigDecimal must be an integer value
+    assertThrows(
+        JsonParseException.class,
+        () -> gson.fromJson("{\"value\": 1.5e-1.0031}", ClassWithBigDecimal.class));
+  }
+
+  @Test
+  public void testBigIntegerFieldSerialization() {
+    ClassWithBigInteger target = new ClassWithBigInteger("23232323215323234234324324324324324324");
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testBigIntegerFieldDeserialization() {
+    ClassWithBigInteger expected = new ClassWithBigInteger("879697697697697697697697697697697697");
+    String json = expected.getExpectedJson();
+    ClassWithBigInteger actual = gson.fromJson(json, ClassWithBigInteger.class);
+    assertThat(actual.value).isEqualTo(expected.value);
+  }
+
+  @Test
+  public void testOverrideBigIntegerTypeAdapter() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(BigInteger.class, new NumberAsStringAdapter(BigInteger.class))
+            .create();
+    assertThat(gson.toJson(new BigInteger("123"), BigInteger.class)).isEqualTo("\"123\"");
+    assertThat(gson.fromJson("\"123\"", BigInteger.class)).isEqualTo(new BigInteger("123"));
+  }
+
+  @Test
+  public void testOverrideBigDecimalTypeAdapter() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(BigDecimal.class, new NumberAsStringAdapter(BigDecimal.class))
+            .create();
+    assertThat(gson.toJson(new BigDecimal("1.1"), BigDecimal.class)).isEqualTo("\"1.1\"");
+    assertThat(gson.fromJson("\"1.1\"", BigDecimal.class)).isEqualTo(new BigDecimal("1.1"));
+  }
+
+  @Test
+  public void testSetSerialization() {
+    Gson gson = new Gson();
+    HashSet<String> s = new HashSet<>();
+    s.add("blah");
+    String json = gson.toJson(s);
+    assertThat(json).isEqualTo("[\"blah\"]");
+
+    json = gson.toJson(s, Set.class);
+    assertThat(json).isEqualTo("[\"blah\"]");
+  }
+
+  @Test
+  public void testBitSetSerialization() {
+    Gson gson = new Gson();
+    BitSet bits = new BitSet();
+    bits.set(1);
+    bits.set(3, 6);
+    bits.set(9);
+    String json = gson.toJson(bits);
+    assertThat(json).isEqualTo("[0,1,0,1,1,1,0,0,0,1]");
+  }
+
+  @Test
+  public void testBitSetDeserialization() {
+    BitSet expected = new BitSet();
+    expected.set(0);
+    expected.set(2, 6);
+    expected.set(8);
+
+    Gson gson = new Gson();
+    String json = gson.toJson(expected);
+    assertThat(gson.fromJson(json, BitSet.class)).isEqualTo(expected);
+
+    json = "[1,0,1,1,1,1,0,0,1,0,0,0]";
+    assertThat(gson.fromJson(json, BitSet.class)).isEqualTo(expected);
+
+    json = "[\"1\",\"0\",\"1\",\"1\",\"1\",\"1\",\"0\",\"0\",\"1\"]";
+    assertThat(gson.fromJson(json, BitSet.class)).isEqualTo(expected);
+
+    json = "[true,false,true,true,true,true,false,false,true,false,false]";
+    assertThat(gson.fromJson(json, BitSet.class)).isEqualTo(expected);
+
+    var exception =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1, []]", BitSet.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo("Invalid bitset value type: BEGIN_ARRAY; at path $[1]");
+
+    exception =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1, 2]", BitSet.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo("Invalid bitset value 2, expected 0 or 1; at path $[1]");
+  }
+
+  @Test
+  public void testDefaultDateSerialization() {
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\"");
+  }
+
+  @Test
+  public void testDefaultDateDeserialization() {
+    String json = "'Dec 13, 2009 07:18:02 AM'";
+    Date extracted = gson.fromJson(json, Date.class);
+    assertEqualsDate(extracted, 2009, 11, 13);
+    assertEqualsTime(extracted, 7, 18, 2);
+  }
+
+  // Date can not directly be compared with another instance since the deserialization loses the
+  // millisecond portion.
+  @SuppressWarnings("deprecation")
+  public static void assertEqualsDate(Date date, int year, int month, int day) {
+    assertThat(date.getYear()).isEqualTo(year - 1900);
+    assertThat(date.getMonth()).isEqualTo(month);
+    assertThat(date.getDate()).isEqualTo(day);
+  }
+
+  @SuppressWarnings("deprecation")
+  public static void assertEqualsTime(Date date, int hours, int minutes, int seconds) {
+    assertThat(date.getHours()).isEqualTo(hours);
+    assertThat(date.getMinutes()).isEqualTo(minutes);
+    assertThat(date.getSeconds()).isEqualTo(seconds);
+  }
+
+  @Test
+  public void testDefaultDateSerializationUsingBuilder() {
+    Gson gson = new GsonBuilder().create();
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    assertThat(json).matches("\"Sep 11, 2011,? 10:55:03\\hPM\"");
+  }
+
+  @Test
+  public void testDefaultDateDeserializationUsingBuilder() {
+    Gson gson = new GsonBuilder().create();
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    Date extracted = gson.fromJson(json, Date.class);
+    assertThat(extracted.toString()).isEqualTo(now.toString());
+  }
+
+  @Test
+  public void testDefaultCalendarSerialization() {
+    Gson gson = new GsonBuilder().create();
+    String json = gson.toJson(Calendar.getInstance());
+    assertThat(json).contains("year");
+    assertThat(json).contains("month");
+    assertThat(json).contains("dayOfMonth");
+    assertThat(json).contains("hourOfDay");
+    assertThat(json).contains("minute");
+    assertThat(json).contains("second");
+  }
+
+  @Test
+  public void testDefaultCalendarDeserialization() {
+    Gson gson = new GsonBuilder().create();
+    String json = "{year:2009,month:2,dayOfMonth:11,hourOfDay:14,minute:29,second:23}";
+    Calendar cal = gson.fromJson(json, Calendar.class);
+    assertThat(cal.get(Calendar.YEAR)).isEqualTo(2009);
+    assertThat(cal.get(Calendar.MONTH)).isEqualTo(2);
+    assertThat(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(11);
+    assertThat(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(14);
+    assertThat(cal.get(Calendar.MINUTE)).isEqualTo(29);
+    assertThat(cal.get(Calendar.SECOND)).isEqualTo(23);
+  }
+
+  @Test
+  public void testDefaultGregorianCalendarSerialization() {
+    GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.US);
+    // Calendar was created with current time, must clear it
+    cal.clear();
+    cal.set(2018, Calendar.JUNE, 25, 10, 20, 30);
+
+    Gson gson = new GsonBuilder().create();
+    String json = gson.toJson(cal);
+    assertThat(json)
+        .isEqualTo(
+            "{\"year\":2018,\"month\":5,\"dayOfMonth\":25,\"hourOfDay\":10,\"minute\":20,\"second\":30}");
+  }
+
+  @Test
+  public void testDefaultGregorianCalendarDeserialization() {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    Locale defaultLocale = Locale.getDefault();
+
+    try {
+      // Calendar deserialization uses default TimeZone and Locale; set them here to make the test
+      // deterministic
+      TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+      Locale.setDefault(Locale.US);
+
+      Gson gson = new GsonBuilder().create();
+      String json =
+          "{\"year\":2009,\"month\":2,\"dayOfMonth\":11,\"hourOfDay\":14,\"minute\":29,\"second\":23}";
+      GregorianCalendar cal = gson.fromJson(json, GregorianCalendar.class);
+      assertThat(cal.get(Calendar.YEAR)).isEqualTo(2009);
+      assertThat(cal.get(Calendar.MONTH)).isEqualTo(2);
+      assertThat(cal.get(Calendar.DAY_OF_MONTH)).isEqualTo(11);
+      assertThat(cal.get(Calendar.HOUR_OF_DAY)).isEqualTo(14);
+      assertThat(cal.get(Calendar.MINUTE)).isEqualTo(29);
+      assertThat(cal.get(Calendar.SECOND)).isEqualTo(23);
+      assertThat(cal.getTimeInMillis()).isEqualTo(1236781763000L);
+
+      // Serializing value again should be equivalent to original JSON
+      assertThat(gson.toJson(cal)).isEqualTo(json);
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testDateSerializationWithStyle() {
+    int style = DateFormat.SHORT;
+    Date date = new Date(0);
+    String expectedFormatted = DateFormat.getDateTimeInstance(style, style, Locale.US).format(date);
+
+    Gson gson = new GsonBuilder().setDateFormat(style, style).create();
+    String json = gson.toJson(date);
+    assertThat(json).isEqualTo("\"" + expectedFormatted + "\"");
+    // Verify that custom style is not equal to default style
+    assertThat(json).isNotEqualTo(new Gson().toJson(date));
+  }
+
+  @Test
+  public void testDateSerializationWithPattern() {
+    String pattern = "yyyy-MM-dd";
+    Gson gson = new GsonBuilder().setDateFormat(DateFormat.FULL).setDateFormat(pattern).create();
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    assertThat(json).isEqualTo("\"2011-09-11\"");
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testDateDeserializationWithPattern() {
+    String pattern = "yyyy-MM-dd";
+    Gson gson = new GsonBuilder().setDateFormat(DateFormat.FULL).setDateFormat(pattern).create();
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    Date extracted = gson.fromJson(json, Date.class);
+    assertThat(extracted.getYear()).isEqualTo(now.getYear());
+    assertThat(extracted.getMonth()).isEqualTo(now.getMonth());
+    assertThat(extracted.getDay()).isEqualTo(now.getDay());
+  }
+
+  @Test
+  public void testDateSerializationWithPatternNotOverridenByTypeAdapter() {
+    String pattern = "yyyy-MM-dd";
+    Gson gson =
+        new GsonBuilder()
+            .setDateFormat(pattern)
+            .registerTypeAdapter(
+                Date.class,
+                new JsonDeserializer<Date>() {
+                  @Override
+                  public Date deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context)
+                      throws JsonParseException {
+                    return new Date(1315806903103L);
+                  }
+                })
+            .create();
+
+    Date now = new Date(1315806903103L);
+    String json = gson.toJson(now);
+    assertThat(json).isEqualTo("\"2011-09-11\"");
+  }
+
+  // http://code.google.com/p/google-gson/issues/detail?id=230
+  @Test
+  public void testDateSerializationInCollection() {
+    Type listOfDates = new TypeToken<List<Date>>() {}.getType();
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    try {
+      Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
+      List<Date> dates = Arrays.asList(new Date(0));
+      String json = gson.toJson(dates, listOfDates);
+      assertThat(json).isEqualTo("[\"1970-01-01\"]");
+      assertThat(gson.<List<Date>>fromJson("[\"1970-01-01\"]", listOfDates).get(0).getTime())
+          .isEqualTo(0L);
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testJsonPrimitiveSerialization() {
+    assertThat(gson.toJson(new JsonPrimitive(5), JsonElement.class)).isEqualTo("5");
+    assertThat(gson.toJson(new JsonPrimitive(true), JsonElement.class)).isEqualTo("true");
+    assertThat(gson.toJson(new JsonPrimitive("foo"), JsonElement.class)).isEqualTo("\"foo\"");
+    assertThat(gson.toJson(new JsonPrimitive('a'), JsonElement.class)).isEqualTo("\"a\"");
+  }
+
+  @Test
+  public void testJsonPrimitiveDeserialization() {
+    assertThat(gson.fromJson("5", JsonElement.class)).isEqualTo(new JsonPrimitive(5));
+    assertThat(gson.fromJson("5", JsonPrimitive.class)).isEqualTo(new JsonPrimitive(5));
+    assertThat(gson.fromJson("true", JsonElement.class)).isEqualTo(new JsonPrimitive(true));
+    assertThat(gson.fromJson("true", JsonPrimitive.class)).isEqualTo(new JsonPrimitive(true));
+    assertThat(gson.fromJson("\"foo\"", JsonElement.class)).isEqualTo(new JsonPrimitive("foo"));
+    assertThat(gson.fromJson("\"foo\"", JsonPrimitive.class)).isEqualTo(new JsonPrimitive("foo"));
+    assertThat(gson.fromJson("\"a\"", JsonElement.class)).isEqualTo(new JsonPrimitive('a'));
+    assertThat(gson.fromJson("\"a\"", JsonPrimitive.class)).isEqualTo(new JsonPrimitive('a'));
+  }
+
+  @Test
+  public void testJsonNullSerialization() {
+    assertThat(gson.toJson(JsonNull.INSTANCE, JsonElement.class)).isEqualTo("null");
+    assertThat(gson.toJson(JsonNull.INSTANCE, JsonNull.class)).isEqualTo("null");
+  }
+
+  @Test
+  public void testNullJsonElementSerialization() {
+    assertThat(gson.toJson(null, JsonElement.class)).isEqualTo("null");
+    assertThat(gson.toJson(null, JsonNull.class)).isEqualTo("null");
+  }
+
+  @Test
+  public void testJsonArraySerialization() {
+    JsonArray array = new JsonArray();
+    array.add(new JsonPrimitive(1));
+    array.add(new JsonPrimitive(2));
+    array.add(new JsonPrimitive(3));
+    assertThat(gson.toJson(array, JsonElement.class)).isEqualTo("[1,2,3]");
+  }
+
+  @Test
+  public void testJsonArrayDeserialization() {
+    JsonArray array = new JsonArray();
+    array.add(new JsonPrimitive(1));
+    array.add(new JsonPrimitive(2));
+    array.add(new JsonPrimitive(3));
+
+    String json = "[1,2,3]";
+    assertThat(gson.fromJson(json, JsonElement.class)).isEqualTo(array);
+    assertThat(gson.fromJson(json, JsonArray.class)).isEqualTo(array);
+  }
+
+  @Test
+  public void testJsonObjectSerialization() {
+    JsonObject object = new JsonObject();
+    object.add("foo", new JsonPrimitive(1));
+    object.add("bar", new JsonPrimitive(2));
+    assertThat(gson.toJson(object, JsonElement.class)).isEqualTo("{\"foo\":1,\"bar\":2}");
+  }
+
+  @Test
+  public void testJsonObjectDeserialization() {
+    JsonObject object = new JsonObject();
+    object.add("foo", new JsonPrimitive(1));
+    object.add("bar", new JsonPrimitive(2));
+
+    String json = "{\"foo\":1,\"bar\":2}";
+    JsonElement actual = gson.fromJson(json, JsonElement.class);
+    assertThat(actual).isEqualTo(object);
+
+    JsonObject actualObj = gson.fromJson(json, JsonObject.class);
+    assertThat(actualObj).isEqualTo(object);
+  }
+
+  @Test
+  public void testJsonNullDeserialization() {
+    assertThat(gson.fromJson("null", JsonElement.class)).isEqualTo(JsonNull.INSTANCE);
+    assertThat(gson.fromJson("null", JsonNull.class)).isEqualTo(JsonNull.INSTANCE);
+  }
+
+  @Test
+  public void testJsonElementTypeMismatch() {
+    var exception =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("\"abc\"", JsonObject.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected a com.google.gson.JsonObject but was com.google.gson.JsonPrimitive;"
+                + " at path $");
+  }
+
+  private static class ClassWithBigDecimal {
+    BigDecimal value;
+
+    ClassWithBigDecimal(String value) {
+      this.value = new BigDecimal(value);
+    }
+
+    String getExpectedJson() {
+      return "{\"value\":" + value.toEngineeringString() + "}";
+    }
+  }
+
+  private static class ClassWithBigInteger {
+    BigInteger value;
+
+    ClassWithBigInteger(String value) {
+      this.value = new BigInteger(value);
+    }
+
+    String getExpectedJson() {
+      return "{\"value\":" + value + "}";
+    }
+  }
+
+  @Test
+  public void testPropertiesSerialization() {
+    Properties props = new Properties();
+    props.setProperty("foo", "bar");
+    String json = gson.toJson(props);
+    String expected = "{\"foo\":\"bar\"}";
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testPropertiesDeserialization() {
+    String json = "{foo:'bar'}";
+    Properties props = gson.fromJson(json, Properties.class);
+    assertThat(props.getProperty("foo")).isEqualTo("bar");
+  }
+
+  @Test
+  public void testTreeSetSerialization() {
+    TreeSet<String> treeSet = new TreeSet<>();
+    treeSet.add("Value1");
+    String json = gson.toJson(treeSet);
+    assertThat(json).isEqualTo("[\"Value1\"]");
+  }
+
+  @Test
+  public void testTreeSetDeserialization() {
+    String json = "['Value1']";
+    Type type = new TypeToken<TreeSet<String>>() {}.getType();
+    TreeSet<String> treeSet = gson.fromJson(json, type);
+    assertThat(treeSet).contains("Value1");
+  }
+
+  @SuppressWarnings("UnnecessaryStringBuilder") // TODO: b/287969247 - remove when EP bug fixed
+  @Test
+  public void testStringBuilderSerialization() {
+    StringBuilder sb = new StringBuilder("abc");
+    String json = gson.toJson(sb);
+    assertThat(json).isEqualTo("\"abc\"");
+  }
+
+  @Test
+  public void testStringBuilderDeserialization() {
+    StringBuilder sb = gson.fromJson("'abc'", StringBuilder.class);
+    assertThat(sb.toString()).isEqualTo("abc");
+  }
+
+  @Test
+  @SuppressWarnings("JdkObsolete")
+  public void testStringBufferSerialization() {
+    StringBuffer sb = new StringBuffer("abc");
+    String json = gson.toJson(sb);
+    assertThat(json).isEqualTo("\"abc\"");
+  }
+
+  @Test
+  public void testStringBufferDeserialization() {
+    StringBuffer sb = gson.fromJson("'abc'", StringBuffer.class);
+    assertThat(sb.toString()).isEqualTo("abc");
+  }
+
+  private static class MyClassTypeAdapter extends TypeAdapter<Class<?>> {
+    @Override
+    public void write(JsonWriter out, Class<?> value) throws IOException {
+      out.value(value.getName());
+    }
+
+    @Override
+    public Class<?> read(JsonReader in) throws IOException {
+      String className = in.nextString();
+      try {
+        return Class.forName(className);
+      } catch (ClassNotFoundException e) {
+        throw new IOException(e);
+      }
+    }
+  }
+
+  static class NumberAsStringAdapter extends TypeAdapter<Number> {
+    private final Constructor<? extends Number> constructor;
+
+    NumberAsStringAdapter(Class<? extends Number> type) throws Exception {
+      this.constructor = type.getConstructor(String.class);
+    }
+
+    @Override
+    public void write(JsonWriter out, Number value) throws IOException {
+      out.value(value.toString());
+    }
+
+    @Override
+    public Number read(JsonReader in) throws IOException {
+      try {
+        return constructor.newInstance(in.nextString());
+      } catch (Exception e) {
+        throw new AssertionError(e);
+      }
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..93d04cae244f04ca74c83da45a8e574c6a100e19
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/DelegateTypeAdapterTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)} method.
+ *
+ * @author Inderjeet Singh
+ */
+public class DelegateTypeAdapterTest {
+
+  private StatsTypeAdapterFactory stats;
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    stats = new StatsTypeAdapterFactory();
+    gson = new GsonBuilder().registerTypeAdapterFactory(stats).create();
+  }
+
+  @Test
+  public void testDelegateInvoked() {
+    List<BagOfPrimitives> bags = new ArrayList<>();
+    for (int i = 0; i < 10; ++i) {
+      bags.add(new BagOfPrimitives(i, i, i % 2 == 0, String.valueOf(i)));
+    }
+    String json = gson.toJson(bags);
+    gson.fromJson(json, new TypeToken<List<BagOfPrimitives>>() {}.getType());
+    // 11: 1 list object, and 10 entries. stats invoked on all 5 fields
+    assertThat(stats.numReads).isEqualTo(51);
+    assertThat(stats.numWrites).isEqualTo(51);
+  }
+
+  @Test
+  public void testDelegateInvokedOnStrings() {
+    String[] bags = {"1", "2", "3", "4"};
+    String json = gson.toJson(bags);
+    gson.fromJson(json, String[].class);
+    // 1 array object with 4 elements.
+    assertThat(stats.numReads).isEqualTo(5);
+    assertThat(stats.numWrites).isEqualTo(5);
+  }
+
+  private static class StatsTypeAdapterFactory implements TypeAdapterFactory {
+    public int numReads = 0;
+    public int numWrites = 0;
+
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+      final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+      return new TypeAdapter<T>() {
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+          ++numWrites;
+          delegate.write(out, value);
+        }
+
+        @Override
+        public T read(JsonReader in) throws IOException {
+          ++numReads;
+          return delegate.read(in);
+        }
+      };
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/EnumTest.java b/gson/gson/src/test/java/com/google/gson/functional/EnumTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..835fb5086b7794d56c58e96ef6ee43c99c2286e6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/EnumTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+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.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.common.MoreAsserts;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Java 5.0 enums.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class EnumTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testTopLevelEnumSerialization() {
+    String result = gson.toJson(MyEnum.VALUE1);
+    assertThat(result).isEqualTo('"' + MyEnum.VALUE1.toString() + '"');
+  }
+
+  @Test
+  public void testTopLevelEnumDeserialization() {
+    MyEnum result = gson.fromJson('"' + MyEnum.VALUE1.toString() + '"', MyEnum.class);
+    assertThat(result).isEqualTo(MyEnum.VALUE1);
+  }
+
+  @Test
+  public void testCollectionOfEnumsSerialization() {
+    Type type = new TypeToken<Collection<MyEnum>>() {}.getType();
+    Collection<MyEnum> target = new ArrayList<>();
+    target.add(MyEnum.VALUE1);
+    target.add(MyEnum.VALUE2);
+    String expectedJson = "[\"VALUE1\",\"VALUE2\"]";
+    String actualJson = gson.toJson(target);
+    assertThat(actualJson).isEqualTo(expectedJson);
+    actualJson = gson.toJson(target, type);
+    assertThat(actualJson).isEqualTo(expectedJson);
+  }
+
+  @Test
+  public void testCollectionOfEnumsDeserialization() {
+    Type type = new TypeToken<Collection<MyEnum>>() {}.getType();
+    String json = "[\"VALUE1\",\"VALUE2\"]";
+    Collection<MyEnum> target = gson.fromJson(json, type);
+    MoreAsserts.assertContains(target, MyEnum.VALUE1);
+    MoreAsserts.assertContains(target, MyEnum.VALUE2);
+  }
+
+  @Test
+  public void testClassWithEnumFieldSerialization() {
+    ClassWithEnumFields target = new ClassWithEnumFields();
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testClassWithEnumFieldDeserialization() {
+    String json = "{value1:'VALUE1',value2:'VALUE2'}";
+    ClassWithEnumFields target = gson.fromJson(json, ClassWithEnumFields.class);
+    assertThat(target.value1).isEqualTo(MyEnum.VALUE1);
+    assertThat(target.value2).isEqualTo(MyEnum.VALUE2);
+  }
+
+  private static enum MyEnum {
+    VALUE1,
+    VALUE2
+  }
+
+  private static class ClassWithEnumFields {
+    private final MyEnum value1 = MyEnum.VALUE1;
+    private final MyEnum value2 = MyEnum.VALUE2;
+
+    public String getExpectedJson() {
+      return "{\"value1\":\"" + value1 + "\",\"value2\":\"" + value2 + "\"}";
+    }
+  }
+
+  /** Test for issue 226. */
+  @Test
+  @SuppressWarnings("GetClassOnEnum")
+  public void testEnumSubclass() {
+    assertThat(Roshambo.ROCK.getClass()).isNotEqualTo(Roshambo.class);
+    assertThat(gson.toJson(Roshambo.ROCK)).isEqualTo("\"ROCK\"");
+    assertThat(gson.toJson(EnumSet.allOf(Roshambo.class)))
+        .isEqualTo("[\"ROCK\",\"PAPER\",\"SCISSORS\"]");
+    assertThat(gson.fromJson("\"ROCK\"", Roshambo.class)).isEqualTo(Roshambo.ROCK);
+    Set<Roshambo> deserialized =
+        gson.fromJson("[\"ROCK\",\"PAPER\",\"SCISSORS\"]", new TypeToken<>() {});
+    assertThat(deserialized).isEqualTo(EnumSet.allOf(Roshambo.class));
+
+    // A bit contrived, but should also work if explicitly deserializing using anonymous enum
+    // subclass
+    assertThat(gson.fromJson("\"ROCK\"", Roshambo.ROCK.getClass())).isEqualTo(Roshambo.ROCK);
+  }
+
+  @Test
+  @SuppressWarnings("GetClassOnEnum")
+  public void testEnumSubclassWithRegisteredTypeAdapter() {
+    gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Roshambo.class, new MyEnumTypeAdapter())
+            .create();
+    assertThat(Roshambo.ROCK.getClass()).isNotEqualTo(Roshambo.class);
+    assertThat(gson.toJson(Roshambo.ROCK)).isEqualTo("\"123ROCK\"");
+    assertThat(gson.toJson(EnumSet.allOf(Roshambo.class)))
+        .isEqualTo("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]");
+    assertThat(gson.fromJson("\"123ROCK\"", Roshambo.class)).isEqualTo(Roshambo.ROCK);
+    Set<Roshambo> deserialized =
+        gson.fromJson("[\"123ROCK\",\"123PAPER\",\"123SCISSORS\"]", new TypeToken<>() {});
+    assertThat(deserialized).isEqualTo(EnumSet.allOf(Roshambo.class));
+  }
+
+  @Test
+  public void testEnumSubclassAsParameterizedType() {
+    Collection<Roshambo> list = new ArrayList<>();
+    list.add(Roshambo.ROCK);
+    list.add(Roshambo.PAPER);
+
+    String json = gson.toJson(list);
+    assertThat(json).isEqualTo("[\"ROCK\",\"PAPER\"]");
+
+    Type collectionType = new TypeToken<Collection<Roshambo>>() {}.getType();
+    Collection<Roshambo> actualJsonList = gson.fromJson(json, collectionType);
+    MoreAsserts.assertContains(actualJsonList, Roshambo.ROCK);
+    MoreAsserts.assertContains(actualJsonList, Roshambo.PAPER);
+  }
+
+  @Test
+  public void testEnumCaseMapping() {
+    assertThat(gson.fromJson("\"boy\"", Gender.class)).isEqualTo(Gender.MALE);
+    assertThat(gson.toJson(Gender.MALE, Gender.class)).isEqualTo("\"boy\"");
+  }
+
+  @Test
+  public void testEnumSet() {
+    EnumSet<Roshambo> foo = EnumSet.of(Roshambo.ROCK, Roshambo.PAPER);
+    String json = gson.toJson(foo);
+    assertThat(json).isEqualTo("[\"ROCK\",\"PAPER\"]");
+
+    Type type = new TypeToken<EnumSet<Roshambo>>() {}.getType();
+    EnumSet<Roshambo> bar = gson.fromJson(json, type);
+    assertThat(bar).containsExactly(Roshambo.ROCK, Roshambo.PAPER).inOrder();
+    assertThat(bar).doesNotContain(Roshambo.SCISSORS);
+  }
+
+  @Test
+  public void testEnumMap() {
+    EnumMap<MyEnum, String> map = new EnumMap<>(MyEnum.class);
+    map.put(MyEnum.VALUE1, "test");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"VALUE1\":\"test\"}");
+
+    Type type = new TypeToken<EnumMap<MyEnum, String>>() {}.getType();
+    EnumMap<?, ?> actualMap = gson.fromJson("{\"VALUE1\":\"test\"}", type);
+    Map<?, ?> expectedMap = Collections.singletonMap(MyEnum.VALUE1, "test");
+    assertThat(actualMap).isEqualTo(expectedMap);
+  }
+
+  private enum Roshambo {
+    ROCK {
+      @Override
+      Roshambo defeats() {
+        return SCISSORS;
+      }
+    },
+    PAPER {
+      @Override
+      Roshambo defeats() {
+        return ROCK;
+      }
+    },
+    SCISSORS {
+      @Override
+      Roshambo defeats() {
+        return PAPER;
+      }
+    };
+
+    @SuppressWarnings("unused")
+    abstract Roshambo defeats();
+  }
+
+  private static class MyEnumTypeAdapter
+      implements JsonSerializer<Roshambo>, JsonDeserializer<Roshambo> {
+    @Override
+    public JsonElement serialize(Roshambo src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("123" + src.name());
+    }
+
+    @Override
+    public Roshambo deserialize(JsonElement json, Type classOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return Roshambo.valueOf(json.getAsString().substring(3));
+    }
+  }
+
+  private enum Gender {
+    @SerializedName("boy")
+    MALE,
+
+    @SerializedName("girl")
+    FEMALE
+  }
+
+  @Test
+  public void testEnumClassWithFields() {
+    assertThat(gson.toJson(Color.RED)).isEqualTo("\"RED\"");
+    assertThat(gson.fromJson("RED", Color.class).value).isEqualTo("red");
+    assertThat(gson.fromJson("BLUE", Color.class).index).isEqualTo(2);
+  }
+
+  private enum Color {
+    RED("red", 1),
+    BLUE("blue", 2),
+    GREEN("green", 3);
+    final String value;
+    final int index;
+
+    private Color(String value, int index) {
+      this.value = value;
+      this.index = index;
+    }
+  }
+
+  @Test
+  public void testEnumToStringRead() {
+    // Should still be able to read constant name
+    assertThat(gson.fromJson("\"A\"", CustomToString.class)).isEqualTo(CustomToString.A);
+    // Should be able to read toString() value
+    assertThat(gson.fromJson("\"test\"", CustomToString.class)).isEqualTo(CustomToString.A);
+
+    assertThat(gson.fromJson("\"other\"", CustomToString.class)).isNull();
+  }
+
+  private enum CustomToString {
+    A;
+
+    @Override
+    public String toString() {
+      return "test";
+    }
+  }
+
+  /** Test that enum constant names have higher precedence than {@code toString()} result. */
+  @Test
+  public void testEnumToStringReadInterchanged() {
+    assertThat(gson.fromJson("\"A\"", InterchangedToString.class))
+        .isEqualTo(InterchangedToString.A);
+    assertThat(gson.fromJson("\"B\"", InterchangedToString.class))
+        .isEqualTo(InterchangedToString.B);
+  }
+
+  private enum InterchangedToString {
+    A("B"),
+    B("A");
+
+    private final String toString;
+
+    InterchangedToString(String toString) {
+      this.toString = toString;
+    }
+
+    @Override
+    public String toString() {
+      return toString;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/EnumWithObfuscatedTest.java b/gson/gson/src/test/java/com/google/gson/functional/EnumWithObfuscatedTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c50c482fbde71483b0585ef395ef6194378e7468
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/EnumWithObfuscatedTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for enums with Proguard.
+ *
+ * @author Young Cha
+ */
+public class EnumWithObfuscatedTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  public enum Gender {
+    @SerializedName("MAIL")
+    MALE,
+
+    @SerializedName("FEMAIL")
+    FEMALE
+  }
+
+  @Test
+  public void testEnumClassWithObfuscated() {
+    for (Gender enumConstant : Gender.class.getEnumConstants()) {
+      try {
+        Gender.class.getField(enumConstant.name());
+        fail("Enum is not obfuscated");
+      } catch (NoSuchFieldException ignore) {
+      }
+    }
+
+    assertThat(gson.fromJson("\"MAIL\"", Gender.class)).isEqualTo(Gender.MALE);
+    assertThat(gson.toJson(Gender.MALE, Gender.class)).isEqualTo("\"MAIL\"");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/EscapingTest.java b/gson/gson/src/test/java/com/google/gson/functional/EscapingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1eeeb1768d45e47e194b96332b1a9aacfc52c77b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/EscapingTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Performs some functional test involving JSON output escaping.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class EscapingTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testEscapingQuotesInStringArray() {
+    String[] valueWithQuotes = {"beforeQuote\"afterQuote"};
+    String jsonRepresentation = gson.toJson(valueWithQuotes);
+    String[] target = gson.fromJson(jsonRepresentation, String[].class);
+    assertThat(target.length).isEqualTo(1);
+    assertThat(target[0]).isEqualTo(valueWithQuotes[0]);
+  }
+
+  @Test
+  public void testEscapeAllHtmlCharacters() {
+    List<String> strings = new ArrayList<>();
+    strings.add("<");
+    strings.add(">");
+    strings.add("=");
+    strings.add("&");
+    strings.add("'");
+    strings.add("\"");
+    assertThat(gson.toJson(strings))
+        .isEqualTo("[\"\\u003c\",\"\\u003e\",\"\\u003d\",\"\\u0026\",\"\\u0027\",\"\\\"\"]");
+  }
+
+  @Test
+  public void testEscapingObjectFields() {
+    BagOfPrimitives objWithPrimitives = new BagOfPrimitives(1L, 1, true, "test with\" <script>");
+    String jsonRepresentation = gson.toJson(objWithPrimitives);
+    assertThat(jsonRepresentation).doesNotContain("<");
+    assertThat(jsonRepresentation).doesNotContain(">");
+    assertThat(jsonRepresentation).contains("\\\"");
+
+    BagOfPrimitives deserialized = gson.fromJson(jsonRepresentation, BagOfPrimitives.class);
+    assertThat(deserialized.getExpectedJson()).isEqualTo(objWithPrimitives.getExpectedJson());
+  }
+
+  @Test
+  public void testGsonAcceptsEscapedAndNonEscapedJsonDeserialization() {
+    Gson escapeHtmlGson = new GsonBuilder().create();
+    Gson noEscapeHtmlGson = new GsonBuilder().disableHtmlEscaping().create();
+
+    BagOfPrimitives target = new BagOfPrimitives(1L, 1, true, "test' / w'ith\" / \\ <script>");
+    String escapedJsonForm = escapeHtmlGson.toJson(target);
+    String nonEscapedJsonForm = noEscapeHtmlGson.toJson(target);
+    assertThat(escapedJsonForm).isNotEqualTo(nonEscapedJsonForm);
+
+    assertThat(noEscapeHtmlGson.fromJson(escapedJsonForm, BagOfPrimitives.class)).isEqualTo(target);
+    assertThat(escapeHtmlGson.fromJson(nonEscapedJsonForm, BagOfPrimitives.class))
+        .isEqualTo(target);
+  }
+
+  @Test
+  public void testGsonDoubleDeserialization() {
+    BagOfPrimitives expected = new BagOfPrimitives(3L, 4, true, "value1");
+    String json = gson.toJson(gson.toJson(expected));
+    String value = gson.fromJson(json, String.class);
+    BagOfPrimitives actual = gson.fromJson(value, BagOfPrimitives.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java b/gson/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..43ffc0eb8017bdd797b6acd45c045dc1bb07950c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ExclusionStrategyFunctionalTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Performs some functional tests when Gson is instantiated with some common user defined {@link
+ * ExclusionStrategy} objects.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ExclusionStrategyFunctionalTest {
+  private static final ExclusionStrategy EXCLUDE_SAMPLE_OBJECT_FOR_TEST =
+      new ExclusionStrategy() {
+        @Override
+        public boolean shouldSkipField(FieldAttributes f) {
+          return false;
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> clazz) {
+          return clazz == SampleObjectForTest.class;
+        }
+      };
+
+  private SampleObjectForTest src;
+
+  @Before
+  public void setUp() throws Exception {
+    src = new SampleObjectForTest();
+  }
+
+  @Test
+  public void testExclusionStrategySerialization() {
+    Gson gson = createGson(new MyExclusionStrategy(String.class), true);
+    String json = gson.toJson(src);
+    assertThat(json).doesNotContain("\"stringField\"");
+    assertThat(json).doesNotContain("\"annotatedField\"");
+    assertThat(json).contains("\"longField\"");
+  }
+
+  @Test
+  public void testExclusionStrategySerializationDoesNotImpactDeserialization() {
+    String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}";
+    Gson gson = createGson(new MyExclusionStrategy(String.class), true);
+    SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class);
+    assertThat(value.annotatedField).isEqualTo(1);
+    assertThat(value.stringField).isEqualTo("x");
+    assertThat(value.longField).isEqualTo(2);
+  }
+
+  @Test
+  public void testExclusionStrategyDeserialization() {
+    Gson gson = createGson(new MyExclusionStrategy(String.class), false);
+    JsonObject json = new JsonObject();
+    json.add("annotatedField", new JsonPrimitive(src.annotatedField + 5));
+    json.add("stringField", new JsonPrimitive(src.stringField + "blah,blah"));
+    json.add("longField", new JsonPrimitive(1212311L));
+
+    SampleObjectForTest target = gson.fromJson(json, SampleObjectForTest.class);
+    assertThat(target.longField).isEqualTo(1212311L);
+
+    // assert excluded fields are set to the defaults
+    assertThat(target.annotatedField).isEqualTo(src.annotatedField);
+    assertThat(target.stringField).isEqualTo(src.stringField);
+  }
+
+  @Test
+  public void testExclusionStrategySerializationDoesNotImpactSerialization() {
+    Gson gson = createGson(new MyExclusionStrategy(String.class), false);
+    String json = gson.toJson(src);
+    assertThat(json).contains("\"stringField\"");
+    assertThat(json).contains("\"annotatedField\"");
+    assertThat(json).contains("\"longField\"");
+  }
+
+  @Test
+  public void testExclusionStrategyWithMode() {
+    SampleObjectForTest testObj =
+        new SampleObjectForTest(
+            src.annotatedField + 5, src.stringField + "blah,blah", src.longField + 655L);
+
+    Gson gson = createGson(new MyExclusionStrategy(String.class), false);
+    JsonObject json = gson.toJsonTree(testObj).getAsJsonObject();
+    assertThat(json.get("annotatedField").getAsInt()).isEqualTo(testObj.annotatedField);
+    assertThat(json.get("stringField").getAsString()).isEqualTo(testObj.stringField);
+    assertThat(json.get("longField").getAsLong()).isEqualTo(testObj.longField);
+
+    SampleObjectForTest target = gson.fromJson(json, SampleObjectForTest.class);
+    assertThat(target.longField).isEqualTo(testObj.longField);
+
+    // assert excluded fields are set to the defaults
+    assertThat(target.annotatedField).isEqualTo(src.annotatedField);
+    assertThat(target.stringField).isEqualTo(src.stringField);
+  }
+
+  @Test
+  public void testExcludeTopLevelClassSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .addSerializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST)
+            .create();
+    assertThat(gson.toJson(new SampleObjectForTest(), SampleObjectForTest.class)).isEqualTo("null");
+  }
+
+  @Test
+  public void testExcludeTopLevelClassSerializationDoesNotImpactDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .addSerializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST)
+            .create();
+    String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}";
+    SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class);
+    assertThat(value.annotatedField).isEqualTo(1);
+    assertThat(value.stringField).isEqualTo("x");
+    assertThat(value.longField).isEqualTo(2);
+  }
+
+  @Test
+  public void testExcludeTopLevelClassDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .addDeserializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST)
+            .create();
+    String json = "{\"annotatedField\":1,\"stringField\":\"x\",\"longField\":2}";
+    SampleObjectForTest value = gson.fromJson(json, SampleObjectForTest.class);
+    assertThat(value).isNull();
+  }
+
+  @Test
+  public void testExcludeTopLevelClassDeserializationDoesNotImpactSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .addDeserializationExclusionStrategy(EXCLUDE_SAMPLE_OBJECT_FOR_TEST)
+            .create();
+    String json = gson.toJson(new SampleObjectForTest(), SampleObjectForTest.class);
+    assertThat(json).contains("\"stringField\"");
+    assertThat(json).contains("\"annotatedField\"");
+    assertThat(json).contains("\"longField\"");
+  }
+
+  private static Gson createGson(ExclusionStrategy exclusionStrategy, boolean serialization) {
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    if (serialization) {
+      gsonBuilder.addSerializationExclusionStrategy(exclusionStrategy);
+    } else {
+      gsonBuilder.addDeserializationExclusionStrategy(exclusionStrategy);
+    }
+    return gsonBuilder.serializeNulls().create();
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.FIELD})
+  private static @interface Foo {
+    // Field tag only annotation
+  }
+
+  private static class SampleObjectForTest {
+    @Foo private final int annotatedField;
+    private final String stringField;
+    private final long longField;
+
+    public SampleObjectForTest() {
+      this(5, "someDefaultValue", 12345L);
+    }
+
+    public SampleObjectForTest(int annotatedField, String stringField, long longField) {
+      this.annotatedField = annotatedField;
+      this.stringField = stringField;
+      this.longField = longField;
+    }
+  }
+
+  private static final class MyExclusionStrategy implements ExclusionStrategy {
+    private final Class<?> typeToSkip;
+
+    private MyExclusionStrategy(Class<?> typeToSkip) {
+      this.typeToSkip = typeToSkip;
+    }
+
+    @Override
+    public boolean shouldSkipClass(Class<?> clazz) {
+      return (clazz == typeToSkip);
+    }
+
+    @Override
+    public boolean shouldSkipField(FieldAttributes f) {
+      return f.getAnnotation(Foo.class) != null;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java b/gson/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..678b1bcad8d5367a9537e77979cf64acc53befaa
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ExposeFieldsTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.Keep;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.annotations.Expose;
+import java.lang.reflect.Type;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for the regarding functional "@Expose" type tests.
+ *
+ * @author Joel Leitch
+ */
+public class ExposeFieldsTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson =
+        new GsonBuilder()
+            .excludeFieldsWithoutExposeAnnotation()
+            .registerTypeAdapter(SomeInterface.class, new SomeInterfaceInstanceCreator())
+            .create();
+  }
+
+  @Test
+  public void testNullExposeFieldSerialization() {
+    ClassWithExposedFields object = new ClassWithExposedFields(null, 1);
+    String json = gson.toJson(object);
+
+    assertThat(json).isEqualTo(object.getExpectedJson());
+  }
+
+  @Test
+  public void testArrayWithOneNullExposeFieldObjectSerialization() {
+    ClassWithExposedFields object1 = new ClassWithExposedFields(1, 1);
+    ClassWithExposedFields object2 = new ClassWithExposedFields(null, 1);
+    ClassWithExposedFields object3 = new ClassWithExposedFields(2, 2);
+    ClassWithExposedFields[] objects = {object1, object2, object3};
+
+    String json = gson.toJson(objects);
+    String expected =
+        '['
+            + object1.getExpectedJson()
+            + ','
+            + object2.getExpectedJson()
+            + ','
+            + object3.getExpectedJson()
+            + ']';
+
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testExposeAnnotationSerialization() {
+    ClassWithExposedFields target = new ClassWithExposedFields(1, 2);
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testExposeAnnotationDeserialization() {
+    String json = "{a:3,b:4,d:20.0}";
+    ClassWithExposedFields target = gson.fromJson(json, ClassWithExposedFields.class);
+
+    assertThat(target.a).isEqualTo(3);
+    assertThat(target.b).isNull();
+    assertThat(target.d).isNotEqualTo(20);
+  }
+
+  @Test
+  public void testNoExposedFieldSerialization() {
+    ClassWithNoExposedFields obj = new ClassWithNoExposedFields();
+    String json = gson.toJson(obj);
+
+    assertThat(json).isEqualTo("{}");
+  }
+
+  @Test
+  public void testNoExposedFieldDeserialization() {
+    String json = "{a:4,b:5}";
+    ClassWithNoExposedFields obj = gson.fromJson(json, ClassWithNoExposedFields.class);
+
+    assertThat(obj.a).isEqualTo(0);
+    assertThat(obj.b).isEqualTo(1);
+  }
+
+  @Test
+  public void testExposedInterfaceFieldSerialization() {
+    String expected = "{\"interfaceField\":{}}";
+    ClassWithInterfaceField target = new ClassWithInterfaceField(new SomeObject());
+    String actual = gson.toJson(target);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testExposedInterfaceFieldDeserialization() {
+    String json = "{\"interfaceField\":{}}";
+    ClassWithInterfaceField obj = gson.fromJson(json, ClassWithInterfaceField.class);
+
+    assertThat(obj.interfaceField).isNotNull();
+  }
+
+  private static class ClassWithExposedFields {
+    @Expose private final Integer a;
+    private final Integer b;
+
+    @Expose(serialize = false)
+    @Keep
+    final long c;
+
+    @Expose(deserialize = false)
+    final double d;
+
+    @Expose(serialize = false, deserialize = false)
+    @Keep
+    final char e;
+
+    public ClassWithExposedFields(Integer a, Integer b) {
+      this(a, b, 1L, 2.0, 'a');
+    }
+
+    public ClassWithExposedFields(Integer a, Integer b, long c, double d, char e) {
+      this.a = a;
+      this.b = b;
+      this.c = c;
+      this.d = d;
+      this.e = e;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder("{");
+      if (a != null) {
+        sb.append("\"a\":").append(a).append(",");
+      }
+      sb.append("\"d\":").append(d);
+      sb.append("}");
+      return sb.toString();
+    }
+  }
+
+  private static class ClassWithNoExposedFields {
+    private final int a = 0;
+    private final int b = 1;
+  }
+
+  private static interface SomeInterface {
+    // Empty interface
+  }
+
+  private static class SomeObject implements SomeInterface {
+    // Do nothing
+  }
+
+  private static class SomeInterfaceInstanceCreator implements InstanceCreator<SomeInterface> {
+    @Override
+    public SomeInterface createInstance(Type type) {
+      return new SomeObject();
+    }
+  }
+
+  private static class ClassWithInterfaceField {
+    @Expose private final SomeInterface interfaceField;
+
+    public ClassWithInterfaceField(SomeInterface interfaceField) {
+      this.interfaceField = interfaceField;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java b/gson/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..591cb2fd9d08ac85738faf529799e705f3b43bae
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/FieldExclusionTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Performs some functional testing to ensure GSON infrastructure properly serializes/deserializes
+ * fields that either should or should not be included in the output based on the GSON
+ * configuration.
+ *
+ * @author Joel Leitch
+ */
+public class FieldExclusionTest {
+  private static final String VALUE = "blah_1234";
+
+  private Outer outer;
+
+  @Before
+  public void setUp() throws Exception {
+    outer = new Outer();
+  }
+
+  @Test
+  public void testDefaultInnerClassExclusion() {
+    Gson gson = new Gson();
+    Outer.Inner target = outer.new Inner(VALUE);
+    String result = gson.toJson(target);
+    assertThat(result).isEqualTo(target.toJson());
+
+    gson = new GsonBuilder().create();
+    target = outer.new Inner(VALUE);
+    result = gson.toJson(target);
+    assertThat(result).isEqualTo(target.toJson());
+  }
+
+  @Test
+  public void testInnerClassExclusion() {
+    Gson gson = new GsonBuilder().disableInnerClassSerialization().create();
+    Outer.Inner target = outer.new Inner(VALUE);
+    String result = gson.toJson(target);
+    assertThat(result).isEqualTo("null");
+  }
+
+  @Test
+  public void testDefaultNestedStaticClassIncluded() {
+    Gson gson = new Gson();
+    Outer.Inner target = outer.new Inner(VALUE);
+    String result = gson.toJson(target);
+    assertThat(result).isEqualTo(target.toJson());
+
+    gson = new GsonBuilder().create();
+    target = outer.new Inner(VALUE);
+    result = gson.toJson(target);
+    assertThat(result).isEqualTo(target.toJson());
+  }
+
+  private static class Outer {
+
+    @SuppressWarnings("ClassCanBeStatic")
+    private class Inner extends NestedClass {
+      public Inner(String value) {
+        super(value);
+      }
+    }
+  }
+
+  private static class NestedClass {
+    private final String value;
+
+    public NestedClass(String value) {
+      this.value = value;
+    }
+
+    public String toJson() {
+      return "{\"value\":\"" + value + "\"}";
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java b/gson/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..36f03f288990cfb5868a1862b9a26e62fdc2754a
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/FieldNamingTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+import static com.google.gson.FieldNamingPolicy.IDENTITY;
+import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_DASHES;
+import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
+import static com.google.gson.FieldNamingPolicy.UPPER_CAMEL_CASE;
+import static com.google.gson.FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES;
+import static com.google.gson.FieldNamingPolicy.UPPER_CASE_WITH_UNDERSCORES;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import org.junit.Test;
+
+public final class FieldNamingTest {
+  @Test
+  public void testIdentity() {
+    Gson gson = getGsonWithNamingPolicy(IDENTITY);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'lowerCamel':1,'UpperCamel':2,'_lowerCamelLeadingUnderscore':3,"
+                + "'_UpperCamelLeadingUnderscore':4,'lower_words':5,'UPPER_WORDS':6,"
+                + "'annotatedName':7,'lowerId':8,'_9':9}");
+  }
+
+  @Test
+  public void testUpperCamelCase() {
+    Gson gson = getGsonWithNamingPolicy(UPPER_CAMEL_CASE);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'LowerCamel':1,'UpperCamel':2,'_LowerCamelLeadingUnderscore':3,"
+                + "'_UpperCamelLeadingUnderscore':4,'Lower_words':5,'UPPER_WORDS':6,"
+                + "'annotatedName':7,'LowerId':8,'_9':9}");
+  }
+
+  @Test
+  public void testUpperCamelCaseWithSpaces() {
+    Gson gson = getGsonWithNamingPolicy(UPPER_CAMEL_CASE_WITH_SPACES);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'Lower Camel':1,'Upper Camel':2,'_Lower Camel Leading Underscore':3,"
+                + "'_ Upper Camel Leading Underscore':4,'Lower_words':5,'U P P E R_ W O R D S':6,"
+                + "'annotatedName':7,'Lower Id':8,'_9':9}");
+  }
+
+  @Test
+  public void testUpperCaseWithUnderscores() {
+    Gson gson = getGsonWithNamingPolicy(UPPER_CASE_WITH_UNDERSCORES);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'LOWER_CAMEL':1,'UPPER_CAMEL':2,'_LOWER_CAMEL_LEADING_UNDERSCORE':3,"
+                + "'__UPPER_CAMEL_LEADING_UNDERSCORE':4,'LOWER_WORDS':5,'U_P_P_E_R__W_O_R_D_S':6,"
+                + "'annotatedName':7,'LOWER_ID':8,'_9':9}");
+  }
+
+  @Test
+  public void testLowerCaseWithUnderscores() {
+    Gson gson = getGsonWithNamingPolicy(LOWER_CASE_WITH_UNDERSCORES);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'lower_camel':1,'upper_camel':2,'_lower_camel_leading_underscore':3,"
+                + "'__upper_camel_leading_underscore':4,'lower_words':5,'u_p_p_e_r__w_o_r_d_s':6,"
+                + "'annotatedName':7,'lower_id':8,'_9':9}");
+  }
+
+  @Test
+  public void testLowerCaseWithDashes() {
+    Gson gson = getGsonWithNamingPolicy(LOWER_CASE_WITH_DASHES);
+    assertThat(gson.toJson(new TestNames()).replace('\"', '\''))
+        .isEqualTo(
+            "{'lower-camel':1,'upper-camel':2,'_lower-camel-leading-underscore':3,"
+                + "'_-upper-camel-leading-underscore':4,'lower_words':5,'u-p-p-e-r_-w-o-r-d-s':6,"
+                + "'annotatedName':7,'lower-id':8,'_9':9}");
+  }
+
+  private static Gson getGsonWithNamingPolicy(FieldNamingPolicy fieldNamingPolicy) {
+    return new GsonBuilder().setFieldNamingPolicy(fieldNamingPolicy).create();
+  }
+
+  // Suppress because fields are used reflectively, and the names are intentionally unconventional
+  @SuppressWarnings({"unused", "MemberName", "ConstantField"})
+  private static class TestNames {
+    int lowerCamel = 1;
+    int UpperCamel = 2;
+    int _lowerCamelLeadingUnderscore = 3;
+    int _UpperCamelLeadingUnderscore = 4;
+    int lower_words = 5;
+    int UPPER_WORDS = 6;
+
+    @SerializedName("annotatedName")
+    int annotated = 7;
+
+    int lowerId = 8;
+    int _9 = 9;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java b/gson/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c4d5519636529eec9ed5d2eb98c7c1c44cd6f6c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.FormattingStyle;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Functional tests for formatting styles.
+ *
+ * @author Mihai Nita
+ */
+@RunWith(JUnit4.class)
+public class FormattingStyleTest {
+
+  // Create new input object every time to protect against tests accidentally modifying input
+  private static Map<String, List<Integer>> createInput() {
+    Map<String, List<Integer>> map = new LinkedHashMap<>();
+    map.put("a", Arrays.asList(1, 2));
+    return map;
+  }
+
+  private static String buildExpected(String newline, String indent, boolean spaceAfterSeparators) {
+    String expected =
+        "{<EOL><INDENT>\"a\":<COLON_SPACE>[<EOL><INDENT><INDENT>1,<COMMA_SPACE><EOL><INDENT><INDENT>2<EOL><INDENT>]<EOL>}";
+    String commaSpace = spaceAfterSeparators && newline.isEmpty() ? " " : "";
+    return expected
+        .replace("<EOL>", newline)
+        .replace("<INDENT>", indent)
+        .replace("<COLON_SPACE>", spaceAfterSeparators ? " " : "")
+        .replace("<COMMA_SPACE>", commaSpace);
+  }
+
+  // Various valid strings that can be used for newline and indent
+  private static final String[] TEST_NEWLINES = {
+    "", "\r", "\n", "\r\n", "\n\r\r\n", System.lineSeparator()
+  };
+  private static final String[] TEST_INDENTS = {"", "  ", "    ", "\t", " \t \t"};
+
+  @Test
+  public void testDefault() {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    String json = gson.toJson(createInput());
+    assertThat(json).isEqualTo(buildExpected("\n", "  ", true));
+  }
+
+  @Test
+  public void testVariousCombinationsParse() {
+    // Mixing various indent and newline styles in the same string, to be parsed.
+    String jsonStringMix = "{\r\t'a':\r\n[        1,2\t]\n}";
+    TypeToken<Map<String, List<Integer>>> inputType =
+        new TypeToken<Map<String, List<Integer>>>() {};
+
+    Map<String, List<Integer>> actualParsed;
+    // Test all that all combinations of newline can be parsed and generate the same INPUT.
+    for (String indent : TEST_INDENTS) {
+      for (String newline : TEST_NEWLINES) {
+        FormattingStyle style = FormattingStyle.PRETTY.withNewline(newline).withIndent(indent);
+        Gson gson = new GsonBuilder().setFormattingStyle(style).create();
+
+        String toParse = buildExpected(newline, indent, true);
+        actualParsed = gson.fromJson(toParse, inputType);
+        assertThat(actualParsed).isEqualTo(createInput());
+
+        // Parse the mixed string with the gson parsers configured with various newline / indents.
+        actualParsed = gson.fromJson(jsonStringMix, inputType);
+        assertThat(actualParsed).isEqualTo(createInput());
+      }
+    }
+  }
+
+  private static String toJson(Object obj, FormattingStyle style) {
+    return new GsonBuilder().setFormattingStyle(style).create().toJson(obj);
+  }
+
+  @Test
+  public void testFormatCompact() {
+    String json = toJson(createInput(), FormattingStyle.COMPACT);
+    String expectedJson = buildExpected("", "", false);
+    assertThat(json).isEqualTo(expectedJson);
+    // Sanity check to verify that `buildExpected` works correctly
+    assertThat(json).isEqualTo("{\"a\":[1,2]}");
+  }
+
+  @Test
+  public void testFormatPretty() {
+    String json = toJson(createInput(), FormattingStyle.PRETTY);
+    String expectedJson = buildExpected("\n", "  ", true);
+    assertThat(json).isEqualTo(expectedJson);
+    // Sanity check to verify that `buildExpected` works correctly
+    assertThat(json)
+        .isEqualTo(
+            "{\n" //
+                + "  \"a\": [\n" //
+                + "    1,\n" //
+                + "    2\n" //
+                + "  ]\n" //
+                + "}");
+  }
+
+  @Test
+  public void testFormatPrettySingleLine() {
+    FormattingStyle style = FormattingStyle.COMPACT.withSpaceAfterSeparators(true);
+    String json = toJson(createInput(), style);
+    String expectedJson = buildExpected("", "", true);
+    assertThat(json).isEqualTo(expectedJson);
+    // Sanity check to verify that `buildExpected` works correctly
+    assertThat(json).isEqualTo("{\"a\": [1, 2]}");
+  }
+
+  @Test
+  public void testFormat() {
+    for (String newline : TEST_NEWLINES) {
+      for (String indent : TEST_INDENTS) {
+        for (boolean spaceAfterSeparators : new boolean[] {true, false}) {
+          FormattingStyle style =
+              FormattingStyle.COMPACT
+                  .withNewline(newline)
+                  .withIndent(indent)
+                  .withSpaceAfterSeparators(spaceAfterSeparators);
+
+          String json = toJson(createInput(), style);
+          String expectedJson = buildExpected(newline, indent, spaceAfterSeparators);
+          assertThat(json).isEqualTo(expectedJson);
+        }
+      }
+    }
+  }
+
+  /**
+   * Should be able to convert {@link FormattingStyle#COMPACT} to {@link FormattingStyle#PRETTY}
+   * using the {@code withX} methods.
+   */
+  @Test
+  public void testCompactToPretty() {
+    FormattingStyle style =
+        FormattingStyle.COMPACT.withNewline("\n").withIndent("  ").withSpaceAfterSeparators(true);
+
+    String json = toJson(createInput(), style);
+    String expectedJson = toJson(createInput(), FormattingStyle.PRETTY);
+    assertThat(json).isEqualTo(expectedJson);
+  }
+
+  /**
+   * Should be able to convert {@link FormattingStyle#PRETTY} to {@link FormattingStyle#COMPACT}
+   * using the {@code withX} methods.
+   */
+  @Test
+  public void testPrettyToCompact() {
+    FormattingStyle style =
+        FormattingStyle.PRETTY.withNewline("").withIndent("").withSpaceAfterSeparators(false);
+
+    String json = toJson(createInput(), style);
+    String expectedJson = toJson(createInput(), FormattingStyle.COMPACT);
+    assertThat(json).isEqualTo(expectedJson);
+  }
+
+  @Test
+  public void testStyleValidations() {
+    try {
+      // TBD if we want to accept \u2028 and \u2029. For now we don't because JSON specification
+      // does not consider them to be newlines
+      FormattingStyle.PRETTY.withNewline("\u2028");
+      fail("Gson should not accept anything but \\r and \\n for newline");
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo("Only combinations of \\n and \\r are allowed in newline.");
+    }
+
+    try {
+      FormattingStyle.PRETTY.withNewline("NL");
+      fail("Gson should not accept anything but \\r and \\n for newline");
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo("Only combinations of \\n and \\r are allowed in newline.");
+    }
+
+    try {
+      FormattingStyle.PRETTY.withIndent("\f");
+      fail("Gson should not accept anything but space and tab for indent");
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo("Only combinations of spaces and tabs are allowed in indent.");
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java b/gson/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cbeb4156e8f9ee4bd3ddd1032f87144f878d3ea
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 Gson Authors
+ *
+ * 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.util.regex.Pattern;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests to validate printing of Gson version on AssertionErrors
+ *
+ * @author Inderjeet Singh
+ */
+public class GsonVersionDiagnosticsTest {
+  // We require a patch number, even if it is .0, consistent with https://semver.org/#spec-item-2.
+  private static final Pattern GSON_VERSION_PATTERN =
+      Pattern.compile("(\\(GSON \\d\\.\\d+\\.\\d)(?:[-.][A-Z]+)?\\)$");
+
+  private Gson gson;
+
+  @Before
+  public void setUp() {
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                TestType.class,
+                new TypeAdapter<TestType>() {
+                  @Override
+                  public void write(JsonWriter out, TestType value) {
+                    throw new AssertionError("Expected during serialization");
+                  }
+
+                  @Override
+                  public TestType read(JsonReader in) {
+                    throw new AssertionError("Expected during deserialization");
+                  }
+                })
+            .create();
+  }
+
+  @Test
+  public void testVersionPattern() {
+    assertThat("(GSON 2.8.5)").matches(GSON_VERSION_PATTERN);
+    assertThat("(GSON 2.8.5-SNAPSHOT)").matches(GSON_VERSION_PATTERN);
+  }
+
+  @Test
+  public void testAssertionErrorInSerializationPrintsVersion() {
+    AssertionError e = assertThrows(AssertionError.class, () -> gson.toJson(new TestType()));
+    ensureAssertionErrorPrintsGsonVersion(e);
+  }
+
+  @Test
+  public void testAssertionErrorInDeserializationPrintsVersion() {
+    AssertionError e =
+        assertThrows(AssertionError.class, () -> gson.fromJson("{'a':'abc'}", TestType.class));
+
+    ensureAssertionErrorPrintsGsonVersion(e);
+  }
+
+  private static void ensureAssertionErrorPrintsGsonVersion(AssertionError expected) {
+    String msg = expected.getMessage();
+    // System.err.println(msg);
+    int start = msg.indexOf("(GSON");
+    assertThat(start > 0).isTrue();
+    int end = msg.indexOf("):") + 1;
+    assertThat(end > 0 && end > start + 6).isTrue();
+    String version = msg.substring(start, end);
+    // System.err.println(version);
+    assertThat(version).matches(GSON_VERSION_PATTERN);
+  }
+
+  private static final class TestType {
+    @SuppressWarnings("unused")
+    String a;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/InheritanceTest.java b/gson/gson/src/test/java/com/google/gson/functional/InheritanceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f41b133fb33e3203935bb9815826b0a32dc6f0a0
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/InheritanceTest.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2009 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.Base;
+import com.google.gson.common.TestTypes.ClassWithBaseArrayField;
+import com.google.gson.common.TestTypes.ClassWithBaseCollectionField;
+import com.google.gson.common.TestTypes.ClassWithBaseField;
+import com.google.gson.common.TestTypes.Nested;
+import com.google.gson.common.TestTypes.Sub;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json serialization and deserialization of classes with inheritance
+ * hierarchies.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class InheritanceTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testSubClassSerialization() {
+    SubTypeOfNested target =
+        new SubTypeOfNested(
+            new BagOfPrimitives(10, 20, false, "stringValue"),
+            new BagOfPrimitives(30, 40, true, "stringValue"));
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testSubClassDeserialization() {
+    String json =
+        "{\"value\":5,\"primitive1\":{\"longValue\":10,\"intValue\":20,"
+            + "\"booleanValue\":false,\"stringValue\":\"stringValue\"},\"primitive2\":"
+            + "{\"longValue\":30,\"intValue\":40,\"booleanValue\":true,"
+            + "\"stringValue\":\"stringValue\"}}";
+    SubTypeOfNested target = gson.fromJson(json, SubTypeOfNested.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testClassWithBaseFieldSerialization() {
+    ClassWithBaseField sub = new ClassWithBaseField(new Sub());
+    JsonObject json = (JsonObject) gson.toJsonTree(sub);
+    JsonElement base = json.getAsJsonObject().get(ClassWithBaseField.FIELD_KEY);
+    assertThat(base.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString()).isEqualTo(Sub.SUB_NAME);
+  }
+
+  @Test
+  public void testClassWithBaseArrayFieldSerialization() {
+    Base[] baseClasses = new Base[] {new Sub(), new Sub()};
+    ClassWithBaseArrayField sub = new ClassWithBaseArrayField(baseClasses);
+    JsonObject json = gson.toJsonTree(sub).getAsJsonObject();
+    JsonArray bases = json.get(ClassWithBaseArrayField.FIELD_KEY).getAsJsonArray();
+    for (JsonElement element : bases) {
+      assertThat(element.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString())
+          .isEqualTo(Sub.SUB_NAME);
+    }
+  }
+
+  @Test
+  public void testClassWithBaseCollectionFieldSerialization() {
+    Collection<Base> baseClasses = new ArrayList<>();
+    baseClasses.add(new Sub());
+    baseClasses.add(new Sub());
+    ClassWithBaseCollectionField sub = new ClassWithBaseCollectionField(baseClasses);
+    JsonObject json = gson.toJsonTree(sub).getAsJsonObject();
+    JsonArray bases = json.get(ClassWithBaseArrayField.FIELD_KEY).getAsJsonArray();
+    for (JsonElement element : bases) {
+      assertThat(element.getAsJsonObject().get(Sub.SUB_FIELD_KEY).getAsString())
+          .isEqualTo(Sub.SUB_NAME);
+    }
+  }
+
+  @Test
+  public void testBaseSerializedAsSub() {
+    Base base = new Sub();
+    JsonObject json = gson.toJsonTree(base).getAsJsonObject();
+    assertThat(json.get(Sub.SUB_FIELD_KEY).getAsString()).isEqualTo(Sub.SUB_NAME);
+  }
+
+  @Test
+  public void testBaseSerializedAsSubForToJsonMethod() {
+    Base base = new Sub();
+    String json = gson.toJson(base);
+    assertThat(json).contains(Sub.SUB_NAME);
+  }
+
+  @Test
+  public void testBaseSerializedAsBaseWhenSpecifiedWithExplicitType() {
+    Base base = new Sub();
+    JsonObject json = gson.toJsonTree(base, Base.class).getAsJsonObject();
+    assertThat(json.get(Base.BASE_FIELD_KEY).getAsString()).isEqualTo(Base.BASE_NAME);
+    assertThat(json.get(Sub.SUB_FIELD_KEY)).isNull();
+  }
+
+  @Test
+  public void testBaseSerializedAsBaseWhenSpecifiedWithExplicitTypeForToJsonMethod() {
+    Base base = new Sub();
+    String json = gson.toJson(base, Base.class);
+    assertThat(json).contains(Base.BASE_NAME);
+    assertThat(json).doesNotContain(Sub.SUB_FIELD_KEY);
+  }
+
+  @Test
+  public void testBaseSerializedAsSubWhenSpecifiedWithExplicitType() {
+    Base base = new Sub();
+    JsonObject json = gson.toJsonTree(base, Sub.class).getAsJsonObject();
+    assertThat(json.get(Sub.SUB_FIELD_KEY).getAsString()).isEqualTo(Sub.SUB_NAME);
+  }
+
+  @Test
+  public void testBaseSerializedAsSubWhenSpecifiedWithExplicitTypeForToJsonMethod() {
+    Base base = new Sub();
+    String json = gson.toJson(base, Sub.class);
+    assertThat(json).contains(Sub.SUB_NAME);
+  }
+
+  private static class SubTypeOfNested extends Nested {
+    private final long value = 5;
+
+    public SubTypeOfNested(BagOfPrimitives primitive1, BagOfPrimitives primitive2) {
+      super(primitive1, primitive2);
+    }
+
+    @Override
+    public void appendFields(StringBuilder sb) {
+      sb.append("\"value\":").append(value).append(",");
+      super.appendFields(sb);
+    }
+  }
+
+  @Test
+  @SuppressWarnings("JdkObsolete")
+  public void testSubInterfacesOfCollectionSerialization() {
+    List<Integer> list = new LinkedList<>();
+    list.add(0);
+    list.add(1);
+    list.add(2);
+    list.add(3);
+    Queue<Long> queue = new LinkedList<>();
+    queue.add(0L);
+    queue.add(1L);
+    queue.add(2L);
+    queue.add(3L);
+    Set<Float> set = new TreeSet<>();
+    set.add(0.1F);
+    set.add(0.2F);
+    set.add(0.3F);
+    set.add(0.4F);
+    SortedSet<Character> sortedSet = new TreeSet<>();
+    sortedSet.add('a');
+    sortedSet.add('b');
+    sortedSet.add('c');
+    sortedSet.add('d');
+    ClassWithSubInterfacesOfCollection target =
+        new ClassWithSubInterfacesOfCollection(list, queue, set, sortedSet);
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testSubInterfacesOfCollectionDeserialization() {
+    String json =
+        "{\"list\":[0,1,2,3],\"queue\":[0,1,2,3],\"set\":[0.1,0.2,0.3,0.4],"
+            + "\"sortedSet\":[\"a\",\"b\",\"c\",\"d\"]"
+            + "}";
+    ClassWithSubInterfacesOfCollection target =
+        gson.fromJson(json, ClassWithSubInterfacesOfCollection.class);
+    assertThat(target.listContains(0, 1, 2, 3)).isTrue();
+    assertThat(target.queueContains(0, 1, 2, 3)).isTrue();
+    assertThat(target.setContains(0.1F, 0.2F, 0.3F, 0.4F)).isTrue();
+    assertThat(target.sortedSetContains('a', 'b', 'c', 'd')).isTrue();
+  }
+
+  private static class ClassWithSubInterfacesOfCollection {
+    private List<Integer> list;
+    private Queue<Long> queue;
+    private Set<Float> set;
+    private SortedSet<Character> sortedSet;
+
+    public ClassWithSubInterfacesOfCollection(
+        List<Integer> list, Queue<Long> queue, Set<Float> set, SortedSet<Character> sortedSet) {
+      this.list = list;
+      this.queue = queue;
+      this.set = set;
+      this.sortedSet = sortedSet;
+    }
+
+    boolean listContains(int... values) {
+      for (int value : values) {
+        if (!list.contains(value)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    boolean queueContains(long... values) {
+      for (long value : values) {
+        if (!queue.contains(value)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    boolean setContains(float... values) {
+      for (float value : values) {
+        if (!set.contains(value)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    boolean sortedSetContains(char... values) {
+      for (char value : values) {
+        if (!sortedSet.contains(value)) {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder();
+      sb.append("{");
+      sb.append("\"list\":");
+      append(sb, list).append(",");
+      sb.append("\"queue\":");
+      append(sb, queue).append(",");
+      sb.append("\"set\":");
+      append(sb, set).append(",");
+      sb.append("\"sortedSet\":");
+      append(sb, sortedSet);
+      sb.append("}");
+      return sb.toString();
+    }
+
+    @CanIgnoreReturnValue
+    private static StringBuilder append(StringBuilder sb, Collection<?> c) {
+      sb.append("[");
+      boolean first = true;
+      for (Object o : c) {
+        if (!first) {
+          sb.append(",");
+        } else {
+          first = false;
+        }
+        if (o instanceof String || o instanceof Character) {
+          sb.append('\"');
+        }
+        sb.append(o.toString());
+        if (o instanceof String || o instanceof Character) {
+          sb.append('\"');
+        }
+      }
+      sb.append("]");
+      return sb;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java b/gson/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..54a9fedce22aa7962baf464789af62a32345d597
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/InstanceCreatorTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2009 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.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 java.util.SortedSet;
+import java.util.TreeSet;
+import org.junit.Test;
+
+/**
+ * Functional Test exercising custom deserialization only. When test applies to both serialization
+ * and deserialization then add it to CustomTypeAdapterTest.
+ *
+ * @author Inderjeet Singh
+ */
+public class InstanceCreatorTest {
+
+  @Test
+  public void testInstanceCreatorReturnsBaseType() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new InstanceCreator<Base>() {
+                  @Override
+                  public Base createInstance(Type type) {
+                    return new Base();
+                  }
+                })
+            .create();
+    String json = "{baseName:'BaseRevised',subName:'Sub'}";
+    Base base = gson.fromJson(json, Base.class);
+    assertThat(base.baseName).isEqualTo("BaseRevised");
+  }
+
+  @Test
+  public void testInstanceCreatorReturnsSubTypeForTopLevelObject() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new InstanceCreator<Base>() {
+                  @Override
+                  public Base createInstance(Type type) {
+                    return new Sub();
+                  }
+                })
+            .create();
+
+    String json = "{baseName:'Base',subName:'SubRevised'}";
+    Base base = gson.fromJson(json, Base.class);
+    assertThat(base).isInstanceOf(Sub.class);
+
+    Sub sub = (Sub) base;
+    assertThat(sub.subName).isNotEqualTo("SubRevised");
+    assertThat(sub.subName).isEqualTo(Sub.SUB_NAME);
+  }
+
+  @Test
+  public void testInstanceCreatorReturnsSubTypeForField() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Base.class,
+                new InstanceCreator<Base>() {
+                  @Override
+                  public Base createInstance(Type type) {
+                    return new Sub();
+                  }
+                })
+            .create();
+    String json = "{base:{baseName:'Base',subName:'SubRevised'}}";
+    ClassWithBaseField target = gson.fromJson(json, ClassWithBaseField.class);
+    assertThat(target.base).isInstanceOf(Sub.class);
+    assertThat(((Sub) target.base).subName).isEqualTo(Sub.SUB_NAME);
+  }
+
+  // This regressed in Gson 2.0 and 2.1
+  @Test
+  public void testInstanceCreatorForCollectionType() {
+    @SuppressWarnings("serial")
+    class SubArrayList<T> extends ArrayList<T> {}
+    InstanceCreator<List<String>> listCreator =
+        new InstanceCreator<List<String>>() {
+          @Override
+          public List<String> createInstance(Type type) {
+            return new SubArrayList<>();
+          }
+        };
+    Type listOfStringType = new TypeToken<List<String>>() {}.getType();
+    Gson gson = new GsonBuilder().registerTypeAdapter(listOfStringType, listCreator).create();
+    List<String> list = gson.fromJson("[\"a\"]", listOfStringType);
+    assertThat(list.getClass()).isEqualTo(SubArrayList.class);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Test
+  public void testInstanceCreatorForParametrizedType() {
+    @SuppressWarnings("serial")
+    class SubTreeSet<T> extends TreeSet<T> {}
+    InstanceCreator<SortedSet<?>> sortedSetCreator =
+        new InstanceCreator<SortedSet<?>>() {
+          @Override
+          public SortedSet<?> createInstance(Type type) {
+            return new SubTreeSet<>();
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(SortedSet.class, sortedSetCreator).create();
+
+    Type sortedSetType = new TypeToken<SortedSet<String>>() {}.getType();
+    SortedSet<String> set = gson.fromJson("[\"a\"]", sortedSetType);
+    assertThat(set.first()).isEqualTo("a");
+    assertThat(set.getClass()).isEqualTo(SubTreeSet.class);
+
+    set = gson.fromJson("[\"b\"]", SortedSet.class);
+    assertThat(set.first()).isEqualTo("b");
+    assertThat(set.getClass()).isEqualTo(SubTreeSet.class);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/InterfaceTest.java b/gson/gson/src/test/java/com/google/gson/functional/InterfaceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4da40419aa087075560443b55fcedab3d8fc0fbf
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/InterfaceTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests involving interfaces.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class InterfaceTest {
+  private static final String OBJ_JSON = "{\"someStringValue\":\"StringValue\"}";
+
+  private Gson gson;
+  private TestObject obj;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+    obj = new TestObject("StringValue");
+  }
+
+  @Test
+  public void testSerializingObjectImplementingInterface() {
+    assertThat(gson.toJson(obj)).isEqualTo(OBJ_JSON);
+  }
+
+  @Test
+  public void testSerializingInterfaceObjectField() {
+    TestObjectWrapper objWrapper = new TestObjectWrapper(obj);
+    assertThat(gson.toJson(objWrapper)).isEqualTo("{\"obj\":" + OBJ_JSON + "}");
+  }
+
+  private static interface TestObjectInterface {
+    // Holder
+  }
+
+  private static class TestObject implements TestObjectInterface {
+    @SuppressWarnings("unused")
+    private String someStringValue;
+
+    private TestObject(String value) {
+      this.someStringValue = value;
+    }
+  }
+
+  private static class TestObjectWrapper {
+    @SuppressWarnings("unused")
+    private TestObjectInterface obj;
+
+    private TestObjectWrapper(TestObjectInterface obj) {
+      this.obj = obj;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java b/gson/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..775c6ffd86b43fa8d0e9721ba7ba7070a582a7b1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/InternationalizationTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for internationalized strings.
+ *
+ * @author Inderjeet Singh
+ */
+public class InternationalizationTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testStringsWithUnicodeChineseCharactersSerialization() {
+    String target = "\u597d\u597d\u597d";
+    String json = gson.toJson(target);
+    String expected = '"' + target + '"';
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testStringsWithUnicodeChineseCharactersDeserialization() {
+    String expected = "\u597d\u597d\u597d";
+    String json = '"' + expected + '"';
+    String actual = gson.fromJson(json, String.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testStringsWithUnicodeChineseCharactersEscapedDeserialization() {
+    String actual = gson.fromJson("'\\u597d\\u597d\\u597d'", String.class);
+    assertThat(actual).isEqualTo("\u597d\u597d\u597d");
+  }
+
+  @Test
+  public void testSupplementaryUnicodeSerialization() {
+    // Supplementary code point U+1F60A
+    String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+    String json = gson.toJson(supplementaryCodePoint);
+    assertThat(json).isEqualTo('"' + supplementaryCodePoint + '"');
+  }
+
+  @Test
+  public void testSupplementaryUnicodeDeserialization() {
+    // Supplementary code point U+1F60A
+    String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+    String actual = gson.fromJson('"' + supplementaryCodePoint + '"', String.class);
+    assertThat(actual).isEqualTo(supplementaryCodePoint);
+  }
+
+  @Test
+  public void testSupplementaryUnicodeEscapedDeserialization() {
+    // Supplementary code point U+1F60A
+    String supplementaryCodePoint = new String(new int[] {0x1F60A}, 0, 1);
+    String actual = gson.fromJson("\"\\uD83D\\uDE0A\"", String.class);
+    assertThat(actual).isEqualTo(supplementaryCodePoint);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..437fe8124ee6022b86441e4cb674fb0d6d6f3cab
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
@@ -0,0 +1,486 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+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
+    assertThat(gson.toJson(target)).isEqualTo("{\"name\":\"v1\",\"name1\":\"v2\"}");
+  }
+
+  @Test
+  public void testMultipleNamesDeserializedCorrectly() {
+    assertThat(gson.fromJson("{'name':'v1'}", RecordWithCustomNames.class).a).isEqualTo("v1");
+
+    // Both name1 and name2 gets deserialized to b
+    assertThat(gson.fromJson("{'name': 'v1', 'name1':'v11'}", RecordWithCustomNames.class).b)
+        .isEqualTo("v11");
+    assertThat(gson.fromJson("{'name': 'v1', 'name2':'v2'}", RecordWithCustomNames.class).b)
+        .isEqualTo("v2");
+    assertThat(gson.fromJson("{'name': 'v1', 'name3':'v3'}", RecordWithCustomNames.class).b)
+        .isEqualTo("v3");
+  }
+
+  @Test
+  public void testMultipleNamesInTheSameString() {
+    // The last value takes precedence
+    assertThat(
+            gson.fromJson(
+                    "{'name': 'foo', 'name1':'v1','name2':'v2','name3':'v3'}",
+                    RecordWithCustomNames.class)
+                .b)
+        .isEqualTo("v3");
+  }
+
+  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));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported");
+  }
+
+  @Test
+  public void testFieldNamingStrategy() {
+    record LocalRecord(int i) {}
+
+    Gson gson = new GsonBuilder().setFieldNamingStrategy(f -> f.getName() + "-custom").create();
+
+    assertThat(gson.toJson(new LocalRecord(1))).isEqualTo("{\"i-custom\":1}");
+    assertThat(gson.fromJson("{\"i-custom\":2}", LocalRecord.class)).isEqualTo(new LocalRecord(2));
+  }
+
+  @Test
+  public void testUnknownJsonProperty() {
+    record LocalRecord(int i) {}
+
+    // Unknown property 'x' should be ignored
+    assertThat(gson.fromJson("{\"i\":1,\"x\":2}", LocalRecord.class)).isEqualTo(new LocalRecord(1));
+  }
+
+  @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
+    assertThat(gson.fromJson(json, LocalRecord.class)).isEqualTo(new LocalRecord(2, null));
+  }
+
+  @Test
+  public void testConstructorRuns() {
+    record LocalRecord(String s) {
+      LocalRecord {
+        s = "custom-" + s;
+      }
+    }
+
+    LocalRecord deserialized = gson.fromJson("{\"s\": null}", LocalRecord.class);
+    assertThat(deserialized).isEqualTo(new LocalRecord(null));
+    assertThat(deserialized.s()).isEqualTo("custom-null");
+  }
+
+  /** Tests behavior when the canonical constructor throws an exception */
+  @Test
+  @SuppressWarnings("StaticAssignmentOfThrowable")
+  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) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "Failed to invoke constructor '"
+                  + LocalRecord.class.getName()
+                  + "(String)' with args [value]");
+      assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException);
+    }
+  }
+
+  @Test
+  public void testAccessorIsCalled() {
+    record LocalRecord(String s) {
+      @Override
+      public String s() {
+        return "accessor-value";
+      }
+    }
+
+    assertThat(gson.toJson(new LocalRecord(null))).isEqualTo("{\"s\":\"accessor-value\"}");
+  }
+
+  /** Tests behavior when a record accessor method throws an exception */
+  @Test
+  @SuppressWarnings("StaticAssignmentOfThrowable")
+  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) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo("Accessor method '" + LocalRecord.class.getName() + "#s()' threw exception");
+      assertThat(e).hasCauseThat().isSameInstanceAs(LocalRecord.thrownException);
+    }
+  }
+
+  /** Tests behavior for a record without components */
+  @Test
+  public void testEmptyRecord() {
+    record EmptyRecord() {}
+
+    assertThat(gson.toJson(new EmptyRecord())).isEqualTo("{}");
+    assertThat(gson.fromJson("{}", EmptyRecord.class)).isEqualTo(new EmptyRecord());
+  }
+
+  /**
+   * 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);
+    assertThat(adapter.toJson(null)).isEqualTo("null");
+    assertThat(adapter.fromJson("null")).isNull();
+  }
+
+  @Test
+  public void testPrimitiveDefaultValues() {
+    RecordWithPrimitives expected =
+        new RecordWithPrimitives("s", (byte) 0, (short) 0, 0, 0, 0, 0, '\0', false);
+    assertThat(gson.fromJson("{'aString': 's'}", RecordWithPrimitives.class)).isEqualTo(expected);
+  }
+
+  @Test
+  public void testPrimitiveJsonNullValue() {
+    String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
+    var e =
+        assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "null is not allowed as value for record component 'aByte' of primitive type; at path"
+                + " $.aByte");
+  }
+
+  /**
+   * 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));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "null is not allowed as value for record component 'aByte' of primitive type; at path"
+                + " $.aByte");
+  }
+
+  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) {}
+
+    assertThat(gson.fromJson("{\"i\":1}", LocalRecord.class)).isEqualTo(new LocalRecord(null, 1));
+  }
+
+  /**
+   * 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
+    assertThat(gson.toJson(new RecordWithStaticField())).isEqualTo("{}");
+
+    Gson gson =
+        new GsonBuilder()
+            // Include static fields
+            .excludeFieldsWithModifiers(0)
+            .create();
+
+    String json = gson.toJson(new RecordWithStaticField());
+    assertThat(json).isEqualTo("{\"s\":\"initial\"}");
+  }
+
+  /**
+   * 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);
+    assertThat(RecordWithStaticField.s).isEqualTo("initial");
+
+    Gson gson =
+        new GsonBuilder()
+            // Include static fields
+            .excludeFieldsWithModifiers(0)
+            .create();
+
+    String oldValue = RecordWithStaticField.s;
+    try {
+      RecordWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", RecordWithStaticField.class);
+      assertThat(obj).isNotNull();
+      // Currently record deserialization always ignores static fields
+      assertThat(RecordWithStaticField.s).isEqualTo("initial");
+    } finally {
+      RecordWithStaticField.s = oldValue;
+    }
+  }
+
+  private record RecordWithStaticField() {
+    @SuppressWarnings("NonFinalStaticField")
+    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));
+    assertThat(json).isEqualTo("{\"a\":1}");
+  }
+
+  @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();
+
+    assertThat(gson.toJson(new LocalRecord(1, 2, 3.0))).isEqualTo("{\"b\":2}");
+  }
+
+  @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) {}
+
+    assertThat(gson.toJson(new LocalRecord("a"))).isEqualTo("{\"s\":\"serializer-a\"}");
+    assertThat(gson.fromJson("{\"s\":\"a\"}", LocalRecord.class))
+        .isEqualTo(new LocalRecord("deserializer-a"));
+  }
+
+  @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));
+    assertThat(json).isEqualTo("{\"a\":1}");
+
+    var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1)));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "ReflectionAccessFilter does not permit using reflection for class "
+                + Blocked.class.getName()
+                + ". Register a TypeAdapter for this type or adjust the access filter.");
+  }
+
+  @Test
+  public void testReflectionFilterBlockInaccessible() {
+    Gson gson =
+        new GsonBuilder().addReflectionAccessFilter(c -> FilterResult.BLOCK_INACCESSIBLE).create();
+
+    var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1)));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not"
+                + " accessible and ReflectionAccessFilter does not permit making it accessible."
+                + " Register a TypeAdapter for the declaring type, adjust the access filter or"
+                + " increase the visibility of the element and its declaring type.");
+
+    exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not"
+                + " accessible and ReflectionAccessFilter does not permit making it accessible."
+                + " Register a TypeAdapter for the declaring type, adjust the access filter or"
+                + " increase the visibility of the element and its declaring type.");
+
+    assertThat(gson.toJson(new PublicRecord(1))).isEqualTo("{\"i\":1}");
+    assertThat(gson.fromJson("{\"i\":2}", PublicRecord.class)).isEqualTo(new PublicRecord(2));
+  }
+
+  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) {}
+
+    assertThat(gson.toJson(new LocalRecord(1), Record.class)).isEqualTo("{}");
+
+    var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Abstract classes can't be instantiated! Adjust the R8 configuration or register an"
+                + " InstanceCreator or a TypeAdapter for this type. Class name: java.lang.Record\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JavaUtilConcurrentAtomicTest.java b/gson/gson/src/test/java/com/google/gson/functional/JavaUtilConcurrentAtomicTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b6f639a9ee1c13891f9c73f39949bfc0ac4ae68
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JavaUtilConcurrentAtomicTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2015 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.LongSerializationPolicy;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicIntegerArray;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicLongArray;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional test for Json serialization and deserialization for classes in
+ * java.util.concurrent.atomic
+ */
+public class JavaUtilConcurrentAtomicTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testAtomicBoolean() {
+    AtomicBoolean target = gson.fromJson("true", AtomicBoolean.class);
+    assertThat(target.get()).isTrue();
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("true");
+  }
+
+  @Test
+  public void testAtomicInteger() {
+    AtomicInteger target = gson.fromJson("10", AtomicInteger.class);
+    assertThat(target.get()).isEqualTo(10);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("10");
+  }
+
+  @Test
+  public void testAtomicLong() {
+    AtomicLong target = gson.fromJson("10", AtomicLong.class);
+    assertThat(target.get()).isEqualTo(10);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("10");
+  }
+
+  @Test
+  public void testAtomicLongWithStringSerializationPolicy() {
+    Gson gson =
+        new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create();
+    AtomicLongHolder target = gson.fromJson("{'value':'10'}", AtomicLongHolder.class);
+    assertThat(target.value.get()).isEqualTo(10);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("{\"value\":\"10\"}");
+  }
+
+  @Test
+  public void testAtomicIntegerArray() {
+    AtomicIntegerArray target = gson.fromJson("[10, 13, 14]", AtomicIntegerArray.class);
+    assertThat(target.length()).isEqualTo(3);
+    assertThat(target.get(0)).isEqualTo(10);
+    assertThat(target.get(1)).isEqualTo(13);
+    assertThat(target.get(2)).isEqualTo(14);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("[10,13,14]");
+  }
+
+  @Test
+  public void testAtomicLongArray() {
+    AtomicLongArray target = gson.fromJson("[10, 13, 14]", AtomicLongArray.class);
+    assertThat(target.length()).isEqualTo(3);
+    assertThat(target.get(0)).isEqualTo(10);
+    assertThat(target.get(1)).isEqualTo(13);
+    assertThat(target.get(2)).isEqualTo(14);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("[10,13,14]");
+  }
+
+  @Test
+  public void testAtomicLongArrayWithStringSerializationPolicy() {
+    Gson gson =
+        new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create();
+    AtomicLongArray target = gson.fromJson("['10', '13', '14']", AtomicLongArray.class);
+    assertThat(target.length()).isEqualTo(3);
+    assertThat(target.get(0)).isEqualTo(10);
+    assertThat(target.get(1)).isEqualTo(13);
+    assertThat(target.get(2)).isEqualTo(14);
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("[\"10\",\"13\",\"14\"]");
+  }
+
+  private static class AtomicLongHolder {
+    AtomicLong value;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JavaUtilTest.java b/gson/gson/src/test/java/com/google/gson/functional/JavaUtilTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6936dd90aea0a8c85a74846130b8954b2803574a
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JavaUtilTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import java.util.Currency;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Functional test for Json serialization and deserialization for classes in java.util */
+public class JavaUtilTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testCurrency() {
+    CurrencyHolder target = gson.fromJson("{'value':'USD'}", CurrencyHolder.class);
+    assertThat(target.value.getCurrencyCode()).isEqualTo("USD");
+    String json = gson.toJson(target);
+    assertThat(json).isEqualTo("{\"value\":\"USD\"}");
+
+    // null handling
+    target = gson.fromJson("{'value':null}", CurrencyHolder.class);
+    assertThat(target.value).isNull();
+    assertThat(gson.toJson(target)).isEqualTo("{}");
+  }
+
+  private static class CurrencyHolder {
+    Currency value;
+  }
+
+  @Test
+  public void testProperties() {
+    Properties props = gson.fromJson("{'a':'v1','b':'v2'}", Properties.class);
+    assertThat(props.getProperty("a")).isEqualTo("v1");
+    assertThat(props.getProperty("b")).isEqualTo("v2");
+    String json = gson.toJson(props);
+    assertThat(json).contains("\"a\":\"v1\"");
+    assertThat(json).contains("\"b\":\"v2\"");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a765f13526edd20ef3d754fd7b58208f5aab674
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnClassesTest.java
@@ -0,0 +1,818 @@
+/*
+ * Copyright (C) 2014 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Splitter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Locale;
+import org.junit.Test;
+
+/** Functional tests for the {@link JsonAdapter} annotation on classes. */
+@SuppressWarnings("ClassNamedLikeTypeParameter") // for dummy classes A, B, ...
+public final class JsonAdapterAnnotationOnClassesTest {
+
+  @Test
+  public void testJsonAdapterInvoked() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new A("bar"));
+    assertThat(json).isEqualTo("\"jsonAdapter\"");
+
+    // Also invoke the JsonAdapter javadoc sample
+    json = gson.toJson(new User("Inderjeet", "Singh"));
+    assertThat(json).isEqualTo("{\"name\":\"Inderjeet Singh\"}");
+    User user = gson.fromJson("{'name':'Joel Leitch'}", User.class);
+    assertThat(user.firstName).isEqualTo("Joel");
+    assertThat(user.lastName).isEqualTo("Leitch");
+
+    json = gson.toJson(Foo.BAR);
+    assertThat(json).isEqualTo("\"bar\"");
+    Foo baz = gson.fromJson("\"baz\"", Foo.class);
+    assertThat(baz).isEqualTo(Foo.BAZ);
+  }
+
+  @Test
+  public void testJsonAdapterFactoryInvoked() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new C("bar"));
+    assertThat(json).isEqualTo("\"jsonAdapterFactory\"");
+    C c = gson.fromJson("\"bar\"", C.class);
+    assertThat(c.value).isEqualTo("jsonAdapterFactory");
+  }
+
+  @Test
+  public void testRegisteredAdapterOverridesJsonAdapter() {
+    TypeAdapter<A> typeAdapter =
+        new TypeAdapter<A>() {
+          @Override
+          public void write(JsonWriter out, A value) throws IOException {
+            out.value("registeredAdapter");
+          }
+
+          @Override
+          public A read(JsonReader in) throws IOException {
+            return new A(in.nextString());
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(A.class, typeAdapter).create();
+    String json = gson.toJson(new A("abcd"));
+    assertThat(json).isEqualTo("\"registeredAdapter\"");
+  }
+
+  /** The serializer overrides field adapter, but for deserializer the fieldAdapter is used. */
+  @Test
+  public void testRegisteredSerializerOverridesJsonAdapter() {
+    JsonSerializer<A> serializer =
+        new JsonSerializer<A>() {
+          @Override
+          public JsonElement serialize(A src, Type typeOfSrc, JsonSerializationContext context) {
+            return new JsonPrimitive("registeredSerializer");
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(A.class, serializer).create();
+    String json = gson.toJson(new A("abcd"));
+    assertThat(json).isEqualTo("\"registeredSerializer\"");
+    A target = gson.fromJson("abcd", A.class);
+    assertThat(target.value).isEqualTo("jsonAdapter");
+  }
+
+  /** The deserializer overrides Json adapter, but for serializer the jsonAdapter is used. */
+  @Test
+  public void testRegisteredDeserializerOverridesJsonAdapter() {
+    JsonDeserializer<A> deserializer =
+        new JsonDeserializer<A>() {
+          @Override
+          public A deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+              throws JsonParseException {
+            return new A("registeredDeserializer");
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(A.class, deserializer).create();
+    String json = gson.toJson(new A("abcd"));
+    assertThat(json).isEqualTo("\"jsonAdapter\"");
+    A target = gson.fromJson("abcd", A.class);
+    assertThat(target.value).isEqualTo("registeredDeserializer");
+  }
+
+  @Test
+  public void testIncorrectTypeAdapterFails() {
+    try {
+      String json = new Gson().toJson(new ClassWithIncorrectJsonAdapter("bar"));
+      fail(json);
+    } catch (ClassCastException expected) {
+    }
+  }
+
+  @Test
+  public void testSuperclassTypeAdapterNotInvoked() {
+    String json = new Gson().toJson(new B("bar"));
+    assertThat(json).doesNotContain("jsonAdapter");
+  }
+
+  @Test
+  public void testNullSafeObject() {
+    Gson gson = new Gson();
+    NullableClass fromJson = gson.fromJson("null", NullableClass.class);
+    assertThat(fromJson).isNull();
+
+    fromJson = gson.fromJson("\"ignored\"", NullableClass.class);
+    assertThat(fromJson).isNotNull();
+
+    String json = gson.toJson(null, NullableClass.class);
+    assertThat(json).isEqualTo("null");
+
+    json = gson.toJson(new NullableClass());
+    assertThat(json).isEqualTo("\"nullable\"");
+  }
+
+  /**
+   * Tests behavior when a {@link TypeAdapterFactory} registered with {@code @JsonAdapter} returns
+   * {@code null}, indicating that it cannot handle the type and Gson should try a different factory
+   * instead.
+   */
+  @Test
+  public void testFactoryReturningNull() {
+    Gson gson = new Gson();
+
+    assertThat(gson.fromJson("null", WithNullReturningFactory.class)).isNull();
+    assertThat(gson.toJson(null, WithNullReturningFactory.class)).isEqualTo("null");
+
+    TypeToken<WithNullReturningFactory<String>> stringTypeArg =
+        new TypeToken<WithNullReturningFactory<String>>() {};
+    WithNullReturningFactory<?> deserialized = gson.fromJson("\"a\"", stringTypeArg);
+    assertThat(deserialized.t).isEqualTo("custom-read:a");
+    assertThat(gson.fromJson("null", stringTypeArg)).isNull();
+    assertThat(gson.toJson(new WithNullReturningFactory<>("b"), stringTypeArg.getType()))
+        .isEqualTo("\"custom-write:b\"");
+    assertThat(gson.toJson(null, stringTypeArg.getType())).isEqualTo("null");
+
+    // Factory should return `null` for this type and Gson should fall back to reflection-based
+    // adapter
+    TypeToken<WithNullReturningFactory<Integer>> numberTypeArg =
+        new TypeToken<WithNullReturningFactory<Integer>>() {};
+    deserialized = gson.fromJson("{\"t\":1}", numberTypeArg);
+    assertThat(deserialized.t).isEqualTo(1);
+    assertThat(gson.toJson(new WithNullReturningFactory<>(2), numberTypeArg.getType()))
+        .isEqualTo("{\"t\":2}");
+  }
+
+  // Also set `nullSafe = true` to verify that this does not cause a NullPointerException if the
+  // factory would accidentally call `nullSafe()` on null adapter
+  @JsonAdapter(value = WithNullReturningFactory.NullReturningFactory.class, nullSafe = true)
+  private static class WithNullReturningFactory<T> {
+    T t;
+
+    public WithNullReturningFactory(T t) {
+      this.t = t;
+    }
+
+    static class NullReturningFactory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        // Don't handle raw (non-parameterized) type
+        if (type.getType() instanceof Class) {
+          return null;
+        }
+        ParameterizedType parameterizedType = (ParameterizedType) type.getType();
+        // Makes this test a bit more realistic by only conditionally returning null (instead of
+        // always)
+        if (parameterizedType.getActualTypeArguments()[0] != String.class) {
+          return null;
+        }
+
+        @SuppressWarnings("unchecked")
+        TypeAdapter<T> adapter =
+            (TypeAdapter<T>)
+                new TypeAdapter<WithNullReturningFactory<String>>() {
+                  @Override
+                  public void write(JsonWriter out, WithNullReturningFactory<String> value)
+                      throws IOException {
+                    out.value("custom-write:" + value.t);
+                  }
+
+                  @Override
+                  public WithNullReturningFactory<String> read(JsonReader in) throws IOException {
+                    return new WithNullReturningFactory<>("custom-read:" + in.nextString());
+                  }
+                };
+        return adapter;
+      }
+    }
+  }
+
+  @JsonAdapter(A.JsonAdapter.class)
+  private static class A {
+    final String value;
+
+    A(String value) {
+      this.value = value;
+    }
+
+    static final class JsonAdapter extends TypeAdapter<A> {
+      @Override
+      public void write(JsonWriter out, A value) throws IOException {
+        out.value("jsonAdapter");
+      }
+
+      @Override
+      public A read(JsonReader in) throws IOException {
+        in.nextString();
+        return new A("jsonAdapter");
+      }
+    }
+  }
+
+  @JsonAdapter(C.JsonAdapterFactory.class)
+  private static class C {
+    final String value;
+
+    C(String value) {
+      this.value = value;
+    }
+
+    static final class JsonAdapterFactory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+        return new TypeAdapter<T>() {
+          @Override
+          public void write(JsonWriter out, T value) throws IOException {
+            out.value("jsonAdapterFactory");
+          }
+
+          @SuppressWarnings("unchecked")
+          @Override
+          public T read(JsonReader in) throws IOException {
+            in.nextString();
+            return (T) new C("jsonAdapterFactory");
+          }
+        };
+      }
+    }
+  }
+
+  private static final class B extends A {
+    B(String value) {
+      super(value);
+    }
+  }
+
+  // Note that the type is NOT TypeAdapter<ClassWithIncorrectJsonAdapter> so this
+  // should cause error
+  @JsonAdapter(A.JsonAdapter.class)
+  private static final class ClassWithIncorrectJsonAdapter {
+    @SuppressWarnings("unused")
+    final String value;
+
+    ClassWithIncorrectJsonAdapter(String value) {
+      this.value = value;
+    }
+  }
+
+  // This class is used in JsonAdapter Javadoc as an example
+  @JsonAdapter(UserJsonAdapter.class)
+  private static class User {
+    final String firstName;
+    final String lastName;
+
+    User(String firstName, String lastName) {
+      this.firstName = firstName;
+      this.lastName = lastName;
+    }
+  }
+
+  private static class UserJsonAdapter extends TypeAdapter<User> {
+    @Override
+    public void write(JsonWriter out, User user) throws IOException {
+      // implement write: combine firstName and lastName into name
+      out.beginObject();
+      out.name("name");
+      out.value(user.firstName + " " + user.lastName);
+      out.endObject();
+    }
+
+    @Override
+    public User read(JsonReader in) throws IOException {
+      // implement read: split name into firstName and lastName
+      in.beginObject();
+      in.nextName();
+      List<String> nameParts = Splitter.on(" ").splitToList(in.nextString());
+      in.endObject();
+      return new User(nameParts.get(0), nameParts.get(1));
+    }
+  }
+
+  // Implicit `nullSafe=true`
+  @JsonAdapter(value = NullableClassJsonAdapter.class)
+  private static class NullableClass {}
+
+  private static class NullableClassJsonAdapter extends TypeAdapter<NullableClass> {
+    @Override
+    public void write(JsonWriter out, NullableClass value) throws IOException {
+      out.value("nullable");
+    }
+
+    @Override
+    public NullableClass read(JsonReader in) throws IOException {
+      in.nextString();
+      return new NullableClass();
+    }
+  }
+
+  @JsonAdapter(FooJsonAdapter.class)
+  private static enum Foo {
+    BAR,
+    BAZ
+  }
+
+  private static class FooJsonAdapter extends TypeAdapter<Foo> {
+    @Override
+    public void write(JsonWriter out, Foo value) throws IOException {
+      out.value(value.name().toLowerCase(Locale.US));
+    }
+
+    @Override
+    public Foo read(JsonReader in) throws IOException {
+      return Foo.valueOf(in.nextString().toUpperCase(Locale.US));
+    }
+  }
+
+  @Test
+  public void testIncorrectJsonAdapterType() {
+    try {
+      new Gson().toJson(new D());
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @JsonAdapter(Integer.class)
+  private static final class D {
+    @SuppressWarnings("unused")
+    final String value = "a";
+  }
+
+  /**
+   * Verifies that {@link TypeAdapterFactory} specified by {@code @JsonAdapter} can call {@link
+   * Gson#getDelegateAdapter} without any issues, despite the factory not being directly registered
+   * on Gson.
+   */
+  @Test
+  public void testDelegatingAdapterFactory() {
+    @SuppressWarnings("unchecked")
+    WithDelegatingFactory<String> deserialized =
+        new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("de");
+
+    deserialized =
+        new Gson()
+            .fromJson(
+                "{\"custom\":{\"f\":\"de\"}}", new TypeToken<WithDelegatingFactory<String>>() {});
+    assertThat(deserialized.f).isEqualTo("de");
+
+    WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
+    assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
+  }
+
+  @JsonAdapter(WithDelegatingFactory.Factory.class)
+  private static class WithDelegatingFactory<T> {
+    T f;
+
+    WithDelegatingFactory(T f) {
+      this.f = f;
+    }
+
+    static class Factory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        if (type.getRawType() != WithDelegatingFactory.class) {
+          return null;
+        }
+
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+
+        return new TypeAdapter<T>() {
+          @Override
+          public T read(JsonReader in) throws IOException {
+            // Perform custom deserialization
+            in.beginObject();
+            assertThat(in.nextName()).isEqualTo("custom");
+            T t = delegate.read(in);
+            in.endObject();
+
+            return t;
+          }
+
+          @Override
+          public void write(JsonWriter out, T value) throws IOException {
+            // Perform custom serialization
+            out.beginObject();
+            out.name("custom");
+            delegate.write(out, value);
+            out.endObject();
+          }
+        };
+      }
+    }
+  }
+
+  /**
+   * Similar to {@link #testDelegatingAdapterFactory}, except that the delegate is not looked up in
+   * {@code create} but instead in the adapter methods.
+   */
+  @Test
+  public void testDelegatingAdapterFactory_Delayed() {
+    WithDelayedDelegatingFactory deserialized =
+        new Gson().fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelayedDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("de");
+
+    WithDelayedDelegatingFactory serialized = new WithDelayedDelegatingFactory("se");
+    assertThat(new Gson().toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
+  }
+
+  @JsonAdapter(WithDelayedDelegatingFactory.Factory.class)
+  private static class WithDelayedDelegatingFactory {
+    String f;
+
+    WithDelayedDelegatingFactory(String f) {
+      this.f = f;
+    }
+
+    static class Factory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        return new TypeAdapter<T>() {
+          // suppress Error Prone warning; should be clear that `Factory` refers to enclosing class
+          @SuppressWarnings("SameNameButDifferent")
+          private TypeAdapter<T> delegate() {
+            return gson.getDelegateAdapter(Factory.this, type);
+          }
+
+          @Override
+          public T read(JsonReader in) throws IOException {
+            // Perform custom deserialization
+            in.beginObject();
+            assertThat(in.nextName()).isEqualTo("custom");
+            T t = delegate().read(in);
+            in.endObject();
+
+            return t;
+          }
+
+          @Override
+          public void write(JsonWriter out, T value) throws IOException {
+            // Perform custom serialization
+            out.beginObject();
+            out.name("custom");
+            delegate().write(out, value);
+            out.endObject();
+          }
+        };
+      }
+    }
+  }
+
+  /**
+   * Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
+   * factory class are used; one registered on the {@code GsonBuilder} and the other implicitly
+   * through {@code @JsonAdapter}.
+   */
+  @Test
+  public void testDelegating_SameFactoryClass() {
+    Gson gson =
+        new GsonBuilder().registerTypeAdapterFactory(new WithDelegatingFactory.Factory()).create();
+
+    // Should use both factories, and therefore have `{"custom": ... }` twice
+    WithDelegatingFactory<?> deserialized =
+        gson.fromJson("{\"custom\":{\"custom\":{\"f\":\"de\"}}}", WithDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("de");
+
+    WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
+    assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"custom\":{\"f\":\"se\"}}}");
+  }
+
+  /**
+   * Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory is
+   * used (through {@link InstanceCreator}).
+   *
+   * <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
+   * to verify that Gson behaves reasonable, mainly that it does not cause a {@link
+   * StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
+   * behavior.
+   */
+  @Test
+  public void testDelegating_SameFactoryInstance() {
+    WithDelegatingFactory.Factory factory = new WithDelegatingFactory.Factory();
+
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapterFactory(factory)
+            // Always provides same instance for factory
+            .registerTypeAdapter(
+                WithDelegatingFactory.Factory.class, (InstanceCreator<?>) type -> factory)
+            .create();
+
+    // Current Gson.getDelegateAdapter implementation cannot tell when call is related to
+    // @JsonAdapter or not, it can only work based on the `skipPast` factory, so if the same factory
+    // instance is used the one registered with `GsonBuilder.registerTypeAdapterFactory` actually
+    // skips past the @JsonAdapter one, so the JSON string is `{"custom": ...}` instead of
+    // `{"custom":{"custom":...}}`
+    WithDelegatingFactory<?> deserialized =
+        gson.fromJson("{\"custom\":{\"f\":\"de\"}}", WithDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("de");
+
+    WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>("se");
+    assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":\"se\"}}");
+  }
+
+  /**
+   * Tests behavior of {@link Gson#getDelegateAdapter} when <i>different</i> instances of the same
+   * factory class are used; one specified with {@code @JsonAdapter} on a class, and the other
+   * specified with {@code @JsonAdapter} on a field of that class.
+   *
+   * <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
+   * to verify that Gson behaves reasonable, mainly that it does not cause a {@link
+   * StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
+   * behavior.
+   */
+  @Test
+  public void testDelegating_SameFactoryClass_OnClassAndField() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                String.class,
+                new TypeAdapter<String>() {
+                  @Override
+                  public String read(JsonReader in) throws IOException {
+                    return in.nextString() + "-str";
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, String value) throws IOException {
+                    out.value(value + "-str");
+                  }
+                })
+            .create();
+
+    // Should use both factories, and therefore have `{"custom": ... }` once for class and once for
+    // the field, and for field also properly delegate to custom String adapter defined above
+    WithDelegatingFactoryOnClassAndField deserialized =
+        gson.fromJson(
+            "{\"custom\":{\"f\":{\"custom\":\"de\"}}}", WithDelegatingFactoryOnClassAndField.class);
+    assertThat(deserialized.f).isEqualTo("de-str");
+
+    WithDelegatingFactoryOnClassAndField serialized =
+        new WithDelegatingFactoryOnClassAndField("se");
+    assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
+  }
+
+  /**
+   * Tests behavior of {@link Gson#getDelegateAdapter} when the <i>same</i> instance of a factory is
+   * used (through {@link InstanceCreator}); specified with {@code @JsonAdapter} on a class, and
+   * also specified with {@code @JsonAdapter} on a field of that class.
+   *
+   * <p><b>Important:</b> This situation is likely a rare corner case; the purpose of this test is
+   * to verify that Gson behaves reasonable, mainly that it does not cause a {@link
+   * StackOverflowError} due to infinite recursion. This test is not intended to dictate an expected
+   * behavior.
+   */
+  @Test
+  public void testDelegating_SameFactoryInstance_OnClassAndField() {
+    WithDelegatingFactoryOnClassAndField.Factory factory =
+        new WithDelegatingFactoryOnClassAndField.Factory();
+
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                String.class,
+                new TypeAdapter<String>() {
+                  @Override
+                  public String read(JsonReader in) throws IOException {
+                    return in.nextString() + "-str";
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, String value) throws IOException {
+                    out.value(value + "-str");
+                  }
+                })
+            // Always provides same instance for factory
+            .registerTypeAdapter(
+                WithDelegatingFactoryOnClassAndField.Factory.class,
+                (InstanceCreator<?>) type -> factory)
+            .create();
+
+    // Because field type (`String`) differs from declaring class,
+    // JsonAdapterAnnotationTypeAdapterFactory does not confuse factories and this behaves as
+    // expected: Both the declaring class and the field each have `{"custom": ...}` and delegation
+    // for the field to the custom String adapter defined above works properly
+    WithDelegatingFactoryOnClassAndField deserialized =
+        gson.fromJson(
+            "{\"custom\":{\"f\":{\"custom\":\"de\"}}}", WithDelegatingFactoryOnClassAndField.class);
+    assertThat(deserialized.f).isEqualTo("de-str");
+
+    WithDelegatingFactoryOnClassAndField serialized =
+        new WithDelegatingFactoryOnClassAndField("se");
+    assertThat(gson.toJson(serialized)).isEqualTo("{\"custom\":{\"f\":{\"custom\":\"se-str\"}}}");
+  }
+
+  // Same factory class specified on class and one of its fields
+  @JsonAdapter(WithDelegatingFactoryOnClassAndField.Factory.class)
+  private static class WithDelegatingFactoryOnClassAndField {
+    // suppress Error Prone warning; should be clear that `Factory` refers to nested class
+    @SuppressWarnings("SameNameButDifferent")
+    @JsonAdapter(Factory.class)
+    String f;
+
+    WithDelegatingFactoryOnClassAndField(String f) {
+      this.f = f;
+    }
+
+    static class Factory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
+
+        return new TypeAdapter<T>() {
+          @Override
+          public T read(JsonReader in) throws IOException {
+            // Perform custom deserialization
+            in.beginObject();
+            assertThat(in.nextName()).isEqualTo("custom");
+            T t = delegate.read(in);
+            in.endObject();
+
+            return t;
+          }
+
+          @Override
+          public void write(JsonWriter out, T value) throws IOException {
+            // Perform custom serialization
+            out.beginObject();
+            out.name("custom");
+            delegate.write(out, value);
+            out.endObject();
+          }
+        };
+      }
+    }
+  }
+
+  /** Tests usage of {@link JsonSerializer} as {@link JsonAdapter} value */
+  @Test
+  public void testJsonSerializer() {
+    Gson gson = new Gson();
+    // Verify that delegate deserializer (reflection deserializer) is used
+    WithJsonSerializer deserialized = gson.fromJson("{\"f\":\"test\"}", WithJsonSerializer.class);
+    assertThat(deserialized.f).isEqualTo("test");
+
+    String json = gson.toJson(new WithJsonSerializer());
+    // Uses custom serializer which always returns `true`
+    assertThat(json).isEqualTo("true");
+  }
+
+  @JsonAdapter(WithJsonSerializer.Serializer.class)
+  private static class WithJsonSerializer {
+    String f = "";
+
+    static class Serializer implements JsonSerializer<WithJsonSerializer> {
+      @Override
+      public JsonElement serialize(
+          WithJsonSerializer src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(true);
+      }
+    }
+  }
+
+  /** Tests usage of {@link JsonDeserializer} as {@link JsonAdapter} value */
+  @Test
+  public void testJsonDeserializer() {
+    Gson gson = new Gson();
+    WithJsonDeserializer deserialized =
+        gson.fromJson("{\"f\":\"test\"}", WithJsonDeserializer.class);
+    // Uses custom deserializer which always uses "123" as field value
+    assertThat(deserialized.f).isEqualTo("123");
+
+    // Verify that delegate serializer (reflection serializer) is used
+    String json = gson.toJson(new WithJsonDeserializer("abc"));
+    assertThat(json).isEqualTo("{\"f\":\"abc\"}");
+  }
+
+  @JsonAdapter(WithJsonDeserializer.Deserializer.class)
+  private static class WithJsonDeserializer {
+    String f;
+
+    WithJsonDeserializer(String f) {
+      this.f = f;
+    }
+
+    static class Deserializer implements JsonDeserializer<WithJsonDeserializer> {
+      @Override
+      public WithJsonDeserializer deserialize(
+          JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+        return new WithJsonDeserializer("123");
+      }
+    }
+  }
+
+  /**
+   * Tests creation of the adapter referenced by {@code @JsonAdapter} using an {@link
+   * InstanceCreator}.
+   */
+  @Test
+  public void testAdapterCreatedByInstanceCreator() {
+    CreatedByInstanceCreator.Serializer serializer =
+        new CreatedByInstanceCreator.Serializer("custom");
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                CreatedByInstanceCreator.Serializer.class, (InstanceCreator<?>) t -> serializer)
+            .create();
+
+    String json = gson.toJson(new CreatedByInstanceCreator());
+    assertThat(json).isEqualTo("\"custom\"");
+  }
+
+  @JsonAdapter(CreatedByInstanceCreator.Serializer.class)
+  private static class CreatedByInstanceCreator {
+    static class Serializer implements JsonSerializer<CreatedByInstanceCreator> {
+      private final String value;
+
+      @SuppressWarnings("unused")
+      public Serializer() {
+        throw new AssertionError("should not be called");
+      }
+
+      public Serializer(String value) {
+        this.value = value;
+      }
+
+      @Override
+      public JsonElement serialize(
+          CreatedByInstanceCreator src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(value);
+      }
+    }
+  }
+
+  /** Tests creation of the adapter referenced by {@code @JsonAdapter} using JDK Unsafe. */
+  @Test
+  public void testAdapterCreatedByJdkUnsafe() {
+    String json = new Gson().toJson(new CreatedByJdkUnsafe());
+    assertThat(json).isEqualTo("false");
+  }
+
+  @JsonAdapter(CreatedByJdkUnsafe.Serializer.class)
+  private static class CreatedByJdkUnsafe {
+    static class Serializer implements JsonSerializer<CreatedByJdkUnsafe> {
+      // JDK Unsafe leaves this at default value `false`
+      private boolean wasInitialized = true;
+
+      // Explicit constructor with args to remove implicit no-args constructor
+      @SuppressWarnings("unused")
+      public Serializer(int i) {
+        throw new AssertionError("should not be called");
+      }
+
+      @Override
+      public JsonElement serialize(
+          CreatedByJdkUnsafe src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(wasInitialized);
+      }
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a5d355f05431a8c8b9eb1248a4ecda2debdf059b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterAnnotationOnFieldsTest.java
@@ -0,0 +1,743 @@
+/*
+ * Copyright (C) 2014 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 com.google.common.truth.Truth.assertThat;
+
+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.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+
+/** Functional tests for the {@link JsonAdapter} annotation on fields. */
+public final class JsonAdapterAnnotationOnFieldsTest {
+  @Test
+  public void testClassAnnotationAdapterTakesPrecedenceOverDefault() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new Computer(new User("Inderjeet Singh")));
+    assertThat(json).isEqualTo("{\"user\":\"UserClassAnnotationAdapter\"}");
+    Computer computer = gson.fromJson("{'user':'Inderjeet Singh'}", Computer.class);
+    assertThat(computer.user.name).isEqualTo("UserClassAnnotationAdapter");
+  }
+
+  @Test
+  public void testClassAnnotationAdapterFactoryTakesPrecedenceOverDefault() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new Gizmo(new Part("Part")));
+    assertThat(json).isEqualTo("{\"part\":\"GizmoPartTypeAdapterFactory\"}");
+    Gizmo computer = gson.fromJson("{'part':'Part'}", Gizmo.class);
+    assertThat(computer.part.name).isEqualTo("GizmoPartTypeAdapterFactory");
+  }
+
+  @Test
+  public void testRegisteredTypeAdapterTakesPrecedenceOverClassAnnotationAdapter() {
+    Gson gson =
+        new GsonBuilder().registerTypeAdapter(User.class, new RegisteredUserAdapter()).create();
+    String json = gson.toJson(new Computer(new User("Inderjeet Singh")));
+    assertThat(json).isEqualTo("{\"user\":\"RegisteredUserAdapter\"}");
+    Computer computer = gson.fromJson("{'user':'Inderjeet Singh'}", Computer.class);
+    assertThat(computer.user.name).isEqualTo("RegisteredUserAdapter");
+  }
+
+  @Test
+  public void testFieldAnnotationTakesPrecedenceOverRegisteredTypeAdapter() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Part.class,
+                new TypeAdapter<Part>() {
+                  @Override
+                  public void write(JsonWriter out, Part part) {
+                    throw new AssertionError();
+                  }
+
+                  @Override
+                  public Part read(JsonReader in) {
+                    throw new AssertionError();
+                  }
+                })
+            .create();
+    String json = gson.toJson(new Gadget(new Part("screen")));
+    assertThat(json).isEqualTo("{\"part\":\"PartJsonFieldAnnotationAdapter\"}");
+    Gadget gadget = gson.fromJson("{'part':'screen'}", Gadget.class);
+    assertThat(gadget.part.name).isEqualTo("PartJsonFieldAnnotationAdapter");
+  }
+
+  @Test
+  public void testFieldAnnotationTakesPrecedenceOverClassAnnotation() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new Computer2(new User("Inderjeet Singh")));
+    assertThat(json).isEqualTo("{\"user\":\"UserFieldAnnotationAdapter\"}");
+    Computer2 target = gson.fromJson("{'user':'Interjeet Singh'}", Computer2.class);
+    assertThat(target.user.name).isEqualTo("UserFieldAnnotationAdapter");
+  }
+
+  private static final class Gadget {
+    @JsonAdapter(PartJsonFieldAnnotationAdapter.class)
+    final Part part;
+
+    Gadget(Part part) {
+      this.part = part;
+    }
+  }
+
+  private static final class Gizmo {
+    @JsonAdapter(GizmoPartTypeAdapterFactory.class)
+    final Part part;
+
+    Gizmo(Part part) {
+      this.part = part;
+    }
+  }
+
+  private static final class Part {
+    final String name;
+
+    public Part(String name) {
+      this.name = name;
+    }
+  }
+
+  private static class PartJsonFieldAnnotationAdapter extends TypeAdapter<Part> {
+    @Override
+    public void write(JsonWriter out, Part part) throws IOException {
+      out.value("PartJsonFieldAnnotationAdapter");
+    }
+
+    @Override
+    public Part read(JsonReader in) throws IOException {
+      String unused = in.nextString();
+      return new Part("PartJsonFieldAnnotationAdapter");
+    }
+  }
+
+  private static class GizmoPartTypeAdapterFactory implements TypeAdapterFactory {
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+      return new TypeAdapter<T>() {
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+          out.value("GizmoPartTypeAdapterFactory");
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public T read(JsonReader in) throws IOException {
+          String unused = in.nextString();
+          return (T) new Part("GizmoPartTypeAdapterFactory");
+        }
+      };
+    }
+  }
+
+  private static final class Computer {
+    final User user;
+
+    Computer(User user) {
+      this.user = user;
+    }
+  }
+
+  @JsonAdapter(UserClassAnnotationAdapter.class)
+  private static class User {
+    public final String name;
+
+    private User(String name) {
+      this.name = name;
+    }
+  }
+
+  private static class UserClassAnnotationAdapter extends TypeAdapter<User> {
+    @Override
+    public void write(JsonWriter out, User user) throws IOException {
+      out.value("UserClassAnnotationAdapter");
+    }
+
+    @Override
+    public User read(JsonReader in) throws IOException {
+      String unused = in.nextString();
+      return new User("UserClassAnnotationAdapter");
+    }
+  }
+
+  private static final class Computer2 {
+    // overrides the JsonAdapter annotation of User with this
+    @JsonAdapter(UserFieldAnnotationAdapter.class)
+    final User user;
+
+    Computer2(User user) {
+      this.user = user;
+    }
+  }
+
+  private static final class UserFieldAnnotationAdapter extends TypeAdapter<User> {
+    @Override
+    public void write(JsonWriter out, User user) throws IOException {
+      out.value("UserFieldAnnotationAdapter");
+    }
+
+    @Override
+    public User read(JsonReader in) throws IOException {
+      String unused = in.nextString();
+      return new User("UserFieldAnnotationAdapter");
+    }
+  }
+
+  private static final class RegisteredUserAdapter extends TypeAdapter<User> {
+    @Override
+    public void write(JsonWriter out, User user) throws IOException {
+      out.value("RegisteredUserAdapter");
+    }
+
+    @Override
+    public User read(JsonReader in) throws IOException {
+      String unused = in.nextString();
+      return new User("RegisteredUserAdapter");
+    }
+  }
+
+  @Test
+  public void testJsonAdapterInvokedOnlyForAnnotatedFields() {
+    Gson gson = new Gson();
+    String json = "{'part1':'name','part2':{'name':'name2'}}";
+    GadgetWithTwoParts gadget = gson.fromJson(json, GadgetWithTwoParts.class);
+    assertThat(gadget.part1.name).isEqualTo("PartJsonFieldAnnotationAdapter");
+    assertThat(gadget.part2.name).isEqualTo("name2");
+  }
+
+  private static final class GadgetWithTwoParts {
+    @JsonAdapter(PartJsonFieldAnnotationAdapter.class)
+    final Part part1;
+
+    final Part part2; // Doesn't have the JsonAdapter annotation
+
+    @SuppressWarnings("unused")
+    GadgetWithTwoParts(Part part1, Part part2) {
+      this.part1 = part1;
+      this.part2 = part2;
+    }
+  }
+
+  @Test
+  public void testJsonAdapterWrappedInNullSafeAsRequested() {
+    Gson gson = new Gson();
+    String fromJson = "{'part':null}";
+
+    GadgetWithOptionalPart gadget = gson.fromJson(fromJson, GadgetWithOptionalPart.class);
+    assertThat(gadget.part).isNull();
+
+    String toJson = gson.toJson(gadget);
+    assertThat(toJson).doesNotContain("PartJsonFieldAnnotationAdapter");
+  }
+
+  private static final class GadgetWithOptionalPart {
+    @JsonAdapter(value = PartJsonFieldAnnotationAdapter.class)
+    final Part part;
+
+    private GadgetWithOptionalPart(Part part) {
+      this.part = part;
+    }
+  }
+
+  /** Regression test contributed through https://github.com/google/gson/issues/831 */
+  @Test
+  public void testNonPrimitiveFieldAnnotationTakesPrecedenceOverDefault() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new GadgetWithOptionalPart(new Part("foo")));
+    assertThat(json).isEqualTo("{\"part\":\"PartJsonFieldAnnotationAdapter\"}");
+    GadgetWithOptionalPart gadget = gson.fromJson("{'part':'foo'}", GadgetWithOptionalPart.class);
+    assertThat(gadget.part.name).isEqualTo("PartJsonFieldAnnotationAdapter");
+  }
+
+  /** Regression test contributed through https://github.com/google/gson/issues/831 */
+  @Test
+  public void testPrimitiveFieldAnnotationTakesPrecedenceOverDefault() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new GadgetWithPrimitivePart(42));
+    assertThat(json).isEqualTo("{\"part\":\"42\"}");
+    GadgetWithPrimitivePart gadget = gson.fromJson(json, GadgetWithPrimitivePart.class);
+    assertThat(gadget.part).isEqualTo(42);
+  }
+
+  private static final class GadgetWithPrimitivePart {
+    @JsonAdapter(LongToStringTypeAdapterFactory.class)
+    final long part;
+
+    private GadgetWithPrimitivePart(long part) {
+      this.part = part;
+    }
+  }
+
+  private static final class LongToStringTypeAdapterFactory implements TypeAdapterFactory {
+    static final TypeAdapter<Long> ADAPTER =
+        new TypeAdapter<Long>() {
+          @Override
+          public void write(JsonWriter out, Long value) throws IOException {
+            out.value(value.toString());
+          }
+
+          @Override
+          public Long read(JsonReader in) throws IOException {
+            return in.nextLong();
+          }
+        };
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+      Class<?> cls = type.getRawType();
+      if (Long.class.isAssignableFrom(cls)) {
+        return (TypeAdapter<T>) ADAPTER;
+      } else if (long.class.isAssignableFrom(cls)) {
+        return (TypeAdapter<T>) ADAPTER;
+      }
+      throw new IllegalStateException(
+          "Non-long field of type "
+              + type
+              + " annotated with @JsonAdapter(LongToStringTypeAdapterFactory.class)");
+    }
+  }
+
+  @Test
+  public void testFieldAnnotationWorksForParameterizedType() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new Gizmo2(Arrays.asList(new Part("Part"))));
+    assertThat(json).isEqualTo("{\"part\":\"GizmoPartTypeAdapterFactory\"}");
+    Gizmo2 computer = gson.fromJson("{'part':'Part'}", Gizmo2.class);
+    assertThat(computer.part.get(0).name).isEqualTo("GizmoPartTypeAdapterFactory");
+  }
+
+  private static final class Gizmo2 {
+    @JsonAdapter(Gizmo2PartTypeAdapterFactory.class)
+    List<Part> part;
+
+    Gizmo2(List<Part> part) {
+      this.part = part;
+    }
+  }
+
+  private static class Gizmo2PartTypeAdapterFactory implements TypeAdapterFactory {
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+      return new TypeAdapter<T>() {
+        @Override
+        public void write(JsonWriter out, T value) throws IOException {
+          out.value("GizmoPartTypeAdapterFactory");
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public T read(JsonReader in) throws IOException {
+          String unused = in.nextString();
+          return (T) Arrays.asList(new Part("GizmoPartTypeAdapterFactory"));
+        }
+      };
+    }
+  }
+
+  /**
+   * Verify that {@link JsonAdapter} annotation can overwrite adapters which can normally not be
+   * overwritten (in this case adapter for {@link JsonElement}).
+   */
+  @Test
+  public void testOverwriteBuiltIn() {
+    BuiltInOverwriting obj = new BuiltInOverwriting();
+    obj.f = new JsonPrimitive(true);
+    String json = new Gson().toJson(obj);
+    assertThat(json).isEqualTo("{\"f\":\"" + JsonElementAdapter.SERIALIZED + "\"}");
+
+    BuiltInOverwriting deserialized = new Gson().fromJson("{\"f\": 2}", BuiltInOverwriting.class);
+    assertThat(deserialized.f).isEqualTo(JsonElementAdapter.DESERIALIZED);
+  }
+
+  private static class BuiltInOverwriting {
+    @JsonAdapter(JsonElementAdapter.class)
+    JsonElement f;
+  }
+
+  private static class JsonElementAdapter extends TypeAdapter<JsonElement> {
+    static final JsonPrimitive DESERIALIZED = new JsonPrimitive("deserialized hardcoded");
+
+    @Override
+    public JsonElement read(JsonReader in) throws IOException {
+      in.skipValue();
+      return DESERIALIZED;
+    }
+
+    static final String SERIALIZED = "serialized hardcoded";
+
+    @Override
+    public void write(JsonWriter out, JsonElement value) throws IOException {
+      out.value(SERIALIZED);
+    }
+  }
+
+  /**
+   * Verify that exclusion strategy preventing serialization has higher precedence than {@link
+   * JsonAdapter} annotation.
+   */
+  @Test
+  public void testExcludeSerializePrecedence() {
+    Gson gson =
+        new GsonBuilder()
+            .addSerializationExclusionStrategy(
+                new ExclusionStrategy() {
+                  @Override
+                  public boolean shouldSkipField(FieldAttributes f) {
+                    return true;
+                  }
+
+                  @Override
+                  public boolean shouldSkipClass(Class<?> clazz) {
+                    return false;
+                  }
+                })
+            .create();
+
+    DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
+    obj.f = 1;
+    obj.f2 = new JsonPrimitive(2);
+    obj.f3 = new JsonPrimitive(true);
+    String json = gson.toJson(obj);
+    assertThat(json).isEqualTo("{}");
+
+    DelegatingAndOverwriting deserialized =
+        gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
+    assertThat(deserialized.f).isEqualTo(Integer.valueOf(1));
+    assertThat(deserialized.f2).isEqualTo(new JsonPrimitive(2));
+    // Verify that for deserialization type adapter specified by @JsonAdapter is used
+    assertThat(deserialized.f3).isEqualTo(JsonElementAdapter.DESERIALIZED);
+  }
+
+  /**
+   * Verify that exclusion strategy preventing deserialization has higher precedence than {@link
+   * JsonAdapter} annotation.
+   */
+  @Test
+  public void testExcludeDeserializePrecedence() {
+    Gson gson =
+        new GsonBuilder()
+            .addDeserializationExclusionStrategy(
+                new ExclusionStrategy() {
+                  @Override
+                  public boolean shouldSkipField(FieldAttributes f) {
+                    return true;
+                  }
+
+                  @Override
+                  public boolean shouldSkipClass(Class<?> clazz) {
+                    return false;
+                  }
+                })
+            .create();
+
+    DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
+    obj.f = 1;
+    obj.f2 = new JsonPrimitive(2);
+    obj.f3 = new JsonPrimitive(true);
+    String json = gson.toJson(obj);
+    // Verify that for serialization type adapters specified by @JsonAdapter are used
+    assertThat(json)
+        .isEqualTo("{\"f\":1,\"f2\":2,\"f3\":\"" + JsonElementAdapter.SERIALIZED + "\"}");
+
+    DelegatingAndOverwriting deserialized =
+        gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
+    assertThat(deserialized.f).isNull();
+    assertThat(deserialized.f2).isNull();
+    assertThat(deserialized.f3).isNull();
+  }
+
+  /**
+   * Verify that exclusion strategy preventing serialization and deserialization has higher
+   * precedence than {@link JsonAdapter} annotation.
+   *
+   * <p>This is a separate test method because {@link ReflectiveTypeAdapterFactory} handles this
+   * case differently.
+   */
+  @Test
+  public void testExcludePrecedence() {
+    Gson gson =
+        new GsonBuilder()
+            .setExclusionStrategies(
+                new ExclusionStrategy() {
+                  @Override
+                  public boolean shouldSkipField(FieldAttributes f) {
+                    return true;
+                  }
+
+                  @Override
+                  public boolean shouldSkipClass(Class<?> clazz) {
+                    return false;
+                  }
+                })
+            .create();
+
+    DelegatingAndOverwriting obj = new DelegatingAndOverwriting();
+    obj.f = 1;
+    obj.f2 = new JsonPrimitive(2);
+    obj.f3 = new JsonPrimitive(true);
+    String json = gson.toJson(obj);
+    assertThat(json).isEqualTo("{}");
+
+    DelegatingAndOverwriting deserialized =
+        gson.fromJson("{\"f\":1,\"f2\":2,\"f3\":3}", DelegatingAndOverwriting.class);
+    assertThat(deserialized.f).isNull();
+    assertThat(deserialized.f2).isNull();
+    assertThat(deserialized.f3).isNull();
+  }
+
+  private static class DelegatingAndOverwriting {
+    @JsonAdapter(DelegatingAdapterFactory.class)
+    Integer f;
+
+    @JsonAdapter(DelegatingAdapterFactory.class)
+    JsonElement f2;
+
+    // Also have non-delegating adapter to make tests handle both cases
+    @JsonAdapter(JsonElementAdapter.class)
+    JsonElement f3;
+
+    static class DelegatingAdapterFactory implements TypeAdapterFactory {
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        return gson.getDelegateAdapter(this, type);
+      }
+    }
+  }
+
+  /**
+   * Verifies that {@link TypeAdapterFactory} specified by {@code @JsonAdapter} can call {@link
+   * Gson#getDelegateAdapter} without any issues, despite the factory not being directly registered
+   * on Gson.
+   */
+  @Test
+  public void testDelegatingAdapterFactory() {
+    @SuppressWarnings("unchecked")
+    WithDelegatingFactory<String> deserialized =
+        new Gson().fromJson("{\"f\":\"test\"}", WithDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("test-custom");
+
+    deserialized =
+        new Gson().fromJson("{\"f\":\"test\"}", new TypeToken<WithDelegatingFactory<String>>() {});
+    assertThat(deserialized.f).isEqualTo("test-custom");
+
+    WithDelegatingFactory<String> serialized = new WithDelegatingFactory<>();
+    serialized.f = "value";
+    assertThat(new Gson().toJson(serialized)).isEqualTo("{\"f\":\"value-custom\"}");
+  }
+
+  private static class WithDelegatingFactory<T> {
+    // suppress Error Prone warning; should be clear that `Factory` refers to nested class
+    @SuppressWarnings("SameNameButDifferent")
+    @JsonAdapter(Factory.class)
+    T f;
+
+    static class Factory implements TypeAdapterFactory {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getDelegateAdapter(this, type);
+
+        return (TypeAdapter<T>)
+            new TypeAdapter<String>() {
+              @Override
+              public String read(JsonReader in) throws IOException {
+                // Perform custom deserialization
+                return delegate.read(in) + "-custom";
+              }
+
+              @Override
+              public void write(JsonWriter out, String value) throws IOException {
+                // Perform custom serialization
+                delegate.write(out, value + "-custom");
+              }
+            };
+      }
+    }
+  }
+
+  /**
+   * Similar to {@link #testDelegatingAdapterFactory}, except that the delegate is not looked up in
+   * {@code create} but instead in the adapter methods.
+   */
+  @Test
+  public void testDelegatingAdapterFactory_Delayed() {
+    WithDelayedDelegatingFactory deserialized =
+        new Gson().fromJson("{\"f\":\"test\"}", WithDelayedDelegatingFactory.class);
+    assertThat(deserialized.f).isEqualTo("test-custom");
+
+    WithDelayedDelegatingFactory serialized = new WithDelayedDelegatingFactory();
+    serialized.f = "value";
+    assertThat(new Gson().toJson(serialized)).isEqualTo("{\"f\":\"value-custom\"}");
+  }
+
+  // suppress Error Prone warning; should be clear that `Factory` refers to nested class
+  @SuppressWarnings("SameNameButDifferent")
+  private static class WithDelayedDelegatingFactory {
+    @JsonAdapter(Factory.class)
+    String f;
+
+    static class Factory implements TypeAdapterFactory {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        return (TypeAdapter<T>)
+            new TypeAdapter<String>() {
+              private TypeAdapter<String> delegate() {
+                return (TypeAdapter<String>) gson.getDelegateAdapter(Factory.this, type);
+              }
+
+              @Override
+              public String read(JsonReader in) throws IOException {
+                // Perform custom deserialization
+                return delegate().read(in) + "-custom";
+              }
+
+              @Override
+              public void write(JsonWriter out, String value) throws IOException {
+                // Perform custom serialization
+                delegate().write(out, value + "-custom");
+              }
+            };
+      }
+    }
+  }
+
+  /**
+   * Tests usage of {@link Gson#getAdapter(TypeToken)} in the {@code create} method of the factory.
+   * Existing code was using that as workaround because {@link Gson#getDelegateAdapter} previously
+   * did not work in combination with {@code @JsonAdapter}, see
+   * https://github.com/google/gson/issues/1028.
+   */
+  @Test
+  public void testGetAdapterDelegation() {
+    Gson gson = new Gson();
+    GetAdapterDelegation deserialized = gson.fromJson("{\"f\":\"de\"}", GetAdapterDelegation.class);
+    assertThat(deserialized.f).isEqualTo("de-custom");
+
+    String json = gson.toJson(new GetAdapterDelegation("se"));
+    assertThat(json).isEqualTo("{\"f\":\"se-custom\"}");
+  }
+
+  private static class GetAdapterDelegation {
+    // suppress Error Prone warning; should be clear that `Factory` refers to nested class
+    @SuppressWarnings("SameNameButDifferent")
+    @JsonAdapter(Factory.class)
+    String f;
+
+    GetAdapterDelegation(String f) {
+      this.f = f;
+    }
+
+    static class Factory implements TypeAdapterFactory {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+        // Uses `Gson.getAdapter` instead of `Gson.getDelegateAdapter`
+        TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(type);
+
+        return (TypeAdapter<T>)
+            new TypeAdapter<String>() {
+              @Override
+              public String read(JsonReader in) throws IOException {
+                return delegate.read(in) + "-custom";
+              }
+
+              @Override
+              public void write(JsonWriter out, String value) throws IOException {
+                delegate.write(out, value + "-custom");
+              }
+            };
+      }
+    }
+  }
+
+  /** Tests usage of {@link JsonSerializer} as {@link JsonAdapter} value on a field */
+  @Test
+  public void testJsonSerializer() {
+    Gson gson = new Gson();
+    // Verify that delegate deserializer for List is used
+    WithJsonSerializer deserialized = gson.fromJson("{\"f\":[1,2,3]}", WithJsonSerializer.class);
+    assertThat(deserialized.f).isEqualTo(Arrays.asList(1, 2, 3));
+
+    String json = gson.toJson(new WithJsonSerializer());
+    // Uses custom serializer which always returns `true`
+    assertThat(json).isEqualTo("{\"f\":true}");
+  }
+
+  private static class WithJsonSerializer {
+    @JsonAdapter(Serializer.class)
+    List<Integer> f = Collections.emptyList();
+
+    static class Serializer implements JsonSerializer<List<Integer>> {
+      @Override
+      public JsonElement serialize(
+          List<Integer> src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(true);
+      }
+    }
+  }
+
+  /** Tests usage of {@link JsonDeserializer} as {@link JsonAdapter} value on a field */
+  @Test
+  public void testJsonDeserializer() {
+    Gson gson = new Gson();
+    WithJsonDeserializer deserialized = gson.fromJson("{\"f\":[5]}", WithJsonDeserializer.class);
+    // Uses custom deserializer which always returns `[3, 2, 1]`
+    assertThat(deserialized.f).isEqualTo(Arrays.asList(3, 2, 1));
+
+    // Verify that delegate serializer for List is used
+    String json = gson.toJson(new WithJsonDeserializer(Arrays.asList(4, 5, 6)));
+    assertThat(json).isEqualTo("{\"f\":[4,5,6]}");
+  }
+
+  private static class WithJsonDeserializer {
+    @JsonAdapter(Deserializer.class)
+    List<Integer> f;
+
+    WithJsonDeserializer(List<Integer> f) {
+      this.f = f;
+    }
+
+    static class Deserializer implements JsonDeserializer<List<Integer>> {
+      @Override
+      public List<Integer> deserialize(
+          JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+        return Arrays.asList(3, 2, 1);
+      }
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..393aebe4a61e4e4966fbd11d37335533b1e95c4d
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JsonAdapterSerializerDeserializerTest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2016 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.Keep;
+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.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+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;
+
+/**
+ * Functional tests for the {@link JsonAdapter} annotation on fields where the value is of type
+ * {@link JsonSerializer} or {@link JsonDeserializer}.
+ */
+public final class JsonAdapterSerializerDeserializerTest {
+
+  @Test
+  public void testJsonSerializerDeserializerBasedJsonAdapterOnFields() {
+    Gson gson = new Gson();
+    String json =
+        gson.toJson(new Computer(new User("Inderjeet Singh"), null, new User("Jesse Wilson")));
+    assertThat(json)
+        .isEqualTo("{\"user1\":\"UserSerializer\",\"user3\":\"UserSerializerDeserializer\"}");
+    Computer computer =
+        gson.fromJson("{'user2':'Jesse Wilson','user3':'Jake Wharton'}", Computer.class);
+    assertThat(computer.user2.name).isEqualTo("UserDeserializer");
+    assertThat(computer.user3.name).isEqualTo("UserSerializerDeserializer");
+  }
+
+  private static final class Computer {
+    @JsonAdapter(UserSerializer.class)
+    @Keep
+    final User user1;
+
+    @JsonAdapter(UserDeserializer.class)
+    @Keep
+    final User user2;
+
+    @JsonAdapter(UserSerializerDeserializer.class)
+    @Keep
+    final User user3;
+
+    Computer(User user1, User user2, User user3) {
+      this.user1 = user1;
+      this.user2 = user2;
+      this.user3 = user3;
+    }
+  }
+
+  private static final class User {
+    public final String name;
+
+    private User(String name) {
+      this.name = name;
+    }
+  }
+
+  private static final class UserSerializer implements JsonSerializer<User> {
+    @Override
+    public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("UserSerializer");
+    }
+  }
+
+  private static final class UserDeserializer implements JsonDeserializer<User> {
+    @Override
+    public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return new User("UserDeserializer");
+    }
+  }
+
+  private static final class UserSerializerDeserializer
+      implements JsonSerializer<User>, JsonDeserializer<User> {
+    @Override
+    public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("UserSerializerDeserializer");
+    }
+
+    @Override
+    public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return new User("UserSerializerDeserializer");
+    }
+  }
+
+  @Test
+  public void testJsonSerializerDeserializerBasedJsonAdapterOnClass() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new Computer2(new User2("Inderjeet Singh")));
+    assertThat(json).isEqualTo("{\"user\":\"UserSerializerDeserializer2\"}");
+    Computer2 computer = gson.fromJson("{'user':'Inderjeet Singh'}", Computer2.class);
+    assertThat(computer.user.name).isEqualTo("UserSerializerDeserializer2");
+  }
+
+  private static final class Computer2 {
+    final User2 user;
+
+    Computer2(User2 user) {
+      this.user = user;
+    }
+  }
+
+  @JsonAdapter(UserSerializerDeserializer2.class)
+  private static final class User2 {
+    public final String name;
+
+    private User2(String name) {
+      this.name = name;
+    }
+  }
+
+  private static final class UserSerializerDeserializer2
+      implements JsonSerializer<User2>, JsonDeserializer<User2> {
+    @Override
+    public JsonElement serialize(User2 src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("UserSerializerDeserializer2");
+    }
+
+    @Override
+    public User2 deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return new User2("UserSerializerDeserializer2");
+    }
+  }
+
+  @Test
+  public void testDifferentJsonAdaptersForGenericFieldsOfSameRawType() {
+    Container c = new Container("Foo", 10);
+    Gson gson = new Gson();
+    String json = gson.toJson(c);
+    assertThat(json).contains("\"a\":\"BaseStringAdapter\"");
+    assertThat(json).contains("\"b\":\"BaseIntegerAdapter\"");
+  }
+
+  private static final class Container {
+    @JsonAdapter(BaseStringAdapter.class)
+    @Keep
+    Base<String> a;
+
+    @JsonAdapter(BaseIntegerAdapter.class)
+    @Keep
+    Base<Integer> b;
+
+    Container(String a, int b) {
+      this.a = new Base<>(a);
+      this.b = new Base<>(b);
+    }
+  }
+
+  private static final class Base<T> {
+    @SuppressWarnings("unused")
+    T value;
+
+    Base(T value) {
+      this.value = value;
+    }
+  }
+
+  private static final class BaseStringAdapter implements JsonSerializer<Base<String>> {
+    @Override
+    public JsonElement serialize(
+        Base<String> src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("BaseStringAdapter");
+    }
+  }
+
+  private static final class BaseIntegerAdapter implements JsonSerializer<Base<Integer>> {
+    @Override
+    public JsonElement serialize(
+        Base<Integer> src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("BaseIntegerAdapter");
+    }
+  }
+
+  @Test
+  public void testJsonAdapterNullSafe() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                User.class,
+                new TypeAdapter<User>() {
+                  @Override
+                  public User read(JsonReader in) throws IOException {
+                    in.nextNull();
+                    return new User("fallback-read");
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, User value) throws IOException {
+                    assertThat(value).isNull();
+                    out.value("fallback-write");
+                  }
+                })
+            .serializeNulls()
+            .create();
+
+    String json = gson.toJson(new WithNullSafe(null, null, null, null));
+    // Only nullSafe=true serializer writes null; for @JsonAdapter with deserializer nullSafe is
+    // ignored when serializing
+    assertThat(json)
+        .isEqualTo(
+            "{\"userS\":\"UserSerializer\",\"userSN\":null,\"userD\":\"fallback-write\",\"userDN\":\"fallback-write\"}");
+
+    WithNullSafe deserialized =
+        gson.fromJson(
+            "{\"userS\":null,\"userSN\":null,\"userD\":null,\"userDN\":null}", WithNullSafe.class);
+    // For @JsonAdapter with serializer nullSafe is ignored when deserializing
+    assertThat(deserialized.userS.name).isEqualTo("fallback-read");
+    assertThat(deserialized.userSN.name).isEqualTo("fallback-read");
+    assertThat(deserialized.userD.name).isEqualTo("UserDeserializer");
+    assertThat(deserialized.userDN).isNull();
+  }
+
+  @SuppressWarnings("MemberName")
+  private static final class WithNullSafe {
+    // "userS..." uses JsonSerializer
+    @JsonAdapter(value = UserSerializer.class, nullSafe = false)
+    final User userS;
+
+    @JsonAdapter(value = UserSerializer.class, nullSafe = true)
+    final User userSN;
+
+    // "userD..." uses JsonDeserializer
+    @JsonAdapter(value = UserDeserializer.class, nullSafe = false)
+    final User userD;
+
+    @JsonAdapter(value = UserDeserializer.class, nullSafe = true)
+    final User userDN;
+
+    WithNullSafe(User userS, User userSN, User userD, User userDN) {
+      this.userS = userS;
+      this.userSN = userSN;
+      this.userD = userD;
+      this.userDN = userDN;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JsonParserTest.java b/gson/gson/src/test/java/com/google/gson/functional/JsonParserTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..21e16e826df76e4fa25712c9ba92876a984cfe78
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JsonParserTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.Nested;
+import com.google.gson.reflect.TypeToken;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for that use JsonParser and related Gson methods
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class JsonParserTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testParseInvalidJson() {
+    try {
+      gson.fromJson("[[]", Object[].class);
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testDeserializingCustomTree() {
+    JsonObject obj = new JsonObject();
+    obj.addProperty("stringValue", "foo");
+    obj.addProperty("intValue", 11);
+    BagOfPrimitives target = gson.fromJson(obj, BagOfPrimitives.class);
+    assertThat(target.intValue).isEqualTo(11);
+    assertThat(target.stringValue).isEqualTo("foo");
+  }
+
+  @Test
+  public void testBadTypeForDeserializingCustomTree() {
+    JsonObject obj = new JsonObject();
+    obj.addProperty("stringValue", "foo");
+    obj.addProperty("intValue", 11);
+    JsonArray array = new JsonArray();
+    array.add(obj);
+    try {
+      gson.fromJson(array, BagOfPrimitives.class);
+      fail("BagOfPrimitives is not an array");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testBadFieldTypeForCustomDeserializerCustomTree() {
+    JsonArray array = new JsonArray();
+    array.add(new JsonPrimitive("blah"));
+    JsonObject obj = new JsonObject();
+    obj.addProperty("stringValue", "foo");
+    obj.addProperty("intValue", 11);
+    obj.add("longValue", array);
+
+    try {
+      gson.fromJson(obj, BagOfPrimitives.class);
+      fail("BagOfPrimitives is not an array");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testBadFieldTypeForDeserializingCustomTree() {
+    JsonArray array = new JsonArray();
+    array.add(new JsonPrimitive("blah"));
+    JsonObject primitive1 = new JsonObject();
+    primitive1.addProperty("string", "foo");
+    primitive1.addProperty("intValue", 11);
+
+    JsonObject obj = new JsonObject();
+    obj.add("primitive1", primitive1);
+    obj.add("primitive2", array);
+
+    try {
+      gson.fromJson(obj, Nested.class);
+      fail("Nested has field BagOfPrimitives which is not an array");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testChangingCustomTreeAndDeserializing() {
+    StringReader json =
+        new StringReader("{'stringValue':'no message','intValue':10,'longValue':20}");
+    JsonObject obj = (JsonObject) JsonParser.parseReader(json);
+    obj.remove("stringValue");
+    obj.addProperty("stringValue", "fooBar");
+    BagOfPrimitives target = gson.fromJson(obj, BagOfPrimitives.class);
+    assertThat(target.intValue).isEqualTo(10);
+    assertThat(target.longValue).isEqualTo(20);
+    assertThat(target.stringValue).isEqualTo("fooBar");
+  }
+
+  @Test
+  public void testExtraCommasInArrays() {
+    TypeToken<List<String>> type = new TypeToken<>() {};
+    assertThat(gson.fromJson("[a,,b,,]", type))
+        .isEqualTo(Arrays.asList("a", null, "b", null, null));
+    assertThat(gson.fromJson("[,]", type)).isEqualTo(Arrays.asList(null, null));
+    assertThat(gson.fromJson("[a,]", type)).isEqualTo(Arrays.asList("a", null));
+  }
+
+  @Test
+  public void testExtraCommasInMaps() {
+    Type type = new TypeToken<Map<String, String>>() {}.getType();
+    try {
+      gson.fromJson("{a:b,}", type);
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java b/gson/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a00f399d019344cc6347005a7bd2d4ba25186ca
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/JsonTreeTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2009 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for {@link Gson#toJsonTree(Object)} and {@link Gson#toJsonTree(Object,
+ * java.lang.reflect.Type)}
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class JsonTreeTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testToJsonTree() {
+    BagOfPrimitives bag = new BagOfPrimitives(10L, 5, false, "foo");
+    JsonElement json = gson.toJsonTree(bag);
+    assertThat(json.isJsonObject()).isTrue();
+    JsonObject obj = json.getAsJsonObject();
+    Set<Entry<String, JsonElement>> children = obj.entrySet();
+    assertThat(children).hasSize(4);
+    assertContains(obj, new JsonPrimitive(10L));
+    assertContains(obj, new JsonPrimitive(5));
+    assertContains(obj, new JsonPrimitive(false));
+    assertContains(obj, new JsonPrimitive("foo"));
+  }
+
+  @Test
+  public void testToJsonTreeObjectType() {
+    SubTypeOfBagOfPrimitives bag = new SubTypeOfBagOfPrimitives(10L, 5, false, "foo", 1.4F);
+    JsonElement json = gson.toJsonTree(bag, BagOfPrimitives.class);
+    assertThat(json.isJsonObject()).isTrue();
+    JsonObject obj = json.getAsJsonObject();
+    Set<Entry<String, JsonElement>> children = obj.entrySet();
+    assertThat(children).hasSize(4);
+    assertContains(obj, new JsonPrimitive(10L));
+    assertContains(obj, new JsonPrimitive(5));
+    assertContains(obj, new JsonPrimitive(false));
+    assertContains(obj, new JsonPrimitive("foo"));
+  }
+
+  @Test
+  public void testJsonTreeToString() {
+    SubTypeOfBagOfPrimitives bag = new SubTypeOfBagOfPrimitives(10L, 5, false, "foo", 1.4F);
+    String json1 = gson.toJson(bag);
+    JsonElement jsonElement = gson.toJsonTree(bag, SubTypeOfBagOfPrimitives.class);
+    String json2 = gson.toJson(jsonElement);
+    assertThat(json2).isEqualTo(json1);
+  }
+
+  @Test
+  public void testJsonTreeNull() {
+    BagOfPrimitives bag = new BagOfPrimitives(10L, 5, false, null);
+    JsonObject jsonElement = (JsonObject) gson.toJsonTree(bag, BagOfPrimitives.class);
+    assertThat(jsonElement.has("stringValue")).isFalse();
+  }
+
+  private static void assertContains(JsonObject json, JsonPrimitive child) {
+    for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
+      JsonElement node = entry.getValue();
+      if (node.isJsonPrimitive()) {
+        if (node.getAsJsonPrimitive().equals(child)) {
+          return;
+        }
+      }
+    }
+    fail();
+  }
+
+  private static class SubTypeOfBagOfPrimitives extends BagOfPrimitives {
+    @SuppressWarnings("unused")
+    float f = 1.2F;
+
+    public SubTypeOfBagOfPrimitives(long l, int i, boolean b, String string, float f) {
+      super(l, i, b, string);
+      this.f = f;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/LeniencyTest.java b/gson/gson/src/test/java/com/google/gson/functional/LeniencyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..40197ae75eb5f4969d87d50453e2bc0c4462a747
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/LeniencyTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2016 The Gson Authors
+ *
+ * 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 com.google.common.truth.Truth.assertThat;
+import static java.util.Collections.singletonList;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Functional tests for leniency option. */
+public class LeniencyTest {
+
+  private Gson gson;
+
+  @SuppressWarnings({"deprecation", "InlineMeInliner"}) // for GsonBuilder.setLenient
+  @Before
+  public void setUp() throws Exception {
+    gson = new GsonBuilder().setLenient().create();
+  }
+
+  @Test
+  public void testLenientFromJson() {
+    List<String> json =
+        gson.fromJson(
+            "[ # One!\n" //
+                + "  'Hi' #Element!\n" //
+                + "] # Array!",
+            new TypeToken<List<String>>() {}.getType());
+    assertThat(json).isEqualTo(singletonList("Hi"));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7009ce6cbf04e587d5c2c59a581e70aec71d2ae4
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class MapAsArrayTypeAdapterTest {
+
+  @Test
+  public void testSerializeComplexMapWithTypeAdapter() {
+    Type type = new TypeToken<Map<Point, String>>() {}.getType();
+    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+
+    Map<Point, String> original = new LinkedHashMap<>();
+    original.put(new Point(5, 5), "a");
+    original.put(new Point(8, 8), "b");
+    String json = gson.toJson(original, type);
+    assertThat(json).isEqualTo("[[{\"x\":5,\"y\":5},\"a\"],[{\"x\":8,\"y\":8},\"b\"]]");
+    assertThat(gson.<Map<Point, String>>fromJson(json, type)).isEqualTo(original);
+
+    // test that registering a type adapter for one map doesn't interfere with others
+    Map<String, Boolean> otherMap = new LinkedHashMap<>();
+    otherMap.put("t", true);
+    otherMap.put("f", false);
+    assertThat(gson.toJson(otherMap, Map.class)).isEqualTo("{\"t\":true,\"f\":false}");
+    assertThat(gson.toJson(otherMap, new TypeToken<Map<String, Boolean>>() {}.getType()))
+        .isEqualTo("{\"t\":true,\"f\":false}");
+    assertThat(
+            gson.<Object>fromJson(
+                "{\"t\":true,\"f\":false}", new TypeToken<Map<String, Boolean>>() {}.getType()))
+        .isEqualTo(otherMap);
+  }
+
+  @Test
+  @Ignore
+  public void testTwoTypesCollapseToOneSerialize() {
+    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+
+    Map<Number, String> original = new LinkedHashMap<>();
+    original.put(1.0D, "a");
+    original.put(1.0F, "b");
+    try {
+      gson.toJson(original, new TypeToken<Map<Number, String>>() {}.getType());
+      fail(); // we no longer hash keys at serialization time
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testTwoTypesCollapseToOneDeserialize() {
+    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+
+    String s = "[[\"1.00\",\"a\"],[\"1.0\",\"b\"]]";
+    try {
+      gson.fromJson(s, new TypeToken<Map<Double, String>>() {}.getType());
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testMultipleEnableComplexKeyRegistrationHasNoEffect() {
+    Type type = new TypeToken<Map<Point, String>>() {}.getType();
+    Gson gson =
+        new GsonBuilder()
+            .enableComplexMapKeySerialization()
+            .enableComplexMapKeySerialization()
+            .create();
+
+    Map<Point, String> original = new LinkedHashMap<>();
+    original.put(new Point(6, 5), "abc");
+    original.put(new Point(1, 8), "def");
+    String json = gson.toJson(original, type);
+    assertThat(json).isEqualTo("[[{\"x\":6,\"y\":5},\"abc\"],[{\"x\":1,\"y\":8},\"def\"]]");
+    assertThat(gson.<Map<Point, String>>fromJson(json, type)).isEqualTo(original);
+  }
+
+  @Test
+  public void testMapWithTypeVariableSerialization() {
+    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+    PointWithProperty<Point> map = new PointWithProperty<>();
+    map.map.put(new Point(2, 3), new Point(4, 5));
+    Type type = new TypeToken<PointWithProperty<Point>>() {}.getType();
+    String json = gson.toJson(map, type);
+    assertThat(json).isEqualTo("{\"map\":[[{\"x\":2,\"y\":3},{\"x\":4,\"y\":5}]]}");
+  }
+
+  @Test
+  public void testMapWithTypeVariableDeserialization() {
+    Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+    String json = "{map:[[{x:2,y:3},{x:4,y:5}]]}";
+    Type type = new TypeToken<PointWithProperty<Point>>() {}.getType();
+    PointWithProperty<Point> map = gson.fromJson(json, type);
+    Point key = map.map.keySet().iterator().next();
+    Point value = map.map.values().iterator().next();
+    assertThat(key).isEqualTo(new Point(2, 3));
+    assertThat(value).isEqualTo(new Point(4, 5));
+  }
+
+  static class Point {
+    int x;
+    int y;
+
+    Point(int x, int y) {
+      this.x = x;
+      this.y = y;
+    }
+
+    Point() {}
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Point && ((Point) o).x == x && ((Point) o).y == y;
+    }
+
+    @Override
+    public int hashCode() {
+      return x * 37 + y;
+    }
+
+    @Override
+    public String toString() {
+      return "(" + x + "," + y + ")";
+    }
+  }
+
+  static class PointWithProperty<T> {
+    Map<Point, T> map = new HashMap<>();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/MapTest.java b/gson/gson/src/test/java/com/google/gson/functional/MapTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bd11c4c7aee305fa54ca96c60da55a7a33c7439
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/MapTest.java
@@ -0,0 +1,701 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.common.TestTypes;
+import com.google.gson.internal.$Gson$Types;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional test for Json serialization and deserialization for Maps
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class MapTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testMapSerialization() {
+    Map<String, Integer> map = new LinkedHashMap<>();
+    map.put("a", 1);
+    map.put("b", 2);
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+    assertThat(json).contains("\"a\":1");
+    assertThat(json).contains("\"b\":2");
+  }
+
+  @Test
+  public void testMapDeserialization() {
+    String json = "{\"a\":1,\"b\":2}";
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    Map<String, Integer> target = gson.fromJson(json, typeOfMap);
+    assertThat(target.get("a")).isEqualTo(1);
+    assertThat(target.get("b")).isEqualTo(2);
+  }
+
+  @Test
+  public void testObjectMapSerialization() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("a", 1);
+    map.put("b", "string");
+    String json = gson.toJson(map);
+    assertThat(json).contains("\"a\":1");
+    assertThat(json).contains("\"b\":\"string\"");
+  }
+
+  @Test
+  public void testMapSerializationEmpty() {
+    Map<String, Integer> map = new LinkedHashMap<>();
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+    assertThat(json).isEqualTo("{}");
+  }
+
+  @Test
+  public void testMapDeserializationEmpty() {
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    Map<String, Integer> map = gson.fromJson("{}", typeOfMap);
+    assertThat(map).isEmpty();
+  }
+
+  @Test
+  public void testMapSerializationWithNullValue() {
+    Map<String, Integer> map = new LinkedHashMap<>();
+    map.put("abc", null);
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+
+    // Maps are represented as JSON objects, so ignoring null field
+    assertThat(json).isEqualTo("{}");
+  }
+
+  @Test
+  public void testMapDeserializationWithNullValue() {
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    Map<String, Integer> map = gson.fromJson("{\"abc\":null}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map.get("abc")).isNull();
+  }
+
+  @Test
+  public void testMapSerializationWithNullValueButSerializeNulls() {
+    gson = new GsonBuilder().serializeNulls().create();
+    Map<String, Integer> map = new LinkedHashMap<>();
+    map.put("abc", null);
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+
+    assertThat(json).isEqualTo("{\"abc\":null}");
+  }
+
+  @Test
+  public void testMapSerializationWithNullKey() {
+    Map<String, Integer> map = new LinkedHashMap<>();
+    map.put(null, 123);
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+
+    assertThat(json).isEqualTo("{\"null\":123}");
+  }
+
+  @Test
+  public void testMapDeserializationWithNullKey() {
+    Type typeOfMap = new TypeToken<Map<String, Integer>>() {}.getType();
+    Map<String, Integer> map = gson.fromJson("{\"null\":123}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map.get("null")).isEqualTo(123);
+    assertThat(map.get(null)).isNull();
+
+    map = gson.fromJson("{null:123}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map.get("null")).isEqualTo(123);
+    assertThat(map.get(null)).isNull();
+  }
+
+  @Test
+  public void testMapSerializationWithIntegerKeys() {
+    Map<Integer, String> map = new LinkedHashMap<>();
+    map.put(123, "456");
+    Type typeOfMap = new TypeToken<Map<Integer, String>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+
+    assertThat(json).isEqualTo("{\"123\":\"456\"}");
+  }
+
+  @Test
+  public void testMapDeserializationWithIntegerKeys() {
+    Type typeOfMap = new TypeToken<Map<Integer, String>>() {}.getType();
+    Map<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+  }
+
+  @Test
+  public void testMapDeserializationWithUnquotedIntegerKeys() {
+    Type typeOfMap = new TypeToken<Map<Integer, String>>() {}.getType();
+    Map<Integer, String> map = gson.fromJson("{123:\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+  }
+
+  @Test
+  public void testMapDeserializationWithLongKeys() {
+    long longValue = 9876543210L;
+    String json = String.format("{\"%d\":\"456\"}", longValue);
+    Type typeOfMap = new TypeToken<Map<Long, String>>() {}.getType();
+    Map<Long, String> map = gson.fromJson(json, typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(longValue);
+    assertThat(map.get(longValue)).isEqualTo("456");
+  }
+
+  @Test
+  public void testMapDeserializationWithUnquotedLongKeys() {
+    long longKey = 9876543210L;
+    String json = String.format("{%d:\"456\"}", longKey);
+    Type typeOfMap = new TypeToken<Map<Long, String>>() {}.getType();
+    Map<Long, String> map = gson.fromJson(json, typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(longKey);
+    assertThat(map.get(longKey)).isEqualTo("456");
+  }
+
+  @Test
+  public void testHashMapDeserialization() {
+    Type typeOfMap = new TypeToken<HashMap<Integer, String>>() {}.getType();
+    HashMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+  }
+
+  @Test
+  public void testSortedMap() {
+    Type typeOfMap = new TypeToken<SortedMap<Integer, String>>() {}.getType();
+    SortedMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+  }
+
+  @Test
+  public void testConcurrentMap() {
+    Type typeOfMap = new TypeToken<ConcurrentMap<Integer, String>>() {}.getType();
+    ConcurrentMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"123\":\"456\"}");
+  }
+
+  @Test
+  public void testConcurrentHashMap() {
+    Type typeOfMap = new TypeToken<ConcurrentHashMap<Integer, String>>() {}.getType();
+    ConcurrentHashMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"123\":\"456\"}");
+  }
+
+  @Test
+  public void testConcurrentNavigableMap() {
+    Type typeOfMap = new TypeToken<ConcurrentNavigableMap<Integer, String>>() {}.getType();
+    ConcurrentNavigableMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"123\":\"456\"}");
+  }
+
+  @Test
+  public void testConcurrentSkipListMap() {
+    Type typeOfMap = new TypeToken<ConcurrentSkipListMap<Integer, String>>() {}.getType();
+    ConcurrentSkipListMap<Integer, String> map = gson.fromJson("{\"123\":\"456\"}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map).containsKey(123);
+    assertThat(map.get(123)).isEqualTo("456");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"123\":\"456\"}");
+  }
+
+  @Test
+  public void testParameterizedMapSubclassSerialization() {
+    MyParameterizedMap<String, String> map = new MyParameterizedMap<>(10);
+    map.put("a", "b");
+    Type type = new TypeToken<MyParameterizedMap<String, String>>() {}.getType();
+    String json = gson.toJson(map, type);
+    assertThat(json).contains("\"a\":\"b\"");
+  }
+
+  @SuppressWarnings({"unused", "serial"})
+  private static class MyParameterizedMap<K, V> extends LinkedHashMap<K, V> {
+    final int foo;
+
+    MyParameterizedMap(int foo) {
+      this.foo = foo;
+    }
+  }
+
+  @Test
+  public void testMapSubclassSerialization() {
+    MyMap map = new MyMap();
+    map.put("a", "b");
+    String json = gson.toJson(map, MyMap.class);
+    assertThat(json).contains("\"a\":\"b\"");
+  }
+
+  @Test
+  public void testMapStandardSubclassDeserialization() {
+    String json = "{a:'1',b:'2'}";
+    Type type = new TypeToken<LinkedHashMap<String, String>>() {}.getType();
+    LinkedHashMap<String, String> map = gson.fromJson(json, type);
+    assertThat(map).containsEntry("a", "1");
+    assertThat(map).containsEntry("b", "2");
+  }
+
+  @Test
+  public void testMapSubclassDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                MyMap.class,
+                new InstanceCreator<MyMap>() {
+                  @Override
+                  public MyMap createInstance(Type type) {
+                    return new MyMap();
+                  }
+                })
+            .create();
+    String json = "{\"a\":1,\"b\":2}";
+    MyMap map = gson.fromJson(json, MyMap.class);
+    assertThat(map.get("a")).isEqualTo("1");
+    assertThat(map.get("b")).isEqualTo("2");
+  }
+
+  @Test
+  public void testCustomSerializerForSpecificMapType() {
+    Type type =
+        $Gson$Types.newParameterizedTypeWithOwner(null, Map.class, String.class, Long.class);
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                type,
+                new JsonSerializer<Map<String, Long>>() {
+                  @Override
+                  public JsonElement serialize(
+                      Map<String, Long> src, Type typeOfSrc, JsonSerializationContext context) {
+                    JsonArray array = new JsonArray();
+                    for (long value : src.values()) {
+                      array.add(new JsonPrimitive(value));
+                    }
+                    return array;
+                  }
+                })
+            .create();
+
+    Map<String, Long> src = new LinkedHashMap<>();
+    src.put("one", 1L);
+    src.put("two", 2L);
+    src.put("three", 3L);
+
+    assertThat(gson.toJson(src, type)).isEqualTo("[1,2,3]");
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 */
+  private static class ClassWithAMap {
+    Map<String, String> map = new TreeMap<>();
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 */
+  @Test
+  public void testMapSerializationWithNullValues() {
+    ClassWithAMap target = new ClassWithAMap();
+    target.map.put("name1", null);
+    target.map.put("name2", "value2");
+    String json = gson.toJson(target);
+    assertThat(json).doesNotContain("name1");
+    assertThat(json).contains("name2");
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=99 */
+  @Test
+  public void testMapSerializationWithNullValuesSerialized() {
+    Gson gson = new GsonBuilder().serializeNulls().create();
+    ClassWithAMap target = new ClassWithAMap();
+    target.map.put("name1", null);
+    target.map.put("name2", "value2");
+    String json = gson.toJson(target);
+    assertThat(json).contains("name1");
+    assertThat(json).contains("name2");
+  }
+
+  @Test
+  public void testMapSerializationWithWildcardValues() {
+    Map<String, ? extends Collection<? extends Integer>> map = new LinkedHashMap<>();
+    map.put("test", null);
+    Type typeOfMap =
+        new TypeToken<Map<String, ? extends Collection<? extends Integer>>>() {}.getType();
+    String json = gson.toJson(map, typeOfMap);
+
+    assertThat(json).isEqualTo("{}");
+  }
+
+  @Test
+  public void testMapDeserializationWithWildcardValues() {
+    Type typeOfMap = new TypeToken<Map<String, ? extends Long>>() {}.getType();
+    Map<String, ? extends Long> map = gson.fromJson("{\"test\":123}", typeOfMap);
+    assertThat(map).hasSize(1);
+    assertThat(map.get("test")).isEqualTo(123L);
+  }
+
+  private static class MyMap extends LinkedHashMap<String, String> {
+    private static final long serialVersionUID = 1L;
+
+    @SuppressWarnings("unused")
+    int foo = 10;
+  }
+
+  /** From bug report http://code.google.com/p/google-gson/issues/detail?id=95 */
+  @Test
+  public void testMapOfMapSerialization() {
+    Map<String, Map<String, String>> map = new HashMap<>();
+    Map<String, String> nestedMap = new HashMap<>();
+    nestedMap.put("1", "1");
+    nestedMap.put("2", "2");
+    map.put("nestedMap", nestedMap);
+    String json = gson.toJson(map);
+    assertThat(json).contains("nestedMap");
+    assertThat(json).contains("\"1\":\"1\"");
+    assertThat(json).contains("\"2\":\"2\"");
+  }
+
+  /** From bug report http://code.google.com/p/google-gson/issues/detail?id=95 */
+  @Test
+  public void testMapOfMapDeserialization() {
+    String json = "{nestedMap:{'2':'2','1':'1'}}";
+    Type type = new TypeToken<Map<String, Map<String, String>>>() {}.getType();
+    Map<String, Map<String, String>> map = gson.fromJson(json, type);
+    Map<String, String> nested = map.get("nestedMap");
+    assertThat(nested.get("1")).isEqualTo("1");
+    assertThat(nested.get("2")).isEqualTo("2");
+  }
+
+  /** From bug report http://code.google.com/p/google-gson/issues/detail?id=178 */
+  @Test
+  public void testMapWithQuotes() {
+    Map<String, String> map = new HashMap<>();
+    map.put("a\"b", "c\"d");
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\"a\\\"b\":\"c\\\"d\"}");
+  }
+
+  /** From issue 227. */
+  @Test
+  public void testWriteMapsWithEmptyStringKey() {
+    Map<String, Boolean> map = new HashMap<>();
+    map.put("", true);
+    assertThat(gson.toJson(map)).isEqualTo("{\"\":true}");
+  }
+
+  @Test
+  public void testReadMapsWithEmptyStringKey() {
+    Map<String, Boolean> map =
+        gson.fromJson("{\"\":true}", new TypeToken<Map<String, Boolean>>() {}.getType());
+    assertThat(map.get("")).isEqualTo(Boolean.TRUE);
+  }
+
+  /** From bug report http://code.google.com/p/google-gson/issues/detail?id=204 */
+  @Test
+  public void testSerializeMaps() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    map.put("a", 12);
+    map.put("b", null);
+
+    LinkedHashMap<String, Object> innerMap = new LinkedHashMap<>();
+    innerMap.put("test", 1);
+    innerMap.put("TestStringArray", new String[] {"one", "two"});
+    map.put("c", innerMap);
+
+    assertThat(new GsonBuilder().serializeNulls().create().toJson(map))
+        .isEqualTo(
+            "{\"a\":12,\"b\":null,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"]}}");
+    assertThat(new GsonBuilder().setPrettyPrinting().serializeNulls().create().toJson(map))
+        .isEqualTo(
+            "{\n  \"a\": 12,\n  \"b\": null,\n  \"c\": "
+                + "{\n    \"test\": 1,\n    \"TestStringArray\": "
+                + "[\n      \"one\",\n      \"two\"\n    ]\n  }\n}");
+    assertThat(new GsonBuilder().create().toJson(map))
+        .isEqualTo("{\"a\":12,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"]}}");
+    assertThat(new GsonBuilder().setPrettyPrinting().create().toJson(map))
+        .isEqualTo(
+            "{\n  \"a\": 12,\n  \"c\": "
+                + "{\n    \"test\": 1,\n    \"TestStringArray\": "
+                + "[\n      \"one\",\n      \"two\"\n    ]\n  }\n}");
+    innerMap.put("d", "e");
+    assertThat(new Gson().toJson(map))
+        .isEqualTo(
+            "{\"a\":12,\"c\":{\"test\":1,\"TestStringArray\":[\"one\",\"two\"],\"d\":\"e\"}}");
+  }
+
+  @Test
+  public final void testInterfaceTypeMap() {
+    MapClass element = new MapClass();
+    TestTypes.Sub subType = new TestTypes.Sub();
+    element.addBase("Test", subType);
+    element.addSub("Test", subType);
+
+    String subTypeJson = new Gson().toJson(subType);
+    String expected =
+        "{\"bases\":{\"Test\":" + subTypeJson + "},\"subs\":{\"Test\":" + subTypeJson + "}}";
+
+    Gson gsonWithComplexKeys = new GsonBuilder().enableComplexMapKeySerialization().create();
+    String json = gsonWithComplexKeys.toJson(element);
+    assertThat(json).isEqualTo(expected);
+
+    Gson gson = new Gson();
+    json = gson.toJson(element);
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public final void testInterfaceTypeMapWithSerializer() {
+    MapClass element = new MapClass();
+    TestTypes.Sub subType = new TestTypes.Sub();
+    element.addBase("Test", subType);
+    element.addSub("Test", subType);
+
+    Gson tempGson = new Gson();
+    String subTypeJson = tempGson.toJson(subType);
+    final JsonElement baseTypeJsonElement = tempGson.toJsonTree(subType, TestTypes.Base.class);
+    String baseTypeJson = tempGson.toJson(baseTypeJsonElement);
+    String expected =
+        "{\"bases\":{\"Test\":" + baseTypeJson + "},\"subs\":{\"Test\":" + subTypeJson + "}}";
+
+    JsonSerializer<TestTypes.Base> baseTypeAdapter =
+        new JsonSerializer<TestTypes.Base>() {
+          @Override
+          public JsonElement serialize(
+              TestTypes.Base src, Type typeOfSrc, JsonSerializationContext context) {
+            return baseTypeJsonElement;
+          }
+        };
+
+    Gson gson =
+        new GsonBuilder()
+            .enableComplexMapKeySerialization()
+            .registerTypeAdapter(TestTypes.Base.class, baseTypeAdapter)
+            .create();
+    String json = gson.toJson(element);
+    assertThat(json).isEqualTo(expected);
+
+    gson = new GsonBuilder().registerTypeAdapter(TestTypes.Base.class, baseTypeAdapter).create();
+    json = gson.toJson(element);
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testGeneralMapField() {
+    MapWithGeneralMapParameters map = new MapWithGeneralMapParameters();
+    map.map.put("string", "testString");
+    map.map.put("stringArray", new String[] {"one", "two"});
+    map.map.put("objectArray", new Object[] {1, 2L, "three"});
+
+    String expected =
+        "{\"map\":{\"string\":\"testString\",\"stringArray\":"
+            + "[\"one\",\"two\"],\"objectArray\":[1,2,\"three\"]}}";
+    assertThat(gson.toJson(map)).isEqualTo(expected);
+
+    gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+    assertThat(gson.toJson(map)).isEqualTo(expected);
+  }
+
+  @Test
+  public void testComplexKeysSerialization() {
+    Map<Point, String> map = new LinkedHashMap<>();
+    map.put(new Point(2, 3), "a");
+    map.put(new Point(5, 7), "b");
+    String json = "{\"2,3\":\"a\",\"5,7\":\"b\"}";
+    assertThat(gson.toJson(map, new TypeToken<Map<Point, String>>() {}.getType())).isEqualTo(json);
+    assertThat(gson.toJson(map, Map.class)).isEqualTo(json);
+  }
+
+  @Test
+  public void testComplexKeysDeserialization() {
+    String json = "{'2,3':'a','5,7':'b'}";
+    try {
+      gson.fromJson(json, new TypeToken<Map<Point, String>>() {}.getType());
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testStringKeyDeserialization() {
+    String json = "{'2,3':'a','5,7':'b'}";
+    Map<String, String> map = new LinkedHashMap<>();
+    map.put("2,3", "a");
+    map.put("5,7", "b");
+    assertThat(gson.fromJson(json, new TypeToken<Map<String, String>>() {})).isEqualTo(map);
+  }
+
+  @Test
+  public void testNumberKeyDeserialization() {
+    String json = "{'2.3':'a','5.7':'b'}";
+    Map<Double, String> map = new LinkedHashMap<>();
+    map.put(2.3, "a");
+    map.put(5.7, "b");
+    assertThat(gson.fromJson(json, new TypeToken<Map<Double, String>>() {})).isEqualTo(map);
+  }
+
+  @Test
+  public void testBooleanKeyDeserialization() {
+    String json = "{'true':'a','false':'b'}";
+    Map<Boolean, String> map = new LinkedHashMap<>();
+    map.put(true, "a");
+    map.put(false, "b");
+    assertThat(gson.fromJson(json, new TypeToken<Map<Boolean, String>>() {})).isEqualTo(map);
+  }
+
+  @Test
+  public void testMapDeserializationWithDuplicateKeys() {
+    try {
+      gson.fromJson("{'a':1,'a':2}", new TypeToken<Map<String, Integer>>() {}.getType());
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testSerializeMapOfMaps() {
+    Type type = new TypeToken<Map<String, Map<String, String>>>() {}.getType();
+    Map<String, Map<String, String>> map =
+        newMap(
+            "a", newMap("ka1", "va1", "ka2", "va2"),
+            "b", newMap("kb1", "vb1", "kb2", "vb2"));
+    assertThat(gson.toJson(map, type).replace('"', '\''))
+        .isEqualTo("{'a':{'ka1':'va1','ka2':'va2'},'b':{'kb1':'vb1','kb2':'vb2'}}");
+  }
+
+  @Test
+  public void testDeserializeMapOfMaps() {
+    TypeToken<Map<String, Map<String, String>>> type = new TypeToken<>() {};
+    Map<String, Map<String, String>> map =
+        newMap(
+            "a", newMap("ka1", "va1", "ka2", "va2"),
+            "b", newMap("kb1", "vb1", "kb2", "vb2"));
+    String json = "{'a':{'ka1':'va1','ka2':'va2'},'b':{'kb1':'vb1','kb2':'vb2'}}";
+    assertThat(gson.fromJson(json, type)).isEqualTo(map);
+  }
+
+  private static <K, V> Map<K, V> newMap(K key1, V value1, K key2, V value2) {
+    Map<K, V> result = new LinkedHashMap<>();
+    result.put(key1, value1);
+    result.put(key2, value2);
+    return result;
+  }
+
+  @Test
+  public void testMapNamePromotionWithJsonElementReader() {
+    String json = "{'2.3':'a'}";
+    Map<Double, String> map = new LinkedHashMap<>();
+    map.put(2.3, "a");
+    JsonElement tree = JsonParser.parseString(json);
+    assertThat(gson.fromJson(tree, new TypeToken<Map<Double, String>>() {})).isEqualTo(map);
+  }
+
+  static class Point {
+    private final int x;
+    private final int y;
+
+    Point(int x, int y) {
+      this.x = x;
+      this.y = y;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Point && x == ((Point) o).x && y == ((Point) o).y;
+    }
+
+    @Override
+    public int hashCode() {
+      return x * 37 + y;
+    }
+
+    @Override
+    public String toString() {
+      return x + "," + y;
+    }
+  }
+
+  static final class MapClass {
+    private final Map<String, TestTypes.Base> bases = new HashMap<>();
+    private final Map<String, TestTypes.Sub> subs = new HashMap<>();
+
+    public final void addBase(String name, TestTypes.Base value) {
+      bases.put(name, value);
+    }
+
+    public final void addSub(String name, TestTypes.Sub value) {
+      subs.put(name, value);
+    }
+  }
+
+  static final class MapWithGeneralMapParameters {
+    final Map<String, Object> map = new LinkedHashMap<>();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java b/gson/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..07028841433bcf933b586ab716541b1925f3bec9
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for Gson serialization of a sub-class object while encountering a base-class type
+ *
+ * @author Inderjeet Singh
+ */
+@SuppressWarnings("unused")
+public class MoreSpecificTypeSerializationTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testSubclassFields() {
+    ClassWithBaseFields target = new ClassWithBaseFields(new Sub(1, 2));
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"b\":1");
+    assertThat(json).contains("\"s\":2");
+  }
+
+  @Test
+  public void testListOfSubclassFields() {
+    List<Base> list = new ArrayList<>();
+    list.add(new Base(1));
+    list.add(new Sub(2, 3));
+    ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(list, null);
+    String json = gson.toJson(target);
+    assertThat(json).contains("{\"b\":1}");
+    assertThat(json).contains("{\"s\":3,\"b\":2}");
+  }
+
+  @Test
+  public void testMapOfSubclassFields() {
+    Map<String, Base> map = new HashMap<>();
+    map.put("base", new Base(1));
+    map.put("sub", new Sub(2, 3));
+    ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(null, map);
+    JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject();
+    assertThat(json.get("base").getAsJsonObject().get("b").getAsInt()).isEqualTo(1);
+    JsonObject sub = json.get("sub").getAsJsonObject();
+    assertThat(sub.get("b").getAsInt()).isEqualTo(2);
+    assertThat(sub.get("s").getAsInt()).isEqualTo(3);
+  }
+
+  /** For parameterized type, Gson ignores the more-specific type and sticks to the declared type */
+  @Test
+  public void testParameterizedSubclassFields() {
+    ClassWithParameterizedBaseFields target =
+        new ClassWithParameterizedBaseFields(new ParameterizedSub<>("one", "two"));
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"t\":\"one\"");
+    assertThat(json).doesNotContain("\"s\"");
+  }
+
+  /**
+   * For parameterized type in a List, Gson ignores the more-specific type and sticks to the
+   * declared type
+   */
+  @Test
+  public void testListOfParameterizedSubclassFields() {
+    List<ParameterizedBase<String>> list = new ArrayList<>();
+    list.add(new ParameterizedBase<>("one"));
+    list.add(new ParameterizedSub<>("two", "three"));
+    ClassWithContainersOfParameterizedBaseFields target =
+        new ClassWithContainersOfParameterizedBaseFields(list, null);
+    String json = gson.toJson(target);
+    assertThat(json).contains("{\"t\":\"one\"}");
+    assertThat(json).doesNotContain("\"s\":");
+  }
+
+  /**
+   * For parameterized type in a map, Gson ignores the more-specific type and sticks to the declared
+   * type
+   */
+  @Test
+  public void testMapOfParameterizedSubclassFields() {
+    Map<String, ParameterizedBase<String>> map = new HashMap<>();
+    map.put("base", new ParameterizedBase<>("one"));
+    map.put("sub", new ParameterizedSub<>("two", "three"));
+    ClassWithContainersOfParameterizedBaseFields target =
+        new ClassWithContainersOfParameterizedBaseFields(null, map);
+    JsonObject json = gson.toJsonTree(target).getAsJsonObject().get("map").getAsJsonObject();
+    assertThat(json.get("base").getAsJsonObject().get("t").getAsString()).isEqualTo("one");
+    JsonObject sub = json.get("sub").getAsJsonObject();
+    assertThat(sub.get("t").getAsString()).isEqualTo("two");
+    assertThat(sub.get("s")).isNull();
+  }
+
+  private static class Base {
+    int b;
+
+    Base(int b) {
+      this.b = b;
+    }
+  }
+
+  private static class Sub extends Base {
+    int s;
+
+    Sub(int b, int s) {
+      super(b);
+      this.s = s;
+    }
+  }
+
+  private static class ClassWithBaseFields {
+    Base b;
+
+    ClassWithBaseFields(Base b) {
+      this.b = b;
+    }
+  }
+
+  private static class ClassWithContainersOfBaseFields {
+    Collection<Base> collection;
+    Map<String, Base> map;
+
+    ClassWithContainersOfBaseFields(Collection<Base> collection, Map<String, Base> map) {
+      this.collection = collection;
+      this.map = map;
+    }
+  }
+
+  private static class ParameterizedBase<T> {
+    T t;
+
+    ParameterizedBase(T t) {
+      this.t = t;
+    }
+  }
+
+  private static class ParameterizedSub<T> extends ParameterizedBase<T> {
+    T s;
+
+    ParameterizedSub(T t, T s) {
+      super(t);
+      this.s = s;
+    }
+  }
+
+  private static class ClassWithParameterizedBaseFields {
+    ParameterizedBase<String> b;
+
+    ClassWithParameterizedBaseFields(ParameterizedBase<String> b) {
+      this.b = b;
+    }
+  }
+
+  private static class ClassWithContainersOfParameterizedBaseFields {
+    Collection<ParameterizedBase<String>> collection;
+    Map<String, ParameterizedBase<String>> map;
+
+    ClassWithContainersOfParameterizedBaseFields(
+        Collection<ParameterizedBase<String>> collection,
+        Map<String, ParameterizedBase<String>> map) {
+      this.collection = collection;
+      this.map = map;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java b/gson/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a1d91481e0a58db5dc72142c9e431ca4c55762c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.FieldNamingStrategy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.common.TestTypes.ClassWithSerializedNameFields;
+import com.google.gson.common.TestTypes.StringWrapper;
+import java.lang.reflect.Field;
+import java.util.Locale;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for naming policies.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class NamingPolicyTest {
+  private GsonBuilder builder;
+
+  @Before
+  public void setUp() throws Exception {
+    builder = new GsonBuilder();
+  }
+
+  @Test
+  public void testGsonWithNonDefaultFieldNamingPolicySerialization() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"SomeConstantStringInstanceField\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithNonDefaultFieldNamingPolicyDeserialiation() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).create();
+    String target = "{\"SomeConstantStringInstanceField\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseDashPolicySerialization() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"some-constant-string-instance-field\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseDotPolicySerialization() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DOTS).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"some.constant.string.instance.field\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseDotPolicyDeserialiation() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DOTS).create();
+    String target = "{\"some.constant.string.instance.field\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseDashPolicyDeserialiation() {
+    Gson gson = builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES).create();
+    String target = "{\"some-constant-string-instance-field\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseUnderscorePolicySerialization() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"some_constant_string_instance_field\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithLowerCaseUnderscorePolicyDeserialiation() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+    String target = "{\"some_constant_string_instance_field\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testGsonWithSerializedNameFieldNamingPolicySerialization() {
+    Gson gson = builder.create();
+    ClassWithSerializedNameFields expected = new ClassWithSerializedNameFields(5, 6);
+    String actual = gson.toJson(expected);
+    assertThat(actual).isEqualTo(expected.getExpectedJson());
+  }
+
+  @Test
+  public void testGsonWithSerializedNameFieldNamingPolicyDeserialization() {
+    Gson gson = builder.create();
+    ClassWithSerializedNameFields expected = new ClassWithSerializedNameFields(5, 7);
+    ClassWithSerializedNameFields actual =
+        gson.fromJson(expected.getExpectedJson(), ClassWithSerializedNameFields.class);
+    assertThat(actual.f).isEqualTo(expected.f);
+  }
+
+  @Test
+  public void testGsonDuplicateNameUsingSerializedNameFieldNamingPolicySerialization() {
+    Gson gson = builder.create();
+    try {
+      ClassWithDuplicateFields target = new ClassWithDuplicateFields(10);
+      gson.toJson(target);
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Class com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields declares"
+                  + " multiple JSON fields named 'a'; conflict is caused by fields"
+                  + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#a and"
+                  + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#b\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#duplicate-fields");
+    }
+  }
+
+  @Test
+  public void testGsonDuplicateNameDueToBadNamingPolicy() {
+    Gson gson =
+        builder
+            .setFieldNamingStrategy(
+                new FieldNamingStrategy() {
+                  @Override
+                  public String translateName(Field f) {
+                    return "x";
+                  }
+                })
+            .create();
+
+    try {
+      gson.toJson(new ClassWithTwoFields());
+      fail();
+    } catch (IllegalArgumentException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Class com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields declares"
+                  + " multiple JSON fields named 'x'; conflict is caused by fields"
+                  + " com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#a and"
+                  + " com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#b\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#duplicate-fields");
+    }
+  }
+
+  @Test
+  public void testGsonWithUpperCamelCaseSpacesPolicySerialiation() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"Some Constant String Instance Field\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithUpperCamelCaseSpacesPolicyDeserialiation() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE_WITH_SPACES).create();
+    String target = "{\"Some Constant String Instance Field\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testGsonWithUpperCaseUnderscorePolicySerialization() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CASE_WITH_UNDERSCORES).create();
+    StringWrapper target = new StringWrapper("blah");
+    assertThat(gson.toJson(target))
+        .isEqualTo(
+            "{\"SOME_CONSTANT_STRING_INSTANCE_FIELD\":\""
+                + target.someConstantStringInstanceField
+                + "\"}");
+  }
+
+  @Test
+  public void testGsonWithUpperCaseUnderscorePolicyDeserialiation() {
+    Gson gson =
+        builder.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CASE_WITH_UNDERSCORES).create();
+    String target = "{\"SOME_CONSTANT_STRING_INSTANCE_FIELD\":\"someValue\"}";
+    StringWrapper deserializedObject = gson.fromJson(target, StringWrapper.class);
+    assertThat(deserializedObject.someConstantStringInstanceField).isEqualTo("someValue");
+  }
+
+  @Test
+  public void testDeprecatedNamingStrategy() {
+    Gson gson = builder.setFieldNamingStrategy(new UpperCaseNamingStrategy()).create();
+    ClassWithDuplicateFields target = new ClassWithDuplicateFields(10);
+    String actual = gson.toJson(target);
+    assertThat(actual).isEqualTo("{\"A\":10}");
+  }
+
+  @Test
+  public void testComplexFieldNameStrategy() {
+    Gson gson = new Gson();
+    String json = gson.toJson(new ClassWithComplexFieldName(10));
+    String escapedFieldName = "@value\\\"_s$\\\\";
+    assertThat(json).isEqualTo("{\"" + escapedFieldName + "\":10}");
+
+    ClassWithComplexFieldName obj = gson.fromJson(json, ClassWithComplexFieldName.class);
+    assertThat(obj.value).isEqualTo(10);
+  }
+
+  /** http://code.google.com/p/google-gson/issues/detail?id=349 */
+  @Test
+  public void testAtSignInSerializedName() {
+    assertThat(new Gson().toJson(new AtName())).isEqualTo("{\"@foo\":\"bar\"}");
+  }
+
+  static final class AtName {
+    @SerializedName("@foo")
+    String f = "bar";
+  }
+
+  private static final class UpperCaseNamingStrategy implements FieldNamingStrategy {
+    @Override
+    public String translateName(Field f) {
+      return f.getName().toUpperCase(Locale.ROOT);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class ClassWithDuplicateFields {
+    public Integer a;
+
+    @SerializedName("a")
+    public Double b;
+
+    public ClassWithDuplicateFields(Integer a) {
+      this(a, null);
+    }
+
+    public ClassWithDuplicateFields(Double b) {
+      this(null, b);
+    }
+
+    public ClassWithDuplicateFields(Integer a, Double b) {
+      this.a = a;
+      this.b = b;
+    }
+  }
+
+  private static class ClassWithComplexFieldName {
+    @SerializedName("@value\"_s$\\")
+    public final long value;
+
+    ClassWithComplexFieldName(long value) {
+      this.value = value;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class ClassWithTwoFields {
+    public int a;
+    public int b;
+
+    public ClassWithTwoFields() {}
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java b/gson/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d84baa4b62a09b39384a4184e05914cef2b7b49c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/NullObjectAndFieldTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+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.JsonNull;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.ClassWithObjects;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for the different cases for serializing (or ignoring) null fields and object.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class NullObjectAndFieldTest {
+  private GsonBuilder gsonBuilder;
+
+  @Before
+  public void setUp() throws Exception {
+    gsonBuilder = new GsonBuilder().serializeNulls();
+  }
+
+  @Test
+  public void testTopLevelNullObjectSerialization() {
+    Gson gson = gsonBuilder.create();
+    String actual = gson.toJson(null);
+    assertThat(actual).isEqualTo("null");
+
+    actual = gson.toJson(null, String.class);
+    assertThat(actual).isEqualTo("null");
+  }
+
+  @Test
+  public void testTopLevelNullObjectDeserialization() {
+    Gson gson = gsonBuilder.create();
+    String actual = gson.fromJson("null", String.class);
+    assertThat(actual).isNull();
+  }
+
+  @Test
+  public void testExplicitSerializationOfNulls() {
+    Gson gson = gsonBuilder.create();
+    ClassWithObjects target = new ClassWithObjects(null);
+    String actual = gson.toJson(target);
+    String expected = "{\"bag\":null}";
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testExplicitDeserializationOfNulls() {
+    Gson gson = gsonBuilder.create();
+    ClassWithObjects target = gson.fromJson("{\"bag\":null}", ClassWithObjects.class);
+    assertThat(target.bag).isNull();
+  }
+
+  @Test
+  public void testExplicitSerializationOfNullArrayMembers() {
+    Gson gson = gsonBuilder.create();
+    ClassWithMembers target = new ClassWithMembers();
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"array\":null");
+  }
+
+  /** Added to verify http://code.google.com/p/google-gson/issues/detail?id=68 */
+  @Test
+  public void testNullWrappedPrimitiveMemberSerialization() {
+    Gson gson = gsonBuilder.serializeNulls().create();
+    ClassWithNullWrappedPrimitive target = new ClassWithNullWrappedPrimitive();
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"value\":null");
+  }
+
+  /** Added to verify http://code.google.com/p/google-gson/issues/detail?id=68 */
+  @Test
+  public void testNullWrappedPrimitiveMemberDeserialization() {
+    Gson gson = gsonBuilder.create();
+    String json = "{'value':null}";
+    ClassWithNullWrappedPrimitive target = gson.fromJson(json, ClassWithNullWrappedPrimitive.class);
+    assertThat(target.value).isNull();
+  }
+
+  @Test
+  public void testExplicitSerializationOfNullCollectionMembers() {
+    Gson gson = gsonBuilder.create();
+    ClassWithMembers target = new ClassWithMembers();
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"col\":null");
+  }
+
+  @Test
+  public void testExplicitSerializationOfNullStringMembers() {
+    Gson gson = gsonBuilder.create();
+    ClassWithMembers target = new ClassWithMembers();
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"str\":null");
+  }
+
+  @Test
+  public void testCustomSerializationOfNulls() {
+    gsonBuilder.registerTypeAdapter(ClassWithObjects.class, new ClassWithObjectsSerializer());
+    Gson gson = gsonBuilder.create();
+    ClassWithObjects target = new ClassWithObjects(new BagOfPrimitives());
+    String actual = gson.toJson(target);
+    String expected = "{\"bag\":null}";
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testPrintPrintingObjectWithNulls() {
+    gsonBuilder = new GsonBuilder();
+    Gson gson = gsonBuilder.create();
+    String result = gson.toJson(new ClassWithMembers());
+    assertThat(result).isEqualTo("{}");
+
+    gson = gsonBuilder.serializeNulls().create();
+    result = gson.toJson(new ClassWithMembers());
+    assertThat(result).contains("\"str\":null");
+  }
+
+  @Test
+  public void testPrintPrintingArraysWithNulls() {
+    gsonBuilder = new GsonBuilder();
+    Gson gson = gsonBuilder.create();
+    String result = gson.toJson(new String[] {"1", null, "3"});
+    assertThat(result).isEqualTo("[\"1\",null,\"3\"]");
+
+    gson = gsonBuilder.serializeNulls().create();
+    result = gson.toJson(new String[] {"1", null, "3"});
+    assertThat(result).isEqualTo("[\"1\",null,\"3\"]");
+  }
+
+  // test for issue 389
+  @Test
+  public void testAbsentJsonElementsAreSetToNull() {
+    Gson gson = new Gson();
+    ClassWithInitializedMembers target =
+        gson.fromJson("{array:[1,2,3]}", ClassWithInitializedMembers.class);
+    assertThat(target.array).hasLength(3);
+    assertThat(target.array[1]).isEqualTo(2);
+    assertThat(target.str1).isEqualTo(ClassWithInitializedMembers.MY_STRING_DEFAULT);
+    assertThat(target.str2).isNull();
+    assertThat(target.int1).isEqualTo(ClassWithInitializedMembers.MY_INT_DEFAULT);
+    // test the default value of a primitive int field per JVM spec
+    assertThat(target.int2).isEqualTo(0);
+    assertThat(target.bool1).isEqualTo(ClassWithInitializedMembers.MY_BOOLEAN_DEFAULT);
+    // test the default value of a primitive boolean field per JVM spec
+    assertThat(target.bool2).isFalse();
+  }
+
+  public static class ClassWithInitializedMembers {
+    // Using a mix of no-args constructor and field initializers
+    // Also, some fields are initialized and some are not (so initialized per JVM spec)
+    public static final String MY_STRING_DEFAULT = "string";
+    private static final int MY_INT_DEFAULT = 2;
+    private static final boolean MY_BOOLEAN_DEFAULT = true;
+    int[] array;
+    String str1;
+    String str2;
+    int int1 = MY_INT_DEFAULT;
+    int int2;
+    boolean bool1 = MY_BOOLEAN_DEFAULT;
+    boolean bool2;
+
+    public ClassWithInitializedMembers() {
+      str1 = MY_STRING_DEFAULT;
+    }
+  }
+
+  private static class ClassWithNullWrappedPrimitive {
+    private Long value;
+  }
+
+  @SuppressWarnings("unused")
+  private static class ClassWithMembers {
+    String str;
+    int[] array;
+    Collection<String> col;
+  }
+
+  private static class ClassWithObjectsSerializer implements JsonSerializer<ClassWithObjects> {
+    @Override
+    public JsonElement serialize(
+        ClassWithObjects src, Type typeOfSrc, JsonSerializationContext context) {
+      JsonObject obj = new JsonObject();
+      obj.add("bag", JsonNull.INSTANCE);
+      return obj;
+    }
+  }
+
+  @Test
+  public void testExplicitNullSetsFieldToNullDuringDeserialization() {
+    Gson gson = new Gson();
+    String json = "{value:null}";
+    ObjectWithField obj = gson.fromJson(json, ObjectWithField.class);
+    assertThat(obj.value).isNull();
+  }
+
+  @Test
+  public void testCustomTypeAdapterPassesNullSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ObjectWithField.class,
+                new JsonSerializer<ObjectWithField>() {
+                  @Override
+                  public JsonElement serialize(
+                      ObjectWithField src, Type typeOfSrc, JsonSerializationContext context) {
+                    return context.serialize(null);
+                  }
+                })
+            .create();
+    ObjectWithField target = new ObjectWithField();
+    target.value = "value1";
+    String json = gson.toJson(target);
+    assertThat(json).doesNotContain("value1");
+  }
+
+  @Test
+  public void testCustomTypeAdapterPassesNullDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                ObjectWithField.class,
+                new JsonDeserializer<ObjectWithField>() {
+                  @Override
+                  public ObjectWithField deserialize(
+                      JsonElement json, Type type, JsonDeserializationContext context) {
+                    return context.deserialize(null, type);
+                  }
+                })
+            .create();
+    String json = "{value:'value1'}";
+    ObjectWithField target = gson.fromJson(json, ObjectWithField.class);
+    assertThat(target).isNull();
+  }
+
+  private static class ObjectWithField {
+    String value = "";
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/NumberLimitsTest.java b/gson/gson/src/test/java/com/google/gson/functional/NumberLimitsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9df4d9fc653a41a2705b79b304ba80289394ccdb
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/NumberLimitsTest.java
@@ -0,0 +1,228 @@
+package com.google.gson.functional;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.ToNumberStrategy;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.LazilyParsedNumber;
+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.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.junit.Test;
+
+public class NumberLimitsTest {
+  private static final int MAX_LENGTH = 10_000;
+
+  private static JsonReader jsonReader(String json) {
+    return new JsonReader(new StringReader(json));
+  }
+
+  /**
+   * Tests how {@link JsonReader} behaves for large numbers.
+   *
+   * <p>Currently {@link JsonReader} itself does not enforce any limits. The reasons for this are:
+   *
+   * <ul>
+   *   <li>Methods such as {@link JsonReader#nextDouble()} seem to have no problem parsing extremely
+   *       large or small numbers (it rounds to 0 or Infinity) (to be verified?; if it had
+   *       performance problems with certain numbers, then it would affect other parts of Gson which
+   *       parse as float or double as well)
+   *   <li>Enforcing limits only when a JSON number is encountered would be ineffective when users
+   *       want to consume a JSON number as string using {@link JsonReader#nextString()} unless they
+   *       explicitly call {@link JsonReader#peek()} and check if the value is a JSON number.
+   *       Otherwise the limits could be circumvented because {@link JsonReader#nextString()} reads
+   *       both strings and numbers, and for JSON strings no restrictions are enforced.
+   * </ul>
+   */
+  @Test
+  public void testJsonReader() throws IOException {
+    JsonReader reader = jsonReader("1".repeat(1000));
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextString()).isEqualTo("1".repeat(1000));
+
+    JsonReader reader2 = jsonReader("1".repeat(MAX_LENGTH + 1));
+    // Currently JsonReader does not recognize large JSON numbers as numbers but treats them
+    // as unquoted string
+    MalformedJsonException e = assertThrows(MalformedJsonException.class, () -> reader2.peek());
+    assertThat(e)
+        .hasMessageThat()
+        .startsWith("Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON");
+
+    reader = jsonReader("1e9999");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextString()).isEqualTo("1e9999");
+
+    reader = jsonReader("1e+9999");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextString()).isEqualTo("1e+9999");
+
+    reader = jsonReader("1e10000");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextString()).isEqualTo("1e10000");
+
+    reader = jsonReader("1e00001");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextString()).isEqualTo("1e00001");
+  }
+
+  @Test
+  public void testJsonPrimitive() {
+    assertThat(new JsonPrimitive("1".repeat(MAX_LENGTH)).getAsBigDecimal())
+        .isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
+    assertThat(new JsonPrimitive("1e9999").getAsBigDecimal()).isEqualTo(new BigDecimal("1e9999"));
+    assertThat(new JsonPrimitive("1e-9999").getAsBigDecimal()).isEqualTo(new BigDecimal("1e-9999"));
+
+    NumberFormatException e =
+        assertThrows(
+            NumberFormatException.class,
+            () -> new JsonPrimitive("1".repeat(MAX_LENGTH + 1)).getAsBigDecimal());
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+
+    e =
+        assertThrows(
+            NumberFormatException.class, () -> new JsonPrimitive("1e10000").getAsBigDecimal());
+    assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
+
+    e =
+        assertThrows(
+            NumberFormatException.class, () -> new JsonPrimitive("1e-10000").getAsBigDecimal());
+    assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e-10000");
+
+    assertThat(new JsonPrimitive("1".repeat(MAX_LENGTH)).getAsBigInteger())
+        .isEqualTo(new BigInteger("1".repeat(MAX_LENGTH)));
+
+    e =
+        assertThrows(
+            NumberFormatException.class,
+            () -> new JsonPrimitive("1".repeat(MAX_LENGTH + 1)).getAsBigInteger());
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+  }
+
+  @Test
+  public void testToNumberPolicy() throws IOException {
+    ToNumberStrategy strategy = ToNumberPolicy.BIG_DECIMAL;
+
+    assertThat(strategy.readNumber(jsonReader("\"" + "1".repeat(MAX_LENGTH) + "\"")))
+        .isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
+    assertThat(strategy.readNumber(jsonReader("1e9999"))).isEqualTo(new BigDecimal("1e9999"));
+
+    JsonParseException e =
+        assertThrows(
+            JsonParseException.class,
+            () -> strategy.readNumber(jsonReader("\"" + "1".repeat(MAX_LENGTH + 1) + "\"")));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Cannot parse " + "1".repeat(MAX_LENGTH + 1) + "; at path $");
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+
+    e =
+        assertThrows(
+            JsonParseException.class, () -> strategy.readNumber(jsonReader("\"1e10000\"")));
+    assertThat(e).hasMessageThat().isEqualTo("Cannot parse 1e10000; at path $");
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Number has unsupported scale: 1e10000");
+  }
+
+  @Test
+  public void testLazilyParsedNumber() throws IOException {
+    assertThat(new LazilyParsedNumber("1".repeat(MAX_LENGTH)).intValue())
+        .isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)).intValue());
+    assertThat(new LazilyParsedNumber("1e9999").intValue())
+        .isEqualTo(new BigDecimal("1e9999").intValue());
+
+    NumberFormatException e =
+        assertThrows(
+            NumberFormatException.class,
+            () -> new LazilyParsedNumber("1".repeat(MAX_LENGTH + 1)).intValue());
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+
+    e =
+        assertThrows(
+            NumberFormatException.class, () -> new LazilyParsedNumber("1e10000").intValue());
+    assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
+
+    e =
+        assertThrows(
+            NumberFormatException.class, () -> new LazilyParsedNumber("1e10000").longValue());
+    assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
+
+    ObjectOutputStream objOut = new ObjectOutputStream(OutputStream.nullOutputStream());
+    // Number is serialized as BigDecimal; should also enforce limits during this conversion
+    e =
+        assertThrows(
+            NumberFormatException.class,
+            () -> objOut.writeObject(new LazilyParsedNumber("1e10000")));
+    assertThat(e).hasMessageThat().isEqualTo("Number has unsupported scale: 1e10000");
+  }
+
+  @Test
+  public void testBigDecimalAdapter() throws IOException {
+    TypeAdapter<BigDecimal> adapter = new Gson().getAdapter(BigDecimal.class);
+
+    assertThat(adapter.fromJson("\"" + "1".repeat(MAX_LENGTH) + "\""))
+        .isEqualTo(new BigDecimal("1".repeat(MAX_LENGTH)));
+    assertThat(adapter.fromJson("\"1e9999\"")).isEqualTo(new BigDecimal("1e9999"));
+
+    JsonSyntaxException e =
+        assertThrows(
+            JsonSyntaxException.class,
+            () -> adapter.fromJson("\"" + "1".repeat(MAX_LENGTH + 1) + "\""));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Failed parsing '" + "1".repeat(MAX_LENGTH + 1) + "' as BigDecimal; at path $");
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+
+    e = assertThrows(JsonSyntaxException.class, () -> adapter.fromJson("\"1e10000\""));
+    assertThat(e).hasMessageThat().isEqualTo("Failed parsing '1e10000' as BigDecimal; at path $");
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Number has unsupported scale: 1e10000");
+  }
+
+  @Test
+  public void testBigIntegerAdapter() throws IOException {
+    TypeAdapter<BigInteger> adapter = new Gson().getAdapter(BigInteger.class);
+
+    assertThat(adapter.fromJson("\"" + "1".repeat(MAX_LENGTH) + "\""))
+        .isEqualTo(new BigInteger("1".repeat(MAX_LENGTH)));
+
+    JsonSyntaxException e =
+        assertThrows(
+            JsonSyntaxException.class,
+            () -> adapter.fromJson("\"" + "1".repeat(MAX_LENGTH + 1) + "\""));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("Failed parsing '" + "1".repeat(MAX_LENGTH + 1) + "' as BigInteger; at path $");
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Number string too large: 111111111111111111111111111111...");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/gson/src/test/java/com/google/gson/functional/ObjectTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0f7e38d76a33aa1b348fa10276e6edb5540da69
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ObjectTest.java
@@ -0,0 +1,775 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+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.InstanceCreator;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.common.TestTypes.ArrayOfObjects;
+import com.google.gson.common.TestTypes.BagOfPrimitiveWrappers;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.ClassWithArray;
+import com.google.gson.common.TestTypes.ClassWithNoFields;
+import com.google.gson.common.TestTypes.ClassWithObjects;
+import com.google.gson.common.TestTypes.ClassWithTransientFields;
+import com.google.gson.common.TestTypes.Nested;
+import com.google.gson.common.TestTypes.PrimitiveArray;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json serialization and deserialization of regular classes.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ObjectTest {
+  private Gson gson;
+  private TimeZone oldTimeZone;
+  private Locale oldLocale;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+
+    oldTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+    oldLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+  }
+
+  @After
+  public void tearDown() {
+    TimeZone.setDefault(oldTimeZone);
+    Locale.setDefault(oldLocale);
+  }
+
+  @Test
+  public void testJsonInSingleQuotesDeserialization() {
+    String json = "{'stringValue':'no message','intValue':10,'longValue':20}";
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.stringValue).isEqualTo("no message");
+    assertThat(target.intValue).isEqualTo(10);
+    assertThat(target.longValue).isEqualTo(20);
+  }
+
+  @Test
+  public void testJsonInMixedQuotesDeserialization() {
+    String json = "{\"stringValue\":'no message','intValue':10,'longValue':20}";
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.stringValue).isEqualTo("no message");
+    assertThat(target.intValue).isEqualTo(10);
+    assertThat(target.longValue).isEqualTo(20);
+  }
+
+  @Test
+  public void testBagOfPrimitivesSerialization() {
+    BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue");
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testBagOfPrimitivesDeserialization() {
+    BagOfPrimitives src = new BagOfPrimitives(10, 20, false, "stringValue");
+    String json = src.getExpectedJson();
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testBagOfPrimitiveWrappersSerialization() {
+    BagOfPrimitiveWrappers target = new BagOfPrimitiveWrappers(10L, 20, false);
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testBagOfPrimitiveWrappersDeserialization() {
+    BagOfPrimitiveWrappers target = new BagOfPrimitiveWrappers(10L, 20, false);
+    String jsonString = target.getExpectedJson();
+    target = gson.fromJson(jsonString, BagOfPrimitiveWrappers.class);
+    assertThat(target.getExpectedJson()).isEqualTo(jsonString);
+  }
+
+  @Test
+  public void testClassWithTransientFieldsSerialization() {
+    ClassWithTransientFields<Long> target = new ClassWithTransientFields<>(1L);
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testClassWithTransientFieldsDeserialization() {
+    String json = "{\"longValue\":[1]}";
+    ClassWithTransientFields<?> target = gson.fromJson(json, ClassWithTransientFields.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testClassWithTransientFieldsDeserializationTransientFieldsPassedInJsonAreIgnored() {
+    String json = "{\"transientLongValue\":5,\"longValue\":[1]}";
+    ClassWithTransientFields<?> target = gson.fromJson(json, ClassWithTransientFields.class);
+    assertThat(target.transientLongValue).isEqualTo(1);
+  }
+
+  @Test
+  public void testClassWithNoFieldsSerialization() {
+    assertThat(gson.toJson(new ClassWithNoFields())).isEqualTo("{}");
+  }
+
+  @Test
+  public void testClassWithNoFieldsDeserialization() {
+    String json = "{}";
+    ClassWithNoFields target = gson.fromJson(json, ClassWithNoFields.class);
+    ClassWithNoFields expected = new ClassWithNoFields();
+    assertThat(target).isEqualTo(expected);
+  }
+
+  private static class Subclass extends Superclass1 {}
+
+  private static class Superclass1 extends Superclass2 {
+    @SuppressWarnings({"unused", "HidingField"})
+    String s;
+  }
+
+  private static class Superclass2 {
+    @SuppressWarnings("unused")
+    String s;
+  }
+
+  @Test
+  public void testClassWithDuplicateFields() {
+    String expectedMessage =
+        "Class com.google.gson.functional.ObjectTest$Subclass declares multiple JSON fields named"
+            + " 's'; conflict is caused by fields"
+            + " com.google.gson.functional.ObjectTest$Superclass1#s and"
+            + " com.google.gson.functional.ObjectTest$Superclass2#s\n"
+            + "See https://github.com/google/gson/blob/main/Troubleshooting.md#duplicate-fields";
+
+    try {
+      gson.getAdapter(Subclass.class);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+    }
+
+    // Detection should also work properly when duplicate fields exist only for serialization
+    Gson gson =
+        new GsonBuilder()
+            .addDeserializationExclusionStrategy(
+                new ExclusionStrategy() {
+                  @Override
+                  public boolean shouldSkipField(FieldAttributes f) {
+                    // Skip all fields for deserialization
+                    return true;
+                  }
+
+                  @Override
+                  public boolean shouldSkipClass(Class<?> clazz) {
+                    return false;
+                  }
+                })
+            .create();
+
+    try {
+      gson.getAdapter(Subclass.class);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+    }
+  }
+
+  @Test
+  public void testNestedSerialization() {
+    Nested target =
+        new Nested(
+            new BagOfPrimitives(10, 20, false, "stringValue"),
+            new BagOfPrimitives(30, 40, true, "stringValue"));
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testNestedDeserialization() {
+    String json =
+        "{\"primitive1\":{\"longValue\":10,\"intValue\":20,\"booleanValue\":false,"
+            + "\"stringValue\":\"stringValue\"},\"primitive2\":{\"longValue\":30,\"intValue\":40,"
+            + "\"booleanValue\":true,\"stringValue\":\"stringValue\"}}";
+    Nested target = gson.fromJson(json, Nested.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testNullSerialization() {
+    assertThat(gson.toJson(null)).isEqualTo("null");
+  }
+
+  @Test
+  public void testEmptyStringDeserialization() {
+    Object object = gson.fromJson("", Object.class);
+    assertThat(object).isNull();
+  }
+
+  @Test
+  public void testTruncatedDeserialization() {
+    try {
+      gson.fromJson("[\"a\", \"b\",", new TypeToken<List<String>>() {}.getType());
+      fail();
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testNullDeserialization() {
+    String myNullObject = null;
+    Object object = gson.fromJson(myNullObject, Object.class);
+    assertThat(object).isNull();
+  }
+
+  @Test
+  public void testNullFieldsSerialization() {
+    Nested target = new Nested(new BagOfPrimitives(10, 20, false, "stringValue"), null);
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testNullFieldsDeserialization() {
+    String json =
+        "{\"primitive1\":{\"longValue\":10,\"intValue\":20,\"booleanValue\":false"
+            + ",\"stringValue\":\"stringValue\"}}";
+    Nested target = gson.fromJson(json, Nested.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testArrayOfObjectsSerialization() {
+    ArrayOfObjects target = new ArrayOfObjects();
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testArrayOfObjectsDeserialization() {
+    String json = new ArrayOfObjects().getExpectedJson();
+    ArrayOfObjects target = gson.fromJson(json, ArrayOfObjects.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testArrayOfArraysSerialization() {
+    ArrayOfArrays target = new ArrayOfArrays();
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testArrayOfArraysDeserialization() {
+    String json = new ArrayOfArrays().getExpectedJson();
+    ArrayOfArrays target = gson.fromJson(json, ArrayOfArrays.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  @Test
+  public void testArrayOfObjectsAsFields() {
+    ClassWithObjects classWithObjects = new ClassWithObjects();
+    BagOfPrimitives bagOfPrimitives = new BagOfPrimitives();
+    String stringValue = "someStringValueInArray";
+    String classWithObjectsJson = gson.toJson(classWithObjects);
+    String bagOfPrimitivesJson = gson.toJson(bagOfPrimitives);
+
+    ClassWithArray classWithArray =
+        new ClassWithArray(new Object[] {stringValue, classWithObjects, bagOfPrimitives});
+    String json = gson.toJson(classWithArray);
+
+    assertThat(json).contains(classWithObjectsJson);
+    assertThat(json).contains(bagOfPrimitivesJson);
+    assertThat(json).contains("\"" + stringValue + "\"");
+  }
+
+  /** Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 */
+  @Test
+  public void testNullArraysDeserialization() {
+    String json = "{\"array\": null}";
+    ClassWithArray target = gson.fromJson(json, ClassWithArray.class);
+    assertThat(target.array).isNull();
+  }
+
+  /** Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 */
+  @Test
+  public void testNullObjectFieldsDeserialization() {
+    String json = "{\"bag\": null}";
+    ClassWithObjects target = gson.fromJson(json, ClassWithObjects.class);
+    assertThat(target.bag).isNull();
+  }
+
+  @Test
+  public void testEmptyCollectionInAnObjectDeserialization() {
+    String json = "{\"children\":[]}";
+    ClassWithCollectionField target = gson.fromJson(json, ClassWithCollectionField.class);
+    assertThat(target).isNotNull();
+    assertThat(target.children).isEmpty();
+  }
+
+  private static class ClassWithCollectionField {
+    Collection<String> children = new ArrayList<>();
+  }
+
+  @Test
+  public void testPrimitiveArrayInAnObjectDeserialization() {
+    String json = "{\"longArray\":[0,1,2,3,4,5,6,7,8,9]}";
+    PrimitiveArray target = gson.fromJson(json, PrimitiveArray.class);
+    assertThat(target.getExpectedJson()).isEqualTo(json);
+  }
+
+  /** Created in response to Issue 14: http://code.google.com/p/google-gson/issues/detail?id=14 */
+  @Test
+  public void testNullPrimitiveFieldsDeserialization() {
+    String json = "{\"longValue\":null}";
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.longValue).isEqualTo(BagOfPrimitives.DEFAULT_VALUE);
+  }
+
+  @Test
+  public void testEmptyCollectionInAnObjectSerialization() {
+    ClassWithCollectionField target = new ClassWithCollectionField();
+    assertThat(gson.toJson(target)).isEqualTo("{\"children\":[]}");
+  }
+
+  @Test
+  public void testPrivateNoArgConstructorDeserialization() {
+    ClassWithPrivateNoArgsConstructor target =
+        gson.fromJson("{\"a\":20}", ClassWithPrivateNoArgsConstructor.class);
+    assertThat(target.a).isEqualTo(20);
+  }
+
+  @Test
+  public void testAnonymousLocalClassesSerialization() {
+    assertThat(
+            gson.toJson(
+                new ClassWithNoFields() {
+                  // empty anonymous class
+                }))
+        .isEqualTo("null");
+
+    class Local {}
+    assertThat(gson.toJson(new Local())).isEqualTo("null");
+  }
+
+  @Test
+  public void testAnonymousLocalClassesCustomSerialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                ClassWithNoFields.class,
+                new JsonSerializer<ClassWithNoFields>() {
+                  @Override
+                  public JsonElement serialize(
+                      ClassWithNoFields src, Type typeOfSrc, JsonSerializationContext context) {
+                    return new JsonPrimitive("custom-value");
+                  }
+                })
+            .create();
+
+    assertThat(
+            gson.toJson(
+                new ClassWithNoFields() {
+                  // empty anonymous class
+                }))
+        .isEqualTo("\"custom-value\"");
+
+    class Local {}
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Local.class,
+                new JsonSerializer<Local>() {
+                  @Override
+                  public JsonElement serialize(
+                      Local src, Type typeOfSrc, JsonSerializationContext context) {
+                    return new JsonPrimitive("custom-value");
+                  }
+                })
+            .create();
+    assertThat(gson.toJson(new Local())).isEqualTo("\"custom-value\"");
+  }
+
+  @Test
+  public void testAnonymousLocalClassesCustomDeserialization() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                ClassWithNoFields.class,
+                new JsonDeserializer<ClassWithNoFields>() {
+                  @Override
+                  public ClassWithNoFields deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+                    return new ClassWithNoFields();
+                  }
+                })
+            .create();
+
+    assertThat(gson.fromJson("{}", ClassWithNoFields.class)).isNotNull();
+    Class<?> anonymousClass = new ClassWithNoFields() {}.getClass();
+    // Custom deserializer is ignored
+    assertThat(gson.fromJson("{}", anonymousClass)).isNull();
+
+    class Local {}
+    gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Local.class,
+                new JsonDeserializer<Local>() {
+                  @Override
+                  public Local deserialize(
+                      JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+                    throw new AssertionError("should not be called");
+                  }
+                })
+            .create();
+    // Custom deserializer is ignored
+    assertThat(gson.fromJson("{}", Local.class)).isNull();
+  }
+
+  @Test
+  public void testPrimitiveArrayFieldSerialization() {
+    PrimitiveArray target = new PrimitiveArray(new long[] {1L, 2L, 3L});
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  /** Tests that a class field with type Object can be serialized properly. See issue 54 */
+  @Test
+  public void testClassWithObjectFieldSerialization() {
+    ClassWithObjectField obj = new ClassWithObjectField();
+    obj.member = "abc";
+    String json = gson.toJson(obj);
+    assertThat(json).contains("abc");
+  }
+
+  private static class ClassWithObjectField {
+    @SuppressWarnings("unused")
+    Object member;
+  }
+
+  @Test
+  public void testInnerClassSerialization() {
+    Parent p = new Parent();
+    Parent.Child c = p.new Child();
+    String json = gson.toJson(c);
+    assertThat(json).contains("value2");
+    assertThat(json).doesNotContain("value1");
+  }
+
+  @Test
+  public void testInnerClassDeserialization() {
+    final Parent p = new Parent();
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                Parent.Child.class,
+                new InstanceCreator<Parent.Child>() {
+                  @Override
+                  public Parent.Child createInstance(Type type) {
+                    return p.new Child();
+                  }
+                })
+            .create();
+    String json = "{'value2':3}";
+    Parent.Child c = gson.fromJson(json, Parent.Child.class);
+    assertThat(c.value2).isEqualTo(3);
+  }
+
+  private static class Parent {
+    @SuppressWarnings("unused")
+    int value1 = 1;
+
+    @SuppressWarnings("ClassCanBeStatic")
+    private class Child {
+      int value2 = 2;
+    }
+  }
+
+  private static class ArrayOfArrays {
+    private final BagOfPrimitives[][] elements;
+
+    public ArrayOfArrays() {
+      elements = new BagOfPrimitives[3][2];
+      for (int i = 0; i < elements.length; ++i) {
+        BagOfPrimitives[] row = elements[i];
+        for (int j = 0; j < row.length; ++j) {
+          row[j] = new BagOfPrimitives(i + j, i * j, false, i + "_" + j);
+        }
+      }
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder("{\"elements\":[");
+      boolean first = true;
+      for (BagOfPrimitives[] row : elements) {
+        if (first) {
+          first = false;
+        } else {
+          sb.append(",");
+        }
+        boolean firstOfRow = true;
+        sb.append("[");
+        for (BagOfPrimitives element : row) {
+          if (firstOfRow) {
+            firstOfRow = false;
+          } else {
+            sb.append(",");
+          }
+          sb.append(element.getExpectedJson());
+        }
+        sb.append("]");
+      }
+      sb.append("]}");
+      return sb.toString();
+    }
+  }
+
+  private static class ClassWithPrivateNoArgsConstructor {
+    public int a;
+
+    private ClassWithPrivateNoArgsConstructor() {
+      a = 10;
+    }
+  }
+
+  /** In response to Issue 41 http://code.google.com/p/google-gson/issues/detail?id=41 */
+  @Test
+  public void testObjectFieldNamesWithoutQuotesDeserialization() {
+    String json = "{longValue:1,'booleanValue':true,\"stringValue\":'bar'}";
+    BagOfPrimitives bag = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(bag.longValue).isEqualTo(1);
+    assertThat(bag.booleanValue).isTrue();
+    assertThat(bag.stringValue).isEqualTo("bar");
+  }
+
+  @Test
+  public void testStringFieldWithNumberValueDeserialization() {
+    String json = "{\"stringValue\":1}";
+    BagOfPrimitives bag = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(bag.stringValue).isEqualTo("1");
+
+    json = "{\"stringValue\":1.5E+6}";
+    bag = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(bag.stringValue).isEqualTo("1.5E+6");
+
+    json = "{\"stringValue\":true}";
+    bag = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(bag.stringValue).isEqualTo("true");
+  }
+
+  /** Created to reproduce issue 140 */
+  @Test
+  public void testStringFieldWithEmptyValueSerialization() {
+    ClassWithEmptyStringFields target = new ClassWithEmptyStringFields();
+    target.a = "5794749";
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"a\":\"5794749\"");
+    assertThat(json).contains("\"b\":\"\"");
+    assertThat(json).contains("\"c\":\"\"");
+  }
+
+  /** Created to reproduce issue 140 */
+  @Test
+  public void testStringFieldWithEmptyValueDeserialization() {
+    String json = "{a:\"5794749\",b:\"\",c:\"\"}";
+    ClassWithEmptyStringFields target = gson.fromJson(json, ClassWithEmptyStringFields.class);
+    assertThat(target.a).isEqualTo("5794749");
+    assertThat(target.b).isEqualTo("");
+    assertThat(target.c).isEqualTo("");
+  }
+
+  private static class ClassWithEmptyStringFields {
+    String a = "";
+    String b = "";
+    String c = "";
+  }
+
+  @Test
+  public void testJsonObjectSerialization() {
+    Gson gson = new GsonBuilder().serializeNulls().create();
+    JsonObject obj = new JsonObject();
+    String json = gson.toJson(obj);
+    assertThat(json).isEqualTo("{}");
+  }
+
+  /** Test for issue 215. */
+  @Test
+  public void testSingletonLists() {
+    Gson gson = new Gson();
+    Product product = new Product();
+    assertThat(gson.toJson(product)).isEqualTo("{\"attributes\":[],\"departments\":[]}");
+    Product unused1 = gson.fromJson(gson.toJson(product), Product.class);
+
+    product.departments.add(new Department());
+    assertThat(gson.toJson(product))
+        .isEqualTo("{\"attributes\":[],\"departments\":[{\"name\":\"abc\",\"code\":\"123\"}]}");
+    Product unused2 = gson.fromJson(gson.toJson(product), Product.class);
+
+    product.attributes.add("456");
+    assertThat(gson.toJson(product))
+        .isEqualTo(
+            "{\"attributes\":[\"456\"],\"departments\":[{\"name\":\"abc\",\"code\":\"123\"}]}");
+    Product unused3 = 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
+  @Test
+  @SuppressWarnings("JavaUtilDate")
+  public void testDateAsMapObjectField() {
+    HasObjectMap a = new HasObjectMap();
+    a.map.put("date", new Date(0));
+    assertThat(gson.toJson(a))
+        .matches("\\{\"map\":\\{\"date\":\"Dec 31, 1969,? 4:00:00\\hPM\"\\}\\}");
+  }
+
+  static class HasObjectMap {
+    Map<String, Object> map = new HashMap<>();
+  }
+
+  /**
+   * 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.
+   */
+  @Test
+  public void testStaticFieldSerialization() {
+    // By default Gson should ignore static fields
+    assertThat(gson.toJson(new ClassWithStaticField())).isEqualTo("{}");
+
+    Gson gson =
+        new GsonBuilder()
+            // Include static fields
+            .excludeFieldsWithModifiers(0)
+            .create();
+
+    String json = gson.toJson(new ClassWithStaticField());
+    assertThat(json).isEqualTo("{\"s\":\"initial\"}");
+
+    json = gson.toJson(new ClassWithStaticFinalField());
+    assertThat(json).isEqualTo("{\"s\":\"initial\"}");
+  }
+
+  /**
+   * 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.
+   */
+  @Test
+  public void testStaticFieldDeserialization() {
+    // By default Gson should ignore static fields
+    ClassWithStaticField unused = gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class);
+    assertThat(ClassWithStaticField.s).isEqualTo("initial");
+
+    Gson gson =
+        new GsonBuilder()
+            // Include static fields
+            .excludeFieldsWithModifiers(0)
+            .create();
+
+    String oldValue = ClassWithStaticField.s;
+    try {
+      ClassWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class);
+      assertThat(obj).isNotNull();
+      assertThat(ClassWithStaticField.s).isEqualTo("custom");
+    } finally {
+      ClassWithStaticField.s = oldValue;
+    }
+
+    try {
+      gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticFinalField.class);
+      fail();
+    } catch (JsonIOException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "Cannot set value of 'static final' field"
+                  + " 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'");
+    }
+  }
+
+  @SuppressWarnings({"PrivateConstructorForUtilityClass", "NonFinalStaticField"})
+  static class ClassWithStaticField {
+    static String s = "initial";
+  }
+
+  @SuppressWarnings("PrivateConstructorForUtilityClass")
+  static class ClassWithStaticFinalField {
+    static final String s = "initial";
+  }
+
+  @Test
+  public void testThrowingDefaultConstructor() {
+    try {
+      gson.fromJson("{}", ClassWithThrowingConstructor.class);
+      fail();
+    }
+    // TODO: Adjust this once Gson throws more specific exception type
+    catch (RuntimeException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .isEqualTo(
+              "Failed to invoke constructor"
+                  + " 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with"
+                  + " no args");
+      assertThat(e).hasCauseThat().isSameInstanceAs(ClassWithThrowingConstructor.thrownException);
+    }
+  }
+
+  @SuppressWarnings("StaticAssignmentOfThrowable")
+  static class ClassWithThrowingConstructor {
+    static final RuntimeException thrownException = new RuntimeException("Custom exception");
+
+    public ClassWithThrowingConstructor() {
+      throw thrownException;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java b/gson/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..42e77840e3092210a85faf88dbe3fac75d80a52e
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java
@@ -0,0 +1,551 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Objects;
+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 com.google.gson.stream.JsonReader;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for the serialization and deserialization of parameterized types in Gson.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ParameterizedTypesTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testParameterizedTypesSerialization() {
+    MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
+    Type typeOfSrc = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
+    String json = gson.toJson(src, typeOfSrc);
+    assertThat(json).isEqualTo(src.getExpectedJson());
+  }
+
+  @Test
+  public void testParameterizedTypeDeserialization() {
+    BagOfPrimitives bag = new BagOfPrimitives();
+    MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<>(bag);
+    Type expectedType = new TypeToken<MyParameterizedType<BagOfPrimitives>>() {}.getType();
+    BagOfPrimitives bagDefaultInstance = new BagOfPrimitives();
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                expectedType, new MyParameterizedTypeInstanceCreator<>(bagDefaultInstance))
+            .create();
+
+    String json = expected.getExpectedJson();
+    MyParameterizedType<BagOfPrimitives> actual = gson.fromJson(json, expectedType);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testTypesWithMultipleParametersSerialization() {
+    MultiParameters<Integer, Float, Double, String, BagOfPrimitives> src =
+        new MultiParameters<>(10, 1.0F, 2.1D, "abc", new BagOfPrimitives());
+    Type typeOfSrc =
+        new TypeToken<
+            MultiParameters<Integer, Float, Double, String, BagOfPrimitives>>() {}.getType();
+    String json = gson.toJson(src, typeOfSrc);
+    String expected =
+        "{\"a\":10,\"b\":1.0,\"c\":2.1,\"d\":\"abc\","
+            + "\"e\":{\"longValue\":0,\"intValue\":0,\"booleanValue\":false,\"stringValue\":\"\"}}";
+    assertThat(json).isEqualTo(expected);
+  }
+
+  @Test
+  public void testTypesWithMultipleParametersDeserialization() {
+    Type typeOfTarget =
+        new TypeToken<
+            MultiParameters<Integer, Float, Double, String, BagOfPrimitives>>() {}.getType();
+    String json =
+        "{\"a\":10,\"b\":1.0,\"c\":2.1,\"d\":\"abc\","
+            + "\"e\":{\"longValue\":0,\"intValue\":0,\"booleanValue\":false,\"stringValue\":\"\"}}";
+    MultiParameters<Integer, Float, Double, String, BagOfPrimitives> target =
+        gson.fromJson(json, typeOfTarget);
+    MultiParameters<Integer, Float, Double, String, BagOfPrimitives> expected =
+        new MultiParameters<>(10, 1.0F, 2.1D, "abc", new BagOfPrimitives());
+    assertThat(target).isEqualTo(expected);
+  }
+
+  @Test
+  public void testParameterizedTypeWithCustomSerializer() {
+    Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
+    Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType();
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(ptIntegerType, new MyParameterizedTypeAdapter<Integer>())
+            .registerTypeAdapter(ptStringType, new MyParameterizedTypeAdapter<String>())
+            .create();
+    MyParameterizedType<Integer> intTarget = new MyParameterizedType<>(10);
+    String json = gson.toJson(intTarget, ptIntegerType);
+    assertThat(json).isEqualTo(MyParameterizedTypeAdapter.<Integer>getExpectedJson(intTarget));
+
+    MyParameterizedType<String> stringTarget = new MyParameterizedType<>("abc");
+    json = gson.toJson(stringTarget, ptStringType);
+    assertThat(json).isEqualTo(MyParameterizedTypeAdapter.<String>getExpectedJson(stringTarget));
+  }
+
+  @Test
+  public void testParameterizedTypesWithCustomDeserializer() {
+    Type ptIntegerType = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
+    Type ptStringType = new TypeToken<MyParameterizedType<String>>() {}.getType();
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(ptIntegerType, new MyParameterizedTypeAdapter<Integer>())
+            .registerTypeAdapter(ptStringType, new MyParameterizedTypeAdapter<String>())
+            .registerTypeAdapter(ptStringType, new MyParameterizedTypeInstanceCreator<>(""))
+            .registerTypeAdapter(ptIntegerType, new MyParameterizedTypeInstanceCreator<>(0))
+            .create();
+
+    MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
+    String json = MyParameterizedTypeAdapter.<Integer>getExpectedJson(src);
+    MyParameterizedType<Integer> intTarget = gson.fromJson(json, ptIntegerType);
+    assertThat(intTarget.value).isEqualTo(10);
+
+    MyParameterizedType<String> srcStr = new MyParameterizedType<>("abc");
+    json = MyParameterizedTypeAdapter.<String>getExpectedJson(srcStr);
+    MyParameterizedType<String> stringTarget = gson.fromJson(json, ptStringType);
+    assertThat(stringTarget.value).isEqualTo("abc");
+  }
+
+  @Test
+  public void testParameterizedTypesWithWriterSerialization() {
+    Writer writer = new StringWriter();
+    MyParameterizedType<Integer> src = new MyParameterizedType<>(10);
+    Type typeOfSrc = new TypeToken<MyParameterizedType<Integer>>() {}.getType();
+    gson.toJson(src, typeOfSrc, writer);
+    assertThat(writer.toString()).isEqualTo(src.getExpectedJson());
+  }
+
+  @Test
+  public void testParameterizedTypeWithReaderDeserialization() {
+    BagOfPrimitives bag = new BagOfPrimitives();
+    MyParameterizedType<BagOfPrimitives> expected = new MyParameterizedType<>(bag);
+    Type expectedType = new TypeToken<MyParameterizedType<BagOfPrimitives>>() {}.getType();
+    BagOfPrimitives bagDefaultInstance = new BagOfPrimitives();
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(
+                expectedType, new MyParameterizedTypeInstanceCreator<>(bagDefaultInstance))
+            .create();
+
+    Reader json = new StringReader(expected.getExpectedJson());
+    MyParameterizedType<BagOfPrimitives> actual = gson.fromJson(json, expectedType);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @SuppressWarnings("varargs")
+  @SafeVarargs
+  private static <T> T[] arrayOf(T... args) {
+    return args;
+  }
+
+  @Test
+  public void testVariableTypeFieldsAndGenericArraysSerialization() {
+    Integer obj = 0;
+    Integer[] array = {1, 2, 3};
+    List<Integer> list = new ArrayList<>();
+    list.add(4);
+    list.add(5);
+    List<Integer>[] arrayOfLists = arrayOf(list, list);
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(obj, array, list, arrayOfLists, list, arrayOfLists);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+
+    assertThat(json).isEqualTo(objToSerialize.getExpectedJson());
+  }
+
+  @Test
+  public void testVariableTypeFieldsAndGenericArraysDeserialization() {
+    Integer obj = 0;
+    Integer[] array = {1, 2, 3};
+    List<Integer> list = new ArrayList<>();
+    list.add(4);
+    list.add(5);
+    List<Integer>[] arrayOfLists = arrayOf(list, list);
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(obj, array, list, arrayOfLists, list, arrayOfLists);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc);
+
+    assertThat(json).isEqualTo(objAfterDeserialization.getExpectedJson());
+  }
+
+  @Test
+  public void testVariableTypeDeserialization() {
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(0, null, null, null, null, null);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc);
+
+    assertThat(json).isEqualTo(objAfterDeserialization.getExpectedJson());
+  }
+
+  @Test
+  public void testVariableTypeArrayDeserialization() {
+    Integer[] array = {1, 2, 3};
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(null, array, null, null, null, null);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc);
+
+    assertThat(json).isEqualTo(objAfterDeserialization.getExpectedJson());
+  }
+
+  @Test
+  public void testParameterizedTypeWithVariableTypeDeserialization() {
+    List<Integer> list = new ArrayList<>();
+    list.add(4);
+    list.add(5);
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(null, null, list, null, null, null);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc);
+
+    assertThat(json).isEqualTo(objAfterDeserialization.getExpectedJson());
+  }
+
+  @Test
+  public void testParameterizedTypeGenericArraysSerialization() {
+    List<Integer> list = new ArrayList<>();
+    list.add(1);
+    list.add(2);
+    List<Integer>[] arrayOfLists = arrayOf(list, list);
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(null, null, null, arrayOfLists, null, null);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    assertThat(json).isEqualTo("{\"arrayOfListOfTypeParameters\":[[1,2],[1,2]]}");
+  }
+
+  @Test
+  public void testParameterizedTypeGenericArraysDeserialization() {
+    List<Integer> list = new ArrayList<>();
+    list.add(1);
+    list.add(2);
+    List<Integer>[] arrayOfLists = arrayOf(list, list);
+
+    Type typeOfSrc = new TypeToken<ObjectWithTypeVariables<Integer>>() {}.getType();
+    ObjectWithTypeVariables<Integer> objToSerialize =
+        new ObjectWithTypeVariables<>(null, null, null, arrayOfLists, null, null);
+    String json = gson.toJson(objToSerialize, typeOfSrc);
+    ObjectWithTypeVariables<Integer> objAfterDeserialization = gson.fromJson(json, typeOfSrc);
+
+    assertThat(json).isEqualTo(objAfterDeserialization.getExpectedJson());
+  }
+
+  /**
+   * An test object that has fields that are type variables.
+   *
+   * @param <T> Enforce T to be a string to make writing the "toExpectedJson" method easier.
+   */
+  private static class ObjectWithTypeVariables<T extends Number> {
+    private final T typeParameterObj;
+    private final T[] typeParameterArray;
+    private final List<T> listOfTypeParameters;
+    private final List<T>[] arrayOfListOfTypeParameters;
+    private final List<? extends T> listOfWildcardTypeParameters;
+    private final List<? extends T>[] arrayOfListOfWildcardTypeParameters;
+
+    // For use by Gson
+    @SuppressWarnings("unused")
+    private ObjectWithTypeVariables() {
+      this(null, null, null, null, null, null);
+    }
+
+    public ObjectWithTypeVariables(
+        T obj,
+        T[] array,
+        List<T> list,
+        List<T>[] arrayOfList,
+        List<? extends T> wildcardList,
+        List<? extends T>[] arrayOfWildcardList) {
+      this.typeParameterObj = obj;
+      this.typeParameterArray = array;
+      this.listOfTypeParameters = list;
+      this.arrayOfListOfTypeParameters = arrayOfList;
+      this.listOfWildcardTypeParameters = wildcardList;
+      this.arrayOfListOfWildcardTypeParameters = arrayOfWildcardList;
+    }
+
+    public String getExpectedJson() {
+      StringBuilder sb = new StringBuilder().append("{");
+
+      boolean needsComma = false;
+      if (typeParameterObj != null) {
+        sb.append("\"typeParameterObj\":").append(toString(typeParameterObj));
+        needsComma = true;
+      }
+
+      if (typeParameterArray != null) {
+        if (needsComma) {
+          sb.append(',');
+        }
+        sb.append("\"typeParameterArray\":[");
+        appendObjectsToBuilder(sb, Arrays.asList(typeParameterArray));
+        sb.append(']');
+        needsComma = true;
+      }
+
+      if (listOfTypeParameters != null) {
+        if (needsComma) {
+          sb.append(',');
+        }
+        sb.append("\"listOfTypeParameters\":[");
+        appendObjectsToBuilder(sb, listOfTypeParameters);
+        sb.append(']');
+        needsComma = true;
+      }
+
+      if (arrayOfListOfTypeParameters != null) {
+        if (needsComma) {
+          sb.append(',');
+        }
+        sb.append("\"arrayOfListOfTypeParameters\":[");
+        appendObjectsToBuilder(sb, arrayOfListOfTypeParameters);
+        sb.append(']');
+        needsComma = true;
+      }
+
+      if (listOfWildcardTypeParameters != null) {
+        if (needsComma) {
+          sb.append(',');
+        }
+        sb.append("\"listOfWildcardTypeParameters\":[");
+        appendObjectsToBuilder(sb, listOfWildcardTypeParameters);
+        sb.append(']');
+        needsComma = true;
+      }
+
+      if (arrayOfListOfWildcardTypeParameters != null) {
+        if (needsComma) {
+          sb.append(',');
+        }
+        sb.append("\"arrayOfListOfWildcardTypeParameters\":[");
+        appendObjectsToBuilder(sb, arrayOfListOfWildcardTypeParameters);
+        sb.append(']');
+        needsComma = true;
+      }
+      sb.append('}');
+      return sb.toString();
+    }
+
+    private void appendObjectsToBuilder(StringBuilder sb, Iterable<? extends T> iterable) {
+      boolean isFirst = true;
+      for (T obj : iterable) {
+        if (!isFirst) {
+          sb.append(',');
+        }
+        isFirst = false;
+        sb.append(toString(obj));
+      }
+    }
+
+    private void appendObjectsToBuilder(StringBuilder sb, List<? extends T>[] arrayOfList) {
+      boolean isFirst = true;
+      for (List<? extends T> list : arrayOfList) {
+        if (!isFirst) {
+          sb.append(',');
+        }
+        isFirst = false;
+        if (list != null) {
+          sb.append('[');
+          appendObjectsToBuilder(sb, list);
+          sb.append(']');
+        } else {
+          sb.append("null");
+        }
+      }
+    }
+
+    public String toString(T obj) {
+      return obj.toString();
+    }
+  }
+
+  private static final class MultiParameters<A, B, C, D, E> {
+    A a;
+    B b;
+    C c;
+    D d;
+    E e;
+
+    // For use by Gson
+    @SuppressWarnings("unused")
+    private MultiParameters() {}
+
+    MultiParameters(A a, B b, C c, D d, E e) {
+      super();
+      this.a = a;
+      this.b = b;
+      this.c = c;
+      this.d = d;
+      this.e = e;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((a == null) ? 0 : a.hashCode());
+      result = prime * result + ((b == null) ? 0 : b.hashCode());
+      result = prime * result + ((c == null) ? 0 : c.hashCode());
+      result = prime * result + ((d == null) ? 0 : d.hashCode());
+      result = prime * result + ((e == null) ? 0 : e.hashCode());
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof MultiParameters<?, ?, ?, ?, ?>)) {
+        return false;
+      }
+      MultiParameters<?, ?, ?, ?, ?> that = (MultiParameters<?, ?, ?, ?, ?>) o;
+      return Objects.equal(a, that.a)
+          && Objects.equal(b, that.b)
+          && Objects.equal(c, that.c)
+          && Objects.equal(d, that.d)
+          && Objects.equal(e, that.e);
+    }
+  }
+
+  // Begin: tests to reproduce issue 103
+  private static class Quantity {
+    @SuppressWarnings("unused")
+    int q = 10;
+  }
+
+  private static class MyQuantity extends Quantity {
+    @SuppressWarnings("unused")
+    int q2 = 20;
+  }
+
+  private interface Measurable<T> {}
+
+  private interface Field<T> {}
+
+  private interface Immutable {}
+
+  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);
+    assertThat(json).contains("value");
+    assertThat(json).contains("30");
+  }
+
+  @Test
+  public void testDeepParameterizedTypeDeserialization() {
+    String json = "{value:30}";
+    Type type = new TypeToken<Amount<MyQuantity>>() {}.getType();
+    Amount<MyQuantity> amount = gson.fromJson(json, type);
+    assertThat(amount.value).isEqualTo(30);
+  }
+
+  // End: tests to reproduce issue 103
+
+  private static void assertCorrectlyDeserialized(Object object) {
+    @SuppressWarnings("unchecked")
+    List<Quantity> list = (List<Quantity>) object;
+    assertThat(list.size()).isEqualTo(1);
+    assertThat(list.get(0).q).isEqualTo(4);
+  }
+
+  @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/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java b/gson/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1671d501945cf43a245bf72e5cb7b58637cdeeb7
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.common.TestTypes.ArrayOfObjects;
+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;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for pretty printing option.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class PrettyPrintingTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new GsonBuilder().setPrettyPrinting().create();
+  }
+
+  @Test
+  public void testPrettyPrintList() {
+    BagOfPrimitives b = new BagOfPrimitives();
+    List<BagOfPrimitives> listOfB = new ArrayList<>();
+    for (int i = 0; i < 3; ++i) {
+      listOfB.add(b);
+    }
+    Type typeOfSrc = new TypeToken<List<BagOfPrimitives>>() {}.getType();
+    String json = gson.toJson(listOfB, typeOfSrc);
+    assertThat(json)
+        .isEqualTo(
+            "[\n"
+                + "  {\n"
+                + "    \"longValue\": 0,\n"
+                + "    \"intValue\": 0,\n"
+                + "    \"booleanValue\": false,\n"
+                + "    \"stringValue\": \"\"\n"
+                + "  },\n"
+                + "  {\n"
+                + "    \"longValue\": 0,\n"
+                + "    \"intValue\": 0,\n"
+                + "    \"booleanValue\": false,\n"
+                + "    \"stringValue\": \"\"\n"
+                + "  },\n"
+                + "  {\n"
+                + "    \"longValue\": 0,\n"
+                + "    \"intValue\": 0,\n"
+                + "    \"booleanValue\": false,\n"
+                + "    \"stringValue\": \"\"\n"
+                + "  }\n"
+                + "]");
+  }
+
+  @Test
+  public void testPrettyPrintArrayOfObjects() {
+    ArrayOfObjects target = new ArrayOfObjects();
+    String json = gson.toJson(target);
+    assertThat(json)
+        .isEqualTo(
+            "{\n"
+                + "  \"elements\": [\n"
+                + "    {\n"
+                + "      \"longValue\": 0,\n"
+                + "      \"intValue\": 2,\n"
+                + "      \"booleanValue\": false,\n"
+                + "      \"stringValue\": \"i0\"\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"longValue\": 1,\n"
+                + "      \"intValue\": 3,\n"
+                + "      \"booleanValue\": false,\n"
+                + "      \"stringValue\": \"i1\"\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"longValue\": 2,\n"
+                + "      \"intValue\": 4,\n"
+                + "      \"booleanValue\": false,\n"
+                + "      \"stringValue\": \"i2\"\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}");
+  }
+
+  @Test
+  public void testPrettyPrintArrayOfPrimitives() {
+    int[] ints = {1, 2, 3, 4, 5};
+    String json = gson.toJson(ints);
+    assertThat(json).isEqualTo("[\n  1,\n  2,\n  3,\n  4,\n  5\n]");
+  }
+
+  @Test
+  public void testPrettyPrintArrayOfPrimitiveArrays() {
+    int[][] ints = {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 0}, {10}};
+    String json = gson.toJson(ints);
+    assertThat(json)
+        .isEqualTo(
+            "[\n  [\n    1,\n    2\n  ],\n  [\n    3,\n    4\n  ],\n  [\n    5,\n    6\n  ],"
+                + "\n  [\n    7,\n    8\n  ],\n  [\n    9,\n    0\n  ],\n  [\n    10\n  ]\n]");
+  }
+
+  @Test
+  public void testPrettyPrintListOfPrimitiveArrays() {
+    List<Integer[]> list =
+        Arrays.asList(new Integer[][] {{1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 0}, {10}});
+    String json = gson.toJson(list);
+    assertThat(json)
+        .isEqualTo(
+            "[\n  [\n    1,\n    2\n  ],\n  [\n    3,\n    4\n  ],\n  [\n    5,\n    6\n  ],"
+                + "\n  [\n    7,\n    8\n  ],\n  [\n    9,\n    0\n  ],\n  [\n    10\n  ]\n]");
+  }
+
+  @Test
+  public void testMap() {
+    Map<String, Integer> map = new LinkedHashMap<>();
+    map.put("abc", 1);
+    map.put("def", 5);
+    String json = gson.toJson(map);
+    assertThat(json).isEqualTo("{\n  \"abc\": 1,\n  \"def\": 5\n}");
+  }
+
+  // In response to bug 153
+  @Test
+  public void testEmptyMapField() {
+    ClassWithMap obj = new ClassWithMap();
+    obj.map = new LinkedHashMap<>();
+    String json = gson.toJson(obj);
+    assertThat(json).contains("{\n  \"map\": {},\n  \"value\": 2\n}");
+  }
+
+  @SuppressWarnings("unused")
+  private static class ClassWithMap {
+    Map<String, Integer> map;
+    int value = 2;
+  }
+
+  @Test
+  public void testMultipleArrays() {
+    int[][][] ints = {{{1}, {2}}};
+    String json = gson.toJson(ints);
+    assertThat(json).isEqualTo("[\n  [\n    [\n      1\n    ],\n    [\n      2\n    ]\n  ]\n]");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java b/gson/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f6f894ffeb3f22c6ce87377b37ec1e7b8fd4f6b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/PrimitiveCharacterTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Java Character values.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class PrimitiveCharacterTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testPrimitiveCharacterAutoboxedSerialization() {
+    assertThat(gson.toJson('A')).isEqualTo("\"A\"");
+    assertThat(gson.toJson('A', char.class)).isEqualTo("\"A\"");
+    assertThat(gson.toJson('A', Character.class)).isEqualTo("\"A\"");
+  }
+
+  @Test
+  public void testPrimitiveCharacterAutoboxedDeserialization() {
+    char expected = 'a';
+    char actual = gson.fromJson("a", char.class);
+    assertThat(actual).isEqualTo(expected);
+
+    actual = gson.fromJson("\"a\"", char.class);
+    assertThat(actual).isEqualTo(expected);
+
+    actual = gson.fromJson("a", Character.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c51f09750f0d6550dc2061c6aa1e6e0ed49d460
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
@@ -0,0 +1,978 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.LongSerializationPolicy;
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.reflect.TypeToken;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json primitive values: integers, and floating point numbers.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class PrimitiveTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testPrimitiveIntegerAutoboxedSerialization() {
+    assertThat(gson.toJson(1)).isEqualTo("1");
+  }
+
+  @Test
+  public void testPrimitiveIntegerAutoboxedDeserialization() {
+    int expected = 1;
+    int actual = gson.fromJson("1", int.class);
+    assertThat(actual).isEqualTo(expected);
+
+    actual = gson.fromJson("1", Integer.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testByteSerialization() {
+    assertThat(gson.toJson(1, byte.class)).isEqualTo("1");
+    assertThat(gson.toJson(1, Byte.class)).isEqualTo("1");
+    assertThat(gson.toJson(Byte.MIN_VALUE, Byte.class)).isEqualTo(Byte.toString(Byte.MIN_VALUE));
+    assertThat(gson.toJson(Byte.MAX_VALUE, Byte.class)).isEqualTo(Byte.toString(Byte.MAX_VALUE));
+    // Should perform narrowing conversion
+    assertThat(gson.toJson(128, Byte.class)).isEqualTo("-128");
+    assertThat(gson.toJson(1.5, Byte.class)).isEqualTo("1");
+  }
+
+  @Test
+  public void testByteDeserialization() {
+    Byte boxed = gson.fromJson("1", Byte.class);
+    assertThat(boxed).isEqualTo(1);
+    byte primitive = gson.fromJson("1", byte.class);
+    assertThat(primitive).isEqualTo(1);
+
+    byte[] bytes = gson.fromJson("[-128, 0, 127, 255]", byte[].class);
+    assertThat(bytes).isEqualTo(new byte[] {-128, 0, 127, -1});
+  }
+
+  @Test
+  public void testByteDeserializationLossy() {
+    JsonSyntaxException e =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("-129", byte.class));
+    assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -129 to byte; at path $");
+
+    e = assertThrows(JsonSyntaxException.class, () -> gson.fromJson("256", byte.class));
+    assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 256 to byte; at path $");
+
+    e = assertThrows(JsonSyntaxException.class, () -> gson.fromJson("2147483648", byte.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "java.lang.NumberFormatException: Expected an int but was 2147483648"
+                + " at line 1 column 11 path $");
+  }
+
+  @Test
+  public void testShortSerialization() {
+    assertThat(gson.toJson(1, short.class)).isEqualTo("1");
+    assertThat(gson.toJson(1, Short.class)).isEqualTo("1");
+    assertThat(gson.toJson(Short.MIN_VALUE, Short.class))
+        .isEqualTo(Short.toString(Short.MIN_VALUE));
+    assertThat(gson.toJson(Short.MAX_VALUE, Short.class))
+        .isEqualTo(Short.toString(Short.MAX_VALUE));
+    // Should perform widening conversion
+    assertThat(gson.toJson((byte) 1, Short.class)).isEqualTo("1");
+    // Should perform narrowing conversion
+    assertThat(gson.toJson(32768, Short.class)).isEqualTo("-32768");
+    assertThat(gson.toJson(1.5, Short.class)).isEqualTo("1");
+  }
+
+  @Test
+  public void testShortDeserialization() {
+    Short boxed = gson.fromJson("1", Short.class);
+    assertThat(boxed).isEqualTo(1);
+    short primitive = gson.fromJson("1", short.class);
+    assertThat(primitive).isEqualTo(1);
+
+    short[] shorts = gson.fromJson("[-32768, 0, 32767, 65535]", short[].class);
+    assertThat(shorts).isEqualTo(new short[] {-32768, 0, 32767, -1});
+  }
+
+  @Test
+  public void testShortDeserializationLossy() {
+    JsonSyntaxException e =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("-32769", short.class));
+    assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -32769 to short; at path $");
+
+    e = assertThrows(JsonSyntaxException.class, () -> gson.fromJson("65536", short.class));
+    assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 65536 to short; at path $");
+
+    e = assertThrows(JsonSyntaxException.class, () -> gson.fromJson("2147483648", short.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "java.lang.NumberFormatException: Expected an int but was 2147483648"
+                + " at line 1 column 11 path $");
+  }
+
+  @Test
+  public void testIntSerialization() {
+    assertThat(gson.toJson(1, int.class)).isEqualTo("1");
+    assertThat(gson.toJson(1, Integer.class)).isEqualTo("1");
+    assertThat(gson.toJson(Integer.MIN_VALUE, Integer.class))
+        .isEqualTo(Integer.toString(Integer.MIN_VALUE));
+    assertThat(gson.toJson(Integer.MAX_VALUE, Integer.class))
+        .isEqualTo(Integer.toString(Integer.MAX_VALUE));
+    // Should perform widening conversion
+    assertThat(gson.toJson((byte) 1, Integer.class)).isEqualTo("1");
+    // Should perform narrowing conversion
+    assertThat(gson.toJson(2147483648L, Integer.class)).isEqualTo("-2147483648");
+    assertThat(gson.toJson(1.5, Integer.class)).isEqualTo("1");
+  }
+
+  @Test
+  public void testLongSerialization() {
+    assertThat(gson.toJson(1L, long.class)).isEqualTo("1");
+    assertThat(gson.toJson(1L, Long.class)).isEqualTo("1");
+    assertThat(gson.toJson(Long.MIN_VALUE, Long.class)).isEqualTo(Long.toString(Long.MIN_VALUE));
+    assertThat(gson.toJson(Long.MAX_VALUE, Long.class)).isEqualTo(Long.toString(Long.MAX_VALUE));
+    // Should perform widening conversion
+    assertThat(gson.toJson((byte) 1, Long.class)).isEqualTo("1");
+    // Should perform narrowing conversion
+    assertThat(gson.toJson(1.5, Long.class)).isEqualTo("1");
+  }
+
+  @Test
+  public void testFloatSerialization() {
+    assertThat(gson.toJson(1.5f, float.class)).isEqualTo("1.5");
+    assertThat(gson.toJson(1.5f, Float.class)).isEqualTo("1.5");
+    assertThat(gson.toJson(Float.MIN_VALUE, Float.class))
+        .isEqualTo(Float.toString(Float.MIN_VALUE));
+    assertThat(gson.toJson(Float.MAX_VALUE, Float.class))
+        .isEqualTo(Float.toString(Float.MAX_VALUE));
+    // Should perform widening conversion
+    assertThat(gson.toJson((byte) 1, Float.class)).isEqualTo("1.0");
+    // (This widening conversion is actually lossy)
+    assertThat(gson.toJson(Long.MAX_VALUE - 10L, Float.class))
+        .isEqualTo(Float.toString((float) (Long.MAX_VALUE - 10L)));
+    // Should perform narrowing conversion
+    gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    assertThat(gson.toJson(Double.MAX_VALUE, Float.class)).isEqualTo("Infinity");
+  }
+
+  @Test
+  public void testDoubleSerialization() {
+    assertThat(gson.toJson(1.5, double.class)).isEqualTo("1.5");
+    assertThat(gson.toJson(1.5, Double.class)).isEqualTo("1.5");
+    assertThat(gson.toJson(Double.MIN_VALUE, Double.class))
+        .isEqualTo(Double.toString(Double.MIN_VALUE));
+    assertThat(gson.toJson(Double.MAX_VALUE, Double.class))
+        .isEqualTo(Double.toString(Double.MAX_VALUE));
+    // Should perform widening conversion
+    assertThat(gson.toJson((byte) 1, Double.class)).isEqualTo("1.0");
+    // (This widening conversion is actually lossy)
+    assertThat(gson.toJson(Long.MAX_VALUE - 10L, Double.class))
+        .isEqualTo(Double.toString((double) (Long.MAX_VALUE - 10L)));
+  }
+
+  @Test
+  public void testPrimitiveIntegerAutoboxedInASingleElementArraySerialization() {
+    int[] target = {-9332};
+    assertThat(gson.toJson(target)).isEqualTo("[-9332]");
+    assertThat(gson.toJson(target, int[].class)).isEqualTo("[-9332]");
+    assertThat(gson.toJson(target, Integer[].class)).isEqualTo("[-9332]");
+  }
+
+  @Test
+  public void testReallyLongValuesSerialization() {
+    long value = 333961828784581L;
+    assertThat(gson.toJson(value)).isEqualTo("333961828784581");
+  }
+
+  @Test
+  public void testReallyLongValuesDeserialization() {
+    String json = "333961828784581";
+    long value = gson.fromJson(json, Long.class);
+    assertThat(value).isEqualTo(333961828784581L);
+  }
+
+  @Test
+  public void testPrimitiveLongAutoboxedSerialization() {
+    assertThat(gson.toJson(1L, long.class)).isEqualTo("1");
+    assertThat(gson.toJson(1L, Long.class)).isEqualTo("1");
+  }
+
+  @Test
+  public void testPrimitiveLongAutoboxedDeserialization() {
+    long expected = 1L;
+    long actual = gson.fromJson("1", long.class);
+    assertThat(actual).isEqualTo(expected);
+
+    actual = gson.fromJson("1", Long.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testPrimitiveLongAutoboxedInASingleElementArraySerialization() {
+    long[] target = {-23L};
+    assertThat(gson.toJson(target)).isEqualTo("[-23]");
+    assertThat(gson.toJson(target, long[].class)).isEqualTo("[-23]");
+    assertThat(gson.toJson(target, Long[].class)).isEqualTo("[-23]");
+  }
+
+  @Test
+  public void testPrimitiveBooleanAutoboxedSerialization() {
+    assertThat(gson.toJson(true)).isEqualTo("true");
+    assertThat(gson.toJson(false)).isEqualTo("false");
+  }
+
+  @Test
+  public void testBooleanDeserialization() {
+    boolean value = gson.fromJson("false", boolean.class);
+    assertThat(value).isEqualTo(false);
+    value = gson.fromJson("true", boolean.class);
+    assertThat(value).isEqualTo(true);
+  }
+
+  @Test
+  public void testPrimitiveBooleanAutoboxedInASingleElementArraySerialization() {
+    boolean[] target = {false};
+    assertThat(gson.toJson(target)).isEqualTo("[false]");
+    assertThat(gson.toJson(target, boolean[].class)).isEqualTo("[false]");
+    assertThat(gson.toJson(target, Boolean[].class)).isEqualTo("[false]");
+  }
+
+  @Test
+  public void testNumberSerialization() {
+    Number expected = 1L;
+    String json = gson.toJson(expected);
+    assertThat(json).isEqualTo(expected.toString());
+
+    json = gson.toJson(expected, Number.class);
+    assertThat(json).isEqualTo(expected.toString());
+  }
+
+  @Test
+  public void testNumberDeserialization() {
+    String json = "1";
+    Number expected = Integer.valueOf(json);
+    Number actual = gson.fromJson(json, Number.class);
+    assertThat(actual.intValue()).isEqualTo(expected.intValue());
+
+    json = String.valueOf(Long.MAX_VALUE);
+    expected = Long.valueOf(json);
+    actual = gson.fromJson(json, Number.class);
+    assertThat(actual.longValue()).isEqualTo(expected.longValue());
+
+    json = "1.0";
+    actual = gson.fromJson(json, Number.class);
+    assertThat(actual.longValue()).isEqualTo(1L);
+  }
+
+  @Test
+  public void testNumberAsStringDeserialization() {
+    Number value = gson.fromJson("\"18\"", Number.class);
+    assertThat(value.intValue()).isEqualTo(18);
+  }
+
+  @Test
+  public void testPrimitiveDoubleAutoboxedSerialization() {
+    assertThat(gson.toJson(-122.08234335D)).isEqualTo("-122.08234335");
+    assertThat(gson.toJson(122.08112002D)).isEqualTo("122.08112002");
+  }
+
+  @Test
+  public void testPrimitiveDoubleAutoboxedDeserialization() {
+    double actual = gson.fromJson("-122.08858585", double.class);
+    assertThat(actual).isEqualTo(-122.08858585D);
+
+    actual = gson.fromJson("122.023900008000", Double.class);
+    assertThat(actual).isEqualTo(122.023900008D);
+  }
+
+  @Test
+  public void testPrimitiveDoubleAutoboxedInASingleElementArraySerialization() {
+    double[] target = {-122.08D};
+    assertThat(gson.toJson(target)).isEqualTo("[-122.08]");
+    assertThat(gson.toJson(target, double[].class)).isEqualTo("[-122.08]");
+    assertThat(gson.toJson(target, Double[].class)).isEqualTo("[-122.08]");
+  }
+
+  @Test
+  public void testDoubleAsStringRepresentationDeserialization() {
+    String doubleValue = "1.0043E+5";
+    Double expected = Double.valueOf(doubleValue);
+    Double actual = gson.fromJson(doubleValue, Double.class);
+    assertThat(actual).isEqualTo(expected);
+
+    double actual1 = gson.fromJson(doubleValue, double.class);
+    assertThat(actual1).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDoubleNoFractAsStringRepresentationDeserialization() {
+    String doubleValue = "1E+5";
+    Double expected = Double.valueOf(doubleValue);
+    Double actual = gson.fromJson(doubleValue, Double.class);
+    assertThat(actual).isEqualTo(expected);
+
+    double actual1 = gson.fromJson(doubleValue, double.class);
+    assertThat(actual1).isEqualTo(expected);
+  }
+
+  @Test
+  public void testDoubleArrayDeserialization() {
+    String json =
+        "[0.0, 0.004761904761904762, 3.4013606962703525E-4, 7.936508173034305E-4,"
+            + "0.0011904761904761906, 0.0]";
+    double[] values = gson.fromJson(json, double[].class);
+
+    assertThat(values).hasLength(6);
+    assertThat(values[0]).isEqualTo(0.0);
+    assertThat(values[1]).isEqualTo(0.004761904761904762);
+    assertThat(values[2]).isEqualTo(3.4013606962703525E-4);
+    assertThat(values[3]).isEqualTo(7.936508173034305E-4);
+    assertThat(values[4]).isEqualTo(0.0011904761904761906);
+    assertThat(values[5]).isEqualTo(0.0);
+  }
+
+  @Test
+  public void testLargeDoubleDeserialization() {
+    String doubleValue = "1.234567899E8";
+    Double expected = Double.valueOf(doubleValue);
+    Double actual = gson.fromJson(doubleValue, Double.class);
+    assertThat(actual).isEqualTo(expected);
+
+    double actual1 = gson.fromJson(doubleValue, double.class);
+    assertThat(actual1).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBigDecimalSerialization() {
+    BigDecimal target = new BigDecimal("-122.0e-21");
+    String json = gson.toJson(target);
+    assertThat(new BigDecimal(json)).isEqualTo(target);
+  }
+
+  @Test
+  public void testBigDecimalDeserialization() {
+    BigDecimal target = new BigDecimal("-122.0e-21");
+    String json = "-122.0e-21";
+    assertThat(gson.fromJson(json, BigDecimal.class)).isEqualTo(target);
+  }
+
+  @Test
+  public void testBigDecimalInASingleElementArraySerialization() {
+    BigDecimal[] target = {new BigDecimal("-122.08e-21")};
+    String json = gson.toJson(target);
+    String actual = extractElementFromArray(json);
+    assertThat(new BigDecimal(actual)).isEqualTo(target[0]);
+
+    json = gson.toJson(target, BigDecimal[].class);
+    actual = extractElementFromArray(json);
+    assertThat(new BigDecimal(actual)).isEqualTo(target[0]);
+  }
+
+  @Test
+  public void testSmallValueForBigDecimalSerialization() {
+    BigDecimal target = new BigDecimal("1.55");
+    String actual = gson.toJson(target);
+    assertThat(actual).isEqualTo(target.toString());
+  }
+
+  @Test
+  public void testSmallValueForBigDecimalDeserialization() {
+    BigDecimal expected = new BigDecimal("1.55");
+    BigDecimal actual = gson.fromJson("1.55", BigDecimal.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBigDecimalPreservePrecisionSerialization() {
+    String expectedValue = "1.000";
+    BigDecimal obj = new BigDecimal(expectedValue);
+    String actualValue = gson.toJson(obj);
+
+    assertThat(actualValue).isEqualTo(expectedValue);
+  }
+
+  @Test
+  public void testBigDecimalPreservePrecisionDeserialization() {
+    String json = "1.000";
+    BigDecimal expected = new BigDecimal(json);
+    BigDecimal actual = gson.fromJson(json, BigDecimal.class);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBigDecimalAsStringRepresentationDeserialization() {
+    String doubleValue = "0.05E+5";
+    BigDecimal expected = new BigDecimal(doubleValue);
+    BigDecimal actual = gson.fromJson(doubleValue, BigDecimal.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBigDecimalNoFractAsStringRepresentationDeserialization() {
+    String doubleValue = "5E+5";
+    BigDecimal expected = new BigDecimal(doubleValue);
+    BigDecimal actual = gson.fromJson(doubleValue, BigDecimal.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBigIntegerSerialization() {
+    BigInteger target = new BigInteger("12121211243123245845384534687435634558945453489543985435");
+    assertThat(gson.toJson(target)).isEqualTo(target.toString());
+  }
+
+  @Test
+  public void testBigIntegerDeserialization() {
+    String json = "12121211243123245845384534687435634558945453489543985435";
+    BigInteger target = new BigInteger(json);
+    assertThat(gson.fromJson(json, BigInteger.class)).isEqualTo(target);
+  }
+
+  @Test
+  public void testBigIntegerInASingleElementArraySerialization() {
+    BigInteger[] target = {new BigInteger("1212121243434324323254365345367456456456465464564564")};
+    String json = gson.toJson(target);
+    String actual = extractElementFromArray(json);
+    assertThat(new BigInteger(actual)).isEqualTo(target[0]);
+
+    json = gson.toJson(target, BigInteger[].class);
+    actual = extractElementFromArray(json);
+    assertThat(new BigInteger(actual)).isEqualTo(target[0]);
+  }
+
+  @Test
+  public void testSmallValueForBigIntegerSerialization() {
+    BigInteger target = new BigInteger("15");
+    String actual = gson.toJson(target);
+    assertThat(actual).isEqualTo(target.toString());
+  }
+
+  @Test
+  public void testSmallValueForBigIntegerDeserialization() {
+    BigInteger expected = new BigInteger("15");
+    BigInteger actual = gson.fromJson("15", BigInteger.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testBadValueForBigIntegerDeserialization() {
+    try {
+      gson.fromJson("15.099", BigInteger.class);
+      fail("BigInteger can not be decimal values.");
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testLazilyParsedNumberSerialization() {
+    LazilyParsedNumber target = new LazilyParsedNumber("1.5");
+    String actual = gson.toJson(target);
+    assertThat(actual).isEqualTo("1.5");
+  }
+
+  @Test
+  public void testLazilyParsedNumberDeserialization() {
+    LazilyParsedNumber expected = new LazilyParsedNumber("1.5");
+    LazilyParsedNumber actual = gson.fromJson("1.5", LazilyParsedNumber.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testMoreSpecificSerialization() {
+    Gson gson = new Gson();
+    String expected = "This is a string";
+    String expectedJson = gson.toJson(expected);
+
+    Serializable serializableString = expected;
+    String actualJson = gson.toJson(serializableString, Serializable.class);
+    assertThat(actualJson).isNotEqualTo(expectedJson);
+  }
+
+  private static String extractElementFromArray(String json) {
+    return json.substring(json.indexOf('[') + 1, json.indexOf(']'));
+  }
+
+  @Test
+  public void testDoubleNaNSerializationNotSupportedByDefault() {
+    try {
+      double nan = Double.NaN;
+      gson.toJson(nan);
+      fail("Gson should not accept NaN for serialization");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Double.NaN);
+      fail("Gson should not accept NaN for serialization");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDoubleNaNSerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    double nan = Double.NaN;
+    assertThat(gson.toJson(nan)).isEqualTo("NaN");
+    assertThat(gson.toJson(Double.NaN)).isEqualTo("NaN");
+  }
+
+  @Test
+  public void testDoubleNaNDeserialization() {
+    assertThat(gson.fromJson("NaN", Double.class)).isNaN();
+    assertThat(gson.fromJson("NaN", double.class)).isNaN();
+  }
+
+  @Test
+  public void testFloatNaNSerializationNotSupportedByDefault() {
+    try {
+      float nan = Float.NaN;
+      gson.toJson(nan);
+      fail("Gson should not accept NaN for serialization");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Float.NaN);
+      fail("Gson should not accept NaN for serialization");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testFloatNaNSerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    float nan = Float.NaN;
+    assertThat(gson.toJson(nan)).isEqualTo("NaN");
+    assertThat(gson.toJson(Float.NaN)).isEqualTo("NaN");
+  }
+
+  @Test
+  public void testFloatNaNDeserialization() {
+    assertThat(gson.fromJson("NaN", Float.class)).isNaN();
+    assertThat(gson.fromJson("NaN", float.class)).isNaN();
+  }
+
+  @Test
+  public void testBigDecimalNaNDeserializationNotSupported() {
+    try {
+      gson.fromJson("NaN", BigDecimal.class);
+      fail("Gson should not accept NaN for deserialization by default.");
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testDoubleInfinitySerializationNotSupportedByDefault() {
+    try {
+      double infinity = Double.POSITIVE_INFINITY;
+      gson.toJson(infinity);
+      fail("Gson should not accept positive infinity for serialization by default.");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Double.POSITIVE_INFINITY);
+      fail("Gson should not accept positive infinity for serialization by default.");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testDoubleInfinitySerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    double infinity = Double.POSITIVE_INFINITY;
+    assertThat(gson.toJson(infinity)).isEqualTo("Infinity");
+    assertThat(gson.toJson(Double.POSITIVE_INFINITY)).isEqualTo("Infinity");
+  }
+
+  @Test
+  public void testDoubleInfinityDeserialization() {
+    assertThat(gson.fromJson("Infinity", Double.class)).isPositiveInfinity();
+    assertThat(gson.fromJson("Infinity", double.class)).isPositiveInfinity();
+  }
+
+  @Test
+  public void testFloatInfinitySerializationNotSupportedByDefault() {
+    try {
+      float infinity = Float.POSITIVE_INFINITY;
+      gson.toJson(infinity);
+      fail("Gson should not accept positive infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Float.POSITIVE_INFINITY);
+      fail("Gson should not accept positive infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testFloatInfinitySerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    float infinity = Float.POSITIVE_INFINITY;
+    assertThat(gson.toJson(infinity)).isEqualTo("Infinity");
+    assertThat(gson.toJson(Float.POSITIVE_INFINITY)).isEqualTo("Infinity");
+  }
+
+  @Test
+  public void testFloatInfinityDeserialization() {
+    assertThat(gson.fromJson("Infinity", Float.class)).isPositiveInfinity();
+    assertThat(gson.fromJson("Infinity", float.class)).isPositiveInfinity();
+  }
+
+  @Test
+  public void testBigDecimalInfinityDeserializationNotSupported() {
+    try {
+      gson.fromJson("Infinity", BigDecimal.class);
+      fail("Gson should not accept positive infinity for deserialization with BigDecimal");
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testNegativeInfinitySerializationNotSupportedByDefault() {
+    try {
+      double negativeInfinity = Double.NEGATIVE_INFINITY;
+      gson.toJson(negativeInfinity);
+      fail("Gson should not accept negative infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Double.NEGATIVE_INFINITY);
+      fail("Gson should not accept negative infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNegativeInfinitySerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    double negativeInfinity = Double.NEGATIVE_INFINITY;
+    assertThat(gson.toJson(negativeInfinity)).isEqualTo("-Infinity");
+    assertThat(gson.toJson(Double.NEGATIVE_INFINITY)).isEqualTo("-Infinity");
+  }
+
+  @Test
+  public void testNegativeInfinityDeserialization() {
+    assertThat(gson.fromJson("-Infinity", double.class)).isNegativeInfinity();
+    assertThat(gson.fromJson("-Infinity", Double.class)).isNegativeInfinity();
+  }
+
+  @Test
+  public void testNegativeInfinityFloatSerializationNotSupportedByDefault() {
+    try {
+      float negativeInfinity = Float.NEGATIVE_INFINITY;
+      gson.toJson(negativeInfinity);
+      fail("Gson should not accept negative infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      gson.toJson(Float.NEGATIVE_INFINITY);
+      fail("Gson should not accept negative infinity for serialization by default");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNegativeInfinityFloatSerialization() {
+    Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
+    float negativeInfinity = Float.NEGATIVE_INFINITY;
+    assertThat(gson.toJson(negativeInfinity)).isEqualTo("-Infinity");
+    assertThat(gson.toJson(Float.NEGATIVE_INFINITY)).isEqualTo("-Infinity");
+  }
+
+  @Test
+  public void testNegativeInfinityFloatDeserialization() {
+    assertThat(gson.fromJson("-Infinity", float.class)).isNegativeInfinity();
+    assertThat(gson.fromJson("-Infinity", Float.class)).isNegativeInfinity();
+  }
+
+  @Test
+  public void testBigDecimalNegativeInfinityDeserializationNotSupported() {
+    try {
+      gson.fromJson("-Infinity", BigDecimal.class);
+      fail("Gson should not accept positive infinity for deserialization");
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testLongAsStringSerialization() {
+    gson = new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create();
+    String result = gson.toJson(15L);
+    assertThat(result).isEqualTo("\"15\"");
+
+    // Test with an integer and ensure its still a number
+    result = gson.toJson(2);
+    assertThat(result).isEqualTo("2");
+  }
+
+  @Test
+  public void testLongAsStringDeserialization() {
+    long value = gson.fromJson("\"15\"", long.class);
+    assertThat(value).isEqualTo(15);
+
+    gson = new GsonBuilder().setLongSerializationPolicy(LongSerializationPolicy.STRING).create();
+    value = gson.fromJson("\"25\"", long.class);
+    assertThat(value).isEqualTo(25);
+  }
+
+  @Test
+  public void testQuotedStringSerializationAndDeserialization() {
+    String value = "String Blah Blah Blah...1, 2, 3";
+    String serializedForm = gson.toJson(value);
+    assertThat(serializedForm).isEqualTo("\"" + value + "\"");
+
+    String actual = gson.fromJson(serializedForm, String.class);
+    assertThat(actual).isEqualTo(value);
+  }
+
+  @Test
+  public void testUnquotedStringDeserializationFails() {
+    assertThat(gson.fromJson("UnquotedSingleWord", String.class)).isEqualTo("UnquotedSingleWord");
+
+    String value = "String Blah Blah Blah...1, 2, 3";
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson(value, String.class));
+  }
+
+  @Test
+  public void testHtmlCharacterSerialization() {
+    String target = "<script>var a = 12;</script>";
+    String result = gson.toJson(target);
+    assertThat(result).isNotEqualTo('"' + target + '"');
+
+    gson = new GsonBuilder().disableHtmlEscaping().create();
+    result = gson.toJson(target);
+    assertThat(result).isEqualTo('"' + target + '"');
+  }
+
+  @Test
+  public void testDeserializePrimitiveWrapperAsObjectField() {
+    String json = "{i:10}";
+    ClassWithIntegerField target = gson.fromJson(json, ClassWithIntegerField.class);
+    assertThat(target.i).isEqualTo(10);
+  }
+
+  private static class ClassWithIntegerField {
+    Integer i;
+  }
+
+  @Test
+  public void testPrimitiveClassLiteral() {
+    assertThat(gson.fromJson("1", int.class)).isEqualTo(1);
+    assertThat(gson.fromJson(new StringReader("1"), int.class)).isEqualTo(1);
+    assertThat(gson.fromJson(new JsonPrimitive(1), int.class)).isEqualTo(1);
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsLongPrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", long.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsLongWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3]", Long.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsInt() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1, 2, 3, 4]", int.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsInteger() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{}", Integer.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsShortPrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", short.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsShortWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("['a','b']", Short.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsDoublePrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2]", double.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsDoubleWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", Double.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsFloatPrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", float.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsFloatWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3]", Float.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsBytePrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", byte.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsByteWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3,4]", Byte.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsBooleanPrimitive() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'abc':1}", boolean.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsBooleanWrapper() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3,4]", Boolean.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsBigDecimal() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3,4]", BigDecimal.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsBigDecimal() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'a':1}", BigDecimal.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsBigInteger() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3,4]", BigInteger.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsBigInteger() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'c':2}", BigInteger.class));
+  }
+
+  @Test
+  public void testDeserializeJsonArrayAsNumber() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("[1,2,3,4]", Number.class));
+  }
+
+  @Test
+  public void testDeserializeJsonObjectAsNumber() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("{'c':2}", Number.class));
+  }
+
+  @Test
+  public void testDeserializingDecimalPointValueZeroSucceeds() {
+    assertThat(gson.fromJson("1.0", Integer.class)).isEqualTo(1);
+  }
+
+  @Test
+  public void testDeserializingNonZeroDecimalPointValuesAsIntegerFails() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("1.02", Byte.class));
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("1.02", Short.class));
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("1.02", Integer.class));
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("1.02", Long.class));
+  }
+
+  @Test
+  public void testDeserializingBigDecimalAsIntegerFails() {
+    JsonSyntaxException e =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson("-122.08e-213", Integer.class));
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Expected an int but was -122.08e-213 at line 1 column 13 path $");
+  }
+
+  @Test
+  public void testDeserializingBigIntegerAsInteger() {
+    String number = "12121211243123245845384534687435634558945453489543985435";
+    JsonSyntaxException e =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson(number, Integer.class));
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Expected an int but was " + number + " at line 1 column 57 path $");
+  }
+
+  @Test
+  public void testDeserializingBigIntegerAsLong() {
+    String number = "12121211243123245845384534687435634558945453489543985435";
+    JsonSyntaxException e =
+        assertThrows(JsonSyntaxException.class, () -> gson.fromJson(number, Long.class));
+    assertThat(e)
+        .hasCauseThat()
+        .hasMessageThat()
+        .isEqualTo("Expected a long but was " + number + " at line 1 column 57 path $");
+  }
+
+  @Test
+  public void testValueVeryCloseToZeroIsZero() {
+    assertThat(gson.fromJson("-122.08e-2132", byte.class)).isEqualTo(0);
+    assertThat(gson.fromJson("-122.08e-2132", short.class)).isEqualTo(0);
+    assertThat(gson.fromJson("-122.08e-2132", int.class)).isEqualTo(0);
+    assertThat(gson.fromJson("-122.08e-2132", long.class)).isEqualTo(0);
+    assertThat(gson.fromJson("-122.08e-2132", float.class)).isEqualTo(-0.0f);
+    assertThat(gson.fromJson("-122.08e-2132", double.class)).isEqualTo(-0.0);
+    assertThat(gson.fromJson("122.08e-2132", float.class)).isEqualTo(0.0f);
+    assertThat(gson.fromJson("122.08e-2132", double.class)).isEqualTo(0.0);
+  }
+
+  @Test
+  public void testDeserializingBigDecimalAsBigIntegerFails() {
+    assertThrows(JsonSyntaxException.class, () -> gson.fromJson("-122.08e-213", BigInteger.class));
+  }
+
+  @Test
+  public void testDeserializingBigIntegerAsBigDecimal() {
+    BigDecimal actual =
+        gson.fromJson("12121211243123245845384534687435634558945453489543985435", BigDecimal.class);
+    assertThat(actual.toPlainString())
+        .isEqualTo("12121211243123245845384534687435634558945453489543985435");
+  }
+
+  @Test
+  public void testStringsAsBooleans() {
+    String json = "['true', 'false', 'TRUE', 'yes', '1']";
+    List<Boolean> deserialized = gson.fromJson(json, new TypeToken<List<Boolean>>() {});
+    assertThat(deserialized).isEqualTo(Arrays.asList(true, false, true, false, false));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java b/gson/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd53944fa3ffc6ee3bfc74e1d1115a0db54cf4b6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/PrintFormattingTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.ClassWithTransientFields;
+import com.google.gson.common.TestTypes.Nested;
+import com.google.gson.common.TestTypes.PrimitiveArray;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for print formatting.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class PrintFormattingTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testCompactFormattingLeavesNoWhiteSpace() {
+    List<Object> list = new ArrayList<>();
+    list.add(new BagOfPrimitives());
+    list.add(new Nested());
+    list.add(new PrimitiveArray());
+    list.add(new ClassWithTransientFields<>());
+
+    String json = gson.toJson(list);
+    assertContainsNoWhiteSpace(json);
+  }
+
+  @Test
+  public void testJsonObjectWithNullValues() {
+    JsonObject obj = new JsonObject();
+    obj.addProperty("field1", "value1");
+    obj.addProperty("field2", (String) null);
+    String json = gson.toJson(obj);
+    assertThat(json).contains("field1");
+    assertThat(json).doesNotContain("field2");
+  }
+
+  @Test
+  public void testJsonObjectWithNullValuesSerialized() {
+    gson = new GsonBuilder().serializeNulls().create();
+    JsonObject obj = new JsonObject();
+    obj.addProperty("field1", "value1");
+    obj.addProperty("field2", (String) null);
+    String json = gson.toJson(obj);
+    assertThat(json).contains("field1");
+    assertThat(json).contains("field2");
+  }
+
+  @SuppressWarnings("LoopOverCharArray")
+  private static void assertContainsNoWhiteSpace(String str) {
+    for (char c : str.toCharArray()) {
+      assertThat(Character.isWhitespace(c)).isFalse();
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java b/gson/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c317fec52fa0600ce239f1d8889f31d5962f050
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/RawSerializationTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests to validate serialization of parameterized types without explicit types
+ *
+ * @author Inderjeet Singh
+ */
+public class RawSerializationTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testCollectionOfPrimitives() {
+    Collection<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
+    String json = gson.toJson(ints);
+    assertThat(json).isEqualTo("[1,2,3,4,5]");
+  }
+
+  @Test
+  public void testCollectionOfObjects() {
+    Collection<Foo> foos = Arrays.asList(new Foo(1), new Foo(2));
+    String json = gson.toJson(foos);
+    assertThat(json).isEqualTo("[{\"b\":1},{\"b\":2}]");
+  }
+
+  @Test
+  public void testParameterizedObject() {
+    Bar<Foo> bar = new Bar<>(new Foo(1));
+    String expectedJson = "{\"t\":{\"b\":1}}";
+    // Ensure that serialization works without specifying the type explicitly
+    String json = gson.toJson(bar);
+    assertThat(json).isEqualTo(expectedJson);
+    // Ensure that serialization also works when the type is specified explicitly
+    json = gson.toJson(bar, new TypeToken<Bar<Foo>>() {}.getType());
+    assertThat(json).isEqualTo(expectedJson);
+  }
+
+  @Test
+  public void testTwoLevelParameterizedObject() {
+    Bar<Bar<Foo>> bar = new Bar<>(new Bar<>(new Foo(1)));
+    String expectedJson = "{\"t\":{\"t\":{\"b\":1}}}";
+    // Ensure that serialization works without specifying the type explicitly
+    String json = gson.toJson(bar);
+    assertThat(json).isEqualTo(expectedJson);
+    // Ensure that serialization also works when the type is specified explicitly
+    json = gson.toJson(bar, new TypeToken<Bar<Bar<Foo>>>() {}.getType());
+    assertThat(json).isEqualTo(expectedJson);
+  }
+
+  @Test
+  public void testThreeLevelParameterizedObject() {
+    Bar<Bar<Bar<Foo>>> bar = new Bar<>(new Bar<>(new Bar<>(new Foo(1))));
+    String expectedJson = "{\"t\":{\"t\":{\"t\":{\"b\":1}}}}";
+    // Ensure that serialization works without specifying the type explicitly
+    String json = gson.toJson(bar);
+    assertThat(json).isEqualTo(expectedJson);
+    // Ensure that serialization also works when the type is specified explicitly
+    json = gson.toJson(bar, new TypeToken<Bar<Bar<Bar<Foo>>>>() {}.getType());
+    assertThat(json).isEqualTo(expectedJson);
+  }
+
+  private static class Foo {
+    @SuppressWarnings("unused")
+    int b;
+
+    Foo(int b) {
+      this.b = b;
+    }
+  }
+
+  private static class Bar<T> {
+    @SuppressWarnings("unused")
+    T t;
+
+    Bar(T t) {
+      this.t = t;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java b/gson/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..660a0ae4309a0beea2eb120e5839d13b84a3519a
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ReadersWritersTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonStreamParser;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.reflect.TypeToken;
+import java.io.CharArrayReader;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for the support of {@link Reader}s and {@link Writer}s.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class ReadersWritersTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testWriterForSerialization() {
+    Writer writer = new StringWriter();
+    BagOfPrimitives src = new BagOfPrimitives();
+    gson.toJson(src, writer);
+    assertThat(writer.toString()).isEqualTo(src.getExpectedJson());
+  }
+
+  @Test
+  public void testReaderForDeserialization() {
+    BagOfPrimitives expected = new BagOfPrimitives();
+    Reader json = new StringReader(expected.getExpectedJson());
+    BagOfPrimitives actual = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testTopLevelNullObjectSerializationWithWriter() {
+    StringWriter writer = new StringWriter();
+    gson.toJson(null, writer);
+    assertThat(writer.toString()).isEqualTo("null");
+  }
+
+  @Test
+  public void testTopLevelNullObjectDeserializationWithReader() {
+    StringReader reader = new StringReader("null");
+    Integer nullIntObject = gson.fromJson(reader, Integer.class);
+    assertThat(nullIntObject).isNull();
+  }
+
+  @Test
+  public void testTopLevelNullObjectSerializationWithWriterAndSerializeNulls() {
+    Gson gson = new GsonBuilder().serializeNulls().create();
+    StringWriter writer = new StringWriter();
+    gson.toJson(null, writer);
+    assertThat(writer.toString()).isEqualTo("null");
+  }
+
+  @Test
+  public void testTopLevelNullObjectDeserializationWithReaderAndSerializeNulls() {
+    Gson gson = new GsonBuilder().serializeNulls().create();
+    StringReader reader = new StringReader("null");
+    Integer nullIntObject = gson.fromJson(reader, Integer.class);
+    assertThat(nullIntObject).isNull();
+  }
+
+  @Test
+  public void testReadWriteTwoStrings() throws IOException {
+    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());
+    JsonStreamParser parser = new JsonStreamParser(reader);
+    String actualOne = gson.fromJson(parser.next(), String.class);
+    assertThat(actualOne).isEqualTo("one");
+    String actualTwo = gson.fromJson(parser.next(), String.class);
+    assertThat(actualTwo).isEqualTo("two");
+  }
+
+  @Test
+  public void testReadWriteTwoObjects() throws IOException {
+    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");
+    writer.write(gson.toJson(expectedTwo).toCharArray());
+    CharArrayReader reader = new CharArrayReader(writer.toCharArray());
+    JsonStreamParser parser = new JsonStreamParser(reader);
+    BagOfPrimitives actualOne = gson.fromJson(parser.next(), BagOfPrimitives.class);
+    assertThat(actualOne.stringValue).isEqualTo("one");
+    BagOfPrimitives actualTwo = gson.fromJson(parser.next(), BagOfPrimitives.class);
+    assertThat(actualTwo.stringValue).isEqualTo("two");
+    assertThat(parser.hasNext()).isFalse();
+  }
+
+  @Test
+  public void testTypeMismatchThrowsJsonSyntaxExceptionForStrings() {
+    try {
+      gson.fromJson("true", new TypeToken<Map<String, String>>() {}.getType());
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  @Test
+  public void testTypeMismatchThrowsJsonSyntaxExceptionForReaders() {
+    try {
+      gson.fromJson(new StringReader("true"), new TypeToken<Map<String, String>>() {}.getType());
+      fail();
+    } catch (JsonSyntaxException expected) {
+    }
+  }
+
+  /**
+   * Verifies that passing an {@link Appendable} which is not an instance of {@link Writer} to
+   * {@code Gson.toJson} works correctly.
+   */
+  @Test
+  public void testToJsonAppendable() {
+    class CustomAppendable implements Appendable {
+      final StringBuilder stringBuilder = new StringBuilder();
+      int toStringCallCount = 0;
+
+      @CanIgnoreReturnValue
+      @Override
+      public Appendable append(char c) throws IOException {
+        stringBuilder.append(c);
+        return this;
+      }
+
+      @CanIgnoreReturnValue
+      @Override
+      public Appendable append(CharSequence csq) throws IOException {
+        if (csq == null) {
+          csq = "null"; // Requirement by Writer.append
+        }
+        append(csq, 0, csq.length());
+        return this;
+      }
+
+      @CanIgnoreReturnValue
+      @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
+    assertThat(appendable.toStringCallCount).isAtLeast(2);
+    assertThat(appendable.stringBuilder.toString()).isEqualTo("[\"test\",123,true]");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java b/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba06a190d20ba8015791341d9c46c6b7b3fdf4a8
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
@@ -0,0 +1,542 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNotNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonIOException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.ReflectionAccessFilter;
+import com.google.gson.ReflectionAccessFilter.FilterResult;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.ConstructorConstructor;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import org.junit.Test;
+
+public class ReflectionAccessFilterTest {
+  // Reader has protected `lock` field which cannot be accessed
+  private static class ClassExtendingJdkClass extends Reader {
+    @Override
+    public int read(char[] cbuf, int off, int len) throws IOException {
+      return 0;
+    }
+
+    @Override
+    public void close() throws IOException {}
+  }
+
+  @Test
+  public void testBlockInaccessibleJava() throws ReflectiveOperationException {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
+            .create();
+
+    // Serialization should fail for classes with non-public fields
+    try {
+      gson.toJson(new File("a"));
+      fail("Expected exception; test needs to be run with Java >= 9");
+    } catch (JsonIOException expected) {
+      // Note: This test is rather brittle and depends on the JDK implementation
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+
+    // 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);
+    assertThat(json).isEqualTo("{\"x\":1,\"y\":2}");
+  }
+
+  @Test
+  public void testBlockInaccessibleJavaExtendingJdkClass() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
+            .create();
+
+    try {
+      gson.toJson(new ClassExtendingJdkClass());
+      fail("Expected exception; test needs to be run with Java >= 9");
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+  }
+
+  @Test
+  public void testBlockAllJava() {
+    Gson gson =
+        new GsonBuilder().addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA).create();
+
+    // Serialization should fail for any Java class
+    try {
+      gson.toJson(Thread.currentThread());
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread."
+                  + " Register a TypeAdapter for this type or adjust the access filter.");
+    }
+  }
+
+  @Test
+  public void testBlockAllJavaExtendingJdkClass() {
+    Gson gson =
+        new GsonBuilder().addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA).create();
+
+    try {
+      gson.toJson(new ClassExtendingJdkClass());
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+  }
+
+  private static class ClassWithStaticField {
+    @SuppressWarnings({"unused", "NonFinalStaticField"})
+    private static int i = 1;
+  }
+
+  @Test
+  public void testBlockInaccessibleStaticField() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_INACCESSIBLE;
+                  }
+                })
+            // Include static fields
+            .excludeFieldsWithModifiers(0)
+            .create();
+
+    try {
+      gson.toJson(new ClassWithStaticField());
+      fail("Expected exception; test needs to be run with Java >= 9");
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+  }
+
+  private static class SuperTestClass {}
+
+  private static class SubTestClass extends SuperTestClass {
+    @SuppressWarnings("unused")
+    public int i = 1;
+  }
+
+  private static class OtherClass {
+    @SuppressWarnings("unused")
+    public int i = 2;
+  }
+
+  @Test
+  public void testDelegation() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    // INDECISIVE in last filter should act like ALLOW
+                    return SuperTestClass.class.isAssignableFrom(rawClass)
+                        ? FilterResult.BLOCK_ALL
+                        : FilterResult.INDECISIVE;
+                  }
+                })
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    // INDECISIVE should delegate to previous filter
+                    return rawClass == SubTestClass.class
+                        ? FilterResult.ALLOW
+                        : FilterResult.INDECISIVE;
+                  }
+                })
+            .create();
+
+    // Filter disallows SuperTestClass
+    try {
+      gson.toJson(new SuperTestClass());
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+
+    // But registration order is reversed, so filter for SubTestClass allows reflection
+    String json = gson.toJson(new SubTestClass());
+    assertThat(json).isEqualTo("{\"i\":1}");
+
+    // And unrelated class should not be affected
+    json = gson.toJson(new OtherClass());
+    assertThat(json).isEqualTo("{\"i\":2}");
+  }
+
+  private static class ClassWithPrivateField {
+    @SuppressWarnings("unused")
+    private int i = 1;
+  }
+
+  private static class ExtendingClassWithPrivateField extends ClassWithPrivateField {}
+
+  @Test
+  public void testAllowForSupertype() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_INACCESSIBLE;
+                  }
+                })
+            .create();
+
+    // First make sure test is implemented correctly and access is blocked
+    try {
+      gson.toJson(new ExtendingClassWithPrivateField());
+      fail("Expected exception; test needs to be run with Java >= 9");
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+
+    gson =
+        gson.newBuilder()
+            // Allow reflective access for supertype
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return rawClass == ClassWithPrivateField.class
+                        ? FilterResult.ALLOW
+                        : FilterResult.INDECISIVE;
+                  }
+                })
+            .create();
+
+    // Inherited (inaccessible) private field should have been made accessible
+    String json = gson.toJson(new ExtendingClassWithPrivateField());
+    assertThat(json).isEqualTo("{\"i\":1}");
+  }
+
+  private static class ClassWithPrivateNoArgsConstructor {
+    private ClassWithPrivateNoArgsConstructor() {}
+  }
+
+  @Test
+  public void testInaccessibleNoArgsConstructor() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_INACCESSIBLE;
+                  }
+                })
+            .create();
+
+    try {
+      gson.fromJson("{}", ClassWithPrivateNoArgsConstructor.class);
+      fail("Expected exception; test needs to be run with Java >= 9");
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+  }
+
+  private static class ClassWithoutNoArgsConstructor {
+    public String s;
+
+    public ClassWithoutNoArgsConstructor(String s) {
+      this.s = s;
+    }
+  }
+
+  @Test
+  public void testClassWithoutNoArgsConstructor() {
+    GsonBuilder gsonBuilder =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    // Even BLOCK_INACCESSIBLE should prevent usage of Unsafe for object creation
+                    return FilterResult.BLOCK_INACCESSIBLE;
+                  }
+                });
+    Gson gson = gsonBuilder.create();
+
+    try {
+      gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+
+    // But should not fail when custom TypeAdapter is specified
+    gson =
+        gson.newBuilder()
+            .registerTypeAdapter(
+                ClassWithoutNoArgsConstructor.class,
+                new TypeAdapter<ClassWithoutNoArgsConstructor>() {
+                  @Override
+                  public ClassWithoutNoArgsConstructor read(JsonReader in) throws IOException {
+                    in.skipValue();
+                    return new ClassWithoutNoArgsConstructor("TypeAdapter");
+                  }
+
+                  @Override
+                  public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) {
+                    throw new AssertionError("Not needed for test");
+                  }
+                })
+            .create();
+    ClassWithoutNoArgsConstructor deserialized =
+        gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
+    assertThat(deserialized.s).isEqualTo("TypeAdapter");
+
+    // But should not fail when custom InstanceCreator is specified
+    gson =
+        gsonBuilder
+            .registerTypeAdapter(
+                ClassWithoutNoArgsConstructor.class,
+                new InstanceCreator<ClassWithoutNoArgsConstructor>() {
+                  @Override
+                  public ClassWithoutNoArgsConstructor createInstance(Type type) {
+                    return new ClassWithoutNoArgsConstructor("InstanceCreator");
+                  }
+                })
+            .create();
+    deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class);
+    assertThat(deserialized.s).isEqualTo("InstanceCreator");
+  }
+
+  /**
+   * When using {@link FilterResult#BLOCK_ALL}, registering only a {@link JsonSerializer} but not
+   * performing any deserialization should not throw any exception.
+   */
+  @Test
+  public void testBlockAllPartial() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_ALL;
+                  }
+                })
+            .registerTypeAdapter(
+                OtherClass.class,
+                new JsonSerializer<OtherClass>() {
+                  @Override
+                  public JsonElement serialize(
+                      OtherClass src, Type typeOfSrc, JsonSerializationContext context) {
+                    return new JsonPrimitive(123);
+                  }
+                })
+            .create();
+
+    String json = gson.toJson(new OtherClass());
+    assertThat(json).isEqualTo("123");
+
+    // But deserialization should fail
+    try {
+      gson.fromJson("{}", OtherClass.class);
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "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.");
+    }
+  }
+
+  /**
+   * Should not fail when deserializing collection interface (even though this goes through {@link
+   * ConstructorConstructor} as well)
+   */
+  @Test
+  public void testBlockAllCollectionInterface() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_ALL;
+                  }
+                })
+            .create();
+    List<?> deserialized = gson.fromJson("[1.0]", List.class);
+    assertThat(deserialized.get(0)).isEqualTo(1.0);
+  }
+
+  /**
+   * Should not fail when deserializing specific collection implementation (even though this goes
+   * through {@link ConstructorConstructor} as well)
+   */
+  @Test
+  public void testBlockAllCollectionImplementation() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_ALL;
+                  }
+                })
+            .create();
+    List<?> deserialized = gson.fromJson("[1.0]", LinkedList.class);
+    assertThat(deserialized.get(0)).isEqualTo(1.0);
+  }
+
+  /**
+   * When trying to deserialize interface an exception for that should be thrown, even if {@link
+   * FilterResult#BLOCK_INACCESSIBLE} is used
+   */
+  @Test
+  public void testBlockInaccessibleInterface() {
+    Gson gson =
+        new GsonBuilder()
+            .addReflectionAccessFilter(
+                new ReflectionAccessFilter() {
+                  @Override
+                  public FilterResult check(Class<?> rawClass) {
+                    return FilterResult.BLOCK_INACCESSIBLE;
+                  }
+                })
+            .create();
+
+    try {
+      gson.fromJson("{}", Runnable.class);
+      fail();
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+                  + " this type. Interface name: java.lang.Runnable");
+    }
+  }
+
+  /**
+   * Verifies that the predefined filter constants have meaningful {@code toString()} output to make
+   * debugging easier.
+   */
+  @Test
+  public void testConstantsToString() throws Exception {
+    List<Field> constantFields = new ArrayList<>();
+
+    for (Field f : ReflectionAccessFilter.class.getFields()) {
+      // Only include ReflectionAccessFilter constants (in case any other fields are added in the
+      // future)
+      if (f.getType() == ReflectionAccessFilter.class) {
+        constantFields.add(f);
+      }
+    }
+
+    assertThat(constantFields).isNotEmpty();
+    for (Field f : constantFields) {
+      Object constant = f.get(null);
+      assertThat(constant.toString()).isEqualTo("ReflectionAccessFilter#" + f.getName());
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7b2e3350293a2471cc177bbccfd9aa2b4af2b15
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2021 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+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;
+import java.io.IOException;
+import java.lang.reflect.ReflectPermission;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.security.Permission;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Test;
+
+public class ReflectionAccessTest {
+  @SuppressWarnings("unused")
+  private static class ClassWithPrivateMembers {
+    private String s;
+
+    private ClassWithPrivateMembers() {}
+  }
+
+  private static Class<?> loadClassWithDifferentClassLoader(Class<?> c) throws Exception {
+    URL url = c.getProtectionDomain().getCodeSource().getLocation();
+    URLClassLoader classLoader = new URLClassLoader(new URL[] {url}, null);
+    return classLoader.loadClass(c.getName());
+  }
+
+  @SuppressWarnings("removal") // java.lang.SecurityManager deprecation in Java 17
+  @Test
+  public void testRestrictiveSecurityManager() throws Exception {
+    // Skip for newer Java versions where `System.setSecurityManager` is unsupported
+    assumeTrue(Runtime.version().feature() <= 17);
+
+    // Must use separate class loader, otherwise permission is not checked, see
+    // Class.getDeclaredFields()
+    Class<?> clazz = loadClassWithDifferentClassLoader(ClassWithPrivateMembers.class);
+
+    final Permission accessDeclaredMembers = new RuntimePermission("accessDeclaredMembers");
+    final Permission suppressAccessChecks = new ReflectPermission("suppressAccessChecks");
+    SecurityManager original = System.getSecurityManager();
+    SecurityManager restrictiveManager =
+        new SecurityManager() {
+          @Override
+          public void checkPermission(Permission perm) {
+            if (accessDeclaredMembers.equals(perm)) {
+              throw new SecurityException("Gson: no-member-access");
+            }
+            if (suppressAccessChecks.equals(perm)) {
+              throw new SecurityException("Gson: no-suppress-access-check");
+            }
+          }
+        };
+    System.setSecurityManager(restrictiveManager);
+
+    try {
+      Gson gson = new Gson();
+      try {
+        // Getting reflection based adapter should fail
+        gson.getAdapter(clazz);
+        fail();
+      } catch (SecurityException e) {
+        assertThat(e).hasMessageThat().isEqualTo("Gson: no-member-access");
+      }
+
+      final AtomicBoolean wasReadCalled = new AtomicBoolean(false);
+      gson =
+          new GsonBuilder()
+              .registerTypeAdapter(
+                  clazz,
+                  new TypeAdapter<Object>() {
+                    @Override
+                    public void write(JsonWriter out, Object value) throws IOException {
+                      out.value("custom-write");
+                    }
+
+                    @Override
+                    public Object read(JsonReader in) throws IOException {
+                      in.skipValue();
+                      wasReadCalled.set(true);
+                      return null;
+                    }
+                  })
+              .create();
+
+      assertThat(gson.toJson(null, clazz)).isEqualTo("\"custom-write\"");
+      assertThat(gson.fromJson("{}", clazz)).isNull();
+      assertThat(wasReadCalled.get()).isTrue();
+    } finally {
+      System.setSecurityManager(original);
+    }
+  }
+
+  private static JsonIOException assertInaccessibleException(String json, Class<?> toDeserialize) {
+    Gson gson = new Gson();
+    try {
+      gson.fromJson(json, toDeserialize);
+      throw new AssertionError(
+          "Missing exception; test has to be run with `--illegal-access=deny`");
+    } catch (JsonSyntaxException e) {
+      throw new AssertionError(
+          "Unexpected exception; test has to be run with `--illegal-access=deny`", e);
+    } catch (JsonIOException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .endsWith(
+              "\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible");
+      // Return exception for further assertions
+      return expected;
+    }
+  }
+
+  /**
+   * Test serializing an instance of a non-accessible internal class, but where Gson supports
+   * serializing one of its superinterfaces.
+   *
+   * <p>Here {@link Collections#emptyList()} is used which returns an instance of the internal class
+   * {@code java.util.Collections.EmptyList}. Gson should serialize the object as {@code List}
+   * despite the internal class not being accessible.
+   *
+   * <p>See https://github.com/google/gson/issues/1875
+   */
+  @Test
+  public void testSerializeInternalImplementationObject() {
+    Gson gson = new Gson();
+    String json = gson.toJson(Collections.emptyList());
+    assertThat(json).isEqualTo("[]");
+
+    // But deserialization should fail
+    Class<?> internalClass = Collections.emptyList().getClass();
+    JsonIOException exception = assertInaccessibleException("[]", internalClass);
+    // Don't check exact class name because it is a JDK implementation detail
+    assertThat(exception).hasMessageThat().startsWith("Failed making constructor '");
+    assertThat(exception)
+        .hasMessageThat()
+        .contains(
+            "' accessible; either increase its visibility or"
+                + " write a custom InstanceCreator or TypeAdapter for its declaring type: ");
+  }
+
+  @Test
+  public void testInaccessibleField() {
+    JsonIOException exception = assertInaccessibleException("{}", Throwable.class);
+    // Don't check exact field name because it is a JDK implementation detail
+    assertThat(exception).hasMessageThat().startsWith("Failed making field 'java.lang.Throwable#");
+    assertThat(exception)
+        .hasMessageThat()
+        .contains(
+            "' accessible; either increase its visibility or"
+                + " write a custom TypeAdapter for its declaring type.");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ReusedTypeVariablesFullyResolveTest.java b/gson/gson/src/test/java/com/google/gson/functional/ReusedTypeVariablesFullyResolveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e83c5771b518b3bfb64e4c333a0cb316bdd25652
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ReusedTypeVariablesFullyResolveTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test covers the scenario described in #1390 where a type variable needs to be used by a type
+ * definition multiple times. Both type variable references should resolve to the same underlying
+ * concrete type.
+ */
+public class ReusedTypeVariablesFullyResolveTest {
+
+  private Gson gson;
+
+  @Before
+  public void setUp() {
+    gson = new GsonBuilder().create();
+  }
+
+  // The instances were being unmarshaled as Strings instead of TestEnums
+  @SuppressWarnings("ConstantConditions")
+  @Test
+  public void testGenericsPreservation() {
+    TestEnumSetCollection withSet =
+        gson.fromJson("{\"collection\":[\"ONE\",\"THREE\"]}", TestEnumSetCollection.class);
+    Iterator<TestEnum> iterator = withSet.collection.iterator();
+    assertThat(withSet).isNotNull();
+    assertThat(withSet.collection).isNotNull();
+    assertThat(withSet.collection).hasSize(2);
+    TestEnum first = iterator.next();
+    TestEnum second = iterator.next();
+
+    assertThat(first).isInstanceOf(TestEnum.class);
+    assertThat(second).isInstanceOf(TestEnum.class);
+  }
+
+  enum TestEnum {
+    ONE,
+    TWO,
+    THREE
+  }
+
+  private static class TestEnumSetCollection extends SetCollection<TestEnum> {}
+
+  private static class SetCollection<T> extends BaseCollection<T, Set<T>> {}
+
+  private static class BaseCollection<U, C extends Collection<U>> {
+    public C collection;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java b/gson/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..667d231f36731da465d148fa668ee31cf7e70fa3
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/RuntimeTypeAdapterFactoryFunctionalTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.internal.Streams;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.Test;
+
+/** Functional tests for the RuntimeTypeAdapterFactory feature in extras. */
+public final class RuntimeTypeAdapterFactoryFunctionalTest {
+
+  private final Gson gson = new Gson();
+
+  /**
+   * This test also ensures that {@link TypeAdapterFactory} registered through {@link JsonAdapter}
+   * work correctly for {@link Gson#getDelegateAdapter(TypeAdapterFactory, TypeToken)}.
+   */
+  @Test
+  public void testSubclassesAutomaticallySerialized() {
+    Shape shape = new Circle(25);
+    String json = gson.toJson(shape);
+    shape = gson.fromJson(json, Shape.class);
+    assertThat(((Circle) shape).radius).isEqualTo(25);
+
+    shape = new Square(15);
+    json = gson.toJson(shape);
+    shape = gson.fromJson(json, Shape.class);
+    assertThat(((Square) shape).side).isEqualTo(15);
+    assertThat(shape.type).isEqualTo(ShapeType.SQUARE);
+  }
+
+  @JsonAdapter(Shape.JsonAdapterFactory.class)
+  static class Shape {
+    final ShapeType type;
+
+    Shape(ShapeType type) {
+      this.type = type;
+    }
+
+    private static final class JsonAdapterFactory extends RuntimeTypeAdapterFactory<Shape> {
+      public JsonAdapterFactory() {
+        super(Shape.class, "type");
+        registerSubtype(Circle.class, ShapeType.CIRCLE.toString());
+        registerSubtype(Square.class, ShapeType.SQUARE.toString());
+      }
+    }
+  }
+
+  public enum ShapeType {
+    SQUARE,
+    CIRCLE
+  }
+
+  private static final class Circle extends Shape {
+    final int radius;
+
+    Circle(int radius) {
+      super(ShapeType.CIRCLE);
+      this.radius = radius;
+    }
+  }
+
+  private static final class Square extends Shape {
+    final int side;
+
+    Square(int side) {
+      super(ShapeType.SQUARE);
+      this.side = side;
+    }
+  }
+
+  // Copied from the extras package
+  static class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
+    private final Class<?> baseType;
+    private final String typeFieldName;
+    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
+    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
+
+    protected RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
+      if (typeFieldName == null || baseType == null) {
+        throw new NullPointerException();
+      }
+      this.baseType = baseType;
+      this.typeFieldName = typeFieldName;
+    }
+
+    /**
+     * Creates a new runtime type adapter for {@code baseType} using {@code typeFieldName} as the
+     * type field name. Type field names are case sensitive.
+     */
+    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
+      return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName);
+    }
+
+    /**
+     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as the type
+     * field name.
+     */
+    public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
+      return new RuntimeTypeAdapterFactory<>(baseType, "type");
+    }
+
+    /**
+     * Registers {@code type} identified by {@code label}. Labels are case sensitive.
+     *
+     * @throws IllegalArgumentException if either {@code type} or {@code label} have already been
+     *     registered on this type adapter.
+     */
+    @CanIgnoreReturnValue
+    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
+      if (type == null || label == null) {
+        throw new NullPointerException();
+      }
+      if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
+        throw new IllegalArgumentException("types and labels must be unique");
+      }
+      labelToSubtype.put(label, type);
+      subtypeToLabel.put(type, label);
+      return this;
+    }
+
+    /**
+     * Registers {@code type} identified by its {@link Class#getSimpleName simple name}. Labels are
+     * case sensitive.
+     *
+     * @throws IllegalArgumentException if either {@code type} or its simple name have already been
+     *     registered on this type adapter.
+     */
+    @CanIgnoreReturnValue
+    public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
+      return registerSubtype(type, type.getSimpleName());
+    }
+
+    @Override
+    public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
+      if (type.getRawType() != baseType) {
+        return null;
+      }
+
+      final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
+      final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
+      for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
+        TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
+        labelToDelegate.put(entry.getKey(), delegate);
+        subtypeToDelegate.put(entry.getValue(), delegate);
+      }
+
+      return new TypeAdapter<R>() {
+        @Override
+        public R read(JsonReader in) {
+          JsonElement jsonElement = Streams.parse(in);
+          JsonElement labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
+          if (labelJsonElement == null) {
+            throw new JsonParseException(
+                "cannot deserialize "
+                    + baseType
+                    + " because it does not define a field named "
+                    + typeFieldName);
+          }
+          String label = labelJsonElement.getAsString();
+          @SuppressWarnings("unchecked") // registration requires that subtype extends T
+          TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
+          if (delegate == null) {
+            throw new JsonParseException(
+                "cannot deserialize "
+                    + baseType
+                    + " subtype named "
+                    + label
+                    + "; did you forget to register a subtype?");
+          }
+          return delegate.fromJsonTree(jsonElement);
+        }
+
+        @Override
+        public void write(JsonWriter out, R value) throws IOException {
+          Class<?> srcType = value.getClass();
+          String label = subtypeToLabel.get(srcType);
+          @SuppressWarnings("unchecked") // registration requires that subtype extends T
+          TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
+          if (delegate == null) {
+            throw new JsonParseException(
+                "cannot serialize "
+                    + srcType.getName()
+                    + "; did you forget to register a subtype?");
+          }
+          JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
+          if (!jsonObject.has(typeFieldName)) {
+            JsonObject clone = new JsonObject();
+            clone.add(typeFieldName, new JsonPrimitive(label));
+            for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
+              clone.add(e.getKey(), e.getValue());
+            }
+            jsonObject = clone;
+          }
+          Streams.write(jsonObject, out);
+        }
+      };
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/SecurityTest.java b/gson/gson/src/test/java/com/google/gson/functional/SecurityTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..246ac1c794c69acff0750e0c1fa0395136421ea7
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/SecurityTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for security-related aspects of Gson
+ *
+ * @author Inderjeet Singh
+ */
+public class SecurityTest {
+  /** Keep this in sync with Gson.JSON_NON_EXECUTABLE_PREFIX */
+  private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
+
+  private GsonBuilder gsonBuilder;
+
+  @Before
+  public void setUp() throws Exception {
+    gsonBuilder = new GsonBuilder();
+  }
+
+  @Test
+  public void testNonExecutableJsonSerialization() {
+    Gson gson = gsonBuilder.generateNonExecutableJson().create();
+    String json = gson.toJson(new BagOfPrimitives());
+    assertThat(json).startsWith(JSON_NON_EXECUTABLE_PREFIX);
+  }
+
+  @Test
+  public void testNonExecutableJsonDeserialization() {
+    String json = JSON_NON_EXECUTABLE_PREFIX + "{longValue:1}";
+    Gson gson = gsonBuilder.create();
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.longValue).isEqualTo(1);
+  }
+
+  @Test
+  public void testJsonWithNonExectuableTokenSerialization() {
+    Gson gson = gsonBuilder.generateNonExecutableJson().create();
+    String json = gson.toJson(JSON_NON_EXECUTABLE_PREFIX);
+    assertThat(json).contains(")]}'\n");
+  }
+
+  /**
+   * Gson should be able to deserialize a stream with non-exectuable token even if it is created
+   * without {@link GsonBuilder#generateNonExecutableJson()}.
+   */
+  @Test
+  public void testJsonWithNonExectuableTokenWithRegularGsonDeserialization() {
+    Gson gson = gsonBuilder.create();
+    String json = JSON_NON_EXECUTABLE_PREFIX + "{stringValue:')]}\\u0027\\n'}";
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.stringValue).isEqualTo(")]}'\n");
+  }
+
+  /**
+   * Gson should be able to deserialize a stream with non-exectuable token if it is created with
+   * {@link GsonBuilder#generateNonExecutableJson()}.
+   */
+  @Test
+  public void testJsonWithNonExectuableTokenWithConfiguredGsonDeserialization() {
+    // Gson should be able to deserialize a stream with non-exectuable token even if it is created
+    Gson gson = gsonBuilder.generateNonExecutableJson().create();
+    String json = JSON_NON_EXECUTABLE_PREFIX + "{intValue:2,stringValue:')]}\\u0027\\n'}";
+    BagOfPrimitives target = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(target.stringValue).isEqualTo(")]}'\n");
+    assertThat(target.intValue).isEqualTo(2);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java b/gson/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..487abded87c53cd95784b94a3794c5307ce7345c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/SerializedNameTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2015 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+import org.junit.Test;
+
+public final class SerializedNameTest {
+  private final Gson gson = new Gson();
+
+  @Test
+  public void testFirstNameIsChosenForSerialization() {
+    MyClass target = new MyClass("v1", "v2");
+    // Ensure name1 occurs exactly once, and name2 and name3 don't appear
+    assertThat(gson.toJson(target)).isEqualTo("{\"name\":\"v1\",\"name1\":\"v2\"}");
+  }
+
+  @Test
+  public void testMultipleNamesDeserializedCorrectly() {
+    assertThat(gson.fromJson("{'name':'v1'}", MyClass.class).a).isEqualTo("v1");
+
+    // Both name1 and name2 gets deserialized to b
+    assertThat(gson.fromJson("{'name1':'v11'}", MyClass.class).b).isEqualTo("v11");
+    assertThat(gson.fromJson("{'name2':'v2'}", MyClass.class).b).isEqualTo("v2");
+    assertThat(gson.fromJson("{'name3':'v3'}", MyClass.class).b).isEqualTo("v3");
+  }
+
+  @Test
+  public void testMultipleNamesInTheSameString() {
+    // The last value takes precedence
+    assertThat(gson.fromJson("{'name1':'v1','name2':'v2','name3':'v3'}", MyClass.class).b)
+        .isEqualTo("v3");
+  }
+
+  private static final class MyClass {
+    @SerializedName("name")
+    String a;
+
+    @SerializedName(
+        value = "name1",
+        alternate = {"name2", "name3"})
+    String b;
+
+    MyClass(String a, String b) {
+      this.a = a;
+      this.b = b;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java b/gson/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b047e4c51e2ccb892d7b64c11565cfc1a4fe4915
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Splitter;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSyntaxException;
+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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public final class StreamingTypeAdaptersTest {
+  private Gson miniGson = new GsonBuilder().create();
+  private TypeAdapter<Truck> truckAdapter = miniGson.getAdapter(Truck.class);
+  private TypeAdapter<Map<String, Double>> mapAdapter =
+      miniGson.getAdapter(new TypeToken<Map<String, Double>>() {});
+
+  @Test
+  public void testSerialize() {
+    Truck truck = new Truck();
+    truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29));
+    truck.horsePower = 300;
+
+    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
+        .isEqualTo(
+            "{'horsePower':300.0,"
+                + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}");
+  }
+
+  @Test
+  public void testDeserialize() throws IOException {
+    String json =
+        "{'horsePower':300.0,"
+            + "'passengers':[{'age':29,'name':'Jesse'},{'age':29,'name':'Jodie'}]}";
+    Truck truck = truckAdapter.fromJson(json.replace('\'', '\"'));
+    assertThat(truck.horsePower).isEqualTo(300.0);
+    assertThat(truck.passengers)
+        .isEqualTo(Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29)));
+  }
+
+  @Test
+  public void testSerializeNullField() {
+    Truck truck = new Truck();
+    truck.passengers = null;
+    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
+        .isEqualTo("{'horsePower':0.0,'passengers':null}");
+  }
+
+  @Test
+  public void testDeserializeNullField() throws IOException {
+    Truck truck = truckAdapter.fromJson("{'horsePower':0.0,'passengers':null}".replace('\'', '\"'));
+    assertThat(truck.passengers).isNull();
+  }
+
+  @Test
+  public void testSerializeNullObject() {
+    Truck truck = new Truck();
+    truck.passengers = Arrays.asList((Person) null);
+    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
+        .isEqualTo("{'horsePower':0.0,'passengers':[null]}");
+  }
+
+  @Test
+  public void testDeserializeNullObject() throws IOException {
+    Truck truck =
+        truckAdapter.fromJson("{'horsePower':0.0,'passengers':[null]}".replace('\'', '\"'));
+    assertThat(truck.passengers).isEqualTo(Arrays.asList((Person) null));
+  }
+
+  @Test
+  public void testSerializeWithCustomTypeAdapter() {
+    usePersonNameAdapter();
+    Truck truck = new Truck();
+    truck.passengers = Arrays.asList(new Person("Jesse", 29), new Person("Jodie", 29));
+    assertThat(truckAdapter.toJson(truck).replace('\"', '\''))
+        .isEqualTo("{'horsePower':0.0,'passengers':['Jesse','Jodie']}");
+  }
+
+  @Test
+  public void testDeserializeWithCustomTypeAdapter() throws IOException {
+    usePersonNameAdapter();
+    Truck truck =
+        truckAdapter.fromJson(
+            "{'horsePower':0.0,'passengers':['Jesse','Jodie']}".replace('\'', '\"'));
+    assertThat(truck.passengers)
+        .isEqualTo(Arrays.asList(new Person("Jesse", -1), new Person("Jodie", -1)));
+  }
+
+  private void usePersonNameAdapter() {
+    TypeAdapter<Person> personNameAdapter =
+        new TypeAdapter<Person>() {
+          @Override
+          public Person read(JsonReader in) throws IOException {
+            String name = in.nextString();
+            return new Person(name, -1);
+          }
+
+          @Override
+          public void write(JsonWriter out, Person value) throws IOException {
+            out.value(value.name);
+          }
+        };
+    miniGson = new GsonBuilder().registerTypeAdapter(Person.class, personNameAdapter).create();
+    truckAdapter = miniGson.getAdapter(Truck.class);
+  }
+
+  @Test
+  public void testSerializeMap() {
+    Map<String, Double> map = new LinkedHashMap<>();
+    map.put("a", 5.0);
+    map.put("b", 10.0);
+    assertThat(mapAdapter.toJson(map).replace('"', '\'')).isEqualTo("{'a':5.0,'b':10.0}");
+  }
+
+  @Test
+  public void testDeserializeMap() throws IOException {
+    Map<String, Double> map = new LinkedHashMap<>();
+    map.put("a", 5.0);
+    map.put("b", 10.0);
+    assertThat(mapAdapter.fromJson("{'a':5.0,'b':10.0}".replace('\'', '\"'))).isEqualTo(map);
+  }
+
+  @Test
+  public void testSerialize1dArray() {
+    TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {});
+    assertThat(arrayAdapter.toJson(new double[] {1.0, 2.0, 3.0})).isEqualTo("[1.0,2.0,3.0]");
+  }
+
+  @Test
+  public void testDeserialize1dArray() throws IOException {
+    TypeAdapter<double[]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[]>() {});
+    double[] array = arrayAdapter.fromJson("[1.0,2.0,3.0]");
+    assertThat(array).isEqualTo(new double[] {1.0, 2.0, 3.0});
+  }
+
+  @Test
+  public void testSerialize2dArray() {
+    TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {});
+    double[][] array = {{1.0, 2.0}, {3.0}};
+    assertThat(arrayAdapter.toJson(array)).isEqualTo("[[1.0,2.0],[3.0]]");
+  }
+
+  @Test
+  public void testDeserialize2dArray() throws IOException {
+    TypeAdapter<double[][]> arrayAdapter = miniGson.getAdapter(new TypeToken<double[][]>() {});
+    double[][] array = arrayAdapter.fromJson("[[1.0,2.0],[3.0]]");
+    double[][] expected = {{1.0, 2.0}, {3.0}};
+    assertThat(array).isEqualTo(expected);
+  }
+
+  @Test
+  public void testNullSafe() {
+    TypeAdapter<Person> typeAdapter =
+        new TypeAdapter<Person>() {
+          @Override
+          public Person read(JsonReader in) throws IOException {
+            List<String> values = Splitter.on(',').splitToList(in.nextString());
+            return new Person(values.get(0), Integer.parseInt(values.get(1)));
+          }
+
+          @Override
+          public void write(JsonWriter out, Person person) throws IOException {
+            out.value(person.name + "," + person.age);
+          }
+        };
+    Gson gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter).create();
+    Truck truck = new Truck();
+    truck.horsePower = 1.0D;
+    truck.passengers = new ArrayList<>();
+    truck.passengers.add(null);
+    truck.passengers.add(new Person("jesse", 30));
+    try {
+      gson.toJson(truck, Truck.class);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+    String json = "{horsePower:1.0,passengers:[null,'jesse,30']}";
+    try {
+      gson.fromJson(json, Truck.class);
+      fail();
+    } catch (JsonSyntaxException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "java.lang.IllegalStateException: Expected a string but was NULL at line 1 column 33"
+                  + " path $.passengers[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe");
+    }
+    gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter.nullSafe()).create();
+    assertThat(gson.toJson(truck, Truck.class))
+        .isEqualTo("{\"horsePower\":1.0,\"passengers\":[null,\"jesse,30\"]}");
+    truck = gson.fromJson(json, Truck.class);
+    assertThat(truck.horsePower).isEqualTo(1.0D);
+    assertThat(truck.passengers.get(0)).isNull();
+    assertThat(truck.passengers.get(1).name).isEqualTo("jesse");
+  }
+
+  @Test
+  public void testSerializeRecursive() {
+    TypeAdapter<Node> nodeAdapter = miniGson.getAdapter(Node.class);
+    Node root = new Node("root");
+    root.left = new Node("left");
+    root.right = new Node("right");
+    assertThat(nodeAdapter.toJson(root).replace('"', '\''))
+        .isEqualTo(
+            "{'label':'root',"
+                + "'left':{'label':'left','left':null,'right':null},"
+                + "'right':{'label':'right','left':null,'right':null}}");
+  }
+
+  @Test
+  public void testFromJsonTree() {
+    JsonObject truckObject = new JsonObject();
+    truckObject.add("horsePower", new JsonPrimitive(300));
+    JsonArray passengersArray = new JsonArray();
+    JsonObject jesseObject = new JsonObject();
+    jesseObject.add("age", new JsonPrimitive(30));
+    jesseObject.add("name", new JsonPrimitive("Jesse"));
+    passengersArray.add(jesseObject);
+    truckObject.add("passengers", passengersArray);
+
+    Truck truck = truckAdapter.fromJsonTree(truckObject);
+    assertThat(truck.horsePower).isEqualTo(300.0);
+    assertThat(truck.passengers).isEqualTo(Arrays.asList(new Person("Jesse", 30)));
+  }
+
+  static class Truck {
+    double horsePower;
+    List<Person> passengers = Collections.emptyList();
+  }
+
+  static class Person {
+    int age;
+    String name;
+
+    Person(String name, int age) {
+      this.name = name;
+      this.age = age;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Person && ((Person) o).name.equals(name) && ((Person) o).age == age;
+    }
+
+    @Override
+    public int hashCode() {
+      return name.hashCode() ^ age;
+    }
+  }
+
+  static class Node {
+    String label;
+    Node left;
+    Node right;
+
+    Node(String label) {
+      this.label = label;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/StringTest.java b/gson/gson/src/test/java/com/google/gson/functional/StringTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..075d8dfe01eb008612167f9a895028af6d818ae0
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/StringTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for Json serialization and deserialization of strings.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class StringTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testStringValueSerialization() {
+    String value = "someRandomStringValue";
+    assertThat(gson.toJson(value)).isEqualTo('"' + value + '"');
+  }
+
+  @Test
+  public void testStringValueDeserialization() {
+    String value = "someRandomStringValue";
+    String actual = gson.fromJson("\"" + value + "\"", String.class);
+    assertThat(actual).isEqualTo(value);
+  }
+
+  @Test
+  public void testSingleQuoteInStringSerialization() {
+    String valueWithQuotes = "beforeQuote'afterQuote";
+    String jsonRepresentation = gson.toJson(valueWithQuotes);
+    assertThat(gson.fromJson(jsonRepresentation, String.class)).isEqualTo(valueWithQuotes);
+  }
+
+  @Test
+  public void testEscapedCtrlNInStringSerialization() {
+    String value = "a\nb";
+    String json = gson.toJson(value);
+    assertThat(json).isEqualTo("\"a\\nb\"");
+  }
+
+  @Test
+  public void testEscapedCtrlNInStringDeserialization() {
+    String json = "'a\\nb'";
+    String actual = gson.fromJson(json, String.class);
+    assertThat(actual).isEqualTo("a\nb");
+  }
+
+  @Test
+  public void testEscapedCtrlRInStringSerialization() {
+    String value = "a\rb";
+    String json = gson.toJson(value);
+    assertThat(json).isEqualTo("\"a\\rb\"");
+  }
+
+  @Test
+  public void testEscapedCtrlRInStringDeserialization() {
+    String json = "'a\\rb'";
+    String actual = gson.fromJson(json, String.class);
+    assertThat(actual).isEqualTo("a\rb");
+  }
+
+  @Test
+  public void testEscapedBackslashInStringSerialization() {
+    String value = "a\\b";
+    String json = gson.toJson(value);
+    assertThat(json).isEqualTo("\"a\\\\b\"");
+  }
+
+  @Test
+  public void testEscapedBackslashInStringDeserialization() {
+    String actual = gson.fromJson("'a\\\\b'", String.class);
+    assertThat(actual).isEqualTo("a\\b");
+  }
+
+  @Test
+  public void testSingleQuoteInStringDeserialization() {
+    String value = "beforeQuote'afterQuote";
+    String actual = gson.fromJson("\"" + value + "\"", String.class);
+    assertThat(actual).isEqualTo(value);
+  }
+
+  @Test
+  public void testEscapingQuotesInStringSerialization() {
+    String valueWithQuotes = "beforeQuote\"afterQuote";
+    String jsonRepresentation = gson.toJson(valueWithQuotes);
+    String target = gson.fromJson(jsonRepresentation, String.class);
+    assertThat(target).isEqualTo(valueWithQuotes);
+  }
+
+  @Test
+  public void testEscapingQuotesInStringDeserialization() {
+    String value = "beforeQuote\\\"afterQuote";
+    String actual = gson.fromJson("\"" + value + "\"", String.class);
+    String expected = "beforeQuote\"afterQuote";
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testStringValueAsSingleElementArraySerialization() {
+    String[] target = {"abc"};
+    assertThat(gson.toJson(target)).isEqualTo("[\"abc\"]");
+    assertThat(gson.toJson(target, String[].class)).isEqualTo("[\"abc\"]");
+  }
+
+  @Test
+  public void testStringWithEscapedSlashDeserialization() {
+    String value = "/";
+    String json = "'\\/'";
+    String actual = gson.fromJson(json, String.class);
+    assertThat(actual).isEqualTo(value);
+  }
+
+  /**
+   * Created in response to
+   * http://groups.google.com/group/google-gson/browse_thread/thread/2431d4a3d0d6cb23
+   */
+  @Test
+  public void testAssignmentCharSerialization() {
+    String value = "abc=";
+    String json = gson.toJson(value);
+    assertThat(json).isEqualTo("\"abc\\u003d\"");
+  }
+
+  /**
+   * Created in response to
+   * http://groups.google.com/group/google-gson/browse_thread/thread/2431d4a3d0d6cb23
+   */
+  @Test
+  public void testAssignmentCharDeserialization() {
+    String json = "\"abc=\"";
+    String value = gson.fromJson(json, String.class);
+    assertThat(value).isEqualTo("abc=");
+
+    json = "'abc\\u003d'";
+    value = gson.fromJson(json, String.class);
+    assertThat(value).isEqualTo("abc=");
+  }
+
+  @Test
+  public void testJavascriptKeywordsInStringSerialization() {
+    String value = "null true false function";
+    String json = gson.toJson(value);
+    assertThat(json).isEqualTo("\"" + value + "\"");
+  }
+
+  @Test
+  public void testJavascriptKeywordsInStringDeserialization() {
+    String json = "'null true false function'";
+    String value = gson.fromJson(json, String.class);
+    assertThat(json.substring(1, json.length() - 1)).isEqualTo(value);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java b/gson/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d427e71246cba02026e80d19cbfb9dd6b3d23b5a
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/ToNumberPolicyFunctionalTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 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 com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.ToNumberStrategy;
+import com.google.gson.internal.LazilyParsedNumber;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+
+public class ToNumberPolicyFunctionalTest {
+  @Test
+  public void testDefault() {
+    Gson gson = new Gson();
+    assertThat(gson.fromJson("null", Object.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Object.class)).isEqualTo(10D);
+    assertThat(gson.fromJson("null", Number.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Number.class)).isEqualTo(new LazilyParsedNumber("10"));
+  }
+
+  @Test
+  public void testAsDoubles() {
+    Gson gson =
+        new GsonBuilder()
+            .setObjectToNumberStrategy(ToNumberPolicy.DOUBLE)
+            .setNumberToNumberStrategy(ToNumberPolicy.DOUBLE)
+            .create();
+    assertThat(gson.fromJson("null", Object.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Object.class)).isEqualTo(10.0);
+    assertThat(gson.fromJson("null", Number.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Number.class)).isEqualTo(10.0);
+  }
+
+  @Test
+  public void testAsLazilyParsedNumbers() {
+    Gson gson =
+        new GsonBuilder()
+            .setObjectToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER)
+            .setNumberToNumberStrategy(ToNumberPolicy.LAZILY_PARSED_NUMBER)
+            .create();
+    assertThat(gson.fromJson("null", Object.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Object.class)).isEqualTo(new LazilyParsedNumber("10"));
+    assertThat(gson.fromJson("null", Number.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Number.class)).isEqualTo(new LazilyParsedNumber("10"));
+  }
+
+  @Test
+  public void testAsLongsOrDoubles() {
+    Gson gson =
+        new GsonBuilder()
+            .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+            .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+            .create();
+    assertThat(gson.fromJson("null", Object.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Object.class)).isEqualTo(10L);
+    assertThat(gson.fromJson("10.0", Object.class)).isEqualTo(10.0);
+    assertThat(gson.fromJson("null", Number.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Number.class)).isEqualTo(10L);
+    assertThat(gson.fromJson("10.0", Number.class)).isEqualTo(10.0);
+  }
+
+  @Test
+  public void testAsBigDecimals() {
+    Gson gson =
+        new GsonBuilder()
+            .setObjectToNumberStrategy(ToNumberPolicy.BIG_DECIMAL)
+            .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL)
+            .create();
+    assertThat(gson.fromJson("null", Object.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Object.class)).isEqualTo(new BigDecimal("10"));
+    assertThat(gson.fromJson("10.0", Object.class)).isEqualTo(new BigDecimal("10.0"));
+    assertThat(gson.fromJson("null", Number.class)).isEqualTo(null);
+    assertThat(gson.fromJson("10", Number.class)).isEqualTo(new BigDecimal("10"));
+    assertThat(gson.fromJson("10.0", Number.class)).isEqualTo(new BigDecimal("10.0"));
+    assertThat(gson.fromJson("3.141592653589793238462643383279", BigDecimal.class))
+        .isEqualTo(new BigDecimal("3.141592653589793238462643383279"));
+    assertThat(gson.fromJson("1e400", BigDecimal.class)).isEqualTo(new BigDecimal("1e400"));
+  }
+
+  @Test
+  public void testAsListOfLongsOrDoubles() {
+    Gson gson =
+        new GsonBuilder()
+            .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+            .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+            .create();
+    Type objectCollectionType = new TypeToken<Collection<Object>>() {}.getType();
+    Collection<Object> objects = gson.fromJson("[null,10,10.0]", objectCollectionType);
+    assertThat(objects).containsExactly(null, 10L, 10.0).inOrder();
+    Type numberCollectionType = new TypeToken<Collection<Number>>() {}.getType();
+    Collection<Object> numbers = gson.fromJson("[null,10,10.0]", numberCollectionType);
+    assertThat(numbers).containsExactly(null, 10L, 10.0).inOrder();
+  }
+
+  @Test
+  public void testCustomStrategiesCannotAffectConcreteDeclaredNumbers() {
+    ToNumberStrategy fail =
+        new ToNumberStrategy() {
+          @Override
+          public Byte readNumber(JsonReader in) {
+            throw new UnsupportedOperationException();
+          }
+        };
+    Gson gson =
+        new GsonBuilder().setObjectToNumberStrategy(fail).setNumberToNumberStrategy(fail).create();
+    List<Object> numbers =
+        gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Byte>>() {}.getType());
+    assertThat(numbers).containsExactly(null, (byte) 10, (byte) 20, (byte) 30).inOrder();
+    try {
+      gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Object>>() {}.getType());
+      fail();
+    } catch (UnsupportedOperationException ex) {
+    }
+    try {
+      gson.fromJson("[null, 10, 20, 30]", new TypeToken<List<Number>>() {}.getType());
+      fail();
+    } catch (UnsupportedOperationException ex) {
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java b/gson/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2aa043f424ef9bed35957711a92a4cc72d8aa9c1
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/TreeTypeAdaptersTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+
+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.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Collection of functional tests for DOM tree based type adapters. */
+public class TreeTypeAdaptersTest {
+  private static final Id<Student> STUDENT1_ID = new Id<>("5", Student.class);
+  private static final Id<Student> STUDENT2_ID = new Id<>("6", Student.class);
+  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();
+  private static final Id<Course<HistoryCourse>> COURSE_ID = new Id<>("10", TYPE_COURSE_HISTORY);
+
+  private Gson gson;
+  private Course<HistoryCourse> course;
+
+  @Before
+  public void setUp() {
+    gson = new GsonBuilder().registerTypeAdapter(Id.class, new IdTreeTypeAdapter()).create();
+    course =
+        new Course<>(
+            COURSE_ID,
+            4,
+            new Assignment<HistoryCourse>(null, null),
+            Arrays.asList(STUDENT1, STUDENT2));
+  }
+
+  @Test
+  public void testSerializeId() {
+    String json = gson.toJson(course, TYPE_COURSE_HISTORY);
+    assertThat(json).contains(String.valueOf(COURSE_ID.getValue()));
+    assertThat(json).contains(String.valueOf(STUDENT1_ID.getValue()));
+    assertThat(json).contains(String.valueOf(STUDENT2_ID.getValue()));
+  }
+
+  @Test
+  public void testDeserializeId() {
+    String json =
+        "{courseId:1,students:[{id:1,name:'first'},{id:6,name:'second'}],"
+            + "numAssignments:4,assignment:{}}";
+    Course<HistoryCourse> target = gson.fromJson(json, TYPE_COURSE_HISTORY);
+    assertThat(target.getStudents().get(0).id.getValue()).isEqualTo("1");
+    assertThat(target.getStudents().get(1).id.getValue()).isEqualTo("6");
+    assertThat(target.getId().getValue()).isEqualTo("1");
+  }
+
+  @SuppressWarnings("UnusedTypeParameter")
+  private static final class Id<R> {
+    final String value;
+
+    @SuppressWarnings("unused")
+    final Type typeOfId;
+
+    private Id(String value, Type typeOfId) {
+      this.value = value;
+      this.typeOfId = typeOfId;
+    }
+
+    public String getValue() {
+      return value;
+    }
+  }
+
+  private static final class IdTreeTypeAdapter
+      implements JsonSerializer<Id<?>>, JsonDeserializer<Id<?>> {
+
+    @Override
+    public Id<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      if (!(typeOfT instanceof ParameterizedType)) {
+        throw new JsonParseException("Id of unknown type: " + typeOfT);
+      }
+      ParameterizedType parameterizedType = (ParameterizedType) typeOfT;
+      // 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);
+    }
+
+    @Override
+    public JsonElement serialize(Id<?> src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive(src.getValue());
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class Student {
+    Id<Student> id;
+    String name;
+
+    private Student() {
+      this(null, null);
+    }
+
+    public Student(Id<Student> id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class Course<T> {
+    final List<Student> students;
+    private final Id<Course<T>> courseId;
+    private final int numAssignments;
+    private final Assignment<T> assignment;
+
+    private Course() {
+      this(null, 0, null, new ArrayList<Student>());
+    }
+
+    public Course(
+        Id<Course<T>> courseId,
+        int numAssignments,
+        Assignment<T> assignment,
+        List<Student> players) {
+      this.courseId = courseId;
+      this.numAssignments = numAssignments;
+      this.assignment = assignment;
+      this.students = players;
+    }
+
+    public Id<Course<T>> getId() {
+      return courseId;
+    }
+
+    List<Student> getStudents() {
+      return students;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class Assignment<T> {
+    private final Id<Assignment<T>> id;
+    private final T data;
+
+    private Assignment() {
+      this(null, null);
+    }
+
+    public Assignment(Id<Assignment<T>> id, T data) {
+      this.id = id;
+      this.data = data;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class HistoryCourse {
+    int numClasses;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java b/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0252ec50526606a594ec34eddc22f80e21d3718b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+
+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 final class TypeAdapterPrecedenceTest {
+  @Test
+  public void testNonstreamingFollowedByNonstreaming() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Foo.class, newSerializer("serializer 1"))
+            .registerTypeAdapter(Foo.class, newSerializer("serializer 2"))
+            .registerTypeAdapter(Foo.class, newDeserializer("deserializer 1"))
+            .registerTypeAdapter(Foo.class, newDeserializer("deserializer 2"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via serializer 2\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer 2");
+  }
+
+  @Test
+  public void testStreamingFollowedByStreaming() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter 1"))
+            .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter 2"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via type adapter 2\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via type adapter 2");
+  }
+
+  @Test
+  public void testSerializeNonstreamingTypeAdapterFollowedByStreamingTypeAdapter() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Foo.class, newSerializer("serializer"))
+            .registerTypeAdapter(Foo.class, newDeserializer("deserializer"))
+            .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via type adapter\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via type adapter");
+  }
+
+  @Test
+  public void testStreamingFollowedByNonstreaming() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter"))
+            .registerTypeAdapter(Foo.class, newSerializer("serializer"))
+            .registerTypeAdapter(Foo.class, newDeserializer("deserializer"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via serializer\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer");
+  }
+
+  @Test
+  public void testStreamingHierarchicalFollowedByNonstreaming() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Foo.class, newTypeAdapter("type adapter"))
+            .registerTypeAdapter(Foo.class, newSerializer("serializer"))
+            .registerTypeAdapter(Foo.class, newDeserializer("deserializer"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via serializer\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer");
+  }
+
+  @Test
+  public void testStreamingFollowedByNonstreamingHierarchical() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeAdapter(Foo.class, newTypeAdapter("type adapter"))
+            .registerTypeHierarchyAdapter(Foo.class, newSerializer("serializer"))
+            .registerTypeHierarchyAdapter(Foo.class, newDeserializer("deserializer"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via type adapter\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via type adapter");
+  }
+
+  @Test
+  public void testStreamingHierarchicalFollowedByNonstreamingHierarchical() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Foo.class, newSerializer("serializer"))
+            .registerTypeHierarchyAdapter(Foo.class, newDeserializer("deserializer"))
+            .registerTypeHierarchyAdapter(Foo.class, newTypeAdapter("type adapter"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via type adapter\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via type adapter");
+  }
+
+  @Test
+  public void testNonstreamingHierarchicalFollowedByNonstreaming() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Foo.class, newSerializer("hierarchical"))
+            .registerTypeHierarchyAdapter(Foo.class, newDeserializer("hierarchical"))
+            .registerTypeAdapter(Foo.class, newSerializer("non hierarchical"))
+            .registerTypeAdapter(Foo.class, newDeserializer("non hierarchical"))
+            .create();
+    assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via non hierarchical\"");
+    assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via non hierarchical");
+  }
+
+  private static class Foo {
+    final String name;
+
+    private Foo(String name) {
+      this.name = name;
+    }
+  }
+
+  private static JsonSerializer<Foo> newSerializer(final String name) {
+    return new JsonSerializer<Foo>() {
+      @Override
+      public JsonElement serialize(Foo src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(src.name + " via " + name);
+      }
+    };
+  }
+
+  private static JsonDeserializer<Foo> newDeserializer(final String name) {
+    return new JsonDeserializer<Foo>() {
+      @Override
+      public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+        return new Foo(json.getAsString() + " via " + name);
+      }
+    };
+  }
+
+  private static TypeAdapter<Foo> newTypeAdapter(final String name) {
+    return new TypeAdapter<Foo>() {
+      @Override
+      public Foo read(JsonReader in) throws IOException {
+        return new Foo(in.nextString() + " via " + name);
+      }
+
+      @Override
+      public void write(JsonWriter out, Foo value) throws IOException {
+        out.value(value.name + " via " + name);
+      }
+    };
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..174c0f061a26125023c8e82e340ae99d67520e19
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+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());
+    assertThat(json).isEqualTo("{\"b\":\"serializer\"}");
+  }
+
+  /**
+   * 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());
+    assertThat(json).isEqualTo("{\"b\":{\"f\":\"test\"}}");
+  }
+
+  /**
+   * 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());
+    assertThat(json).isEqualTo("{\"b\":\"custom delegate\"}");
+  }
+
+  /**
+   * 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());
+    assertThat(json).isEqualTo("{\"b\":{\"f\":\"test\"}}");
+  }
+
+  /**
+   * 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());
+    assertThat(json).isEqualTo("{\"b\":\"custom delegate\"}");
+  }
+
+  /**
+   * 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());
+    assertThat(json).isEqualTo("{\"b\":{\"f\":\"test\"}}");
+  }
+
+  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);
+    assertThat(json).isEqualTo("{\"f\":{\"i\":2}}");
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java b/gson/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..973afb04aca4b547fe998a82ac5571dbcca87774
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2011 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 com.google.common.truth.Truth.assertThat;
+
+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.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+import org.junit.Test;
+
+/** Test that the hierarchy adapter works when subtypes are used. */
+public final class TypeHierarchyAdapterTest {
+
+  @Test
+  public void testTypeHierarchy() {
+    Manager andy = new Manager();
+    andy.userid = "andy";
+    andy.startDate = 2005;
+    andy.minions =
+        new Employee[] {
+          new Employee("inder", 2007), new Employee("joel", 2006), new Employee("jesse", 2006),
+        };
+
+    CEO eric = new CEO();
+    eric.userid = "eric";
+    eric.startDate = 2001;
+    eric.assistant = new Employee("jerome", 2006);
+
+    eric.minions =
+        new Employee[] {
+          new Employee("larry", 1998), new Employee("sergey", 1998), andy,
+        };
+
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter())
+            .setPrettyPrinting()
+            .create();
+
+    Company company = new Company();
+    company.ceo = eric;
+
+    String json = gson.toJson(company, Company.class);
+    assertThat(json)
+        .isEqualTo(
+            "{\n"
+                + "  \"ceo\": {\n"
+                + "    \"userid\": \"eric\",\n"
+                + "    \"startDate\": 2001,\n"
+                + "    \"minions\": [\n"
+                + "      {\n"
+                + "        \"userid\": \"larry\",\n"
+                + "        \"startDate\": 1998\n"
+                + "      },\n"
+                + "      {\n"
+                + "        \"userid\": \"sergey\",\n"
+                + "        \"startDate\": 1998\n"
+                + "      },\n"
+                + "      {\n"
+                + "        \"userid\": \"andy\",\n"
+                + "        \"startDate\": 2005,\n"
+                + "        \"minions\": [\n"
+                + "          {\n"
+                + "            \"userid\": \"inder\",\n"
+                + "            \"startDate\": 2007\n"
+                + "          },\n"
+                + "          {\n"
+                + "            \"userid\": \"joel\",\n"
+                + "            \"startDate\": 2006\n"
+                + "          },\n"
+                + "          {\n"
+                + "            \"userid\": \"jesse\",\n"
+                + "            \"startDate\": 2006\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    ],\n"
+                + "    \"assistant\": {\n"
+                + "      \"userid\": \"jerome\",\n"
+                + "      \"startDate\": 2006\n"
+                + "    }\n"
+                + "  }\n"
+                + "}");
+
+    Company copied = gson.fromJson(json, Company.class);
+    assertThat(gson.toJson(copied, Company.class)).isEqualTo(json);
+    assertThat(company.ceo.userid).isEqualTo(copied.ceo.userid);
+    assertThat(company.ceo.assistant.userid).isEqualTo(copied.ceo.assistant.userid);
+    assertThat(company.ceo.minions[0].userid).isEqualTo(copied.ceo.minions[0].userid);
+    assertThat(company.ceo.minions[1].userid).isEqualTo(copied.ceo.minions[1].userid);
+    assertThat(company.ceo.minions[2].userid).isEqualTo(copied.ceo.minions[2].userid);
+    assertThat(((Manager) company.ceo.minions[2]).minions[0].userid)
+        .isEqualTo(((Manager) copied.ceo.minions[2]).minions[0].userid);
+    assertThat(((Manager) company.ceo.minions[2]).minions[1].userid)
+        .isEqualTo(((Manager) copied.ceo.minions[2]).minions[1].userid);
+  }
+
+  @Test
+  public void testRegisterSuperTypeFirst() {
+    Gson gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter())
+            .registerTypeHierarchyAdapter(Manager.class, new ManagerAdapter())
+            .create();
+
+    Manager manager = new Manager();
+    manager.userid = "inder";
+
+    String json = gson.toJson(manager, Manager.class);
+    assertThat(json).isEqualTo("\"inder\"");
+    Manager copied = gson.fromJson(json, Manager.class);
+    assertThat(copied.userid).isEqualTo(manager.userid);
+  }
+
+  /** This behaviour changed in Gson 2.1; it used to throw. */
+  @Test
+  public void testRegisterSubTypeFirstAllowed() {
+    Gson unused =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(Manager.class, new ManagerAdapter())
+            .registerTypeHierarchyAdapter(Employee.class, new EmployeeAdapter())
+            .create();
+  }
+
+  static class ManagerAdapter implements JsonSerializer<Manager>, JsonDeserializer<Manager> {
+    @Override
+    public Manager deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
+      Manager result = new Manager();
+      result.userid = json.getAsString();
+      return result;
+    }
+
+    @Override
+    public JsonElement serialize(Manager src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive(src.userid);
+    }
+  }
+
+  static class EmployeeAdapter implements JsonSerializer<Employee>, JsonDeserializer<Employee> {
+    @Override
+    public JsonElement serialize(
+        Employee employee, Type typeOfSrc, JsonSerializationContext context) {
+      JsonObject result = new JsonObject();
+      result.add("userid", context.serialize(employee.userid, String.class));
+      result.add("startDate", context.serialize(employee.startDate, long.class));
+      if (employee instanceof Manager) {
+        result.add("minions", context.serialize(((Manager) employee).minions, Employee[].class));
+        if (employee instanceof CEO) {
+          result.add("assistant", context.serialize(((CEO) employee).assistant, Employee.class));
+        }
+      }
+      return result;
+    }
+
+    @Override
+    public Employee deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      JsonObject object = json.getAsJsonObject();
+      Employee result = null;
+
+      // if the employee has an assistant, she must be the CEO
+      JsonElement assistant = object.get("assistant");
+      if (assistant != null) {
+        result = new CEO();
+        ((CEO) result).assistant = context.deserialize(assistant, Employee.class);
+      }
+
+      // only managers have minions
+      JsonElement minons = object.get("minions");
+      if (minons != null) {
+        if (result == null) {
+          result = new Manager();
+        }
+        ((Manager) result).minions = context.deserialize(minons, Employee[].class);
+      }
+
+      if (result == null) {
+        result = new Employee();
+      }
+      result.userid = context.deserialize(object.get("userid"), String.class);
+      result.startDate = context.<Long>deserialize(object.get("startDate"), long.class);
+      return result;
+    }
+  }
+
+  static class Employee {
+    String userid;
+    long startDate;
+
+    Employee(String userid, long startDate) {
+      this.userid = userid;
+      this.startDate = startDate;
+    }
+
+    Employee() {}
+  }
+
+  static class Manager extends Employee {
+    Employee[] minions;
+  }
+
+  static class CEO extends Manager {
+    Employee assistant;
+  }
+
+  static class Company {
+    CEO ceo;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java b/gson/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1076fdb154affdd9e7a61362f571233b275b17c
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/TypeVariableTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2010 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 com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+/**
+ * Functional test for Gson serialization and deserialization of classes with type variables.
+ *
+ * @author Joel Leitch
+ */
+public class TypeVariableTest {
+
+  @Test
+  public void testAdvancedTypeVariables() {
+    Gson gson = new Gson();
+    Bar bar1 = new Bar("someString", 1, true);
+    ArrayList<Integer> arrayList = new ArrayList<>();
+    arrayList.add(1);
+    arrayList.add(2);
+    arrayList.add(3);
+    bar1.map.put("key1", arrayList);
+    bar1.map.put("key2", new ArrayList<Integer>());
+    String json = gson.toJson(bar1);
+
+    Bar bar2 = gson.fromJson(json, Bar.class);
+    assertThat(bar2).isEqualTo(bar1);
+  }
+
+  @Test
+  public void testTypeVariablesViaTypeParameter() {
+    Gson gson = new Gson();
+    Foo<String, Integer> original = new Foo<>("e", 5, false);
+    original.map.put("f", Arrays.asList(6, 7));
+    Type type = new TypeToken<Foo<String, Integer>>() {}.getType();
+    String json = gson.toJson(original, type);
+    assertThat(json)
+        .isEqualTo(
+            "{\"someSField\":\"e\",\"someTField\":5,\"map\":{\"f\":[6,7]},\"redField\":false}");
+    assertThat(gson.<Foo<String, Integer>>fromJson(json, type)).isEqualTo(original);
+  }
+
+  @Test
+  public void testBasicTypeVariables() {
+    Gson gson = new Gson();
+    Blue blue1 = new Blue(true);
+    String json = gson.toJson(blue1);
+
+    Blue blue2 = gson.fromJson(json, Blue.class);
+    assertThat(blue2).isEqualTo(blue1);
+  }
+
+  // for missing hashCode() override
+  @SuppressWarnings({"overrides", "EqualsHashCode"})
+  public static class Blue extends Red<Boolean> {
+    public Blue() {
+      super(false);
+    }
+
+    public Blue(boolean value) {
+      super(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (!(o instanceof Blue)) {
+        return false;
+      }
+      Blue blue = (Blue) o;
+      return redField.equals(blue.redField);
+    }
+  }
+
+  public static class Red<S> {
+    protected S redField;
+
+    public Red() {}
+
+    public Red(S redField) {
+      this.redField = redField;
+    }
+  }
+
+  @SuppressWarnings({"overrides", "EqualsHashCode"}) // for missing hashCode() override
+  public static class Foo<S, T> extends Red<Boolean> {
+    private S someSField;
+    private T someTField;
+    public final Map<S, List<T>> map = new HashMap<>();
+
+    public Foo() {}
+
+    public Foo(S sValue, T tValue, Boolean redField) {
+      super(redField);
+      this.someSField = sValue;
+      this.someTField = tValue;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public boolean equals(Object o) {
+      if (!(o instanceof Foo<?, ?>)) {
+        return false;
+      }
+      Foo<S, T> realFoo = (Foo<S, T>) o;
+      return redField.equals(realFoo.redField)
+          && someTField.equals(realFoo.someTField)
+          && someSField.equals(realFoo.someSField)
+          && map.equals(realFoo.map);
+    }
+  }
+
+  public static class Bar extends Foo<String, Integer> {
+    public Bar() {
+      this("", 0, false);
+    }
+
+    public Bar(String s, Integer i, boolean b) {
+      super(s, i, b);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java b/gson/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ba6549fe961e824bfc2fc9f01c5fe21c8e1c467
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+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.JsonParseException;
+import com.google.gson.common.TestTypes.BagOfPrimitives;
+import com.google.gson.common.TestTypes.ClassOverridingEquals;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests that do not fall neatly into any of the existing classification.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+public class UncategorizedTest {
+
+  private Gson gson = null;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testInvalidJsonDeserializationFails() throws Exception {
+    try {
+      gson.fromJson("adfasdf1112,,,\":", BagOfPrimitives.class);
+      fail("Bad JSON should throw a ParseException");
+    } catch (JsonParseException expected) {
+    }
+
+    try {
+      gson.fromJson("{adfasdf1112,,,\":}", BagOfPrimitives.class);
+      fail("Bad JSON should throw a ParseException");
+    } catch (JsonParseException expected) {
+    }
+  }
+
+  @Test
+  public void testObjectEqualButNotSameSerialization() {
+    ClassOverridingEquals objA = new ClassOverridingEquals();
+    ClassOverridingEquals objB = new ClassOverridingEquals();
+    objB.ref = objA;
+    String json = gson.toJson(objB);
+    assertThat(json).isEqualTo(objB.getExpectedJson());
+  }
+
+  @Test
+  public void testStaticFieldsAreNotSerialized() {
+    BagOfPrimitives target = new BagOfPrimitives();
+    assertThat(gson.toJson(target)).doesNotContain("DEFAULT_VALUE");
+  }
+
+  @Test
+  public void testGsonInstanceReusableForSerializationAndDeserialization() {
+    BagOfPrimitives bag = new BagOfPrimitives();
+    String json = gson.toJson(bag);
+    BagOfPrimitives deserialized = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(deserialized).isEqualTo(bag);
+  }
+
+  /**
+   * This test ensures that a custom deserializer is able to return a derived class instance for a
+   * base class object. For a motivation for this test, see Issue 37 and
+   * http://groups.google.com/group/google-gson/browse_thread/thread/677d56e9976d7761
+   */
+  @Test
+  public void testReturningDerivedClassesDuringDeserialization() {
+    Gson gson = new GsonBuilder().registerTypeAdapter(Base.class, new BaseTypeAdapter()).create();
+    String json = "{\"opType\":\"OP1\"}";
+    Base base = gson.fromJson(json, Base.class);
+    assertThat(base).isInstanceOf(Derived1.class);
+    assertThat(base.opType).isEqualTo(OperationType.OP1);
+
+    json = "{\"opType\":\"OP2\"}";
+    base = gson.fromJson(json, Base.class);
+    assertThat(base).isInstanceOf(Derived2.class);
+    assertThat(base.opType).isEqualTo(OperationType.OP2);
+  }
+
+  /**
+   * Test that trailing whitespace is ignored.
+   * http://code.google.com/p/google-gson/issues/detail?id=302
+   */
+  @Test
+  public void testTrailingWhitespace() throws Exception {
+    List<Integer> integers =
+        gson.fromJson("[1,2,3]  \n\n  ", new TypeToken<List<Integer>>() {}.getType());
+    assertThat(integers).containsExactly(1, 2, 3).inOrder();
+  }
+
+  private enum OperationType {
+    OP1,
+    OP2
+  }
+
+  private static class Base {
+    OperationType opType;
+  }
+
+  private static class Derived1 extends Base {
+    Derived1() {
+      opType = OperationType.OP1;
+    }
+  }
+
+  private static class Derived2 extends Base {
+    Derived2() {
+      opType = OperationType.OP2;
+    }
+  }
+
+  private static class BaseTypeAdapter implements JsonDeserializer<Base> {
+    @Override
+    public Base deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      String opTypeStr = json.getAsJsonObject().get("opType").getAsString();
+      OperationType opType = OperationType.valueOf(opTypeStr);
+      switch (opType) {
+        case OP1:
+          return new Derived1();
+        case OP2:
+          return new Derived2();
+      }
+      throw new JsonParseException("unknown type: " + json);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/functional/VersioningTest.java b/gson/gson/src/test/java/com/google/gson/functional/VersioningTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..698cf60dddab13dfaa2952ff7d969db257f17601
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/functional/VersioningTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+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 org.junit.Test;
+
+/**
+ * Functional tests for versioning support in Gson.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+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 static Gson gsonWithVersion(double version) {
+    return new GsonBuilder().setVersion(version).create();
+  }
+
+  @Test
+  public void testVersionedUntilSerialization() {
+    Version1 target = new Version1();
+    Gson gson = gsonWithVersion(1.29);
+    String json = gson.toJson(target);
+    assertThat(json).contains("\"a\":" + A);
+
+    gson = gsonWithVersion(1.3);
+    json = gson.toJson(target);
+    assertThat(json).doesNotContain("\"a\":" + A);
+
+    gson = gsonWithVersion(1.31);
+    json = gson.toJson(target);
+    assertThat(json).doesNotContain("\"a\":" + A);
+  }
+
+  @Test
+  public void testVersionedUntilDeserialization() {
+    String json = "{\"a\":3,\"b\":4,\"c\":5}";
+
+    Gson gson = gsonWithVersion(1.29);
+    Version1 version1 = gson.fromJson(json, Version1.class);
+    assertThat(version1.a).isEqualTo(3);
+
+    gson = gsonWithVersion(1.3);
+    version1 = gson.fromJson(json, Version1.class);
+    assertThat(version1.a).isEqualTo(A);
+
+    gson = gsonWithVersion(1.31);
+    version1 = gson.fromJson(json, Version1.class);
+    assertThat(version1.a).isEqualTo(A);
+  }
+
+  @Test
+  public void testVersionedClassesSerialization() {
+    Gson gson = gsonWithVersion(1.0);
+    String json1 = gson.toJson(new Version1());
+    String json2 = gson.toJson(new Version1_1());
+    assertThat(json2).isEqualTo(json1);
+  }
+
+  @Test
+  public void testVersionedClassesDeserialization() {
+    Gson gson = gsonWithVersion(1.0);
+    String json = "{\"a\":3,\"b\":4,\"c\":5}";
+
+    Version1 version1 = gson.fromJson(json, Version1.class);
+    assertThat(version1.a).isEqualTo(3);
+    assertThat(version1.b).isEqualTo(4);
+
+    @SuppressWarnings("MemberName")
+    Version1_1 version1_1 = gson.fromJson(json, Version1_1.class);
+    assertThat(version1_1.a).isEqualTo(3);
+    assertThat(version1_1.b).isEqualTo(4);
+    assertThat(version1_1.c).isEqualTo(C);
+  }
+
+  @Test
+  public void testIgnoreLaterVersionClassSerialization() {
+    Gson gson = gsonWithVersion(1.0);
+    assertThat(gson.toJson(new Version1_2())).isEqualTo("null");
+  }
+
+  @Test
+  public void testIgnoreLaterVersionClassDeserialization() {
+    Gson gson = gsonWithVersion(1.0);
+    String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}";
+
+    @SuppressWarnings("MemberName")
+    Version1_2 version1_2 = gson.fromJson(json, Version1_2.class);
+    // Since the class is versioned to be after 1.0, we expect null
+    // This is the new behavior in Gson 2.0
+    assertThat(version1_2).isNull();
+  }
+
+  @Test
+  public void testVersionedGsonWithUnversionedClassesSerialization() {
+    Gson gson = gsonWithVersion(1.0);
+    BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue");
+    assertThat(gson.toJson(target)).isEqualTo(target.getExpectedJson());
+  }
+
+  @Test
+  public void testVersionedGsonWithUnversionedClassesDeserialization() {
+    Gson gson = gsonWithVersion(1.0);
+    String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}";
+
+    BagOfPrimitives expected = new BagOfPrimitives();
+    expected.longValue = 10;
+    expected.intValue = 20;
+    expected.booleanValue = false;
+    BagOfPrimitives actual = gson.fromJson(json, BagOfPrimitives.class);
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void testVersionedGsonMixingSinceAndUntilSerialization() {
+    Gson gson = gsonWithVersion(1.0);
+    SinceUntilMixing target = new SinceUntilMixing();
+    String json = gson.toJson(target);
+    assertThat(json).doesNotContain("\"b\":" + B);
+
+    gson = gsonWithVersion(1.2);
+    json = gson.toJson(target);
+    assertThat(json).contains("\"b\":" + B);
+
+    gson = gsonWithVersion(1.3);
+    json = gson.toJson(target);
+    assertThat(json).doesNotContain("\"b\":" + B);
+
+    gson = gsonWithVersion(1.4);
+    json = gson.toJson(target);
+    assertThat(json).doesNotContain("\"b\":" + B);
+  }
+
+  @Test
+  public void testVersionedGsonMixingSinceAndUntilDeserialization() {
+    String json = "{\"a\":5,\"b\":6}";
+    Gson gson = gsonWithVersion(1.0);
+    SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class);
+    assertThat(result.a).isEqualTo(5);
+    assertThat(result.b).isEqualTo(B);
+
+    gson = gsonWithVersion(1.2);
+    result = gson.fromJson(json, SinceUntilMixing.class);
+    assertThat(result.a).isEqualTo(5);
+    assertThat(result.b).isEqualTo(6);
+
+    gson = gsonWithVersion(1.3);
+    result = gson.fromJson(json, SinceUntilMixing.class);
+    assertThat(result.a).isEqualTo(5);
+    assertThat(result.b).isEqualTo(B);
+
+    gson = gsonWithVersion(1.4);
+    result = gson.fromJson(json, SinceUntilMixing.class);
+    assertThat(result.a).isEqualTo(5);
+    assertThat(result.b).isEqualTo(B);
+  }
+
+  private static class Version1 {
+    @Until(1.3)
+    int a = A;
+
+    @Since(1.0)
+    int b = B;
+  }
+
+  private static class Version1_1 extends Version1 {
+    @Since(1.1)
+    int c = C;
+  }
+
+  @Since(1.2)
+  private static class Version1_2 extends Version1_1 {
+    @SuppressWarnings("unused")
+    int d = D;
+  }
+
+  private static class SinceUntilMixing {
+    int a = A;
+
+    @Since(1.1)
+    @Until(1.3)
+    int b = B;
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..32253abb1753934d39c24a22fb80e79d05299789
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.reflect.TypeToken;
+import java.util.Collections;
+import org.junit.Test;
+
+public class ConstructorConstructorTest {
+  private ConstructorConstructor constructorConstructor =
+      new ConstructorConstructor(Collections.emptyMap(), true, Collections.emptyList());
+
+  private abstract static class AbstractClass {
+    @SuppressWarnings("unused")
+    public AbstractClass() {}
+  }
+
+  private interface Interface {}
+
+  /**
+   * Verify that ConstructorConstructor does not try to invoke no-args constructor of abstract
+   * class.
+   */
+  @Test
+  public void testGet_AbstractClassNoArgConstructor() {
+    ObjectConstructor<AbstractClass> constructor =
+        constructorConstructor.get(TypeToken.get(AbstractClass.class));
+    try {
+      constructor.construct();
+      fail("Expected exception");
+    } catch (RuntimeException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              "Abstract classes can't be instantiated! Adjust the R8 configuration or register an"
+                  + " InstanceCreator or a TypeAdapter for this type. Class name:"
+                  + " com.google.gson.internal.ConstructorConstructorTest$AbstractClass\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
+    }
+  }
+
+  @Test
+  public void testGet_Interface() {
+    ObjectConstructor<Interface> constructor =
+        constructorConstructor.get(TypeToken.get(Interface.class));
+    try {
+      constructor.construct();
+      fail("Expected exception");
+    } catch (RuntimeException exception) {
+      assertThat(exception)
+          .hasMessageThat()
+          .isEqualTo(
+              "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for"
+                  + " this type. Interface name:"
+                  + " com.google.gson.internal.ConstructorConstructorTest$Interface");
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/GsonBuildConfigTest.java b/gson/gson/src/test/java/com/google/gson/internal/GsonBuildConfigTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1af3a0861ce75d398faf85a81128a0f8258da60
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/GsonBuildConfigTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Gson authors
+ *
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@code GsonBuildConfig}
+ *
+ * @author Inderjeet Singh
+ */
+public class GsonBuildConfigTest {
+
+  @Test
+  public void testEnsureGsonBuildConfigGetsUpdatedToMavenVersion() {
+    assertThat("${project.version}").isNotEqualTo(GsonBuildConfig.VERSION);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java b/gson/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8534e99195a93ebfd7cc195923285dd5f2144327
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/GsonTypesTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2014 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import org.junit.Test;
+
+@SuppressWarnings("ClassNamedLikeTypeParameter") // for dummy classes A, B, ...
+public final class GsonTypesTest {
+
+  @Test
+  public void testNewParameterizedTypeWithoutOwner() throws Exception {
+    // List<A>. List is a top-level class
+    ParameterizedType type = $Gson$Types.newParameterizedTypeWithOwner(null, List.class, A.class);
+    assertThat(type.getOwnerType()).isNull();
+    assertThat(type.getRawType()).isEqualTo(List.class);
+    assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class);
+
+    // A<B>. A is a static inner class.
+    type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, B.class);
+    assertThat(getFirstTypeArgument(type)).isEqualTo(B.class);
+
+    IllegalArgumentException e =
+        assertThrows(
+            IllegalArgumentException.class,
+            // NonStaticInner<A> is not allowed without owner
+            () -> $Gson$Types.newParameterizedTypeWithOwner(null, NonStaticInner.class, A.class));
+    assertThat(e).hasMessageThat().isEqualTo("Must specify owner type for " + NonStaticInner.class);
+
+    type =
+        $Gson$Types.newParameterizedTypeWithOwner(
+            GsonTypesTest.class, NonStaticInner.class, A.class);
+    assertThat(type.getOwnerType()).isEqualTo(GsonTypesTest.class);
+    assertThat(type.getRawType()).isEqualTo(NonStaticInner.class);
+    assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class);
+
+    final class D {}
+
+    // D<A> is allowed since D has no owner type
+    type = $Gson$Types.newParameterizedTypeWithOwner(null, D.class, A.class);
+    assertThat(type.getOwnerType()).isNull();
+    assertThat(type.getRawType()).isEqualTo(D.class);
+    assertThat(type.getActualTypeArguments()).asList().containsExactly(A.class);
+
+    // A<D> is allowed.
+    type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, D.class);
+    assertThat(getFirstTypeArgument(type)).isEqualTo(D.class);
+  }
+
+  @Test
+  public void testGetFirstTypeArgument() throws Exception {
+    assertThat(getFirstTypeArgument(A.class)).isNull();
+
+    Type type = $Gson$Types.newParameterizedTypeWithOwner(null, A.class, B.class, C.class);
+    assertThat(getFirstTypeArgument(type)).isEqualTo(B.class);
+  }
+
+  private static final class A {}
+
+  private static final class B {}
+
+  private static final class C {}
+
+  @SuppressWarnings({"ClassCanBeStatic", "UnusedTypeParameter"})
+  private final class NonStaticInner<T> {}
+
+  /**
+   * Given a parameterized type {@code A<B, C>}, returns B. If the specified type is not a generic
+   * type, returns null.
+   */
+  public static Type getFirstTypeArgument(Type type) throws Exception {
+    if (!(type instanceof ParameterizedType)) {
+      return null;
+    }
+    ParameterizedType ptype = (ParameterizedType) type;
+    Type[] actualTypeArguments = ptype.getActualTypeArguments();
+    if (actualTypeArguments.length == 0) {
+      return null;
+    }
+    return $Gson$Types.canonicalize(actualTypeArguments[0]);
+  }
+
+  @Test
+  public void testEqualsOnMethodTypeVariables() throws Exception {
+    Method m1 = TypeVariableTest.class.getMethod("method");
+    Method m2 = TypeVariableTest.class.getMethod("method");
+
+    Type rt1 = m1.getGenericReturnType();
+    Type rt2 = m2.getGenericReturnType();
+
+    assertThat($Gson$Types.equals(rt1, rt2)).isTrue();
+  }
+
+  @Test
+  public void testEqualsOnConstructorParameterTypeVariables() throws Exception {
+    Constructor<TypeVariableTest> c1 = TypeVariableTest.class.getConstructor(Object.class);
+    Constructor<TypeVariableTest> c2 = TypeVariableTest.class.getConstructor(Object.class);
+
+    Type rt1 = c1.getGenericParameterTypes()[0];
+    Type rt2 = c2.getGenericParameterTypes()[0];
+
+    assertThat($Gson$Types.equals(rt1, rt2)).isTrue();
+  }
+
+  private static final class TypeVariableTest {
+
+    @SuppressWarnings({"UnusedMethod", "UnusedVariable", "TypeParameterUnusedInFormals"})
+    public <T> TypeVariableTest(T parameter) {}
+
+    @SuppressWarnings({"UnusedMethod", "UnusedVariable", "TypeParameterUnusedInFormals"})
+    public <T> T method() {
+      return null;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java b/gson/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..28369ffe5419c166784fc106386e76f3029dc47e
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/JavaVersionTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 The Gson authors
+ *
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit and functional tests for {@link JavaVersion}
+ *
+ * @author Inderjeet Singh
+ */
+public class JavaVersionTest {
+  // Borrowed some of test strings from
+  // https://github.com/prestodb/presto/blob/master/presto-main/src/test/java/com/facebook/presto/server/TestJavaVersion.java
+
+  @Test
+  public void testGetMajorJavaVersion() {
+    // Gson currently requires at least Java 7
+    assertThat(JavaVersion.getMajorJavaVersion()).isAtLeast(7);
+  }
+
+  @Test
+  public void testJava6() {
+    // http://www.oracle.com/technetwork/java/javase/version-6-141920.html
+    assertThat(JavaVersion.parseMajorJavaVersion("1.6.0")).isEqualTo(6);
+  }
+
+  @Test
+  public void testJava7() {
+    // http://www.oracle.com/technetwork/java/javase/jdk7-naming-418744.html
+    assertThat(JavaVersion.parseMajorJavaVersion("1.7.0")).isEqualTo(7);
+  }
+
+  @Test
+  public void testJava8() {
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8")).isEqualTo(8);
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0")).isEqualTo(8);
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0_131")).isEqualTo(8);
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0_60-ea")).isEqualTo(8);
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0_111-internal")).isEqualTo(8);
+
+    // openjdk8 per https://github.com/AdoptOpenJDK/openjdk-build/issues/93
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0-internal")).isEqualTo(8);
+    assertThat(JavaVersion.parseMajorJavaVersion("1.8.0_131-adoptopenjdk")).isEqualTo(8);
+  }
+
+  @Test
+  public void testJava9() {
+    // Legacy style
+    assertThat(JavaVersion.parseMajorJavaVersion("9.0.4")).isEqualTo(9); // Oracle JDK 9
+    // Debian as reported in https://github.com/google/gson/issues/1310
+    assertThat(JavaVersion.parseMajorJavaVersion("9-Debian")).isEqualTo(9);
+
+    // New style
+    assertThat(JavaVersion.parseMajorJavaVersion("9-ea+19")).isEqualTo(9);
+    assertThat(JavaVersion.parseMajorJavaVersion("9+100")).isEqualTo(9);
+    assertThat(JavaVersion.parseMajorJavaVersion("9.0.1+20")).isEqualTo(9);
+    assertThat(JavaVersion.parseMajorJavaVersion("9.1.1+20")).isEqualTo(9);
+  }
+
+  @Test
+  public void testJava10() {
+    assertThat(JavaVersion.parseMajorJavaVersion("10.0.1")).isEqualTo(10); // Oracle JDK 10.0.1
+  }
+
+  @Test
+  public void testUnknownVersionFormat() {
+    assertThat(JavaVersion.parseMajorJavaVersion("Java9")).isEqualTo(6); // unknown format
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java b/gson/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5dce3790472982a5a2ac6b6c1c57b3ed48c31397
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/LazilyParsedNumberTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.math.BigDecimal;
+import org.junit.Test;
+
+public class LazilyParsedNumberTest {
+  @Test
+  public void testHashCode() {
+    LazilyParsedNumber n1 = new LazilyParsedNumber("1");
+    LazilyParsedNumber n1Another = new LazilyParsedNumber("1");
+    assertThat(n1Another.hashCode()).isEqualTo(n1.hashCode());
+  }
+
+  @Test
+  public void testEquals() {
+    LazilyParsedNumber n1 = new LazilyParsedNumber("1");
+    LazilyParsedNumber n1Another = new LazilyParsedNumber("1");
+    assertThat(n1.equals(n1Another)).isTrue();
+  }
+
+  @Test
+  public void testJavaSerialization() throws IOException, ClassNotFoundException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    ObjectOutputStream objOut = new ObjectOutputStream(out);
+    objOut.writeObject(new LazilyParsedNumber("123"));
+    objOut.close();
+
+    ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
+    Number deserialized = (Number) objIn.readObject();
+    assertThat(deserialized).isEqualTo(new BigDecimal("123"));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd31817c8313ee393bf18f0b56c47c4b9abcae06
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2012 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.common.MoreAsserts;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import org.junit.Test;
+
+public final class LinkedTreeMapTest {
+
+  @Test
+  public void testIterationOrder() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "android");
+    map.put("c", "cola");
+    map.put("b", "bbq");
+    assertIterationOrder(map.keySet(), "a", "c", "b");
+    assertIterationOrder(map.values(), "android", "cola", "bbq");
+  }
+
+  @Test
+  public void testRemoveRootDoesNotDoubleUnlink() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "android");
+    map.put("c", "cola");
+    map.put("b", "bbq");
+    Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
+    it.next();
+    it.next();
+    it.next();
+    it.remove();
+    assertIterationOrder(map.keySet(), "a", "c");
+  }
+
+  @Test
+  @SuppressWarnings("ModifiedButNotUsed")
+  public void testPutNullKeyFails() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    try {
+      map.put(null, "android");
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  @SuppressWarnings("ModifiedButNotUsed")
+  public void testPutNonComparableKeyFails() {
+    LinkedTreeMap<Object, String> map = new LinkedTreeMap<>();
+    try {
+      map.put(new Object(), "android");
+      fail();
+    } catch (ClassCastException expected) {
+    }
+  }
+
+  @Test
+  public void testPutNullValue() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", null);
+
+    assertThat(map).hasSize(1);
+    assertThat(map.containsKey("a")).isTrue();
+    assertThat(map.containsValue(null)).isTrue();
+    assertThat(map.get("a")).isNull();
+  }
+
+  @Test
+  public void testPutNullValue_Forbidden() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>(false);
+    try {
+      map.put("a", null);
+      fail();
+    } catch (NullPointerException e) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+    assertThat(map).hasSize(0);
+    assertThat(map).doesNotContainKey("a");
+    assertThat(map.containsValue(null)).isFalse();
+  }
+
+  @Test
+  public void testEntrySetValueNull() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "1");
+    assertThat(map.get("a")).isEqualTo("1");
+    Entry<String, String> entry = map.entrySet().iterator().next();
+    assertThat(entry.getKey()).isEqualTo("a");
+    assertThat(entry.getValue()).isEqualTo("1");
+    entry.setValue(null);
+    assertThat(entry.getValue()).isNull();
+
+    assertThat(map.containsKey("a")).isTrue();
+    assertThat(map.containsValue(null)).isTrue();
+    assertThat(map.get("a")).isNull();
+  }
+
+  @Test
+  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) {
+      assertThat(e).hasMessageThat().isEqualTo("value == null");
+    }
+    assertThat(entry.getValue()).isEqualTo("1");
+    assertThat(map.get("a")).isEqualTo("1");
+    assertThat(map.containsValue(null)).isFalse();
+  }
+
+  @Test
+  public void testContainsNonComparableKeyReturnsFalse() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "android");
+    assertThat(map).doesNotContainKey(new Object());
+  }
+
+  @Test
+  public void testContainsNullKeyIsAlwaysFalse() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    assertThat(map.containsKey(null)).isFalse();
+    map.put("a", "android");
+    assertThat(map.containsKey(null)).isFalse();
+  }
+
+  @Test
+  public void testPutOverrides() throws Exception {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    assertThat(map.put("d", "donut")).isNull();
+    assertThat(map.put("e", "eclair")).isNull();
+    assertThat(map.put("f", "froyo")).isNull();
+    assertThat(map).hasSize(3);
+
+    assertThat(map.get("d")).isEqualTo("donut");
+    assertThat(map.put("d", "done")).isEqualTo("donut");
+    assertThat(map).hasSize(3);
+  }
+
+  @Test
+  public void testEmptyStringValues() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "");
+    assertThat(map.containsKey("a")).isTrue();
+    assertThat(map.get("a")).isEqualTo("");
+  }
+
+  @Test
+  public void testLargeSetOfRandomKeys() {
+    Random random = new Random(1367593214724L);
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    String[] keys = new String[1000];
+    for (int i = 0; i < keys.length; i++) {
+      keys[i] = Integer.toString(random.nextInt(), 36) + "-" + i;
+      map.put(keys[i], "" + i);
+    }
+
+    for (int i = 0; i < keys.length; i++) {
+      String key = keys[i];
+      assertThat(map.containsKey(key)).isTrue();
+      assertThat(map.get(key)).isEqualTo("" + i);
+    }
+  }
+
+  @Test
+  public void testClear() {
+    LinkedTreeMap<String, String> map = new LinkedTreeMap<>();
+    map.put("a", "android");
+    map.put("c", "cola");
+    map.put("b", "bbq");
+    map.clear();
+    assertIterationOrder(map.keySet());
+    assertThat(map).hasSize(0);
+  }
+
+  @Test
+  public void testEqualsAndHashCode() throws Exception {
+    LinkedTreeMap<String, Integer> map1 = new LinkedTreeMap<>();
+    map1.put("A", 1);
+    map1.put("B", 2);
+    map1.put("C", 3);
+    map1.put("D", 4);
+
+    LinkedTreeMap<String, Integer> map2 = new LinkedTreeMap<>();
+    map2.put("C", 3);
+    map2.put("B", 2);
+    map2.put("D", 4);
+    map2.put("A", 1);
+
+    MoreAsserts.assertEqualsAndHashCode(map1, map2);
+  }
+
+  @Test
+  public void testJavaSerialization() throws IOException, ClassNotFoundException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    ObjectOutputStream objOut = new ObjectOutputStream(out);
+    Map<String, Integer> map = new LinkedTreeMap<>();
+    map.put("a", 1);
+    objOut.writeObject(map);
+    objOut.close();
+
+    ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
+    @SuppressWarnings("unchecked")
+    Map<String, Integer> deserialized = (Map<String, Integer>) objIn.readObject();
+    assertThat(deserialized).isEqualTo(Collections.singletonMap("a", 1));
+  }
+
+  @SuppressWarnings("varargs")
+  @SafeVarargs
+  private static final <T> void assertIterationOrder(Iterable<T> actual, T... expected) {
+    ArrayList<T> actualList = new ArrayList<>();
+    for (T t : actual) {
+      actualList.add(t);
+    }
+    assertThat(actualList).isEqualTo(Arrays.asList(expected));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/StreamsTest.java b/gson/gson/src/test/java/com/google/gson/internal/StreamsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0aeb4fdfd94289479ef4f8006bf5f0dc5da8e96b
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/StreamsTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+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();
+    assertThat(actualOutput).isEqualTo("a\u1234testnullcdul,a\u1234\u1234,charsha,stringtr");
+
+    writer.flush();
+    writer.close();
+
+    // flush() and close() calls should have had no effect
+    assertThat(stringBuilder.toString()).isEqualTo(actualOutput);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java b/gson/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2a1c61f72cb4fde046d39ebf1e398a792d139229
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+/**
+ * Test unsafe allocator instantiation
+ *
+ * @author Ugljesa Jovanovic
+ */
+public final class UnsafeAllocatorInstantiationTest {
+
+  public interface Interface {}
+
+  public abstract static class AbstractClass {}
+
+  public static class ConcreteClass {}
+
+  /** Ensure that an {@link AssertionError} is thrown when trying to instantiate an interface */
+  @Test
+  public void testInterfaceInstantiation() {
+    AssertionError e =
+        assertThrows(
+            AssertionError.class, () -> UnsafeAllocator.INSTANCE.newInstance(Interface.class));
+
+    assertThat(e).hasMessageThat().startsWith("UnsafeAllocator is used for non-instantiable type");
+  }
+
+  /**
+   * Ensure that an {@link AssertionError} is thrown when trying to instantiate an abstract class
+   */
+  @Test
+  public void testAbstractClassInstantiation() {
+    AssertionError e =
+        assertThrows(
+            AssertionError.class, () -> UnsafeAllocator.INSTANCE.newInstance(AbstractClass.class));
+
+    assertThat(e).hasMessageThat().startsWith("UnsafeAllocator is used for non-instantiable type");
+  }
+
+  /** Ensure that no exception is thrown when trying to instantiate a concrete class */
+  @Test
+  public void testConcreteClassInstantiation() throws Exception {
+    ConcreteClass instance = UnsafeAllocator.INSTANCE.newInstance(ConcreteClass.class);
+    assertThat(instance).isNotNull();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fc13a39e90b7c165f1f47d4c87a6da4958d5b554
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.internal.bind.DefaultDateTypeAdapter.DateType;
+import com.google.gson.reflect.TypeToken;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.Test;
+
+/**
+ * A simple unit test for the {@link DefaultDateTypeAdapter} class.
+ *
+ * @author Joel Leitch
+ */
+@SuppressWarnings("JavaUtilDate")
+public class DefaultDateTypeAdapterTest {
+
+  @Test
+  public void testFormattingInEnUs() {
+    assertFormattingAlwaysEmitsUsLocale(Locale.US);
+  }
+
+  @Test
+  public void testFormattingInFr() {
+    assertFormattingAlwaysEmitsUsLocale(Locale.FRANCE);
+  }
+
+  private static void assertFormattingAlwaysEmitsUsLocale(Locale locale) {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(locale);
+    try {
+      // The patterns here attempt to accommodate minor date-time formatting differences between JDK
+      // versions. Ideally Gson would serialize in a way that is independent of the JDK version.
+      // Note: \h means "horizontal space", because some JDK versions use Narrow No Break Space
+      // (U+202F) before the AM or PM indication.
+      String utcFull = "(Coordinated Universal Time|UTC)";
+      assertFormatted("Jan 1, 1970,? 12:00:00\\hAM", DateType.DATE.createDefaultsAdapterFactory());
+      assertFormatted("1/1/70", DateType.DATE.createAdapterFactory(DateFormat.SHORT));
+      assertFormatted("Jan 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM));
+      assertFormatted("January 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.LONG));
+      assertFormatted(
+          "1/1/70,? 12:00\\hAM",
+          DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT));
+      assertFormatted(
+          "Jan 1, 1970,? 12:00:00\\hAM",
+          DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM));
+      assertFormatted(
+          "January 1, 1970(,| at)? 12:00:00\\hAM UTC",
+          DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG));
+      assertFormatted(
+          "Thursday, January 1, 1970(,| at)? 12:00:00\\hAM " + utcFull,
+          DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL));
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testParsingDatesFormattedWithSystemLocale() throws Exception {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.FRANCE);
+    try {
+      Date date = new Date(0);
+      assertParsed(
+          DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(date),
+          DateType.DATE.createDefaultsAdapterFactory());
+      assertParsed(
+          DateFormat.getDateInstance(DateFormat.SHORT).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.SHORT));
+      assertParsed(
+          DateFormat.getDateInstance(DateFormat.MEDIUM).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.MEDIUM));
+      assertParsed(
+          DateFormat.getDateInstance(DateFormat.LONG).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.LONG));
+      assertParsed(
+          DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT));
+      assertParsed(
+          DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM));
+      assertParsed(
+          DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG));
+      assertParsed(
+          DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(date),
+          DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL));
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testParsingDatesFormattedWithUsLocale() throws Exception {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    try {
+      assertParsed("Jan 1, 1970 0:00:00 AM", DateType.DATE.createDefaultsAdapterFactory());
+      assertParsed("1/1/70", DateType.DATE.createAdapterFactory(DateFormat.SHORT));
+      assertParsed("Jan 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM));
+      assertParsed("January 1, 1970", DateType.DATE.createAdapterFactory(DateFormat.LONG));
+      assertParsed(
+          "1/1/70 0:00 AM", DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT));
+      assertParsed(
+          "Jan 1, 1970 0:00:00 AM",
+          DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM));
+      assertParsed(
+          "January 1, 1970 0:00:00 AM UTC",
+          DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG));
+      assertParsed(
+          "Thursday, January 1, 1970 0:00:00 AM UTC",
+          DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL));
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testFormatUsesDefaultTimezone() throws Exception {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    try {
+      assertFormatted("Dec 31, 1969,? 4:00:00\\hPM", DateType.DATE.createDefaultsAdapterFactory());
+      assertParsed("Dec 31, 1969 4:00:00 PM", DateType.DATE.createDefaultsAdapterFactory());
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testDateDeserializationISO8601() throws Exception {
+    TypeAdapterFactory adapterFactory = DateType.DATE.createDefaultsAdapterFactory();
+    assertParsed("1970-01-01T00:00:00.000Z", adapterFactory);
+    assertParsed("1970-01-01T00:00Z", adapterFactory);
+    assertParsed("1970-01-01T00:00:00+00:00", adapterFactory);
+    assertParsed("1970-01-01T01:00:00+01:00", adapterFactory);
+    assertParsed("1970-01-01T01:00:00+01", adapterFactory);
+  }
+
+  @Test
+  public void testDateSerialization() {
+    int dateStyle = DateFormat.LONG;
+    TypeAdapter<Date> dateTypeAdapter = dateAdapter(DateType.DATE.createAdapterFactory(dateStyle));
+    DateFormat formatter = DateFormat.getDateInstance(dateStyle, Locale.US);
+    Date currentDate = new Date();
+
+    String dateString = dateTypeAdapter.toJson(currentDate);
+    assertThat(dateString).isEqualTo(toLiteral(formatter.format(currentDate)));
+  }
+
+  @Test
+  public void testDatePattern() {
+    String pattern = "yyyy-MM-dd";
+    TypeAdapter<Date> dateTypeAdapter = dateAdapter(DateType.DATE.createAdapterFactory(pattern));
+    DateFormat formatter = new SimpleDateFormat(pattern);
+    Date currentDate = new Date();
+
+    String dateString = dateTypeAdapter.toJson(currentDate);
+    assertThat(dateString).isEqualTo(toLiteral(formatter.format(currentDate)));
+  }
+
+  @Test
+  public void testInvalidDatePattern() {
+    try {
+      DateType.DATE.createAdapterFactory("I am a bad Date pattern....");
+      fail("Invalid date pattern should fail.");
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testNullValue() throws Exception {
+    TypeAdapter<Date> adapter = dateAdapter(DateType.DATE.createDefaultsAdapterFactory());
+    assertThat(adapter.fromJson("null")).isNull();
+    assertThat(adapter.toJson(null)).isEqualTo("null");
+  }
+
+  @Test
+  public void testUnexpectedToken() throws Exception {
+    try {
+      TypeAdapter<Date> adapter = dateAdapter(DateType.DATE.createDefaultsAdapterFactory());
+      adapter.fromJson("{}");
+      fail("Unexpected token should fail.");
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testGsonDateFormat() {
+    TimeZone originalTimeZone = TimeZone.getDefault();
+    // Set the default timezone to UTC
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    try {
+      Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm z").create();
+      Date originalDate = new Date(0);
+
+      // Serialize the date object
+      String json = gson.toJson(originalDate);
+      assertThat(json).isEqualTo("\"1970-01-01 00:00 UTC\"");
+
+      // Deserialize a date string with the PST timezone
+      Date deserializedDate = gson.fromJson("\"1970-01-01 00:00 PST\"", Date.class);
+      // Assert that the deserialized date's time is correct
+      assertThat(deserializedDate.getTime()).isEqualTo(new Date(28800000).getTime());
+
+      // Serialize the deserialized date object again
+      String jsonAfterDeserialization = gson.toJson(deserializedDate);
+      // The expectation is that the date, after deserialization, when serialized again should still
+      // be in the UTC timezone
+      assertThat(jsonAfterDeserialization).isEqualTo("\"1970-01-01 08:00 UTC\"");
+    } finally {
+      TimeZone.setDefault(originalTimeZone);
+    }
+  }
+
+  private static TypeAdapter<Date> dateAdapter(TypeAdapterFactory adapterFactory) {
+    TypeAdapter<Date> adapter = adapterFactory.create(new Gson(), TypeToken.get(Date.class));
+    assertThat(adapter).isNotNull();
+    return adapter;
+  }
+
+  private static void assertFormatted(String formattedPattern, TypeAdapterFactory adapterFactory) {
+    TypeAdapter<Date> adapter = dateAdapter(adapterFactory);
+    String json = adapter.toJson(new Date(0));
+    assertThat(json).matches(toLiteral(formattedPattern));
+  }
+
+  @SuppressWarnings("UndefinedEquals")
+  private static void assertParsed(String date, TypeAdapterFactory adapterFactory)
+      throws IOException {
+    TypeAdapter<Date> adapter = dateAdapter(adapterFactory);
+    assertWithMessage(date).that(adapter.fromJson(toLiteral(date))).isEqualTo(new Date(0));
+    assertWithMessage("ISO 8601")
+        .that(adapter.fromJson(toLiteral("1970-01-01T00:00:00Z")))
+        .isEqualTo(new Date(0));
+  }
+
+  private static String toLiteral(String s) {
+    return '"' + s + '"';
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6ebda56331ec40f0f2d7ef9e0f71b3cabf9f234
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapter;
+import com.google.gson.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);
+    assertThat(defaultReflectionAdapter.getClass()).isNotEqualTo(recordAdapter.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);
+
+    assertThat(deserializedRecordInstance).isEqualTo(recordInstance);
+    assertThat(serialized).isEqualTo("{\"user\":\"user\",\"group\":\"group\"}");
+  }
+
+  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/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..39d76e669a2e8a9067b915c6f83415e85f2498e6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.Strictness;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
+import java.io.IOException;
+import org.junit.Test;
+
+@SuppressWarnings("resource")
+public final class JsonElementReaderTest {
+
+  @Test
+  public void testNumbers() throws IOException {
+    JsonElement element = JsonParser.parseString("[1, 2, 3]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.nextLong()).isEqualTo(2L);
+    assertThat(reader.nextDouble()).isEqualTo(3.0);
+    reader.endArray();
+  }
+
+  @Test
+  public void testLenientNansAndInfinities() throws IOException {
+    JsonElement element = JsonParser.parseString("[NaN, -Infinity, Infinity]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextDouble()).isNaN();
+    assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY);
+    assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY);
+    reader.endArray();
+  }
+
+  @Test
+  public void testStrictNansAndInfinities() throws IOException {
+    JsonElement element = JsonParser.parseString("[NaN, -Infinity, Infinity]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.setStrictness(Strictness.LEGACY_STRICT);
+    reader.beginArray();
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException e) {
+      assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: NaN");
+    }
+    assertThat(reader.nextString()).isEqualTo("NaN");
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException e) {
+      assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: -Infinity");
+    }
+    assertThat(reader.nextString()).isEqualTo("-Infinity");
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException e) {
+      assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity");
+    }
+    assertThat(reader.nextString()).isEqualTo("Infinity");
+    reader.endArray();
+  }
+
+  @Test
+  public void testNumbersFromStrings() throws IOException {
+    JsonElement element = JsonParser.parseString("[\"1\", \"2\", \"3\"]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.nextLong()).isEqualTo(2L);
+    assertThat(reader.nextDouble()).isEqualTo(3.0);
+    reader.endArray();
+  }
+
+  @Test
+  public void testStringsFromNumbers() throws IOException {
+    JsonElement element = JsonParser.parseString("[1]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("1");
+    reader.endArray();
+  }
+
+  @Test
+  public void testBooleans() throws IOException {
+    JsonElement element = JsonParser.parseString("[true, false]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isEqualTo(true);
+    assertThat(reader.nextBoolean()).isEqualTo(false);
+    reader.endArray();
+  }
+
+  @Test
+  public void testNulls() throws IOException {
+    JsonElement element = JsonParser.parseString("[null,null]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    reader.nextNull();
+    reader.nextNull();
+    reader.endArray();
+  }
+
+  @Test
+  public void testStrings() throws IOException {
+    JsonElement element = JsonParser.parseString("[\"A\",\"B\"]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("A");
+    assertThat(reader.nextString()).isEqualTo("B");
+    reader.endArray();
+  }
+
+  @Test
+  public void testArray() throws IOException {
+    JsonElement element = JsonParser.parseString("[1, 2, 3]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    assertThat(reader.peek()).isEqualTo(JsonToken.BEGIN_ARRAY);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextInt()).isEqualTo(2);
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextInt()).isEqualTo(3);
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_ARRAY);
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testObject() throws IOException {
+    JsonElement element = JsonParser.parseString("{\"A\": 1, \"B\": 2}");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    assertThat(reader.peek()).isEqualTo(JsonToken.BEGIN_OBJECT);
+    reader.beginObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.NAME);
+    assertThat(reader.nextName()).isEqualTo("A");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.peek()).isEqualTo(JsonToken.NAME);
+    assertThat(reader.nextName()).isEqualTo("B");
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.nextInt()).isEqualTo(2);
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_OBJECT);
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testEmptyArray() throws IOException {
+    JsonElement element = JsonParser.parseString("[]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    reader.endArray();
+  }
+
+  @Test
+  public void testNestedArrays() throws IOException {
+    JsonElement element = JsonParser.parseString("[[],[[]]]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    reader.beginArray();
+    reader.endArray();
+    reader.beginArray();
+    reader.beginArray();
+    reader.endArray();
+    reader.endArray();
+    reader.endArray();
+  }
+
+  @Test
+  public void testNestedObjects() throws IOException {
+    JsonElement element = JsonParser.parseString("{\"A\":{},\"B\":{\"C\":{}}}");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("A");
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.nextName()).isEqualTo("B");
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("C");
+    reader.beginObject();
+    reader.endObject();
+    reader.endObject();
+    reader.endObject();
+  }
+
+  @Test
+  public void testEmptyObject() throws IOException {
+    JsonElement element = JsonParser.parseString("{}");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginObject();
+    reader.endObject();
+  }
+
+  @Test
+  public void testSkipValue() throws IOException {
+    JsonElement element = JsonParser.parseString("[\"A\",{\"B\":[[]]},\"C\",[[]],\"D\",null]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("A");
+    reader.skipValue();
+    assertThat(reader.nextString()).isEqualTo("C");
+    reader.skipValue();
+    assertThat(reader.nextString()).isEqualTo("D");
+    reader.skipValue();
+    reader.endArray();
+  }
+
+  @Test
+  public void testWrongType() throws IOException {
+    JsonElement element = JsonParser.parseString("[[],\"A\"]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextNull();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextString();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextInt();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextLong();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextName();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.beginObject();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.endArray();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.endObject();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    reader.beginArray();
+    reader.endArray();
+
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextNull();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      reader.nextInt();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    try {
+      reader.nextName();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    assertThat(reader.nextString()).isEqualTo("A");
+    reader.endArray();
+  }
+
+  @Test
+  public void testNextJsonElement() throws IOException {
+    final JsonElement element = JsonParser.parseString("{\"A\": 1, \"B\" : {}, \"C\" : []}");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginObject();
+    try {
+      reader.nextJsonElement();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    String unused1 = reader.nextName();
+    assertThat(new JsonPrimitive(1)).isEqualTo(reader.nextJsonElement());
+    String unused2 = reader.nextName();
+    reader.beginObject();
+    try {
+      reader.nextJsonElement();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    reader.endObject();
+    String unused3 = reader.nextName();
+    reader.beginArray();
+    try {
+      reader.nextJsonElement();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    reader.endArray();
+    reader.endObject();
+    try {
+      reader.nextJsonElement();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testEarlyClose() throws IOException {
+    JsonElement element = JsonParser.parseString("[1, 2, 3]");
+    JsonTreeReader reader = new JsonTreeReader(element);
+    reader.beginArray();
+    reader.close();
+    try {
+      reader.peek();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..23ae773f65ab437f58d62aabcfd86850e31a13a2
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+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 org.junit.Test;
+
+@SuppressWarnings("resource")
+public class JsonTreeReaderTest {
+  @Test
+  public void testSkipValue_emptyJsonObject() throws IOException {
+    JsonTreeReader in = new JsonTreeReader(new JsonObject());
+    in.skipValue();
+    assertThat(in.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(in.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValue_filledJsonObject() throws IOException {
+    JsonObject jsonObject = new JsonObject();
+    JsonArray jsonArray = new JsonArray();
+    jsonArray.add('c');
+    jsonArray.add("text");
+    jsonObject.add("a", jsonArray);
+    jsonObject.addProperty("b", true);
+    jsonObject.addProperty("i", 1);
+    jsonObject.add("n", JsonNull.INSTANCE);
+    JsonObject jsonObject2 = new JsonObject();
+    jsonObject2.addProperty("n", 2L);
+    jsonObject.add("o", jsonObject2);
+    jsonObject.addProperty("s", "text");
+    JsonTreeReader in = new JsonTreeReader(jsonObject);
+    in.skipValue();
+    assertThat(in.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(in.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValue_name() throws IOException {
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("a", "value");
+    JsonTreeReader in = new JsonTreeReader(jsonObject);
+    in.beginObject();
+    in.skipValue();
+    assertThat(in.peek()).isEqualTo(JsonToken.STRING);
+    assertThat(in.getPath()).isEqualTo("$.<skipped>");
+    assertThat(in.nextString()).isEqualTo("value");
+  }
+
+  @Test
+  public void testSkipValue_afterEndOfDocument() throws IOException {
+    JsonTreeReader reader = new JsonTreeReader(new JsonObject());
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValue_atArrayEnd() throws IOException {
+    JsonTreeReader reader = new JsonTreeReader(new JsonArray());
+    reader.beginArray();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValue_atObjectEnd() throws IOException {
+    JsonTreeReader reader = new JsonTreeReader(new JsonObject());
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testHasNext_endOfDocument() throws IOException {
+    JsonTreeReader reader = new JsonTreeReader(new JsonObject());
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.hasNext()).isFalse();
+  }
+
+  @Test
+  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) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Custom JsonElement subclass "
+                  + CustomSubclass.class.getName()
+                  + " is not supported");
+    }
+  }
+
+  /**
+   * {@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.
+   */
+  @Test
+  public void testOverrides() {
+    List<String> ignoredMethods =
+        Arrays.asList(
+            "setLenient(boolean)",
+            "isLenient()",
+            "setStrictness(com.google.gson.Strictness)",
+            "getStrictness()");
+    MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a212bce674ba3a2e8144e650b3392a7dd083c79e
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2011 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.Strictness;
+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 org.junit.Test;
+
+@SuppressWarnings("resource")
+public final class JsonTreeWriterTest {
+  @Test
+  public void testArray() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.beginArray();
+    writer.value(1);
+    writer.value(2);
+    writer.value(3);
+    writer.endArray();
+    assertThat(writer.get().toString()).isEqualTo("[1,2,3]");
+  }
+
+  @Test
+  public void testNestedArray() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.beginArray();
+    writer.beginArray();
+    writer.endArray();
+    writer.beginArray();
+    writer.beginArray();
+    writer.endArray();
+    writer.endArray();
+    writer.endArray();
+    assertThat(writer.get().toString()).isEqualTo("[[],[[]]]");
+  }
+
+  @Test
+  public void testObject() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.beginObject();
+    writer.name("A").value(1);
+    writer.name("B").value(2);
+    writer.endObject();
+    assertThat(writer.get().toString()).isEqualTo("{\"A\":1,\"B\":2}");
+  }
+
+  @Test
+  public void testNestedObject() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.beginObject();
+    writer.name("A");
+    writer.beginObject();
+    writer.name("B");
+    writer.beginObject();
+    writer.endObject();
+    writer.endObject();
+    writer.name("C");
+    writer.beginObject();
+    writer.endObject();
+    writer.endObject();
+    assertThat(writer.get().toString()).isEqualTo("{\"A\":{\"B\":{}},\"C\":{}}");
+  }
+
+  @Test
+  public void testWriteAfterClose() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setStrictness(Strictness.LENIENT);
+    writer.beginArray();
+    writer.value("A");
+    writer.endArray();
+    writer.close();
+    try {
+      writer.beginArray();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testPrematureClose() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setStrictness(Strictness.LENIENT);
+    writer.beginArray();
+    try {
+      writer.close();
+      fail();
+    } catch (IOException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Incomplete document");
+    }
+  }
+
+  @Test
+  public void testNameAsTopLevelValue() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    IllegalStateException e = assertThrows(IllegalStateException.class, () -> writer.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Did not expect a name");
+
+    writer.value(12);
+    writer.close();
+
+    e = assertThrows(IllegalStateException.class, () -> writer.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+  }
+
+  @Test
+  public void testNameInArray() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+
+    writer.beginArray();
+    IllegalStateException e = assertThrows(IllegalStateException.class, () -> writer.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+
+    writer.value(12);
+    e = assertThrows(IllegalStateException.class, () -> writer.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+
+    writer.endArray();
+
+    assertThat(writer.get().toString()).isEqualTo("[12]");
+  }
+
+  @Test
+  public void testTwoNames() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.beginObject();
+    writer.name("a");
+    IllegalStateException e = assertThrows(IllegalStateException.class, () -> writer.name("a"));
+    assertThat(e).hasMessageThat().isEqualTo("Did not expect a name");
+  }
+
+  @Test
+  public void testSerializeNullsFalse() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setSerializeNulls(false);
+    writer.beginObject();
+    writer.name("A");
+    writer.nullValue();
+    writer.endObject();
+    assertThat(writer.get().toString()).isEqualTo("{}");
+  }
+
+  @Test
+  public void testSerializeNullsTrue() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setSerializeNulls(true);
+    writer.beginObject();
+    writer.name("A");
+    writer.nullValue();
+    writer.endObject();
+    assertThat(writer.get().toString()).isEqualTo("{\"A\":null}");
+  }
+
+  @Test
+  public void testEmptyWriter() {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    assertThat(writer.get()).isEqualTo(JsonNull.INSTANCE);
+  }
+
+  @Test
+  public void testBeginArray() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    assertThat(writer.beginArray()).isEqualTo(writer);
+  }
+
+  @Test
+  public void testBeginObject() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    assertThat(writer.beginObject()).isEqualTo(writer);
+  }
+
+  @Test
+  public void testValueString() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    String n = "as";
+    assertThat(writer.value(n)).isEqualTo(writer);
+  }
+
+  @Test
+  public void testBoolValue() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    boolean bool = true;
+    assertThat(writer.value(bool)).isEqualTo(writer);
+  }
+
+  @Test
+  public void testBoolMaisValue() throws Exception {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    Boolean bool = true;
+    assertThat(writer.value(bool)).isEqualTo(writer);
+  }
+
+  @Test
+  public void testLenientNansAndInfinities() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setStrictness(Strictness.LENIENT);
+    writer.beginArray();
+    writer.value(Float.NaN);
+    writer.value(Float.NEGATIVE_INFINITY);
+    writer.value(Float.POSITIVE_INFINITY);
+    writer.value(Double.NaN);
+    writer.value(Double.NEGATIVE_INFINITY);
+    writer.value(Double.POSITIVE_INFINITY);
+    writer.endArray();
+    assertThat(writer.get().toString())
+        .isEqualTo("[NaN,-Infinity,Infinity,NaN,-Infinity,Infinity]");
+  }
+
+  @Test
+  public void testStrictNansAndInfinities() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setStrictness(Strictness.LEGACY_STRICT);
+    writer.beginArray();
+    try {
+      writer.value(Float.NaN);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Float.NEGATIVE_INFINITY);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Float.POSITIVE_INFINITY);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.NaN);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.NEGATIVE_INFINITY);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.POSITIVE_INFINITY);
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void testStrictBoxedNansAndInfinities() throws IOException {
+    JsonTreeWriter writer = new JsonTreeWriter();
+    writer.setStrictness(Strictness.LEGACY_STRICT);
+    writer.beginArray();
+    try {
+      writer.value(Float.valueOf(Float.NaN));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Float.valueOf(Float.NEGATIVE_INFINITY));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Float.valueOf(Float.POSITIVE_INFINITY));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.valueOf(Double.NaN));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.valueOf(Double.NEGATIVE_INFINITY));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+    try {
+      writer.value(Double.valueOf(Double.POSITIVE_INFINITY));
+      fail();
+    } catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  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.
+   */
+  @Test
+  public void testOverrides() {
+    List<String> ignoredMethods =
+        Arrays.asList(
+            "setLenient(boolean)",
+            "isLenient()",
+            "setStrictness(com.google.gson.Strictness)",
+            "getStrictness()",
+            "setIndent(java.lang.String)",
+            "setHtmlSafe(boolean)",
+            "isHtmlSafe()",
+            "setFormattingStyle(com.google.gson.FormattingStyle)",
+            "getFormattingStyle()",
+            "setSerializeNulls(boolean)",
+            "getSerializeNulls()");
+    MoreAsserts.assertOverridesMethods(JsonWriter.class, JsonTreeWriter.class, ignoredMethods);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/RecursiveTypesResolveTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/RecursiveTypesResolveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..78d253dfb07bb562315cc6ef870bd591eadac64f
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/RecursiveTypesResolveTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 Gson Authors
+ *
+ * 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.internal.bind;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.$Gson$Types;
+import org.junit.Test;
+
+/**
+ * Test fixes for infinite recursion on {@link $Gson$Types#resolve(java.lang.reflect.Type, Class,
+ * java.lang.reflect.Type)}, described at <a href="https://github.com/google/gson/issues/440">Issue
+ * #440</a> and similar issues.
+ *
+ * <p>These tests originally caused {@link StackOverflowError} because of infinite recursion on
+ * attempts to resolve generics on types, with intermediate types like {@code Foo2<? extends ? super
+ * ? extends ... ? extends A>}
+ */
+public class RecursiveTypesResolveTest {
+
+  @SuppressWarnings("unused")
+  private static class Foo1<A> {
+    public Foo2<? extends A> foo2;
+  }
+
+  @SuppressWarnings("unused")
+  private static class Foo2<B> {
+    public Foo1<? super B> foo1;
+  }
+
+  /** Test simplest case of recursion. */
+  @Test
+  public void testRecursiveResolveSimple() {
+    @SuppressWarnings("rawtypes")
+    TypeAdapter<Foo1> adapter = new Gson().getAdapter(Foo1.class);
+    assertThat(adapter).isNotNull();
+  }
+
+  /** Tests below check the behavior of the methods changed for the fix. */
+  @Test
+  public void testDoubleSupertype() {
+    assertThat($Gson$Types.supertypeOf($Gson$Types.supertypeOf(Number.class)))
+        .isEqualTo($Gson$Types.supertypeOf(Number.class));
+  }
+
+  @Test
+  public void testDoubleSubtype() {
+    assertThat($Gson$Types.subtypeOf($Gson$Types.subtypeOf(Number.class)))
+        .isEqualTo($Gson$Types.subtypeOf(Number.class));
+  }
+
+  @Test
+  public void testSuperSubtype() {
+    assertThat($Gson$Types.supertypeOf($Gson$Types.subtypeOf(Number.class)))
+        .isEqualTo($Gson$Types.subtypeOf(Object.class));
+  }
+
+  @Test
+  public void testSubSupertype() {
+    assertThat($Gson$Types.subtypeOf($Gson$Types.supertypeOf(Number.class)))
+        .isEqualTo($Gson$Types.subtypeOf(Object.class));
+  }
+
+  /** Tests for recursion while resolving type variables. */
+  @SuppressWarnings("unused")
+  private static class TestType<X> {
+    TestType<? super X> superType;
+  }
+
+  @SuppressWarnings("unused")
+  private static class TestType2<X, Y> {
+    TestType2<? super Y, ? super X> superReversedType;
+  }
+
+  @Test
+  public void testRecursiveTypeVariablesResolve1() {
+    @SuppressWarnings("rawtypes")
+    TypeAdapter<TestType> adapter = new Gson().getAdapter(TestType.class);
+    assertThat(adapter).isNotNull();
+  }
+
+  @Test
+  public void testRecursiveTypeVariablesResolve12() {
+    @SuppressWarnings("rawtypes")
+    TypeAdapter<TestType2> adapter = new Gson().getAdapter(TestType2.class);
+    assertThat(adapter).isNotNull();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java b/gson/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f2b3b20fa800cfc35c0628e50b0697ee1479c9e
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/bind/util/ISO8601UtilsTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2020 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.internal.bind.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.Test;
+
+public class ISO8601UtilsTest {
+
+  private static TimeZone utcTimeZone() {
+    return TimeZone.getTimeZone("UTC");
+  }
+
+  private static GregorianCalendar createUtcCalendar() {
+    TimeZone utc = utcTimeZone();
+    GregorianCalendar calendar = new GregorianCalendar(utc);
+    // Calendar was created with current time, must clear it
+    calendar.clear();
+    return calendar;
+  }
+
+  @Test
+  public void testDateFormatString() {
+    GregorianCalendar calendar = new GregorianCalendar(utcTimeZone(), Locale.US);
+    // Calendar was created with current time, must clear it
+    calendar.clear();
+    calendar.set(2018, Calendar.JUNE, 25);
+    Date date = calendar.getTime();
+    String dateStr = ISO8601Utils.format(date);
+    String expectedDate = "2018-06-25";
+    assertThat(dateStr).startsWith(expectedDate);
+  }
+
+  @Test
+  @SuppressWarnings("JavaUtilDate")
+  public void testDateFormatWithMilliseconds() {
+    long time = 1530209176870L;
+    Date date = new Date(time);
+    String dateStr = ISO8601Utils.format(date, true);
+    String expectedDate = "2018-06-28T18:06:16.870Z";
+    assertThat(dateStr).isEqualTo(expectedDate);
+  }
+
+  @Test
+  @SuppressWarnings("JavaUtilDate")
+  public void testDateFormatWithTimezone() {
+    long time = 1530209176870L;
+    Date date = new Date(time);
+    String dateStr = ISO8601Utils.format(date, true, TimeZone.getTimeZone("Brazil/East"));
+    String expectedDate = "2018-06-28T15:06:16.870-03:00";
+    assertThat(dateStr).isEqualTo(expectedDate);
+  }
+
+  @Test
+  @SuppressWarnings("UndefinedEquals")
+  public void testDateParseWithDefaultTimezone() throws ParseException {
+    String dateStr = "2018-06-25";
+    Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
+    Date expectedDate = new GregorianCalendar(2018, Calendar.JUNE, 25).getTime();
+    assertThat(date).isEqualTo(expectedDate);
+  }
+
+  @Test
+  public void testDateParseInvalidDay() {
+    String dateStr = "2022-12-33";
+    assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
+  }
+
+  @Test
+  public void testDateParseInvalidMonth() {
+    String dateStr = "2022-14-30";
+    assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
+  }
+
+  @Test
+  @SuppressWarnings("UndefinedEquals")
+  public void testDateParseWithTimezone() throws ParseException {
+    String dateStr = "2018-06-25T00:00:00-03:00";
+    Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
+    GregorianCalendar calendar = createUtcCalendar();
+    calendar.set(2018, Calendar.JUNE, 25, 3, 0);
+    Date expectedDate = calendar.getTime();
+    assertThat(date).isEqualTo(expectedDate);
+  }
+
+  @Test
+  @SuppressWarnings("UndefinedEquals")
+  public void testDateParseSpecialTimezone() throws ParseException {
+    String dateStr = "2018-06-25T00:02:00-02:58";
+    Date date = ISO8601Utils.parse(dateStr, new ParsePosition(0));
+    GregorianCalendar calendar = createUtcCalendar();
+    calendar.set(2018, Calendar.JUNE, 25, 3, 0);
+    Date expectedDate = calendar.getTime();
+    assertThat(date).isEqualTo(expectedDate);
+  }
+
+  @Test
+  public void testDateParseInvalidTime() {
+    final String dateStr = "2018-06-25T61:60:62-03:00";
+    assertThrows(ParseException.class, () -> ISO8601Utils.parse(dateStr, new ParsePosition(0)));
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java b/gson/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a208289e9695f62a89fd4be4bd5a1fc08934e412
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.internal.reflect;
+
+import static com.google.common.truth.Truth.assertThat;
+
+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
+    assertThat(ReflectionHelper.isRecord(unixDomainPrincipalClass)).isTrue();
+    // with 2 components
+    assertThat(ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass))
+        .isEqualTo(new String[] {"user", "group"});
+    // Check canonical constructor
+    Constructor<?> constructor =
+        ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass);
+    assertThat(constructor).isNotNull();
+    assertThat(constructor.getParameterTypes())
+        .isEqualTo(new Class<?>[] {UserPrincipal.class, GroupPrincipal.class});
+  }
+
+  @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);
+    assertThat(componentNames).isNotEmpty();
+
+    for (String componentName : componentNames) {
+      Field componentField = unixDomainPrincipalClass.getDeclaredField(componentName);
+      Method accessor = ReflectionHelper.getAccessor(unixDomainPrincipalClass, componentField);
+      Object principal = accessor.invoke(unixDomainPrincipal);
+
+      assertThat(principal).isEqualTo(new PrincipalImpl(componentName));
+    }
+  }
+
+  /** 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/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java b/gson/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..883b530a03f089a53d9c9fe20c5c3206dc0c3a36
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/sql/SqlTypesGsonTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2020 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.internal.sql;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.functional.DefaultTypeAdaptersTest;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Locale;
+import java.util.TimeZone;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+// Suppression for `java.sql.Date` to make it explicit that this is not `java.util.Date`
+@SuppressWarnings("UnnecessarilyFullyQualified")
+public class SqlTypesGsonTest {
+  private Gson gson;
+  private TimeZone oldTimeZone;
+  private Locale oldLocale;
+
+  @Before
+  public void setUp() throws Exception {
+    this.oldTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles"));
+    this.oldLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    gson = new Gson();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    TimeZone.setDefault(oldTimeZone);
+    Locale.setDefault(oldLocale);
+  }
+
+  @Test
+  public void testNullSerializationAndDeserialization() {
+    testNullSerializationAndDeserialization(java.sql.Date.class);
+    testNullSerializationAndDeserialization(Time.class);
+    testNullSerializationAndDeserialization(Timestamp.class);
+  }
+
+  private void testNullSerializationAndDeserialization(Class<?> c) {
+    DefaultTypeAdaptersTest.testNullSerializationAndDeserialization(gson, c);
+  }
+
+  @Test
+  public void testDefaultSqlDateSerialization() {
+    java.sql.Date instant = new java.sql.Date(1259875082000L);
+    String json = gson.toJson(instant);
+    assertThat(json).isEqualTo("\"Dec 3, 2009\"");
+  }
+
+  @Test
+  public void testDefaultSqlDateDeserialization() {
+    String json = "'Dec 3, 2009'";
+    java.sql.Date extracted = gson.fromJson(json, java.sql.Date.class);
+    DefaultTypeAdaptersTest.assertEqualsDate(extracted, 2009, 11, 3);
+  }
+
+  // http://code.google.com/p/google-gson/issues/detail?id=230
+  @Test
+  public void testSqlDateSerialization() throws Exception {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    try {
+      java.sql.Date sqlDate = new java.sql.Date(0L);
+      Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
+      String json = gson.toJson(sqlDate, Timestamp.class);
+      assertThat(json).isEqualTo("\"1970-01-01\"");
+      assertThat(gson.fromJson("\"1970-01-01\"", java.sql.Date.class).getTime()).isEqualTo(0);
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+
+  @Test
+  public void testDefaultSqlTimeSerialization() {
+    Time now = new Time(1259875082000L);
+    String json = gson.toJson(now);
+    assertThat(json).isEqualTo("\"01:18:02 PM\"");
+  }
+
+  @Test
+  public void testDefaultSqlTimeDeserialization() {
+    String json = "'1:18:02 PM'";
+    Time extracted = gson.fromJson(json, Time.class);
+    DefaultTypeAdaptersTest.assertEqualsTime(extracted, 13, 18, 2);
+  }
+
+  @Test
+  public void testDefaultSqlTimestampSerialization() {
+    Timestamp now = new java.sql.Timestamp(1259875082000L);
+    String json = gson.toJson(now);
+    // The exact format of the serialized date-time string depends on the JDK version. The pattern
+    // here allows for an optional comma after the date, and what might be U+202F (Narrow No-Break
+    // Space) before "PM".
+    assertThat(json).matches("\"Dec 3, 2009,? 1:18:02\\hPM\"");
+  }
+
+  @Test
+  public void testDefaultSqlTimestampDeserialization() {
+    String json = "'Dec 3, 2009 1:18:02 PM'";
+    Timestamp extracted = gson.fromJson(json, Timestamp.class);
+    DefaultTypeAdaptersTest.assertEqualsDate(extracted, 2009, 11, 3);
+    DefaultTypeAdaptersTest.assertEqualsTime(extracted, 13, 18, 2);
+  }
+
+  // http://code.google.com/p/google-gson/issues/detail?id=230
+  @Test
+  public void testTimestampSerialization() throws Exception {
+    TimeZone defaultTimeZone = TimeZone.getDefault();
+    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    Locale defaultLocale = Locale.getDefault();
+    Locale.setDefault(Locale.US);
+    try {
+      Timestamp timestamp = new Timestamp(0L);
+      Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
+      String json = gson.toJson(timestamp, Timestamp.class);
+      assertThat(json).isEqualTo("\"1970-01-01\"");
+      assertThat(gson.fromJson("\"1970-01-01\"", Timestamp.class).getTime()).isEqualTo(0);
+    } finally {
+      TimeZone.setDefault(defaultTimeZone);
+      Locale.setDefault(defaultLocale);
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/internal/sql/SqlTypesSupportTest.java b/gson/gson/src/test/java/com/google/gson/internal/sql/SqlTypesSupportTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..23778079c7fcc879c53e9c4d34643ea613fb6dbf
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/internal/sql/SqlTypesSupportTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 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.internal.sql;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SqlTypesSupportTest {
+  @Test
+  public void testSupported() {
+    assertThat(SqlTypesSupport.SUPPORTS_SQL_TYPES).isTrue();
+
+    assertThat(SqlTypesSupport.DATE_DATE_TYPE).isNotNull();
+    assertThat(SqlTypesSupport.TIMESTAMP_DATE_TYPE).isNotNull();
+
+    assertThat(SqlTypesSupport.DATE_FACTORY).isNotNull();
+    assertThat(SqlTypesSupport.TIME_FACTORY).isNotNull();
+    assertThat(SqlTypesSupport.TIMESTAMP_FACTORY).isNotNull();
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java b/gson/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e2add3050b226f449b560b20e93fefb13151be3
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonParseException;
+import com.google.gson.annotations.Expose;
+import com.google.gson.reflect.TypeToken;
+import java.io.StringWriter;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Tests to measure performance for Gson. All tests in this file will be disabled in code. To run
+ * them remove the {@code @Ignore} annotation from the tests.
+ *
+ * @author Inderjeet Singh
+ * @author Joel Leitch
+ */
+@SuppressWarnings("SystemOut") // allow System.out because test is for manual execution anyway
+public class PerformanceTest {
+  private static final int COLLECTION_SIZE = 5000;
+
+  private static final int NUM_ITERATIONS = 100;
+
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson = new Gson();
+  }
+
+  @Test
+  public void testDummy() {
+    // This is here to prevent Junit for complaining when we disable all tests.
+  }
+
+  @Test
+  @Ignore
+  public void testStringDeserialization() {
+    StringBuilder sb = new StringBuilder(8096);
+    sb.append("Error Yippie");
+
+    while (true) {
+      try {
+        String stackTrace = sb.toString();
+        sb.append(stackTrace);
+        String json = "{\"message\":\"Error message.\"," + "\"stackTrace\":\"" + stackTrace + "\"}";
+        parseLongJson(json);
+        System.out.println("Gson could handle a string of size: " + stackTrace.length());
+      } catch (JsonParseException expected) {
+        break;
+      }
+    }
+  }
+
+  private void parseLongJson(String json) throws JsonParseException {
+    ExceptionHolder target = gson.fromJson(json, ExceptionHolder.class);
+    assertThat(target.message).contains("Error");
+    assertThat(target.stackTrace).contains("Yippie");
+  }
+
+  private static class ExceptionHolder {
+    public final String message;
+    public final String stackTrace;
+
+    // For use by Gson
+    @SuppressWarnings("unused")
+    private ExceptionHolder() {
+      this("", "");
+    }
+
+    public ExceptionHolder(String message, String stackTrace) {
+      this.message = message;
+      this.stackTrace = stackTrace;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class CollectionEntry {
+    final String name;
+    final String value;
+
+    // For use by Gson
+    private CollectionEntry() {
+      this(null, null);
+    }
+
+    CollectionEntry(String name, String value) {
+      this.name = name;
+      this.value = value;
+    }
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 */
+  @Test
+  @Ignore
+  public void testLargeCollectionSerialization() {
+    int count = 1400000;
+    List<CollectionEntry> list = new ArrayList<>(count);
+    for (int i = 0; i < count; ++i) {
+      list.add(new CollectionEntry("name" + i, "value" + i));
+    }
+    String unused = gson.toJson(list);
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 */
+  @Test
+  @Ignore
+  public void testLargeCollectionDeserialization() {
+    StringBuilder sb = new StringBuilder();
+    int count = 87000;
+    boolean first = true;
+    sb.append('[');
+    for (int i = 0; i < count; ++i) {
+      if (first) {
+        first = false;
+      } else {
+        sb.append(',');
+      }
+      sb.append("{name:'name").append(i).append("',value:'value").append(i).append("'}");
+    }
+    sb.append(']');
+    String json = sb.toString();
+    Type collectionType = new TypeToken<ArrayList<CollectionEntry>>() {}.getType();
+    List<CollectionEntry> list = gson.fromJson(json, collectionType);
+    assertThat(list).hasSize(count);
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 */
+  // Last I tested, Gson was able to serialize upto 14MB byte array
+  @Test
+  @Ignore
+  public void testByteArraySerialization() {
+    for (int size = 4145152; true; size += 1036288) {
+      byte[] ba = new byte[size];
+      for (int i = 0; i < size; ++i) {
+        ba[i] = 0x05;
+      }
+      String unused = gson.toJson(ba);
+      System.out.printf("Gson could serialize a byte array of size: %d\n", size);
+    }
+  }
+
+  /** Created in response to http://code.google.com/p/google-gson/issues/detail?id=96 */
+  // Last I tested, Gson was able to deserialize a byte array of 11MB
+  @Test
+  @Ignore
+  public void testByteArrayDeserialization() {
+    for (int numElements = 10639296; true; numElements += 16384) {
+      StringBuilder sb = new StringBuilder(numElements * 2);
+      sb.append("[");
+      boolean first = true;
+      for (int i = 0; i < numElements; ++i) {
+        if (first) {
+          first = false;
+        } else {
+          sb.append(",");
+        }
+        sb.append("5");
+      }
+      sb.append("]");
+      String json = sb.toString();
+      byte[] ba = gson.fromJson(json, byte[].class);
+      System.out.printf("Gson could deserialize a byte array of size: %d\n", ba.length);
+    }
+  }
+
+  // The tests to measure serialization and deserialization performance of Gson
+  // Based on the discussion at
+  // http://groups.google.com/group/google-gson/browse_thread/thread/7a50b17a390dfaeb
+  // Test results: 10/19/2009
+  // Serialize classes avg time: 60 ms
+  // Deserialized classes avg time: 70 ms
+  // Serialize exposed classes avg time: 159 ms
+  // Deserialized exposed classes avg time: 173 ms
+
+  @Test
+  @Ignore
+  public void testSerializeClasses() {
+    ClassWithList c = new ClassWithList("str");
+    for (int i = 0; i < COLLECTION_SIZE; ++i) {
+      c.list.add(new ClassWithField("element-" + i));
+    }
+    StringWriter w = new StringWriter();
+    long t1 = System.currentTimeMillis();
+    for (int i = 0; i < NUM_ITERATIONS; ++i) {
+      gson.toJson(c, w);
+    }
+    long t2 = System.currentTimeMillis();
+    long avg = (t2 - t1) / NUM_ITERATIONS;
+    System.out.printf("Serialize classes avg time: %d ms\n", avg);
+  }
+
+  @Test
+  @Ignore
+  public void testDeserializeClasses() {
+    String json = buildJsonForClassWithList();
+    ClassWithList[] target = new ClassWithList[NUM_ITERATIONS];
+    long t1 = System.currentTimeMillis();
+    for (int i = 0; i < NUM_ITERATIONS; ++i) {
+      target[i] = gson.fromJson(json, ClassWithList.class);
+    }
+    long t2 = System.currentTimeMillis();
+    long avg = (t2 - t1) / NUM_ITERATIONS;
+    System.out.printf("Deserialize classes avg time: %d ms\n", avg);
+  }
+
+  @Test
+  @Ignore
+  public void testLargeObjectSerializationAndDeserialization() {
+    Map<String, Long> largeObject = new HashMap<>();
+    for (long l = 0; l < 100000; l++) {
+      largeObject.put("field" + l, l);
+    }
+
+    long t1 = System.currentTimeMillis();
+    String json = gson.toJson(largeObject);
+    long t2 = System.currentTimeMillis();
+    System.out.printf("Large object serialized in: %d ms\n", (t2 - t1));
+
+    t1 = System.currentTimeMillis();
+    Map<String, Long> unused = gson.fromJson(json, new TypeToken<Map<String, Long>>() {}.getType());
+    t2 = System.currentTimeMillis();
+    System.out.printf("Large object deserialized in: %d ms\n", (t2 - t1));
+  }
+
+  @Test
+  @Ignore
+  public void testSerializeExposedClasses() {
+    ClassWithListOfObjects c1 = new ClassWithListOfObjects("str");
+    for (int i1 = 0; i1 < COLLECTION_SIZE; ++i1) {
+      c1.list.add(new ClassWithExposedField("element-" + i1));
+    }
+    ClassWithListOfObjects c = c1;
+    StringWriter w = new StringWriter();
+    long t1 = System.currentTimeMillis();
+    for (int i = 0; i < NUM_ITERATIONS; ++i) {
+      gson.toJson(c, w);
+    }
+    long t2 = System.currentTimeMillis();
+    long avg = (t2 - t1) / NUM_ITERATIONS;
+    System.out.printf("Serialize exposed classes avg time: %d ms\n", avg);
+  }
+
+  @Test
+  @Ignore
+  public void testDeserializeExposedClasses() {
+    String json = buildJsonForClassWithList();
+    ClassWithListOfObjects[] target = new ClassWithListOfObjects[NUM_ITERATIONS];
+    long t1 = System.currentTimeMillis();
+    for (int i = 0; i < NUM_ITERATIONS; ++i) {
+      target[i] = gson.fromJson(json, ClassWithListOfObjects.class);
+    }
+    long t2 = System.currentTimeMillis();
+    long avg = (t2 - t1) / NUM_ITERATIONS;
+    System.out.printf("Deserialize exposed classes avg time: %d ms\n", avg);
+  }
+
+  @Test
+  @Ignore
+  public void testLargeGsonMapRoundTrip() throws Exception {
+    Map<Long, Long> original = new HashMap<>();
+    for (long i = 0; i < 1000000; i++) {
+      original.put(i, i + 1);
+    }
+
+    Gson gson = new Gson();
+    String json = gson.toJson(original);
+    Type longToLong = new TypeToken<Map<Long, Long>>() {}.getType();
+    Map<Long, Long> unused = gson.fromJson(json, longToLong);
+  }
+
+  private static String buildJsonForClassWithList() {
+    StringBuilder sb = new StringBuilder("{");
+    sb.append("field:").append("'str',");
+    sb.append("list:[");
+    boolean first = true;
+    for (int i = 0; i < COLLECTION_SIZE; ++i) {
+      if (first) {
+        first = false;
+      } else {
+        sb.append(',');
+      }
+      sb.append("{field:'element-" + i + "'}");
+    }
+    sb.append(']');
+    sb.append('}');
+    String json = sb.toString();
+    return json;
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ClassWithList {
+    final String field;
+    final List<ClassWithField> list = new ArrayList<>(COLLECTION_SIZE);
+
+    ClassWithList() {
+      this(null);
+    }
+
+    ClassWithList(String field) {
+      this.field = field;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ClassWithField {
+    final String field;
+
+    ClassWithField() {
+      this("");
+    }
+
+    public ClassWithField(String field) {
+      this.field = field;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ClassWithListOfObjects {
+    @Expose final String field;
+    @Expose final List<ClassWithExposedField> list = new ArrayList<>(COLLECTION_SIZE);
+
+    ClassWithListOfObjects() {
+      this(null);
+    }
+
+    ClassWithListOfObjects(String field) {
+      this.field = field;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static final class ClassWithExposedField {
+    @Expose final String field;
+
+    ClassWithExposedField() {
+      this("");
+    }
+
+    ClassWithExposedField(String field) {
+      this.field = field;
+    }
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e0277a406fd480a6e2686bc432ae13ccd94fa4f2
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2010 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.reflect;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.RandomAccess;
+import java.util.Set;
+import org.junit.Test;
+
+/**
+ * Tests for {@link TypeToken}.
+ *
+ * @author Jesse Wilson
+ */
+// Suppress because these classes are only needed for this test, but must be top-level classes
+// to not have an enclosing type
+@SuppressWarnings("MultipleTopLevelClasses")
+public final class TypeTokenTest {
+  // These fields are accessed using reflection by the tests below
+  List<Integer> listOfInteger = null;
+  List<Number> listOfNumber = null;
+  List<String> listOfString = null;
+  List<?> listOfUnknown = null;
+  List<Set<String>> listOfSetOfString = null;
+  List<Set<?>> listOfSetOfUnknown = null;
+
+  @SuppressWarnings({"deprecation"})
+  @Test
+  public void testIsAssignableFromRawTypes() {
+    assertThat(TypeToken.get(Object.class).isAssignableFrom(String.class)).isTrue();
+    assertThat(TypeToken.get(String.class).isAssignableFrom(Object.class)).isFalse();
+    assertThat(TypeToken.get(RandomAccess.class).isAssignableFrom(ArrayList.class)).isTrue();
+    assertThat(TypeToken.get(ArrayList.class).isAssignableFrom(RandomAccess.class)).isFalse();
+  }
+
+  @SuppressWarnings({"deprecation"})
+  @Test
+  public void testIsAssignableFromWithTypeParameters() throws Exception {
+    Type a = getClass().getDeclaredField("listOfInteger").getGenericType();
+    Type b = getClass().getDeclaredField("listOfNumber").getGenericType();
+    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
+    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();
+
+    // listOfInteger = listOfNumber; // doesn't compile; must be false
+    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
+    // listOfNumber = listOfInteger; // doesn't compile; must be false
+    assertThat(TypeToken.get(b).isAssignableFrom(a)).isFalse();
+  }
+
+  @SuppressWarnings({"deprecation"})
+  @Test
+  public void testIsAssignableFromWithBasicWildcards() throws Exception {
+    Type a = getClass().getDeclaredField("listOfString").getGenericType();
+    Type b = getClass().getDeclaredField("listOfUnknown").getGenericType();
+    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
+    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();
+
+    // listOfString = listOfUnknown  // doesn't compile; must be false
+    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
+    listOfUnknown = listOfString; // compiles; must be true
+    // The following assertion is too difficult to support reliably, so disabling
+    // assertThat(TypeToken.get(b).isAssignableFrom(a)).isTrue();
+
+    WildcardType wildcardType = (WildcardType) ((ParameterizedType) b).getActualTypeArguments()[0];
+    TypeToken<?> wildcardTypeToken = TypeToken.get(wildcardType);
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> wildcardTypeToken.isAssignableFrom(b));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Unsupported type, expected one of: java.lang.Class,"
+                + " java.lang.reflect.ParameterizedType, java.lang.reflect.GenericArrayType, but"
+                + " got: com.google.gson.internal.$Gson$Types$WildcardTypeImpl, for type token: "
+                + wildcardTypeToken);
+  }
+
+  @SuppressWarnings({"deprecation"})
+  @Test
+  public void testIsAssignableFromWithNestedWildcards() throws Exception {
+    Type a = getClass().getDeclaredField("listOfSetOfString").getGenericType();
+    Type b = getClass().getDeclaredField("listOfSetOfUnknown").getGenericType();
+    assertThat(TypeToken.get(a).isAssignableFrom(a)).isTrue();
+    assertThat(TypeToken.get(b).isAssignableFrom(b)).isTrue();
+
+    // listOfSetOfString = listOfSetOfUnknown; // doesn't compile; must be false
+    assertThat(TypeToken.get(a).isAssignableFrom(b)).isFalse();
+    // listOfSetOfUnknown = listOfSetOfString; // doesn't compile; must be false
+    assertThat(TypeToken.get(b).isAssignableFrom(a)).isFalse();
+  }
+
+  @Test
+  public void testArrayFactory() {
+    TypeToken<?> expectedStringArray = new TypeToken<String[]>() {};
+    assertThat(TypeToken.getArray(String.class)).isEqualTo(expectedStringArray);
+
+    TypeToken<?> expectedListOfStringArray = new TypeToken<List<String>[]>() {};
+    Type listOfString = new TypeToken<List<String>>() {}.getType();
+    assertThat(TypeToken.getArray(listOfString)).isEqualTo(expectedListOfStringArray);
+
+    TypeToken<?> expectedIntArray = new TypeToken<int[]>() {};
+    assertThat(TypeToken.getArray(int.class)).isEqualTo(expectedIntArray);
+
+    assertThrows(NullPointerException.class, () -> TypeToken.getArray(null));
+  }
+
+  static class NestedGeneric<T> {}
+
+  @Test
+  public void testParameterizedFactory() {
+    TypeToken<?> expectedListOfString = new TypeToken<List<String>>() {};
+    assertThat(TypeToken.getParameterized(List.class, String.class))
+        .isEqualTo(expectedListOfString);
+
+    TypeToken<?> expectedMapOfStringToString = new TypeToken<Map<String, String>>() {};
+    assertThat(TypeToken.getParameterized(Map.class, String.class, String.class))
+        .isEqualTo(expectedMapOfStringToString);
+
+    TypeToken<?> expectedListOfListOfListOfString = new TypeToken<List<List<List<String>>>>() {};
+    Type listOfString = TypeToken.getParameterized(List.class, String.class).getType();
+    Type listOfListOfString = TypeToken.getParameterized(List.class, listOfString).getType();
+    assertThat(TypeToken.getParameterized(List.class, listOfListOfString))
+        .isEqualTo(expectedListOfListOfListOfString);
+
+    TypeToken<?> expectedWithExactArg = new TypeToken<GenericWithBound<Number>>() {};
+    assertThat(TypeToken.getParameterized(GenericWithBound.class, Number.class))
+        .isEqualTo(expectedWithExactArg);
+
+    TypeToken<?> expectedWithSubclassArg = new TypeToken<GenericWithBound<Integer>>() {};
+    assertThat(TypeToken.getParameterized(GenericWithBound.class, Integer.class))
+        .isEqualTo(expectedWithSubclassArg);
+
+    TypeToken<?> expectedSatisfyingTwoBounds =
+        new TypeToken<GenericWithMultiBound<ClassSatisfyingBounds>>() {};
+    assertThat(TypeToken.getParameterized(GenericWithMultiBound.class, ClassSatisfyingBounds.class))
+        .isEqualTo(expectedSatisfyingTwoBounds);
+
+    TypeToken<?> nestedTypeToken = TypeToken.getParameterized(NestedGeneric.class, Integer.class);
+    ParameterizedType nestedParameterizedType = (ParameterizedType) nestedTypeToken.getType();
+    // TODO: This seems to differ from how Java reflection behaves; when using
+    // TypeToken<NestedGeneric<Integer>>, then NestedGeneric<Integer> does have an owner type
+    assertThat(nestedParameterizedType.getOwnerType()).isNull();
+    assertThat(nestedParameterizedType.getRawType()).isEqualTo(NestedGeneric.class);
+    assertThat(nestedParameterizedType.getActualTypeArguments())
+        .asList()
+        .containsExactly(Integer.class);
+
+    class LocalGenericClass<T> {}
+    TypeToken<?> expectedLocalType = new TypeToken<LocalGenericClass<Integer>>() {};
+    assertThat(TypeToken.getParameterized(LocalGenericClass.class, Integer.class))
+        .isEqualTo(expectedLocalType);
+
+    // For legacy reasons, if requesting parameterized type for non-generic class, create a
+    // `TypeToken(Class)`
+    assertThat(TypeToken.getParameterized(String.class)).isEqualTo(TypeToken.get(String.class));
+  }
+
+  @Test
+  public void testParameterizedFactory_Invalid() {
+    assertThrows(NullPointerException.class, () -> TypeToken.getParameterized(null, new Type[0]));
+    assertThrows(
+        NullPointerException.class,
+        () -> TypeToken.getParameterized(List.class, new Type[] {null}));
+
+    GenericArrayType arrayType = (GenericArrayType) TypeToken.getArray(String.class).getType();
+    IllegalArgumentException e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(arrayType, new Type[0]));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("rawType must be of type Class, but was java.lang.String[]");
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(String.class, Number.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo("java.lang.String requires 0 type arguments, but got 1");
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(List.class, new Type[0]));
+    assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 0");
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(List.class, String.class, String.class));
+    assertThat(e).hasMessageThat().isEqualTo("java.util.List requires 1 type arguments, but got 2");
+
+    // Primitive types must not be used as type argument
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(List.class, int.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument int does not satisfy bounds for type variable E declared by "
+                + List.class);
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(GenericWithBound.class, String.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument class java.lang.String does not satisfy bounds"
+                + " for type variable T declared by "
+                + GenericWithBound.class);
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(GenericWithBound.class, Object.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument class java.lang.Object does not satisfy bounds"
+                + " for type variable T declared by "
+                + GenericWithBound.class);
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(GenericWithMultiBound.class, Number.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument class java.lang.Number does not satisfy bounds"
+                + " for type variable T declared by "
+                + GenericWithMultiBound.class);
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(GenericWithMultiBound.class, CharSequence.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument interface java.lang.CharSequence does not satisfy bounds"
+                + " for type variable T declared by "
+                + GenericWithMultiBound.class);
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(GenericWithMultiBound.class, Object.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Type argument class java.lang.Object does not satisfy bounds"
+                + " for type variable T declared by "
+                + GenericWithMultiBound.class);
+
+    class Outer {
+      class NonStaticInner<T> {}
+    }
+
+    e =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> TypeToken.getParameterized(Outer.NonStaticInner.class, Object.class));
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "Raw type "
+                + Outer.NonStaticInner.class.getName()
+                + " is not supported because it requires specifying an owner type");
+  }
+
+  private static class CustomTypeToken extends TypeToken<String> {}
+
+  @Test
+  public void testTypeTokenNonAnonymousSubclass() {
+    TypeToken<?> typeToken = new CustomTypeToken();
+    assertThat(typeToken.getRawType()).isEqualTo(String.class);
+    assertThat(typeToken.getType()).isEqualTo(String.class);
+  }
+
+  /**
+   * User must only create direct subclasses of TypeToken, but not subclasses of subclasses (...) of
+   * TypeToken.
+   */
+  @Test
+  public void testTypeTokenSubSubClass() {
+    class SubTypeToken<T> extends TypeToken<String> {}
+    class SubSubTypeToken1<T> extends SubTypeToken<T> {}
+    class SubSubTypeToken2 extends SubTypeToken<Integer> {}
+
+    IllegalStateException e =
+        assertThrows(IllegalStateException.class, () -> new SubTypeToken<Integer>() {});
+    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");
+
+    e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken1<Integer>());
+    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");
+
+    e = assertThrows(IllegalStateException.class, () -> new SubSubTypeToken2());
+    assertThat(e).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken");
+  }
+
+  private static <M> void createTypeTokenTypeVariable() {
+    new TypeToken<M>() {};
+  }
+
+  /**
+   * TypeToken type argument must not contain a type variable because, due to type erasure, at
+   * runtime only the bound of the type variable is available which is likely not what the user
+   * wanted.
+   *
+   * <p>Note that type variables are allowed for the {@code TypeToken} factory methods calling
+   * {@code TypeToken(Type)} because for them the return type is {@code TypeToken<?>} which does not
+   * give a false sense of type-safety.
+   */
+  @Test
+  public void testTypeTokenTypeVariable() throws Exception {
+    // Put the test code inside generic class to be able to access `T`
+    class Enclosing<T> {
+      class Inner {}
+
+      void test() {
+        String expectedMessage =
+            "TypeToken type argument must not contain a type variable;"
+                + " captured type variable T declared by "
+                + Enclosing.class
+                + "\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable";
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, () -> new TypeToken<T>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<List<List<T>>>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        e =
+            assertThrows(
+                IllegalArgumentException.class, () -> new TypeToken<List<? extends List<T>>>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        e =
+            assertThrows(
+                IllegalArgumentException.class, () -> new TypeToken<List<? super List<T>>>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<List<T>[]>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        e =
+            assertThrows(
+                IllegalArgumentException.class, () -> new TypeToken<Enclosing<T>.Inner>() {});
+        assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+
+        String systemProperty = "gson.allowCapturingTypeVariables";
+        try {
+          // Any value other than 'true' should be ignored
+          System.setProperty(systemProperty, "some-value");
+
+          e = assertThrows(IllegalArgumentException.class, () -> new TypeToken<T>() {});
+          assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
+        } finally {
+          System.clearProperty(systemProperty);
+        }
+
+        try {
+          System.setProperty(systemProperty, "true");
+
+          TypeToken<?> typeToken = new TypeToken<T>() {};
+          assertThat(typeToken.getType()).isEqualTo(Enclosing.class.getTypeParameters()[0]);
+        } finally {
+          System.clearProperty(systemProperty);
+        }
+      }
+
+      <M> void testMethodTypeVariable() throws Exception {
+        Method testMethod = Enclosing.class.getDeclaredMethod("testMethodTypeVariable");
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, () -> new TypeToken<M>() {});
+        assertThat(e)
+            .hasMessageThat()
+            .isAnyOf(
+                "TypeToken type argument must not contain a type variable;"
+                    + " captured type variable M declared by "
+                    + testMethod
+                    + "\n"
+                    + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable",
+                // Note: When running this test in Eclipse IDE or with certain Java versions it
+                // seems to capture `null` instead of the type variable, see
+                // https://github.com/eclipse-jdt/eclipse.jdt.core/issues/975
+                "TypeToken captured `null` as type argument; probably a compiler / runtime bug");
+      }
+    }
+
+    new Enclosing<>().test();
+    new Enclosing<>().testMethodTypeVariable();
+
+    Method testMethod = TypeTokenTest.class.getDeclaredMethod("createTypeTokenTypeVariable");
+    IllegalArgumentException e =
+        assertThrows(IllegalArgumentException.class, () -> createTypeTokenTypeVariable());
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "TypeToken type argument must not contain a type variable;"
+                + " captured type variable M declared by "
+                + testMethod
+                + "\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#typetoken-type-variable");
+
+    // Using type variable as argument for factory methods should be allowed; this is not a
+    // type-safety problem because the user would have to perform unsafe casts
+    TypeVariable<?> typeVar = Enclosing.class.getTypeParameters()[0];
+    TypeToken<?> typeToken = TypeToken.get(typeVar);
+    assertThat(typeToken.getType()).isEqualTo(typeVar);
+
+    TypeToken<?> parameterizedTypeToken = TypeToken.getParameterized(List.class, typeVar);
+    ParameterizedType parameterizedType = (ParameterizedType) parameterizedTypeToken.getType();
+    assertThat(parameterizedType.getRawType()).isEqualTo(List.class);
+    assertThat(parameterizedType.getActualTypeArguments()).asList().containsExactly(typeVar);
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Test
+  public void testTypeTokenRaw() {
+    IllegalStateException e = assertThrows(IllegalStateException.class, () -> new TypeToken() {});
+    assertThat(e)
+        .hasMessageThat()
+        .isEqualTo(
+            "TypeToken must be created with a type argument: new TypeToken<...>() {}; When using"
+                + " code shrinkers (ProGuard, R8, ...) make sure that generic signatures are"
+                + " preserved.\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#type-token-raw");
+  }
+}
+
+// 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/gson/src/test/java/com/google/gson/regression/OSGiTest.java b/gson/gson/src/test/java/com/google/gson/regression/OSGiTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a4a6238a10321599d54f59a586a1b8489cef041d
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/regression/OSGiTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 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.regression;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Splitter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Manifest;
+import org.junit.Test;
+
+public class OSGiTest {
+  @Test
+  public void testComGoogleGsonAnnotationsPackage() throws Exception {
+    Manifest mf = findManifest("com.google.gson");
+    String importPkg = mf.getMainAttributes().getValue("Import-Package");
+    assertWithMessage("Import-Package statement is there").that(importPkg).isNotNull();
+    assertSubstring(
+        "There should be com.google.gson.annotations dependency",
+        importPkg,
+        "com.google.gson.annotations");
+  }
+
+  @Test
+  public void testSunMiscImportPackage() throws Exception {
+    Manifest mf = findManifest("com.google.gson");
+    String importPkg = mf.getMainAttributes().getValue("Import-Package");
+    assertWithMessage("Import-Package statement is there").that(importPkg).isNotNull();
+    for (String dep : Splitter.on(',').split(importPkg)) {
+      if (dep.contains("sun.misc")) {
+        assertSubstring("sun.misc import is optional", dep, "resolution:=optional");
+        return;
+      }
+    }
+    fail("There should be sun.misc dependency, but was: " + importPkg);
+  }
+
+  private Manifest findManifest(String pkg) throws IOException {
+    List<URL> urls = new ArrayList<>();
+    for (URL u :
+        Collections.list(getClass().getClassLoader().getResources("META-INF/MANIFEST.MF"))) {
+      InputStream is = u.openStream();
+      Manifest mf = new Manifest(is);
+      is.close();
+      if (pkg.equals(mf.getMainAttributes().getValue("Bundle-SymbolicName"))) {
+        return mf;
+      }
+      urls.add(u);
+    }
+    fail("Cannot find " + pkg + " OSGi bundle manifest among: " + urls);
+    return null;
+  }
+
+  private static void assertSubstring(String msg, String wholeText, String subString) {
+    if (wholeText.contains(subString)) {
+      return;
+    }
+    fail(msg + ". Expecting " + subString + " but was: " + wholeText);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..de902f553b1b485cb3f9d6a6cd1e9fa8e051c9b7
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2014 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.stream;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+import com.google.gson.JsonElement;
+import com.google.gson.Strictness;
+import com.google.gson.internal.Streams;
+import com.google.gson.internal.bind.JsonTreeReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@SuppressWarnings("resource")
+@RunWith(Parameterized.class)
+public class JsonReaderPathTest {
+  @Parameterized.Parameters(name = "{0}")
+  public static List<Object[]> parameters() {
+    return Arrays.asList(
+        new Object[] {Factory.STRING_READER}, new Object[] {Factory.OBJECT_READER});
+  }
+
+  @Parameterized.Parameter public Factory factory;
+
+  @Test
+  public void path() throws IOException {
+    JsonReader reader = factory.create("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.");
+    assertThat(reader.getPath()).isEqualTo("$.");
+    String unused1 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[0]");
+    assertThat(reader.getPath()).isEqualTo("$.a[0]");
+    int unused2 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[0]");
+    assertThat(reader.getPath()).isEqualTo("$.a[1]");
+    boolean unused3 = reader.nextBoolean();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[1]");
+    assertThat(reader.getPath()).isEqualTo("$.a[2]");
+    boolean unused4 = reader.nextBoolean();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[2]");
+    assertThat(reader.getPath()).isEqualTo("$.a[3]");
+    reader.nextNull();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[3]");
+    assertThat(reader.getPath()).isEqualTo("$.a[4]");
+    String unused5 = reader.nextString();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[4]");
+    assertThat(reader.getPath()).isEqualTo("$.a[5]");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].");
+    assertThat(reader.getPath()).isEqualTo("$.a[5].");
+    String unused6 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].c");
+    assertThat(reader.getPath()).isEqualTo("$.a[5].c");
+    String unused7 = reader.nextString();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[5].c");
+    assertThat(reader.getPath()).isEqualTo("$.a[5].c");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[5]");
+    assertThat(reader.getPath()).isEqualTo("$.a[6]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[6][0]");
+    assertThat(reader.getPath()).isEqualTo("$.a[6][0]");
+    int unused8 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[6][0]");
+    assertThat(reader.getPath()).isEqualTo("$.a[6][1]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a[6]");
+    assertThat(reader.getPath()).isEqualTo("$.a[7]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void objectPath() throws IOException {
+    JsonReader reader = factory.create("{\"a\":1,\"b\":2}");
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+
+    JsonToken unused1 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.");
+    assertThat(reader.getPath()).isEqualTo("$.");
+
+    JsonToken unused2 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.");
+    assertThat(reader.getPath()).isEqualTo("$.");
+    String unused3 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+
+    JsonToken unused4 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    int unused5 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+
+    JsonToken unused6 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    String unused7 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+
+    JsonToken unused8 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+    int unused9 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+
+    JsonToken unused10 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+
+    JsonToken unused11 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.close();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void arrayPath() throws IOException {
+    JsonReader reader = factory.create("[1,2]");
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+
+    JsonToken unused1 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[0]");
+
+    JsonToken unused2 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[0]");
+    int unused3 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+
+    JsonToken unused4 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+    int unused5 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
+    assertThat(reader.getPath()).isEqualTo("$[2]");
+
+    JsonToken unused6 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
+    assertThat(reader.getPath()).isEqualTo("$[2]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+
+    JsonToken unused7 = reader.peek();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.close();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void multipleTopLevelValuesInOneDocument() throws IOException {
+    assumeTrue(factory == Factory.STRING_READER);
+
+    JsonReader reader = factory.create("[][]");
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void skipArrayElements() throws IOException {
+    JsonReader reader = factory.create("[1,2,3]");
+    reader.beginArray();
+    reader.skipValue();
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
+    assertThat(reader.getPath()).isEqualTo("$[2]");
+  }
+
+  @Test
+  public void skipArrayEnd() throws IOException {
+    JsonReader reader = factory.create("[[],1]");
+    reader.beginArray();
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0][0]");
+    assertThat(reader.getPath()).isEqualTo("$[0][0]");
+    reader.skipValue(); // skip end of array
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+  }
+
+  @Test
+  public void skipObjectNames() throws IOException {
+    JsonReader reader = factory.create("{\"a\":[]}");
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.<skipped>");
+    assertThat(reader.getPath()).isEqualTo("$.<skipped>");
+
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.<skipped>[0]");
+    assertThat(reader.getPath()).isEqualTo("$.<skipped>[0]");
+  }
+
+  @Test
+  public void skipObjectValues() throws IOException {
+    JsonReader reader = factory.create("{\"a\":1,\"b\":2}");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.");
+    assertThat(reader.getPath()).isEqualTo("$.");
+    String unused1 = reader.nextName();
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    String unused2 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+  }
+
+  @Test
+  public void skipObjectEnd() throws IOException {
+    JsonReader reader = factory.create("{\"a\":{},\"b\":2}");
+    reader.beginObject();
+    String unused = reader.nextName();
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.");
+    assertThat(reader.getPath()).isEqualTo("$.a.");
+    reader.skipValue(); // skip end of object
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+  }
+
+  @Test
+  public void skipNestedStructures() throws IOException {
+    JsonReader reader = factory.create("[[1,2,3],4]");
+    reader.beginArray();
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+  }
+
+  @Test
+  public void skipEndOfDocument() throws IOException {
+    JsonReader reader = factory.create("[]");
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.skipValue();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void arrayOfObjects() throws IOException {
+    JsonReader reader = factory.create("[{},{},{}]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[0]");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0].");
+    assertThat(reader.getPath()).isEqualTo("$[0].");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1].");
+    assertThat(reader.getPath()).isEqualTo("$[1].");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
+    assertThat(reader.getPath()).isEqualTo("$[2]");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[2].");
+    assertThat(reader.getPath()).isEqualTo("$[2].");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[2]");
+    assertThat(reader.getPath()).isEqualTo("$[3]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void arrayOfArrays() throws IOException {
+    JsonReader reader = factory.create("[[],[],[]]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[0]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0][0]");
+    assertThat(reader.getPath()).isEqualTo("$[0][0]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[0]");
+    assertThat(reader.getPath()).isEqualTo("$[1]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1][0]");
+    assertThat(reader.getPath()).isEqualTo("$[1][0]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[1]");
+    assertThat(reader.getPath()).isEqualTo("$[2]");
+    reader.beginArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[2][0]");
+    assertThat(reader.getPath()).isEqualTo("$[2][0]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$[2]");
+    assertThat(reader.getPath()).isEqualTo("$[3]");
+    reader.endArray();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void objectOfObjects() throws IOException {
+    JsonReader reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.");
+    assertThat(reader.getPath()).isEqualTo("$.");
+    String unused1 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.");
+    assertThat(reader.getPath()).isEqualTo("$.a.");
+    String unused2 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.a1");
+    assertThat(reader.getPath()).isEqualTo("$.a.a1");
+    int unused3 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.a1");
+    assertThat(reader.getPath()).isEqualTo("$.a.a1");
+    String unused4 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.a2");
+    assertThat(reader.getPath()).isEqualTo("$.a.a2");
+    int unused5 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a.a2");
+    assertThat(reader.getPath()).isEqualTo("$.a.a2");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.a");
+    assertThat(reader.getPath()).isEqualTo("$.a");
+    String unused6 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+    reader.beginObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b.");
+    assertThat(reader.getPath()).isEqualTo("$.b.");
+    String unused7 = reader.nextName();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b.b1");
+    assertThat(reader.getPath()).isEqualTo("$.b.b1");
+    int unused8 = reader.nextInt();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b.b1");
+    assertThat(reader.getPath()).isEqualTo("$.b.b1");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$.b");
+    assertThat(reader.getPath()).isEqualTo("$.b");
+    reader.endObject();
+    assertThat(reader.getPreviousPath()).isEqualTo("$");
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  public enum Factory {
+    STRING_READER {
+      @Override
+      public JsonReader create(String data) {
+        return new JsonReader(new StringReader(data));
+      }
+    },
+    OBJECT_READER {
+      @Override
+      public JsonReader create(String data) {
+        JsonElement element = Streams.parse(new JsonReader(new StringReader(data)));
+        return new JsonTreeReader(element);
+      }
+    };
+
+    abstract JsonReader create(String data);
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..45c14920efea57cb9991bb156922e166a0e1ae80
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
@@ -0,0 +1,2441 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+import static com.google.common.truth.Truth.assertThat;
+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;
+import static com.google.gson.stream.JsonToken.END_ARRAY;
+import static com.google.gson.stream.JsonToken.END_OBJECT;
+import static com.google.gson.stream.JsonToken.NAME;
+import static com.google.gson.stream.JsonToken.NULL;
+import static com.google.gson.stream.JsonToken.NUMBER;
+import static com.google.gson.stream.JsonToken.STRING;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.Strictness;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.Arrays;
+import org.junit.Ignore;
+import org.junit.Test;
+
+@SuppressWarnings("resource")
+public final class JsonReaderTest {
+
+  @SuppressWarnings("deprecation") // for JsonReader.setLenient
+  @Test
+  public void testSetLenientTrue() {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.setLenient(true);
+    assertThat(reader.getStrictness()).isEqualTo(Strictness.LENIENT);
+  }
+
+  @SuppressWarnings("deprecation") // for JsonReader.setLenient
+  @Test
+  public void testSetLenientFalse() {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.setLenient(false);
+    assertThat(reader.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT);
+  }
+
+  @Test
+  public void testSetStrictness() {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.setStrictness(Strictness.STRICT);
+    assertThat(reader.getStrictness()).isEqualTo(Strictness.STRICT);
+  }
+
+  @Test
+  public void testSetStrictnessNull() {
+    JsonReader reader = new JsonReader(reader("{}"));
+    assertThrows(NullPointerException.class, () -> reader.setStrictness(null));
+  }
+
+  @Test
+  public void testEscapedNewlineNotAllowedInStrictMode() throws IOException {
+    String json = "\"\\\n\"";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextString);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith("Cannot escape a newline character in strict mode");
+  }
+
+  @Test
+  public void testEscapedNewlineAllowedInDefaultMode() throws IOException {
+    String json = "\"\\\n\"";
+    JsonReader reader = new JsonReader(reader(json));
+    assertThat(reader.nextString()).isEqualTo("\n");
+  }
+
+  @Test
+  public void testStrictModeFailsToParseUnescapedControlCharacter() {
+    String json = "\"\0\"";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextString);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode");
+
+    json = "\"\t\"";
+    reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+
+    expected = assertThrows(IOException.class, reader::nextString);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode");
+
+    json = "\"\u001F\"";
+    reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+
+    expected = assertThrows(IOException.class, reader::nextString);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode");
+  }
+
+  @Test
+  public void testStrictModeAllowsOtherControlCharacters() throws IOException {
+    // JSON specification only forbids control characters U+0000 - U+001F, other control characters
+    // should be allowed
+    String json = "\"\u007F\u009F\"";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+    assertThat(reader.nextString()).isEqualTo("\u007F\u009F");
+  }
+
+  @Test
+  public void testNonStrictModeParsesUnescapedControlCharacter() throws IOException {
+    String json = "\"\t\"";
+    JsonReader reader = new JsonReader(reader(json));
+    assertThat(reader.nextString()).isEqualTo("\t");
+  }
+
+  @Test
+  public void testCapitalizedTrueFailWhenStrict() throws IOException {
+    JsonReader reader = new JsonReader(reader("TRUE"));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextBoolean);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+
+    reader = new JsonReader(reader("True"));
+    reader.setStrictness(Strictness.STRICT);
+
+    expected = assertThrows(IOException.class, reader::nextBoolean);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+  }
+
+  @Test
+  public void testCapitalizedFalseFailWhenStrict() throws IOException {
+    JsonReader reader = new JsonReader(reader("FALSE"));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextBoolean);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+
+    reader = new JsonReader(reader("FaLse"));
+    reader.setStrictness(Strictness.STRICT);
+
+    expected = assertThrows(IOException.class, reader::nextBoolean);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+  }
+
+  @Test
+  public void testCapitalizedNullFailWhenStrict() throws IOException {
+    JsonReader reader = new JsonReader(reader("NULL"));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextNull);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+
+    reader = new JsonReader(reader("nulL"));
+    reader.setStrictness(Strictness.STRICT);
+
+    expected = assertThrows(IOException.class, reader::nextNull);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON"
+                + " at line 1 column 1 path $");
+  }
+
+  @Test
+  public void testReadArray() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true, true]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testReadEmptyArray() throws IOException {
+    JsonReader reader = new JsonReader(reader("[]"));
+    reader.beginArray();
+    assertThat(reader.hasNext()).isFalse();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testReadObject() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\": \"android\", \"b\": \"banana\"}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("android");
+    assertThat(reader.nextName()).isEqualTo("b");
+    assertThat(reader.nextString()).isEqualTo("banana");
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testReadEmptyObject() throws IOException {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.beginObject();
+    assertThat(reader.hasNext()).isFalse();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testHasNextEndOfDocument() throws IOException {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.hasNext()).isFalse();
+  }
+
+  @Test
+  public void testSkipArray() throws IOException {
+    JsonReader reader =
+        new JsonReader(reader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("b");
+    assertThat(reader.nextInt()).isEqualTo(123);
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipArrayAfterPeek() throws Exception {
+    JsonReader reader =
+        new JsonReader(reader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.peek()).isEqualTo(BEGIN_ARRAY);
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("b");
+    assertThat(reader.nextInt()).isEqualTo(123);
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipTopLevelObject() throws Exception {
+    JsonReader reader =
+        new JsonReader(reader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"));
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipObject() throws IOException {
+    JsonReader reader =
+        new JsonReader(
+            reader("{\"a\": { \"c\": [], \"d\": [true, true, {}] }, \"b\": \"banana\"}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("b");
+    reader.skipValue();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipObjectAfterPeek() throws Exception {
+    String json =
+        "{"
+            + "  \"one\": { \"num\": 1 }"
+            + ", \"two\": { \"num\": 2 }"
+            + ", \"three\": { \"num\": 3 }"
+            + "}";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("one");
+    assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT);
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("two");
+    assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT);
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("three");
+    reader.skipValue();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipObjectName() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\": 1}"));
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.getPath()).isEqualTo("$.<skipped>");
+    assertThat(reader.nextInt()).isEqualTo(1);
+  }
+
+  @Test
+  public void testSkipObjectNameSingleQuoted() throws IOException {
+    JsonReader reader = new JsonReader(reader("{'a': 1}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.getPath()).isEqualTo("$.<skipped>");
+    assertThat(reader.nextInt()).isEqualTo(1);
+  }
+
+  @Test
+  public void testSkipObjectNameUnquoted() throws IOException {
+    JsonReader reader = new JsonReader(reader("{a: 1}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.NUMBER);
+    assertThat(reader.getPath()).isEqualTo("$.<skipped>");
+    assertThat(reader.nextInt()).isEqualTo(1);
+  }
+
+  @Test
+  public void testSkipInteger() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":123456789,\"b\":-123456789}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("b");
+    reader.skipValue();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipDouble() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":-123.456e-789,\"b\":123456789.0}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    reader.skipValue();
+    assertThat(reader.nextName()).isEqualTo("b");
+    reader.skipValue();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipValueAfterEndOfDocument() throws IOException {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    assertThat(reader.getPath()).isEqualTo("$");
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValueAtArrayEnd() throws IOException {
+    JsonReader reader = new JsonReader(reader("[]"));
+    reader.beginArray();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testSkipValueAtObjectEnd() throws IOException {
+    JsonReader reader = new JsonReader(reader("{}"));
+    reader.beginObject();
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    assertThat(reader.getPath()).isEqualTo("$");
+  }
+
+  @Test
+  public void testHelloWorld() throws IOException {
+    String json =
+        "{\n" //
+            + "   \"hello\": true,\n" //
+            + "   \"foo\": [\"world\"]\n" //
+            + "}";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("hello");
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextName()).isEqualTo("foo");
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("world");
+    reader.endArray();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testInvalidJsonInput() throws IOException {
+    String json =
+        "{\n" //
+            + "   \"h\\ello\": true,\n" //
+            + "   \"foo\": [\"world\"]\n" //
+            + "}";
+
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginObject();
+    try {
+      reader.nextName();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Invalid escape sequence at line 2 column 8 path $.\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @SuppressWarnings("unused")
+  @Test
+  public void testNulls() {
+    try {
+      new JsonReader(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testEmptyString() throws IOException {
+    try {
+      new JsonReader(reader("")).beginArray();
+      fail();
+    } catch (EOFException expected) {
+    }
+    try {
+      new JsonReader(reader("")).beginObject();
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test
+  public void testCharacterUnescaping() throws IOException {
+    String json =
+        "[\"a\","
+            + "\"a\\\"\","
+            + "\"\\\"\","
+            + "\":\","
+            + "\",\","
+            + "\"\\b\","
+            + "\"\\f\","
+            + "\"\\n\","
+            + "\"\\r\","
+            + "\"\\t\","
+            + "\" \","
+            + "\"\\\\\","
+            + "\"{\","
+            + "\"}\","
+            + "\"[\","
+            + "\"]\","
+            + "\"\\u0000\","
+            + "\"\\u0019\","
+            + "\"\\u20AC\""
+            + "]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("a\"");
+    assertThat(reader.nextString()).isEqualTo("\"");
+    assertThat(reader.nextString()).isEqualTo(":");
+    assertThat(reader.nextString()).isEqualTo(",");
+    assertThat(reader.nextString()).isEqualTo("\b");
+    assertThat(reader.nextString()).isEqualTo("\f");
+    assertThat(reader.nextString()).isEqualTo("\n");
+    assertThat(reader.nextString()).isEqualTo("\r");
+    assertThat(reader.nextString()).isEqualTo("\t");
+    assertThat(reader.nextString()).isEqualTo(" ");
+    assertThat(reader.nextString()).isEqualTo("\\");
+    assertThat(reader.nextString()).isEqualTo("{");
+    assertThat(reader.nextString()).isEqualTo("}");
+    assertThat(reader.nextString()).isEqualTo("[");
+    assertThat(reader.nextString()).isEqualTo("]");
+    assertThat(reader.nextString()).isEqualTo("\0");
+    assertThat(reader.nextString()).isEqualTo("\u0019");
+    assertThat(reader.nextString()).isEqualTo("\u20AC");
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testReaderDoesNotTreatU2028U2029AsNewline() throws IOException {
+    // This test shows that the JSON string [\n"whatever"] is seen as valid
+    // And the JSON string [\u2028"whatever"] is not.
+    String jsonInvalid2028 = "[\u2028\"whatever\"]";
+    JsonReader readerInvalid2028 = new JsonReader(reader(jsonInvalid2028));
+    readerInvalid2028.beginArray();
+    assertThrows(IOException.class, readerInvalid2028::nextString);
+
+    String jsonInvalid2029 = "[\u2029\"whatever\"]";
+    JsonReader readerInvalid2029 = new JsonReader(reader(jsonInvalid2029));
+    readerInvalid2029.beginArray();
+    assertThrows(IOException.class, readerInvalid2029::nextString);
+
+    String jsonValid = "[\n\"whatever\"]";
+    JsonReader readerValid = new JsonReader(reader(jsonValid));
+    readerValid.beginArray();
+    assertThat(readerValid.nextString()).isEqualTo("whatever");
+
+    // And even in STRICT mode U+2028 and U+2029 are not considered control characters
+    // and can appear unescaped in JSON string
+    String jsonValid2028And2029 = "\"whatever\u2028\u2029\"";
+    JsonReader readerValid2028And2029 = new JsonReader(reader(jsonValid2028And2029));
+    readerValid2028And2029.setStrictness(Strictness.STRICT);
+    assertThat(readerValid2028And2029.nextString()).isEqualTo("whatever\u2028\u2029");
+  }
+
+  @Test
+  public void testEscapeCharacterQuoteInStrictMode() throws IOException {
+    String json = "\"\\'\"";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.STRICT);
+
+    IOException expected = assertThrows(IOException.class, reader::nextString);
+    assertThat(expected)
+        .hasMessageThat()
+        .startsWith("Invalid escaped character \"'\" in strict mode");
+  }
+
+  @Test
+  public void testEscapeCharacterQuoteWithoutStrictMode() throws IOException {
+    String json = "\"\\'\"";
+    JsonReader reader = new JsonReader(reader(json));
+    assertThat(reader.nextString()).isEqualTo("'");
+  }
+
+  @Test
+  public void testUnescapingInvalidCharacters() throws IOException {
+    String json = "[\"\\u000g\"]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Malformed Unicode escape \\u000g at line 1 column 5 path $[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testUnescapingTruncatedCharacters() throws IOException {
+    String json = "[\"\\u000";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unterminated escape sequence at line 1 column 5 path $[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testUnescapingTruncatedSequence() throws IOException {
+    String json = "[\"\\";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unterminated escape sequence at line 1 column 4 path $[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testIntegersWithFractionalPartSpecified() throws IOException {
+    JsonReader reader = new JsonReader(reader("[1.0,1.0,1.0]"));
+    reader.beginArray();
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.nextLong()).isEqualTo(1L);
+  }
+
+  @Test
+  public void testDoubles() throws IOException {
+    String json =
+        "[-0.0,"
+            + "1.0,"
+            + "1.7976931348623157E308,"
+            + "4.9E-324,"
+            + "0.0,"
+            + "0.00,"
+            + "-0.5,"
+            + "2.2250738585072014E-308,"
+            + "3.141592653589793,"
+            + "2.718281828459045,"
+            + "0,"
+            + "0.01,"
+            + "0e0,"
+            + "1e+0,"
+            + "1e-0,"
+            + "1e0000," // leading 0 is allowed for exponent
+            + "1e00001,"
+            + "1e+1]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    assertThat(reader.nextDouble()).isEqualTo(-0.0);
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextDouble()).isEqualTo(1.7976931348623157E308);
+    assertThat(reader.nextDouble()).isEqualTo(4.9E-324);
+    assertThat(reader.nextDouble()).isEqualTo(0.0);
+    assertThat(reader.nextDouble()).isEqualTo(0.0);
+    assertThat(reader.nextDouble()).isEqualTo(-0.5);
+    assertThat(reader.nextDouble()).isEqualTo(2.2250738585072014E-308);
+    assertThat(reader.nextDouble()).isEqualTo(3.141592653589793);
+    assertThat(reader.nextDouble()).isEqualTo(2.718281828459045);
+    assertThat(reader.nextDouble()).isEqualTo(0.0);
+    assertThat(reader.nextDouble()).isEqualTo(0.01);
+    assertThat(reader.nextDouble()).isEqualTo(0.0);
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextDouble()).isEqualTo(10.0);
+    assertThat(reader.nextDouble()).isEqualTo(10.0);
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testStrictNonFiniteDoubles() throws IOException {
+    String json = "[NaN]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictQuotedNonFiniteDoubles() throws IOException {
+    String json = "[\"NaN\"]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "JSON forbids NaN and infinities: NaN at line 1 column 7 path $[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testLenientNonFiniteDoubles() throws IOException {
+    String json = "[NaN, -Infinity, Infinity]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextDouble()).isNaN();
+    assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY);
+    assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY);
+    reader.endArray();
+  }
+
+  @Test
+  public void testLenientQuotedNonFiniteDoubles() throws IOException {
+    String json = "[\"NaN\", \"-Infinity\", \"Infinity\"]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextDouble()).isNaN();
+    assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY);
+    assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY);
+    reader.endArray();
+  }
+
+  @Test
+  public void testStrictNonFiniteDoublesWithSkipValue() throws IOException {
+    String json = "[NaN]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLongs() throws IOException {
+    String json =
+        "[0,0,0," + "1,1,1," + "-1,-1,-1," + "-9223372036854775808," + "9223372036854775807]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    assertThat(reader.nextLong()).isEqualTo(0L);
+    assertThat(reader.nextInt()).isEqualTo(0);
+    assertThat(reader.nextDouble()).isEqualTo(0.0);
+    assertThat(reader.nextLong()).isEqualTo(1L);
+    assertThat(reader.nextInt()).isEqualTo(1);
+    assertThat(reader.nextDouble()).isEqualTo(1.0);
+    assertThat(reader.nextLong()).isEqualTo(-1L);
+    assertThat(reader.nextInt()).isEqualTo(-1);
+    assertThat(reader.nextDouble()).isEqualTo(-1.0);
+    try {
+      reader.nextInt();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextLong()).isEqualTo(Long.MIN_VALUE);
+    try {
+      reader.nextInt();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextLong()).isEqualTo(Long.MAX_VALUE);
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  @Ignore(
+      "JsonReader advances after exception for invalid number was thrown; to be decided if that is"
+          + " acceptable")
+  public void testNumberWithOctalPrefix() throws IOException {
+    String json = "[01]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+    try {
+      reader.nextInt();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "TODO");
+    }
+    try {
+      reader.nextLong();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "TODO");
+    }
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "TODO");
+    }
+    assertThat(reader.nextString()).isEqualTo("01");
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testBooleans() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true,false]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextBoolean()).isFalse();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testPeekingUnquotedStringsPrefixedWithBooleans() throws IOException {
+    JsonReader reader = new JsonReader(reader("[truey]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "a boolean", "STRING", "line 1 column 2 path $[0]");
+    }
+    assertThat(reader.nextString()).isEqualTo("truey");
+    reader.endArray();
+  }
+
+  @Test
+  public void testMalformedNumbers() throws IOException {
+    assertNotANumber("-");
+    assertNotANumber(".");
+
+    // plus sign is not allowed for integer part
+    assertNotANumber("+1");
+
+    // leading 0 is not allowed for integer part
+    assertNotANumber("00");
+    assertNotANumber("01");
+
+    // exponent lacks digit
+    assertNotANumber("e");
+    assertNotANumber("0e");
+    assertNotANumber(".e");
+    assertNotANumber("0.e");
+    assertNotANumber("-.0e");
+
+    // no integer
+    assertNotANumber("e1");
+    assertNotANumber(".e1");
+    assertNotANumber("-e1");
+
+    // trailing characters
+    assertNotANumber("1x");
+    assertNotANumber("1.1x");
+    assertNotANumber("1e1x");
+    assertNotANumber("1ex");
+    assertNotANumber("1.1ex");
+    assertNotANumber("1.1e1x");
+
+    // fraction has no digit
+    assertNotANumber("0.");
+    assertNotANumber("-0.");
+    assertNotANumber("0.e1");
+    assertNotANumber("-0.e1");
+
+    // no leading digit
+    assertNotANumber(".0");
+    assertNotANumber("-.0");
+    assertNotANumber(".0e1");
+    assertNotANumber("-.0e1");
+  }
+
+  private static void assertNotANumber(String s) throws IOException {
+    JsonReader reader = new JsonReader(reader(s));
+    reader.setStrictness(Strictness.LENIENT);
+    assertThat(reader.peek()).isEqualTo(JsonToken.STRING);
+    assertThat(reader.nextString()).isEqualTo(s);
+
+    JsonReader strictReader = new JsonReader(reader(s));
+    try {
+      strictReader.nextDouble();
+      fail("Should have failed reading " + s + " as double");
+    } catch (MalformedJsonException e) {
+      assertThat(e)
+          .hasMessageThat()
+          .startsWith("Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON");
+    }
+  }
+
+  @Test
+  public void testPeekingUnquotedStringsPrefixedWithIntegers() throws IOException {
+    JsonReader reader = new JsonReader(reader("[12.34e5x]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+    try {
+      reader.nextInt();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextString()).isEqualTo("12.34e5x");
+  }
+
+  @Test
+  public void testPeekLongMinValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[-9223372036854775808]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    assertThat(reader.nextLong()).isEqualTo(-9223372036854775808L);
+  }
+
+  @Test
+  public void testPeekLongMaxValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[9223372036854775807]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    assertThat(reader.nextLong()).isEqualTo(9223372036854775807L);
+  }
+
+  @Test
+  public void testLongLargerThanMaxLongThatWrapsAround() throws IOException {
+    JsonReader reader = new JsonReader(reader("[22233720368547758070]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+  }
+
+  @Test
+  public void testLongLargerThanMinLongThatWrapsAround() throws IOException {
+    JsonReader reader = new JsonReader(reader("[-22233720368547758070]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+  }
+
+  /** Issue 1053, negative zero. */
+  @Test
+  public void testNegativeZero() throws Exception {
+    JsonReader reader = new JsonReader(reader("[-0]"));
+    reader.setStrictness(Strictness.LEGACY_STRICT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    assertThat(reader.nextString()).isEqualTo("-0");
+  }
+
+  /**
+   * This test fails because there's no double for 9223372036854775808, and our long parsing uses
+   * Double.parseDouble() for fractional values.
+   */
+  @Test
+  @Ignore
+  public void testPeekLargerThanLongMaxValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[9223372036854775808]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException e) {
+    }
+  }
+
+  /**
+   * This test fails because there's no double for -9223372036854775809, and our long parsing uses
+   * Double.parseDouble() for fractional values.
+   */
+  @Test
+  @Ignore
+  public void testPeekLargerThanLongMinValue() throws IOException {
+    @SuppressWarnings("FloatingPointLiteralPrecision")
+    double d = -9223372036854775809d;
+    JsonReader reader = new JsonReader(reader("[-9223372036854775809]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextDouble()).isEqualTo(d);
+  }
+
+  /**
+   * This test fails because there's no double for 9223372036854775806, and our long parsing uses
+   * Double.parseDouble() for fractional values.
+   */
+  @Test
+  @Ignore
+  public void testHighPrecisionLong() throws IOException {
+    String json = "[9223372036854775806.000]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    assertThat(reader.nextLong()).isEqualTo(9223372036854775806L);
+    reader.endArray();
+  }
+
+  @Test
+  public void testPeekMuchLargerThanLongMinValue() throws IOException {
+    @SuppressWarnings("FloatingPointLiteralPrecision")
+    double d = -92233720368547758080d;
+    JsonReader reader = new JsonReader(reader("[-92233720368547758080]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(NUMBER);
+    try {
+      reader.nextLong();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextDouble()).isEqualTo(d);
+  }
+
+  @Test
+  public void testQuotedNumberWithEscape() throws IOException {
+    JsonReader reader = new JsonReader(reader("[\"12\\u00334\"]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+    assertThat(reader.nextInt()).isEqualTo(1234);
+  }
+
+  @Test
+  public void testMixedCaseLiterals() throws IOException {
+    JsonReader reader = new JsonReader(reader("[True,TruE,False,FALSE,NULL,nulL]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextBoolean()).isFalse();
+    assertThat(reader.nextBoolean()).isFalse();
+    reader.nextNull();
+    reader.nextNull();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testMissingValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected value at line 1 column 6 path $.a\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testPrematureEndOfInput() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":true,"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.nextName();
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test
+  public void testPrematurelyClosed() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":[]}"));
+    reader.beginObject();
+    reader.close();
+    try {
+      reader.nextName();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed");
+    }
+
+    reader = new JsonReader(reader("{\"a\":[]}"));
+    reader.close();
+    try {
+      reader.beginObject();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed");
+    }
+
+    reader = new JsonReader(reader("{\"a\":true}"));
+    reader.beginObject();
+    String unused1 = reader.nextName();
+    JsonToken unused2 = reader.peek();
+    reader.close();
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed");
+    }
+  }
+
+  @Test
+  public void testNextFailuresDoNotAdvance() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":true}"));
+    reader.beginObject();
+    try {
+      String unused = reader.nextString();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "a string", "NAME", "line 1 column 3 path $.");
+    }
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      String unused = reader.nextName();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "a name", "BOOLEAN", "line 1 column 10 path $.a");
+    }
+    try {
+      reader.beginArray();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "BEGIN_ARRAY", "BOOLEAN", "line 1 column 10 path $.a");
+    }
+    try {
+      reader.endArray();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "END_ARRAY", "BOOLEAN", "line 1 column 10 path $.a");
+    }
+    try {
+      reader.beginObject();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "BEGIN_OBJECT", "BOOLEAN", "line 1 column 10 path $.a");
+    }
+    try {
+      reader.endObject();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "END_OBJECT", "BOOLEAN", "line 1 column 10 path $.a");
+    }
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.nextString();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "a string", "END_OBJECT", "line 1 column 11 path $.a");
+    }
+    try {
+      reader.nextName();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "a name", "END_OBJECT", "line 1 column 11 path $.a");
+    }
+    try {
+      reader.beginArray();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "BEGIN_ARRAY", "END_OBJECT", "line 1 column 11 path $.a");
+    }
+    try {
+      reader.endArray();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(
+          expected, "END_ARRAY", "END_OBJECT", "line 1 column 11 path $.a");
+    }
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+    reader.close();
+  }
+
+  @Test
+  public void testIntegerMismatchFailuresDoNotAdvance() throws IOException {
+    JsonReader reader = new JsonReader(reader("[1.5]"));
+    reader.beginArray();
+    try {
+      reader.nextInt();
+      fail();
+    } catch (NumberFormatException expected) {
+    }
+    assertThat(reader.nextDouble()).isEqualTo(1.5d);
+    reader.endArray();
+  }
+
+  @Test
+  public void testStringNullIsNotNull() throws IOException {
+    JsonReader reader = new JsonReader(reader("[\"null\"]"));
+    reader.beginArray();
+    try {
+      reader.nextNull();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "null", "STRING", "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testNullLiteralIsNotAString() throws IOException {
+    JsonReader reader = new JsonReader(reader("[null]"));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertUnexpectedStructureError(expected, "a string", "NULL", "line 1 column 6 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictNameValueSeparator() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\"=true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+
+    reader = new JsonReader(reader("{\"a\"=>true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+  }
+
+  @Test
+  public void testLenientNameValueSeparator() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\"=true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextBoolean()).isTrue();
+
+    reader = new JsonReader(reader("{\"a\"=>true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextBoolean()).isTrue();
+  }
+
+  @Test
+  public void testStrictNameValueSeparatorWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\"=true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+
+    reader = new JsonReader(reader("{\"a\"=>true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+  }
+
+  @Test
+  public void testCommentsInStringValue() throws Exception {
+    JsonReader reader = new JsonReader(reader("[\"// comment\"]"));
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("// comment");
+    reader.endArray();
+
+    reader = new JsonReader(reader("{\"a\":\"#someComment\"}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("#someComment");
+    reader.endObject();
+
+    reader = new JsonReader(reader("{\"#//a\":\"#some //Comment\"}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("#//a");
+    assertThat(reader.nextString()).isEqualTo("#some //Comment");
+    reader.endObject();
+  }
+
+  @Test
+  public void testStrictComments() throws IOException {
+    JsonReader reader = new JsonReader(reader("[// comment \n true]"));
+    reader.beginArray();
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[# comment \n true]"));
+    reader.beginArray();
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[/* comment */ true]"));
+    reader.beginArray();
+    try {
+      reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientComments() throws IOException {
+    JsonReader reader = new JsonReader(reader("[// comment \n true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+
+    reader = new JsonReader(reader("[# comment \n true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+
+    reader = new JsonReader(reader("[/* comment */ true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+  }
+
+  @Test
+  public void testStrictCommentsWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[// comment \n true]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[# comment \n true]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[/* comment */ true]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictUnquotedNames() throws IOException {
+    JsonReader reader = new JsonReader(reader("{a:true}"));
+    reader.beginObject();
+    try {
+      reader.nextName();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $.");
+    }
+  }
+
+  @Test
+  public void testLenientUnquotedNames() throws IOException {
+    JsonReader reader = new JsonReader(reader("{a:true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+  }
+
+  @Test
+  public void testStrictUnquotedNamesWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("{a:true}"));
+    reader.beginObject();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $.");
+    }
+  }
+
+  @Test
+  public void testStrictSingleQuotedNames() throws IOException {
+    JsonReader reader = new JsonReader(reader("{'a':true}"));
+    reader.beginObject();
+    try {
+      reader.nextName();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $.");
+    }
+  }
+
+  @Test
+  public void testLenientSingleQuotedNames() throws IOException {
+    JsonReader reader = new JsonReader(reader("{'a':true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+  }
+
+  @Test
+  public void testStrictSingleQuotedNamesWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("{'a':true}"));
+    reader.beginObject();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $.");
+    }
+  }
+
+  @Test
+  public void testStrictUnquotedStrings() throws IOException {
+    JsonReader reader = new JsonReader(reader("[a]"));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictUnquotedStringsWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[a]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientUnquotedStrings() throws IOException {
+    JsonReader reader = new JsonReader(reader("[a]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("a");
+  }
+
+  @Test
+  public void testStrictSingleQuotedStrings() throws IOException {
+    JsonReader reader = new JsonReader(reader("['a']"));
+    reader.beginArray();
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientSingleQuotedStrings() throws IOException {
+    JsonReader reader = new JsonReader(reader("['a']"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo("a");
+  }
+
+  @Test
+  public void testStrictSingleQuotedStringsWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("['a']"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictSemicolonDelimitedArray() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true;true]"));
+    reader.beginArray();
+    try {
+      boolean unused = reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientSemicolonDelimitedArray() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true;true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextBoolean()).isTrue();
+  }
+
+  @Test
+  public void testStrictSemicolonDelimitedArrayWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true;true]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictSemicolonDelimitedNameValuePair() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      boolean unused = reader.nextBoolean();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+  }
+
+  @Test
+  public void testLenientSemicolonDelimitedNameValuePair() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.nextName()).isEqualTo("b");
+  }
+
+  @Test
+  public void testStrictSemicolonDelimitedNameValuePairWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":true;\"b\":true}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 6 path $.a");
+    }
+  }
+
+  @Test
+  public void testStrictUnnecessaryArraySeparators() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true,,true]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.nextNull();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 8 path $[1]");
+    }
+
+    reader = new JsonReader(reader("[,true]"));
+    reader.beginArray();
+    try {
+      reader.nextNull();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[true,]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.nextNull();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 8 path $[1]");
+    }
+
+    reader = new JsonReader(reader("[,]"));
+    reader.beginArray();
+    try {
+      reader.nextNull();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientUnnecessaryArraySeparators() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true,,true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.nextNull();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.endArray();
+
+    reader = new JsonReader(reader("[,true]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.nextNull();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.endArray();
+
+    reader = new JsonReader(reader("[true,]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.nextNull();
+    reader.endArray();
+
+    reader = new JsonReader(reader("[,]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.nextNull();
+    reader.nextNull();
+    reader.endArray();
+  }
+
+  @Test
+  public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[true,,true]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 8 path $[1]");
+    }
+
+    reader = new JsonReader(reader("[,true]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+
+    reader = new JsonReader(reader("[true,]"));
+    reader.beginArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 8 path $[1]");
+    }
+
+    reader = new JsonReader(reader("[,]"));
+    reader.beginArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 3 path $[0]");
+    }
+  }
+
+  @Test
+  public void testStrictMultipleTopLevelValues() throws IOException {
+    JsonReader reader = new JsonReader(reader("[] []"));
+    reader.beginArray();
+    reader.endArray();
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 5 path $");
+    }
+  }
+
+  @Test
+  public void testLenientMultipleTopLevelValues() throws IOException {
+    JsonReader reader = new JsonReader(reader("[] true {}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.nextBoolean()).isTrue();
+    reader.beginObject();
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("[] []"));
+    reader.beginArray();
+    reader.endArray();
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 5 path $");
+    }
+  }
+
+  @Test
+  public void testTopLevelValueTypes() throws IOException {
+    JsonReader reader1 = new JsonReader(reader("true"));
+    assertThat(reader1.nextBoolean()).isTrue();
+    assertThat(reader1.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    JsonReader reader2 = new JsonReader(reader("false"));
+    assertThat(reader2.nextBoolean()).isFalse();
+    assertThat(reader2.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    JsonReader reader3 = new JsonReader(reader("null"));
+    assertThat(reader3.peek()).isEqualTo(JsonToken.NULL);
+    reader3.nextNull();
+    assertThat(reader3.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    JsonReader reader4 = new JsonReader(reader("123"));
+    assertThat(reader4.nextInt()).isEqualTo(123);
+    assertThat(reader4.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    JsonReader reader5 = new JsonReader(reader("123.4"));
+    assertThat(reader5.nextDouble()).isEqualTo(123.4);
+    assertThat(reader5.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+
+    JsonReader reader6 = new JsonReader(reader("\"a\""));
+    assertThat(reader6.nextString()).isEqualTo("a");
+    assertThat(reader6.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testTopLevelValueTypeWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader("true"));
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testStrictNonExecutePrefix() throws IOException {
+    JsonReader reader = new JsonReader(reader(")]}'\n []"));
+    try {
+      reader.beginArray();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 1 path $");
+    }
+  }
+
+  @Test
+  public void testStrictNonExecutePrefixWithSkipValue() throws IOException {
+    JsonReader reader = new JsonReader(reader(")]}'\n []"));
+    try {
+      reader.skipValue();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 1 path $");
+    }
+  }
+
+  @Test
+  public void testLenientNonExecutePrefix() throws IOException {
+    JsonReader reader = new JsonReader(reader(")]}'\n []"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testLenientNonExecutePrefixWithLeadingWhitespace() throws IOException {
+    JsonReader reader = new JsonReader(reader("\r\n \t)]}'\n []"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testLenientPartialNonExecutePrefix() throws IOException {
+    JsonReader reader = new JsonReader(reader(")]}' []"));
+    reader.setStrictness(Strictness.LENIENT);
+    assertThat(reader.nextString()).isEqualTo(")");
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unexpected value at line 1 column 3 path $\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testBomIgnoredAsFirstCharacterOfDocument() throws IOException {
+    JsonReader reader = new JsonReader(reader("\ufeff[]"));
+    reader.beginArray();
+    reader.endArray();
+  }
+
+  @Test
+  public void testBomForbiddenAsOtherCharacterInDocument() throws IOException {
+    JsonReader reader = new JsonReader(reader("[\ufeff]"));
+    reader.beginArray();
+    try {
+      reader.endArray();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @SuppressWarnings("UngroupedOverloads")
+  @Test
+  public void testFailWithPosition() throws IOException {
+    testFailWithPosition("Expected value at line 6 column 5 path $[1]", "[\n\n\n\n\n\"a\",}]");
+  }
+
+  @Test
+  public void testFailWithPositionGreaterThanBufferSize() throws IOException {
+    String spaces = repeat(' ', 8192);
+    testFailWithPosition(
+        "Expected value at line 6 column 5 path $[1]", "[\n\n" + spaces + "\n\n\n\"a\",}]");
+  }
+
+  @Test
+  public void testFailWithPositionOverSlashSlashEndOfLineComment() throws IOException {
+    testFailWithPosition(
+        "Expected value at line 5 column 6 path $[1]", "\n// foo\n\n//bar\r\n[\"a\",}");
+  }
+
+  @Test
+  public void testFailWithPositionOverHashEndOfLineComment() throws IOException {
+    testFailWithPosition(
+        "Expected value at line 5 column 6 path $[1]", "\n# foo\n\n#bar\r\n[\"a\",}");
+  }
+
+  @Test
+  public void testFailWithPositionOverCStyleComment() throws IOException {
+    testFailWithPosition(
+        "Expected value at line 6 column 12 path $[1]", "\n\n/* foo\n*\n*\r\nbar */[\"a\",}");
+  }
+
+  @Test
+  public void testFailWithPositionOverQuotedString() throws IOException {
+    testFailWithPosition(
+        "Expected value at line 5 column 3 path $[1]", "[\"foo\nbar\r\nbaz\n\",\n  }");
+  }
+
+  @Test
+  public void testFailWithPositionOverUnquotedString() throws IOException {
+    testFailWithPosition("Expected value at line 5 column 2 path $[1]", "[\n\nabcd\n\n,}");
+  }
+
+  @Test
+  public void testFailWithEscapedNewlineCharacter() throws IOException {
+    testFailWithPosition("Expected value at line 5 column 3 path $[1]", "[\n\n\"\\\n\n\",}");
+  }
+
+  @Test
+  public void testFailWithPositionIsOffsetByBom() throws IOException {
+    testFailWithPosition("Expected value at line 1 column 6 path $[1]", "\ufeff[\"a\",}]");
+  }
+
+  private static void testFailWithPosition(String message, String json) throws IOException {
+    // Validate that it works reading the string normally.
+    JsonReader reader1 = new JsonReader(reader(json));
+    reader1.setStrictness(Strictness.LENIENT);
+    reader1.beginArray();
+    String unused1 = reader1.nextString();
+    try {
+      JsonToken unused2 = reader1.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              message
+                  + "\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+
+    // Also validate that it works when skipping.
+    JsonReader reader2 = new JsonReader(reader(json));
+    reader2.setStrictness(Strictness.LENIENT);
+    reader2.beginArray();
+    reader2.skipValue();
+    try {
+      JsonToken unused3 = reader2.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              message
+                  + "\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testFailWithPositionDeepPath() throws IOException {
+    JsonReader reader = new JsonReader(reader("[1,{\"a\":[2,3,}"));
+    reader.beginArray();
+    int unused1 = reader.nextInt();
+    reader.beginObject();
+    String unused2 = reader.nextName();
+    reader.beginArray();
+    int unused3 = reader.nextInt();
+    int unused4 = reader.nextInt();
+    try {
+      JsonToken unused5 = reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected value at line 1 column 14 path $[1].a[2]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testStrictVeryLongNumber() throws IOException {
+    JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]"));
+    reader.beginArray();
+    try {
+      reader.nextDouble();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertStrictError(expected, "line 1 column 2 path $[0]");
+    }
+  }
+
+  @Test
+  public void testLenientVeryLongNumber() throws IOException {
+    JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.STRING);
+    assertThat(reader.nextDouble()).isEqualTo(1d);
+    reader.endArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testVeryLongUnquotedLiteral() throws IOException {
+    String literal = "a" + repeat('b', 8192) + "c";
+    JsonReader reader = new JsonReader(reader("[" + literal + "]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo(literal);
+    reader.endArray();
+  }
+
+  @Test
+  public void testDeeplyNestedArrays() throws IOException {
+    // this is nested 40 levels deep; Gson is tuned for nesting is 30 levels deep or fewer
+    JsonReader reader =
+        new JsonReader(
+            reader(
+                "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"));
+    for (int i = 0; i < 40; i++) {
+      reader.beginArray();
+    }
+    assertThat(reader.getPath())
+        .isEqualTo(
+            "$[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]"
+                + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]");
+    for (int i = 0; i < 40; i++) {
+      reader.endArray();
+    }
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testDeeplyNestedObjects() throws IOException {
+    // Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 40 levels deep
+    String array = "{\"a\":%s}";
+    String json = "true";
+    for (int i = 0; i < 40; i++) {
+      json = String.format(array, json);
+    }
+
+    JsonReader reader = new JsonReader(reader(json));
+    for (int i = 0; i < 40; i++) {
+      reader.beginObject();
+      assertThat(reader.nextName()).isEqualTo("a");
+    }
+    assertThat(reader.getPath())
+        .isEqualTo(
+            "$.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a"
+                + ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a");
+    assertThat(reader.nextBoolean()).isTrue();
+    for (int i = 0; i < 40; i++) {
+      reader.endObject();
+    }
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  // http://code.google.com/p/google-gson/issues/detail?id=409
+  @Test
+  public void testStringEndingInSlash() throws IOException {
+    JsonReader reader = new JsonReader(reader("/"));
+    reader.setStrictness(Strictness.LENIENT);
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected value at line 1 column 1 path $\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testDocumentWithCommentEndingInSlash() throws IOException {
+    JsonReader reader = new JsonReader(reader("/* foo *//"));
+    reader.setStrictness(Strictness.LENIENT);
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected value at line 1 column 10 path $\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testStringWithLeadingSlash() throws IOException {
+    JsonReader reader = new JsonReader(reader("/x"));
+    reader.setStrictness(Strictness.LENIENT);
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected value at line 1 column 1 path $\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testUnterminatedObject() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":\"android\"x"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("android");
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unterminated object at line 1 column 16 path $.a\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testVeryLongQuotedString() throws IOException {
+    char[] stringChars = new char[1024 * 16];
+    Arrays.fill(stringChars, 'x');
+    String string = new String(stringChars);
+    String json = "[\"" + string + "\"]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo(string);
+    reader.endArray();
+  }
+
+  @Test
+  public void testVeryLongUnquotedString() throws IOException {
+    char[] stringChars = new char[1024 * 16];
+    Arrays.fill(stringChars, 'x');
+    String string = new String(stringChars);
+    String json = "[" + string + "]";
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo(string);
+    reader.endArray();
+  }
+
+  @Test
+  public void testVeryLongUnterminatedString() throws IOException {
+    char[] stringChars = new char[1024 * 16];
+    Arrays.fill(stringChars, 'x');
+    String string = new String(stringChars);
+    String json = "[" + string;
+    JsonReader reader = new JsonReader(reader(json));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.nextString()).isEqualTo(string);
+    try {
+      reader.peek();
+      fail();
+    } catch (EOFException expected) {
+    }
+  }
+
+  @Test
+  public void testSkipVeryLongUnquotedString() throws IOException {
+    JsonReader reader = new JsonReader(reader("[" + repeat('x', 8192) + "]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    reader.skipValue();
+    reader.endArray();
+  }
+
+  @Test
+  public void testSkipTopLevelUnquotedString() throws IOException {
+    JsonReader reader = new JsonReader(reader(repeat('x', 8192)));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testSkipVeryLongQuotedString() throws IOException {
+    JsonReader reader = new JsonReader(reader("[\"" + repeat('x', 8192) + "\"]"));
+    reader.beginArray();
+    reader.skipValue();
+    reader.endArray();
+  }
+
+  @Test
+  public void testSkipTopLevelQuotedString() throws IOException {
+    JsonReader reader = new JsonReader(reader("\"" + repeat('x', 8192) + "\""));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.skipValue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testStringAsNumberWithTruncatedExponent() throws IOException {
+    JsonReader reader = new JsonReader(reader("[123e]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+  }
+
+  @Test
+  public void testStringAsNumberWithDigitAndNonDigitExponent() throws IOException {
+    JsonReader reader = new JsonReader(reader("[123e4b]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+  }
+
+  @Test
+  public void testStringAsNumberWithNonDigitExponent() throws IOException {
+    JsonReader reader = new JsonReader(reader("[123eb]"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(STRING);
+  }
+
+  @Test
+  public void testEmptyStringName() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"\":true}"));
+    reader.setStrictness(Strictness.LENIENT);
+    assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT);
+    reader.beginObject();
+    assertThat(reader.peek()).isEqualTo(NAME);
+    assertThat(reader.nextName()).isEqualTo("");
+    assertThat(reader.peek()).isEqualTo(JsonToken.BOOLEAN);
+    assertThat(reader.nextBoolean()).isTrue();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_OBJECT);
+    reader.endObject();
+    assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT);
+  }
+
+  @Test
+  public void testStrictExtraCommasInMaps() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":\"b\",}"));
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("b");
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected name at line 1 column 11 path $.a\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  @Test
+  public void testLenientExtraCommasInMaps() throws IOException {
+    JsonReader reader = new JsonReader(reader("{\"a\":\"b\",}"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginObject();
+    assertThat(reader.nextName()).isEqualTo("a");
+    assertThat(reader.nextString()).isEqualTo("b");
+    try {
+      reader.peek();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Expected name at line 1 column 11 path $.a\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  private static String repeat(char c, int count) {
+    char[] array = new char[count];
+    Arrays.fill(array, c);
+    return new String(array);
+  }
+
+  @Test
+  public void testMalformedDocuments() throws IOException {
+    assertDocument("{]", BEGIN_OBJECT, MalformedJsonException.class);
+    assertDocument("{,", BEGIN_OBJECT, MalformedJsonException.class);
+    assertDocument("{{", BEGIN_OBJECT, MalformedJsonException.class);
+    assertDocument("{[", BEGIN_OBJECT, MalformedJsonException.class);
+    assertDocument("{:", BEGIN_OBJECT, MalformedJsonException.class);
+    assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument(
+        "{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class);
+    assertDocument(
+        "{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class);
+    assertDocument(
+        "{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class);
+    assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class);
+    assertDocument("{\"name\"=>\"string\",\"name\"", BEGIN_OBJECT, NAME, STRING, NAME);
+    assertDocument("[}", BEGIN_ARRAY, MalformedJsonException.class);
+    assertDocument("[,]", BEGIN_ARRAY, NULL, NULL, END_ARRAY);
+    assertDocument("{", BEGIN_OBJECT, EOFException.class);
+    assertDocument("{\"name\"", BEGIN_OBJECT, NAME, EOFException.class);
+    assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{'name'", BEGIN_OBJECT, NAME, EOFException.class);
+    assertDocument("{'name',", BEGIN_OBJECT, NAME, MalformedJsonException.class);
+    assertDocument("{name", BEGIN_OBJECT, NAME, EOFException.class);
+    assertDocument("[", BEGIN_ARRAY, EOFException.class);
+    assertDocument("[string", BEGIN_ARRAY, STRING, EOFException.class);
+    assertDocument("[\"string\"", BEGIN_ARRAY, STRING, EOFException.class);
+    assertDocument("['string'", BEGIN_ARRAY, STRING, EOFException.class);
+    assertDocument("[123", BEGIN_ARRAY, NUMBER, EOFException.class);
+    assertDocument("[123,", BEGIN_ARRAY, NUMBER, EOFException.class);
+    assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, EOFException.class);
+    assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, EOFException.class);
+    assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, EOFException.class);
+    assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class);
+    assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, EOFException.class);
+    assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, EOFException.class);
+    assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, EOFException.class);
+    assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, MalformedJsonException.class);
+  }
+
+  /**
+   * This test behaves slightly differently in Gson 2.2 and earlier. It fails during peek rather
+   * than during nextString().
+   */
+  @Test
+  public void testUnterminatedStringFailure() throws IOException {
+    JsonReader reader = new JsonReader(reader("[\"string"));
+    reader.setStrictness(Strictness.LENIENT);
+    reader.beginArray();
+    assertThat(reader.peek()).isEqualTo(JsonToken.STRING);
+    try {
+      reader.nextString();
+      fail();
+    } catch (MalformedJsonException expected) {
+      assertThat(expected)
+          .hasMessageThat()
+          .isEqualTo(
+              "Unterminated string at line 1 column 9 path $[0]\n"
+                  + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+    }
+  }
+
+  /** Regression test for an issue with buffer filling and consumeNonExecutePrefix. */
+  @Test
+  public void testReadAcrossBuffers() throws IOException {
+    StringBuilder sb = new StringBuilder("#");
+    for (int i = 0; i < JsonReader.BUFFER_SIZE - 3; i++) {
+      sb.append(' ');
+    }
+    sb.append("\n)]}'\n3");
+    JsonReader reader = new JsonReader(reader(sb.toString()));
+    reader.setStrictness(Strictness.LENIENT);
+    JsonToken token = reader.peek();
+    assertThat(token).isEqualTo(JsonToken.NUMBER);
+  }
+
+  private static void assertStrictError(MalformedJsonException exception, String expectedLocation) {
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Use JsonReader.setStrictness(Strictness.LENIENT) to accept malformed JSON at "
+                + expectedLocation
+                + "\n"
+                + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json");
+  }
+
+  private static void assertUnexpectedStructureError(
+      IllegalStateException exception,
+      String expectedToken,
+      String actualToken,
+      String expectedLocation) {
+    String troubleshootingId =
+        actualToken.equals("NULL") ? "adapter-not-null-safe" : "unexpected-json-structure";
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Expected "
+                + expectedToken
+                + " but was "
+                + actualToken
+                + " at "
+                + expectedLocation
+                + "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#"
+                + troubleshootingId);
+  }
+
+  private static void assertDocument(String document, Object... expectations) throws IOException {
+    JsonReader reader = new JsonReader(reader(document));
+    reader.setStrictness(Strictness.LENIENT);
+    for (Object expectation : expectations) {
+      if (expectation == BEGIN_OBJECT) {
+        reader.beginObject();
+      } else if (expectation == BEGIN_ARRAY) {
+        reader.beginArray();
+      } else if (expectation == END_OBJECT) {
+        reader.endObject();
+      } else if (expectation == END_ARRAY) {
+        reader.endArray();
+      } else if (expectation == NAME) {
+        assertThat(reader.nextName()).isEqualTo("name");
+      } else if (expectation == BOOLEAN) {
+        assertThat(reader.nextBoolean()).isFalse();
+      } else if (expectation == STRING) {
+        assertThat(reader.nextString()).isEqualTo("string");
+      } else if (expectation == NUMBER) {
+        assertThat(reader.nextInt()).isEqualTo(123);
+      } else if (expectation == NULL) {
+        reader.nextNull();
+      } else if (expectation instanceof Class
+          && Exception.class.isAssignableFrom((Class<?>) expectation)) {
+        try {
+          reader.peek();
+          fail();
+        } catch (Exception expected) {
+          assertThat(expected.getClass()).isEqualTo((Class<?>) expectation);
+        }
+      } else {
+        throw new AssertionError("Unsupported expectation value: " + expectation);
+      }
+    }
+  }
+
+  /** Returns a reader that returns one character at a time. */
+  private static Reader reader(final String s) {
+    /* if (true) */ return new StringReader(s);
+    /* return new Reader() {
+      int position = 0;
+      @Override public int read(char[] buffer, int offset, int count) throws IOException {
+        if (position == s.length()) {
+          return -1;
+        } else if (count > 0) {
+          buffer[offset] = s.charAt(position++);
+          return 1;
+        } else {
+          throw new IllegalArgumentException();
+        }
+      }
+      @Override public void close() throws IOException {
+      }
+    }; */
+  }
+}
diff --git a/gson/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..13857696e054b79539163c116815eb0a1c0ab4e6
--- /dev/null
+++ b/gson/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java
@@ -0,0 +1,1031 @@
+/*
+ * Copyright (C) 2010 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.stream;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import com.google.gson.FormattingStyle;
+import com.google.gson.Strictness;
+import com.google.gson.internal.LazilyParsedNumber;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.junit.Test;
+
+@SuppressWarnings("resource")
+public final class JsonWriterTest {
+
+  @Test
+  public void testDefaultStrictness() throws IOException {
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT);
+    jsonWriter.value(false);
+    jsonWriter.close();
+  }
+
+  @SuppressWarnings("deprecation") // for JsonWriter.setLenient
+  @Test
+  public void testSetLenientTrue() throws IOException {
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.setLenient(true);
+    assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LENIENT);
+    jsonWriter.value(false);
+    jsonWriter.close();
+  }
+
+  @SuppressWarnings("deprecation") // for JsonWriter.setLenient
+  @Test
+  public void testSetLenientFalse() throws IOException {
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.setLenient(false);
+    assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT);
+    jsonWriter.value(false);
+    jsonWriter.close();
+  }
+
+  @Test
+  public void testSetStrictness() throws IOException {
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    jsonWriter.setStrictness(Strictness.STRICT);
+    assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.STRICT);
+    jsonWriter.value(false);
+    jsonWriter.close();
+  }
+
+  @Test
+  public void testSetStrictnessNull() throws IOException {
+    JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+    assertThrows(NullPointerException.class, () -> jsonWriter.setStrictness(null));
+    jsonWriter.value(false);
+    jsonWriter.close();
+  }
+
+  @Test
+  public void testTopLevelValueTypes() throws IOException {
+    StringWriter string1 = new StringWriter();
+    JsonWriter writer1 = new JsonWriter(string1);
+    writer1.value(true);
+    writer1.close();
+    assertThat(string1.toString()).isEqualTo("true");
+
+    StringWriter string2 = new StringWriter();
+    JsonWriter writer2 = new JsonWriter(string2);
+    writer2.nullValue();
+    writer2.close();
+    assertThat(string2.toString()).isEqualTo("null");
+
+    StringWriter string3 = new StringWriter();
+    JsonWriter writer3 = new JsonWriter(string3);
+    writer3.value(123);
+    writer3.close();
+    assertThat(string3.toString()).isEqualTo("123");
+
+    StringWriter string4 = new StringWriter();
+    JsonWriter writer4 = new JsonWriter(string4);
+    writer4.value(123.4);
+    writer4.close();
+    assertThat(string4.toString()).isEqualTo("123.4");
+
+    StringWriter string5 = new StringWriter();
+    JsonWriter writert = new JsonWriter(string5);
+    writert.value("a");
+    writert.close();
+    assertThat(string5.toString()).isEqualTo("\"a\"");
+  }
+
+  @Test
+  public void testNameAsTopLevelValue() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    IllegalStateException e =
+        assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+
+    jsonWriter.value(12);
+    jsonWriter.close();
+
+    e = assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("JsonWriter is closed.");
+  }
+
+  @Test
+  public void testNameInArray() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+
+    jsonWriter.beginArray();
+    IllegalStateException e =
+        assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+
+    jsonWriter.value(12);
+    e = assertThrows(IllegalStateException.class, () -> jsonWriter.name("hello"));
+    assertThat(e).hasMessageThat().isEqualTo("Please begin an object before writing a name.");
+
+    jsonWriter.endArray();
+    jsonWriter.close();
+
+    assertThat(stringWriter.toString()).isEqualTo("[12]");
+  }
+
+  @Test
+  public void testTwoNames() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    try {
+      jsonWriter.name("a");
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Already wrote a name, expecting a value.");
+    }
+  }
+
+  @Test
+  public void testNameWithoutValue() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    try {
+      jsonWriter.endObject();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Dangling name: a");
+    }
+  }
+
+  @Test
+  public void testValueWithoutName() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    try {
+      jsonWriter.value(true);
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Nesting problem.");
+    }
+  }
+
+  @Test
+  public void testMultipleTopLevelValues() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray().endArray();
+
+    IllegalStateException expected =
+        assertThrows(IllegalStateException.class, jsonWriter::beginArray);
+    assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
+  }
+
+  @Test
+  public void testMultipleTopLevelValuesStrict() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.STRICT);
+    jsonWriter.beginArray().endArray();
+
+    IllegalStateException expected =
+        assertThrows(IllegalStateException.class, jsonWriter::beginArray);
+    assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value.");
+  }
+
+  @Test
+  public void testMultipleTopLevelValuesLenient() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.setStrictness(Strictness.LENIENT);
+    writer.beginArray();
+    writer.endArray();
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    assertThat(stringWriter.toString()).isEqualTo("[][]");
+  }
+
+  @Test
+  public void testBadNestingObject() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.beginObject();
+    try {
+      jsonWriter.endArray();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Nesting problem.");
+    }
+  }
+
+  @Test
+  public void testBadNestingArray() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.beginArray();
+    try {
+      jsonWriter.endObject();
+      fail();
+    } catch (IllegalStateException expected) {
+      assertThat(expected).hasMessageThat().isEqualTo("Nesting problem.");
+    }
+  }
+
+  @Test
+  public void testNullName() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    try {
+      jsonWriter.name(null);
+      fail();
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  @Test
+  public void testNullStringValue() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    jsonWriter.value((String) null);
+    jsonWriter.endObject();
+    assertThat(stringWriter.toString()).isEqualTo("{\"a\":null}");
+  }
+
+  @Test
+  public void testJsonValue() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    jsonWriter.jsonValue("{\"b\":true}");
+    jsonWriter.name("c");
+    jsonWriter.value(1);
+    jsonWriter.endObject();
+    assertThat(stringWriter.toString()).isEqualTo("{\"a\":{\"b\":true},\"c\":1}");
+  }
+
+  private static void assertNonFiniteFloatsExceptions(JsonWriter jsonWriter) throws IOException {
+    jsonWriter.beginArray();
+
+    IllegalArgumentException expected =
+        assertThrows(IllegalArgumentException.class, () -> jsonWriter.value(Float.NaN));
+    assertThat(expected).hasMessageThat().isEqualTo("Numeric values must be finite, but was NaN");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class, () -> jsonWriter.value(Float.NEGATIVE_INFINITY));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was -Infinity");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class, () -> jsonWriter.value(Float.POSITIVE_INFINITY));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was Infinity");
+  }
+
+  @Test
+  public void testNonFiniteFloats() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    assertNonFiniteFloatsExceptions(jsonWriter);
+  }
+
+  @Test
+  public void testNonFiniteFloatsWhenStrict() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.STRICT);
+    assertNonFiniteFloatsExceptions(jsonWriter);
+  }
+
+  private static void assertNonFiniteDoublesExceptions(JsonWriter jsonWriter) throws IOException {
+    jsonWriter.beginArray();
+
+    IllegalArgumentException expected =
+        assertThrows(IllegalArgumentException.class, () -> jsonWriter.value(Double.NaN));
+    assertThat(expected).hasMessageThat().isEqualTo("Numeric values must be finite, but was NaN");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class, () -> jsonWriter.value(Double.NEGATIVE_INFINITY));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was -Infinity");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class, () -> jsonWriter.value(Double.POSITIVE_INFINITY));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was Infinity");
+  }
+
+  @Test
+  public void testNonFiniteDoubles() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    assertNonFiniteDoublesExceptions(jsonWriter);
+  }
+
+  @Test
+  public void testNonFiniteDoublesWhenStrict() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.STRICT);
+    assertNonFiniteDoublesExceptions(jsonWriter);
+  }
+
+  private static void assertNonFiniteNumbersExceptions(JsonWriter jsonWriter) throws IOException {
+    jsonWriter.beginArray();
+
+    IllegalArgumentException expected =
+        assertThrows(
+            IllegalArgumentException.class, () -> jsonWriter.value(Double.valueOf(Double.NaN)));
+    assertThat(expected).hasMessageThat().isEqualTo("Numeric values must be finite, but was NaN");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> jsonWriter.value(Double.valueOf(Double.NEGATIVE_INFINITY)));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was -Infinity");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> jsonWriter.value(Double.valueOf(Double.POSITIVE_INFINITY)));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was Infinity");
+
+    expected =
+        assertThrows(
+            IllegalArgumentException.class,
+            () -> jsonWriter.value(new LazilyParsedNumber("Infinity")));
+    assertThat(expected)
+        .hasMessageThat()
+        .isEqualTo("Numeric values must be finite, but was Infinity");
+  }
+
+  @Test
+  public void testNonFiniteNumbers() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    assertNonFiniteNumbersExceptions(jsonWriter);
+  }
+
+  @Test
+  public void testNonFiniteNumbersWhenStrict() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.STRICT);
+    assertNonFiniteNumbersExceptions(jsonWriter);
+  }
+
+  @Test
+  public void testNonFiniteFloatsWhenLenient() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.LENIENT);
+    jsonWriter.beginArray();
+    jsonWriter.value(Float.NaN);
+    jsonWriter.value(Float.NEGATIVE_INFINITY);
+    jsonWriter.value(Float.POSITIVE_INFINITY);
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[NaN,-Infinity,Infinity]");
+  }
+
+  @Test
+  public void testNonFiniteDoublesWhenLenient() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.LENIENT);
+    jsonWriter.beginArray();
+    jsonWriter.value(Double.NaN);
+    jsonWriter.value(Double.NEGATIVE_INFINITY);
+    jsonWriter.value(Double.POSITIVE_INFINITY);
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[NaN,-Infinity,Infinity]");
+  }
+
+  @Test
+  public void testNonFiniteNumbersWhenLenient() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setStrictness(Strictness.LENIENT);
+    jsonWriter.beginArray();
+    jsonWriter.value(Double.valueOf(Double.NaN));
+    jsonWriter.value(Double.valueOf(Double.NEGATIVE_INFINITY));
+    jsonWriter.value(Double.valueOf(Double.POSITIVE_INFINITY));
+    jsonWriter.value(new LazilyParsedNumber("Infinity"));
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[NaN,-Infinity,Infinity,Infinity]");
+  }
+
+  @Test
+  public void testFloats() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value(-0.0f);
+    jsonWriter.value(1.0f);
+    jsonWriter.value(Float.MAX_VALUE);
+    jsonWriter.value(Float.MIN_VALUE);
+    jsonWriter.value(0.0f);
+    jsonWriter.value(-0.5f);
+    jsonWriter.value(2.2250739E-38f);
+    jsonWriter.value(3.723379f);
+    jsonWriter.value((float) Math.PI);
+    jsonWriter.value((float) Math.E);
+    jsonWriter.endArray();
+    jsonWriter.close();
+    assertThat(stringWriter.toString())
+        .isEqualTo(
+            "[-0.0,"
+                + "1.0,"
+                + "3.4028235E38,"
+                + "1.4E-45,"
+                + "0.0,"
+                + "-0.5,"
+                + "2.2250739E-38,"
+                + "3.723379,"
+                + "3.1415927,"
+                + "2.7182817]");
+  }
+
+  @Test
+  public void testDoubles() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value(-0.0);
+    jsonWriter.value(1.0);
+    jsonWriter.value(Double.MAX_VALUE);
+    jsonWriter.value(Double.MIN_VALUE);
+    jsonWriter.value(0.0);
+    jsonWriter.value(-0.5);
+    jsonWriter.value(2.2250738585072014E-308);
+    jsonWriter.value(Math.PI);
+    jsonWriter.value(Math.E);
+    jsonWriter.endArray();
+    jsonWriter.close();
+    assertThat(stringWriter.toString())
+        .isEqualTo(
+            "[-0.0,"
+                + "1.0,"
+                + "1.7976931348623157E308,"
+                + "4.9E-324,"
+                + "0.0,"
+                + "-0.5,"
+                + "2.2250738585072014E-308,"
+                + "3.141592653589793,"
+                + "2.718281828459045]");
+  }
+
+  @Test
+  public void testLongs() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value(0);
+    jsonWriter.value(1);
+    jsonWriter.value(-1);
+    jsonWriter.value(Long.MIN_VALUE);
+    jsonWriter.value(Long.MAX_VALUE);
+    jsonWriter.endArray();
+    jsonWriter.close();
+    assertThat(stringWriter.toString())
+        .isEqualTo("[0," + "1," + "-1," + "-9223372036854775808," + "9223372036854775807]");
+  }
+
+  @Test
+  public void testNumbers() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value(new BigInteger("0"));
+    jsonWriter.value(new BigInteger("9223372036854775808"));
+    jsonWriter.value(new BigInteger("-9223372036854775809"));
+    jsonWriter.value(new BigDecimal("3.141592653589793238462643383"));
+    jsonWriter.endArray();
+    jsonWriter.close();
+    assertThat(stringWriter.toString())
+        .isEqualTo(
+            "[0,"
+                + "9223372036854775808,"
+                + "-9223372036854775809,"
+                + "3.141592653589793238462643383]");
+  }
+
+  /** Tests writing {@code Number} instances which are not one of the standard JDK ones. */
+  @Test
+  public void testNumbersCustomClass() throws IOException {
+    String[] validNumbers = {
+      "-0.0",
+      "1.0",
+      "1.7976931348623157E308",
+      "4.9E-324",
+      "0.0",
+      "0.00",
+      "-0.5",
+      "2.2250738585072014E-308",
+      "3.141592653589793",
+      "2.718281828459045",
+      "0",
+      "0.01",
+      "0e0",
+      "1e+0",
+      "1e-0",
+      "1e0000", // leading 0 is allowed for exponent
+      "1e00001",
+      "1e+1",
+    };
+
+    for (String validNumber : validNumbers) {
+      StringWriter stringWriter = new StringWriter();
+      JsonWriter jsonWriter = new JsonWriter(stringWriter);
+
+      jsonWriter.value(new LazilyParsedNumber(validNumber));
+      jsonWriter.close();
+
+      assertThat(stringWriter.toString()).isEqualTo(validNumber);
+    }
+  }
+
+  @Test
+  public void testMalformedNumbers() throws IOException {
+    String[] malformedNumbers = {
+      "some text",
+      "",
+      ".",
+      "00",
+      "01",
+      "-00",
+      "-",
+      "--1",
+      "+1", // plus sign is not allowed for integer part
+      "+",
+      "1,0",
+      "1,000",
+      "0.", // decimal digit is required
+      ".1", // integer part is required
+      "e1",
+      ".e1",
+      ".1e1",
+      "1e-",
+      "1e+",
+      "1e--1",
+      "1e+-1",
+      "1e1e1",
+      "1+e1",
+      "1e1.0",
+    };
+
+    for (String malformedNumber : malformedNumbers) {
+      JsonWriter jsonWriter = new JsonWriter(new StringWriter());
+      try {
+        jsonWriter.value(new LazilyParsedNumber(malformedNumber));
+        fail("Should have failed writing malformed number: " + malformedNumber);
+      } catch (IllegalArgumentException e) {
+        assertThat(e)
+            .hasMessageThat()
+            .isEqualTo(
+                "String created by class com.google.gson.internal.LazilyParsedNumber is not a valid"
+                    + " JSON number: "
+                    + malformedNumber);
+      }
+    }
+  }
+
+  @Test
+  public void testBooleans() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value(true);
+    jsonWriter.value(false);
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[true,false]");
+  }
+
+  @Test
+  public void testBoxedBooleans() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value((Boolean) true);
+    jsonWriter.value((Boolean) false);
+    jsonWriter.value((Boolean) null);
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[true,false,null]");
+  }
+
+  @Test
+  public void testNulls() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.nullValue();
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[null]");
+  }
+
+  @Test
+  public void testStrings() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value("a");
+    jsonWriter.value("a\"");
+    jsonWriter.value("\"");
+    jsonWriter.value(":");
+    jsonWriter.value(",");
+    jsonWriter.value("\b");
+    jsonWriter.value("\f");
+    jsonWriter.value("\n");
+    jsonWriter.value("\r");
+    jsonWriter.value("\t");
+    jsonWriter.value(" ");
+    jsonWriter.value("\\");
+    jsonWriter.value("{");
+    jsonWriter.value("}");
+    jsonWriter.value("[");
+    jsonWriter.value("]");
+    jsonWriter.value("\0");
+    jsonWriter.value("\u0019");
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString())
+        .isEqualTo(
+            "[\"a\","
+                + "\"a\\\"\","
+                + "\"\\\"\","
+                + "\":\","
+                + "\",\","
+                + "\"\\b\","
+                + "\"\\f\","
+                + "\"\\n\","
+                + "\"\\r\","
+                + "\"\\t\","
+                + "\" \","
+                + "\"\\\\\","
+                + "\"{\","
+                + "\"}\","
+                + "\"[\","
+                + "\"]\","
+                + "\"\\u0000\","
+                + "\"\\u0019\"]");
+  }
+
+  @Test
+  public void testUnicodeLineBreaksEscaped() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.value("\u2028 \u2029");
+    jsonWriter.endArray();
+    // JSON specification does not require that they are escaped, but Gson escapes them for
+    // compatibility with JavaScript where they are considered line breaks
+    assertThat(stringWriter.toString()).isEqualTo("[\"\\u2028 \\u2029\"]");
+  }
+
+  @Test
+  public void testEmptyArray() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString()).isEqualTo("[]");
+  }
+
+  @Test
+  public void testEmptyObject() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.endObject();
+    assertThat(stringWriter.toString()).isEqualTo("{}");
+  }
+
+  @Test
+  public void testObjectsInArrays() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginArray();
+    jsonWriter.beginObject();
+    jsonWriter.name("a").value(5);
+    jsonWriter.name("b").value(false);
+    jsonWriter.endObject();
+    jsonWriter.beginObject();
+    jsonWriter.name("c").value(6);
+    jsonWriter.name("d").value(true);
+    jsonWriter.endObject();
+    jsonWriter.endArray();
+    assertThat(stringWriter.toString())
+        .isEqualTo("[{\"a\":5,\"b\":false}," + "{\"c\":6,\"d\":true}]");
+  }
+
+  @Test
+  public void testArraysInObjects() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    jsonWriter.beginArray();
+    jsonWriter.value(5);
+    jsonWriter.value(false);
+    jsonWriter.endArray();
+    jsonWriter.name("b");
+    jsonWriter.beginArray();
+    jsonWriter.value(6);
+    jsonWriter.value(true);
+    jsonWriter.endArray();
+    jsonWriter.endObject();
+    assertThat(stringWriter.toString()).isEqualTo("{\"a\":[5,false]," + "\"b\":[6,true]}");
+  }
+
+  @Test
+  public void testDeepNestingArrays() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    for (int i = 0; i < 20; i++) {
+      jsonWriter.beginArray();
+    }
+    for (int i = 0; i < 20; i++) {
+      jsonWriter.endArray();
+    }
+    assertThat(stringWriter.toString()).isEqualTo("[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]");
+  }
+
+  @Test
+  public void testDeepNestingObjects() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    for (int i = 0; i < 20; i++) {
+      jsonWriter.name("a");
+      jsonWriter.beginObject();
+    }
+    for (int i = 0; i < 20; i++) {
+      jsonWriter.endObject();
+    }
+    jsonWriter.endObject();
+    assertThat(stringWriter.toString())
+        .isEqualTo(
+            "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":"
+                + "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{"
+                + "}}}}}}}}}}}}}}}}}}}}}");
+  }
+
+  @Test
+  public void testRepeatedName() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.beginObject();
+    jsonWriter.name("a").value(true);
+    jsonWriter.name("a").value(false);
+    jsonWriter.endObject();
+    // JsonWriter doesn't attempt to detect duplicate names
+    assertThat(stringWriter.toString()).isEqualTo("{\"a\":true,\"a\":false}");
+  }
+
+  @Test
+  public void testPrettyPrintObject() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setIndent("   ");
+
+    jsonWriter.beginObject();
+    jsonWriter.name("a").value(true);
+    jsonWriter.name("b").value(false);
+    jsonWriter.name("c").value(5.0);
+    jsonWriter.name("e").nullValue();
+    jsonWriter.name("f").beginArray();
+    jsonWriter.value(6.0);
+    jsonWriter.value(7.0);
+    jsonWriter.endArray();
+    jsonWriter.name("g").beginObject();
+    jsonWriter.name("h").value(8.0);
+    jsonWriter.name("i").value(9.0);
+    jsonWriter.endObject();
+    jsonWriter.endObject();
+
+    String expected =
+        "{\n"
+            + "   \"a\": true,\n"
+            + "   \"b\": false,\n"
+            + "   \"c\": 5.0,\n"
+            + "   \"e\": null,\n"
+            + "   \"f\": [\n"
+            + "      6.0,\n"
+            + "      7.0\n"
+            + "   ],\n"
+            + "   \"g\": {\n"
+            + "      \"h\": 8.0,\n"
+            + "      \"i\": 9.0\n"
+            + "   }\n"
+            + "}";
+    assertThat(stringWriter.toString()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testPrettyPrintArray() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setIndent("   ");
+
+    jsonWriter.beginArray();
+    jsonWriter.value(true);
+    jsonWriter.value(false);
+    jsonWriter.value(5.0);
+    jsonWriter.nullValue();
+    jsonWriter.beginObject();
+    jsonWriter.name("a").value(6.0);
+    jsonWriter.name("b").value(7.0);
+    jsonWriter.endObject();
+    jsonWriter.beginArray();
+    jsonWriter.value(8.0);
+    jsonWriter.value(9.0);
+    jsonWriter.endArray();
+    jsonWriter.endArray();
+
+    String expected =
+        "[\n"
+            + "   true,\n"
+            + "   false,\n"
+            + "   5.0,\n"
+            + "   null,\n"
+            + "   {\n"
+            + "      \"a\": 6.0,\n"
+            + "      \"b\": 7.0\n"
+            + "   },\n"
+            + "   [\n"
+            + "      8.0,\n"
+            + "      9.0\n"
+            + "   ]\n"
+            + "]";
+    assertThat(stringWriter.toString()).isEqualTo(expected);
+  }
+
+  @Test
+  public void testClosedWriterThrowsOnStructure() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    try {
+      writer.beginArray();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      writer.endArray();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      writer.beginObject();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+    try {
+      writer.endObject();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testClosedWriterThrowsOnName() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    try {
+      writer.name("a");
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testClosedWriterThrowsOnValue() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    try {
+      writer.value("a");
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testClosedWriterThrowsOnFlush() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    try {
+      writer.flush();
+      fail();
+    } catch (IllegalStateException expected) {
+    }
+  }
+
+  @Test
+  public void testWriterCloseIsIdempotent() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter writer = new JsonWriter(stringWriter);
+    writer.beginArray();
+    writer.endArray();
+    writer.close();
+    writer.close();
+  }
+
+  @Test
+  public void testSetGetFormattingStyle() throws IOException {
+    String lineSeparator = "\r\n";
+
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    // Default should be FormattingStyle.COMPACT
+    assertThat(jsonWriter.getFormattingStyle()).isSameInstanceAs(FormattingStyle.COMPACT);
+    jsonWriter.setFormattingStyle(
+        FormattingStyle.PRETTY.withIndent(" \t ").withNewline(lineSeparator));
+
+    jsonWriter.beginArray();
+    jsonWriter.value(true);
+    jsonWriter.value("text");
+    jsonWriter.value(5.0);
+    jsonWriter.nullValue();
+    jsonWriter.endArray();
+
+    String expected =
+        "[\r\n" //
+            + " \t true,\r\n" //
+            + " \t \"text\",\r\n" //
+            + " \t 5.0,\r\n" //
+            + " \t null\r\n" //
+            + "]";
+    assertThat(stringWriter.toString()).isEqualTo(expected);
+
+    assertThat(jsonWriter.getFormattingStyle().getNewline()).isEqualTo(lineSeparator);
+  }
+
+  @Test
+  public void testIndentOverwritesFormattingStyle() throws IOException {
+    StringWriter stringWriter = new StringWriter();
+    JsonWriter jsonWriter = new JsonWriter(stringWriter);
+    jsonWriter.setFormattingStyle(FormattingStyle.COMPACT);
+    // Should overwrite formatting style
+    jsonWriter.setIndent("  ");
+
+    jsonWriter.beginObject();
+    jsonWriter.name("a");
+    jsonWriter.beginArray();
+    jsonWriter.value(1);
+    jsonWriter.value(2);
+    jsonWriter.endArray();
+    jsonWriter.endObject();
+
+    String expected =
+        "{\n" //
+            + "  \"a\": [\n" //
+            + "    1,\n" //
+            + "    2\n" //
+            + "  ]\n" //
+            + "}";
+    assertThat(stringWriter.toString()).isEqualTo(expected);
+  }
+}
diff --git a/gson/gson/src/test/resources/testcases-proguard.conf b/gson/gson/src/test/resources/testcases-proguard.conf
new file mode 100644
index 0000000000000000000000000000000000000000..2f41bcf6805a131a77e40cb0497c46954d576321
--- /dev/null
+++ b/gson/gson/src/test/resources/testcases-proguard.conf
@@ -0,0 +1,23 @@
+# Options from Android Gradle Plugins
+# https://android.googlesource.com/platform/tools/base/+/refs/heads/studio-master-dev/build-system/gradle-core/src/main/resources/com/android/build/gradle
+-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
+-optimizationpasses 5
+-allowaccessmodification
+# On Windows mixed case class names might cause problems
+-dontusemixedcaseclassnames
+-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
+-keepclassmembers enum * {
+  public static **[] values();
+  public static ** valueOf(java.lang.String);
+}
+
+-keep enum com.google.gson.functional.EnumWithObfuscatedTest$Gender
+-keep class com.google.gson.functional.EnumWithObfuscatedTest {
+  public void test*();
+  public void setUp();
+}
+
+-dontwarn com.google.gson.functional.EnumWithObfuscatedTest
+-dontwarn junit.framework.TestCase
+# Ignore notes about duplicate JDK classes
+-dontnote module-info,jdk.internal.**
diff --git a/gson/lib/gson-cleanup-styles.xml b/gson/lib/gson-cleanup-styles.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4f9eaad1c25a4e97114d664fcc313e0b7f9fb660
--- /dev/null
+++ b/gson/lib/gson-cleanup-styles.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<profiles version="2">
+<profile kind="CleanUpProfile" name="Gson" version="2">
+<setting id="cleanup.add_missing_nls_tags" value="false"/>
+<setting id="cleanup.format_source_code" value="false"/>
+<setting id="cleanup.add_missing_override_annotations" value="true"/>
+<setting id="cleanup.qualify_static_method_accesses_with_declaring_class" value="false"/>
+<setting id="cleanup.remove_unused_private_types" value="true"/>
+<setting id="cleanup.remove_unused_private_fields" value="true"/>
+<setting id="cleanup.always_use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.never_use_blocks" value="false"/>
+<setting id="cleanup.make_local_variable_final" value="true"/>
+<setting id="cleanup.add_missing_deprecated_annotations" value="true"/>
+<setting id="cleanup.remove_unused_private_methods" value="true"/>
+<setting id="cleanup.convert_to_enhanced_for_loop" value="true"/>
+<setting id="cleanup.remove_unnecessary_nls_tags" value="true"/>
+<setting id="cleanup.remove_unused_imports" value="true"/>
+<setting id="cleanup.remove_trailing_whitespaces_ignore_empty" value="false"/>
+<setting id="cleanup.make_private_fields_final" value="true"/>
+<setting id="cleanup.sort_members" value="false"/>
+<setting id="cleanup.add_generated_serial_version_id" value="false"/>
+<setting id="cleanup.remove_unused_local_variables" value="true"/>
+<setting id="cleanup.organize_imports" value="false"/>
+<setting id="cleanup.remove_unused_private_members" value="false"/>
+<setting id="cleanup.remove_trailing_whitespaces" value="true"/>
+<setting id="cleanup.never_use_parentheses_in_expressions" value="true"/>
+<setting id="cleanup.sort_members_all" value="false"/>
+<setting id="cleanup.remove_unnecessary_casts" value="true"/>
+<setting id="cleanup.make_parameters_final" value="false"/>
+<setting id="cleanup.use_blocks_only_for_return_and_throw" value="false"/>
+<setting id="cleanup.use_this_for_non_static_field_access" value="false"/>
+<setting id="cleanup.remove_private_constructors" value="true"/>
+<setting id="cleanup.use_blocks" value="false"/>
+<setting id="cleanup.add_missing_annotations" value="true"/>
+<setting id="cleanup.always_use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.remove_trailing_whitespaces_all" value="true"/>
+<setting id="cleanup.always_use_this_for_non_static_field_access" value="false"/>
+<setting id="cleanup.use_this_for_non_static_field_access_only_if_necessary" value="true"/>
+<setting id="cleanup.qualify_static_field_accesses_with_declaring_class" value="false"/>
+<setting id="cleanup.add_default_serial_version_id" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access_only_if_necessary" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.qualify_static_member_accesses_through_instances_with_declaring_class" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class" value="true"/>
+<setting id="cleanup.add_serial_version_id" value="false"/>
+<setting id="cleanup.make_variable_declarations_final" value="false"/>
+<setting id="cleanup.always_use_blocks" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_with_declaring_class" value="true"/>
+</profile>
+</profiles>
diff --git a/gson/lib/gson-formatting-styles.xml b/gson/lib/gson-formatting-styles.xml
new file mode 100644
index 0000000000000000000000000000000000000000..177e9997d0cc82721aad1eb1775f375422af5849
--- /dev/null
+++ b/gson/lib/gson-formatting-styles.xml
@@ -0,0 +1,267 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<profiles version="11">
+<profile kind="CodeFormatterProfile" name="Gson" version="11">
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="16"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+</profile>
+</profiles>
diff --git a/gson/metrics/README.md b/gson/metrics/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..8c95485de243b43de119a256f05e073ed45a308a
--- /dev/null
+++ b/gson/metrics/README.md
@@ -0,0 +1,3 @@
+# metrics
+
+This Maven module contains the source code for running internal benchmark tests against Gson.
diff --git a/gson/metrics/pom.xml b/gson/metrics/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a31e6a511695df09a050868b1f60c83da9dd5fe7
--- /dev/null
+++ b/gson/metrics/pom.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2011 Google LLC
+
+  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.
+-->
+<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>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gson-metrics</artifactId>
+  <inceptionYear>2011</inceptionYear>
+  <name>Gson Metrics</name>
+  <description>Performance Metrics for Google Gson library</description>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+  </properties>
+
+  <licenses>
+    <license>
+      <name>Apache-2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <organization>
+    <name>Google, Inc.</name>
+    <url>https://www.google.com</url>
+  </organization>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${project.parent.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.16.1</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.caliper</groupId>
+      <artifactId>caliper</artifactId>
+      <version>1.0-beta-3</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>com.github.siom79.japicmp</groupId>
+          <artifactId>japicmp-maven-plugin</artifactId>
+          <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.codehaus.mojo</groupId>
+          <artifactId>animal-sniffer-maven-plugin</artifactId>
+          <configuration>
+            <!-- This module is not supposed to be consumed as library, so no need to check used classes -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <configuration>
+            <!-- Not deployed -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <developers>
+    <developer>
+      <name>Inderjeet Singh</name>
+      <organization>Google Inc.</organization>
+    </developer>
+    <developer>
+      <name>Joel Leitch</name>
+      <organization>Google Inc.</organization>
+    </developer>
+    <developer>
+      <name>Jesse Wilson</name>
+      <organization>Google Inc.</organization>
+    </developer>
+  </developers>
+</project>
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitives.java b/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitives.java
new file mode 100644
index 0000000000000000000000000000000000000000..44c46199eec70ae0b4b0f17c35e1e69285e0a1f8
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitives.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2011 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.metrics;
+
+import com.google.common.base.Objects;
+
+/**
+ * Class with a bunch of primitive fields
+ *
+ * @author Inderjeet Singh
+ */
+public class BagOfPrimitives {
+  public static final long DEFAULT_VALUE = 0;
+  public long longValue;
+  public int intValue;
+  public boolean booleanValue;
+  public String stringValue;
+
+  public BagOfPrimitives() {
+    this(DEFAULT_VALUE, 0, false, "");
+  }
+
+  public BagOfPrimitives(long longValue, int intValue, boolean booleanValue, String stringValue) {
+    this.longValue = longValue;
+    this.intValue = intValue;
+    this.booleanValue = booleanValue;
+    this.stringValue = stringValue;
+  }
+
+  public int getIntValue() {
+    return intValue;
+  }
+
+  public String getExpectedJson() {
+    return "{"
+        + "\"longValue\":"
+        + longValue
+        + ","
+        + "\"intValue\":"
+        + intValue
+        + ","
+        + "\"booleanValue\":"
+        + booleanValue
+        + ","
+        + "\"stringValue\":\""
+        + stringValue
+        + "\""
+        + "}";
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + (booleanValue ? 1231 : 1237);
+    result = prime * result + intValue;
+    result = prime * result + (int) (longValue ^ (longValue >>> 32));
+    result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof BagOfPrimitives)) {
+      return false;
+    }
+    BagOfPrimitives that = (BagOfPrimitives) o;
+    return longValue == that.longValue
+        && intValue == that.intValue
+        && booleanValue == that.booleanValue
+        && Objects.equal(stringValue, that.stringValue);
+  }
+
+  @Override
+  public String toString() {
+    return String.format(
+        "(longValue=%d,intValue=%d,booleanValue=%b,stringValue=%s)",
+        longValue, intValue, booleanValue, stringValue);
+  }
+}
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java b/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java
new file mode 100644
index 0000000000000000000000000000000000000000..48c8b3a7a6cf04ab23b1b81e2005cf287a140fdb
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/BagOfPrimitivesDeserializationBenchmark.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2011 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.metrics;
+
+import com.google.caliper.BeforeExperiment;
+import com.google.gson.Gson;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+
+/**
+ * Caliper based micro benchmarks for Gson
+ *
+ * @author Inderjeet Singh
+ * @author Jesse Wilson
+ * @author Joel Leitch
+ */
+public class BagOfPrimitivesDeserializationBenchmark {
+
+  private Gson gson;
+  private String json;
+
+  public static void main(String[] args) {
+    NonUploadingCaliperRunner.run(BagOfPrimitivesDeserializationBenchmark.class, args);
+  }
+
+  @BeforeExperiment
+  void setUp() throws Exception {
+    this.gson = new Gson();
+    BagOfPrimitives bag = new BagOfPrimitives(10L, 1, false, "foo");
+    this.json = gson.toJson(bag);
+  }
+
+  /** Benchmark to measure Gson performance for deserializing an object */
+  public void timeBagOfPrimitivesDefault(int reps) {
+    for (int i = 0; i < reps; ++i) {
+      gson.fromJson(json, BagOfPrimitives.class);
+    }
+  }
+
+  /** Benchmark to measure deserializing objects by hand */
+  public void timeBagOfPrimitivesStreaming(int reps) throws IOException {
+    for (int i = 0; i < reps; ++i) {
+      StringReader reader = new StringReader(json);
+      JsonReader jr = new JsonReader(reader);
+      jr.beginObject();
+      long longValue = 0;
+      int intValue = 0;
+      boolean booleanValue = false;
+      String stringValue = null;
+      while (jr.hasNext()) {
+        String name = jr.nextName();
+        if (name.equals("longValue")) {
+          longValue = jr.nextLong();
+        } else if (name.equals("intValue")) {
+          intValue = jr.nextInt();
+        } else if (name.equals("booleanValue")) {
+          booleanValue = jr.nextBoolean();
+        } else if (name.equals("stringValue")) {
+          stringValue = jr.nextString();
+        } else {
+          throw new IOException("Unexpected name: " + name);
+        }
+      }
+      jr.endObject();
+      new BagOfPrimitives(longValue, intValue, booleanValue, stringValue);
+    }
+  }
+
+  /**
+   * This benchmark measures the ideal Gson performance: the cost of parsing a JSON stream and
+   * setting object values by reflection. We should strive to reduce the discrepancy between this
+   * and {@link #timeBagOfPrimitivesDefault(int)} .
+   */
+  public void timeBagOfPrimitivesReflectionStreaming(int reps) throws Exception {
+    for (int i = 0; i < reps; ++i) {
+      StringReader reader = new StringReader(json);
+      JsonReader jr = new JsonReader(reader);
+      jr.beginObject();
+      BagOfPrimitives bag = new BagOfPrimitives();
+      while (jr.hasNext()) {
+        String name = jr.nextName();
+        for (Field field : BagOfPrimitives.class.getDeclaredFields()) {
+          if (field.getName().equals(name)) {
+            Class<?> fieldType = field.getType();
+            if (fieldType.equals(long.class)) {
+              field.setLong(bag, jr.nextLong());
+            } else if (fieldType.equals(int.class)) {
+              field.setInt(bag, jr.nextInt());
+            } else if (fieldType.equals(boolean.class)) {
+              field.setBoolean(bag, jr.nextBoolean());
+            } else if (fieldType.equals(String.class)) {
+              field.set(bag, jr.nextString());
+            } else {
+              throw new RuntimeException("Unexpected: type: " + fieldType + ", name: " + name);
+            }
+          }
+        }
+      }
+      jr.endObject();
+    }
+  }
+}
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java b/gson/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java
new file mode 100644
index 0000000000000000000000000000000000000000..b844abb38197ca7d36e42f35ff2545cf8bbc5c8b
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 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.metrics;
+
+import com.google.caliper.BeforeExperiment;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Caliper based micro benchmarks for Gson
+ *
+ * @author Inderjeet Singh
+ */
+public class CollectionsDeserializationBenchmark {
+
+  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();
+    List<BagOfPrimitives> bags = new ArrayList<>();
+    for (int i = 0; i < 100; ++i) {
+      bags.add(new BagOfPrimitives(10L, 1, false, "foo"));
+    }
+    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_TOKEN);
+    }
+  }
+
+  /** Benchmark to measure deserializing objects by hand */
+  @SuppressWarnings("ModifiedButNotUsed")
+  public void timeCollectionsStreaming(int reps) throws IOException {
+    for (int i = 0; i < reps; ++i) {
+      StringReader reader = new StringReader(json);
+      JsonReader jr = new JsonReader(reader);
+      jr.beginArray();
+      List<BagOfPrimitives> bags = new ArrayList<>();
+      while (jr.hasNext()) {
+        jr.beginObject();
+        long longValue = 0;
+        int intValue = 0;
+        boolean booleanValue = false;
+        String stringValue = null;
+        while (jr.hasNext()) {
+          String name = jr.nextName();
+          if (name.equals("longValue")) {
+            longValue = jr.nextLong();
+          } else if (name.equals("intValue")) {
+            intValue = jr.nextInt();
+          } else if (name.equals("booleanValue")) {
+            booleanValue = jr.nextBoolean();
+          } else if (name.equals("stringValue")) {
+            stringValue = jr.nextString();
+          } else {
+            throw new IOException("Unexpected name: " + name);
+          }
+        }
+        jr.endObject();
+        bags.add(new BagOfPrimitives(longValue, intValue, booleanValue, stringValue));
+      }
+      jr.endArray();
+    }
+  }
+
+  /**
+   * This benchmark measures the ideal Gson performance: the cost of parsing a JSON stream and
+   * setting object values by reflection. We should strive to reduce the discrepancy between this
+   * and {@link #timeCollectionsDefault(int)} .
+   */
+  @SuppressWarnings("ModifiedButNotUsed")
+  public void timeCollectionsReflectionStreaming(int reps) throws Exception {
+    for (int i = 0; i < reps; ++i) {
+      StringReader reader = new StringReader(json);
+      JsonReader jr = new JsonReader(reader);
+      jr.beginArray();
+      List<BagOfPrimitives> bags = new ArrayList<>();
+      while (jr.hasNext()) {
+        jr.beginObject();
+        BagOfPrimitives bag = new BagOfPrimitives();
+        while (jr.hasNext()) {
+          String name = jr.nextName();
+          for (Field field : BagOfPrimitives.class.getDeclaredFields()) {
+            if (field.getName().equals(name)) {
+              Class<?> fieldType = field.getType();
+              if (fieldType.equals(long.class)) {
+                field.setLong(bag, jr.nextLong());
+              } else if (fieldType.equals(int.class)) {
+                field.setInt(bag, jr.nextInt());
+              } else if (fieldType.equals(boolean.class)) {
+                field.setBoolean(bag, jr.nextBoolean());
+              } else if (fieldType.equals(String.class)) {
+                field.set(bag, jr.nextString());
+              } else {
+                throw new RuntimeException("Unexpected: type: " + fieldType + ", name: " + name);
+              }
+            }
+          }
+        }
+        jr.endObject();
+        bags.add(bag);
+      }
+      jr.endArray();
+    }
+  }
+}
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/NonUploadingCaliperRunner.java b/gson/metrics/src/main/java/com/google/gson/metrics/NonUploadingCaliperRunner.java
new file mode 100644
index 0000000000000000000000000000000000000000..57a2a6a18b770af6960d753f06b9c9e6bb6a8801
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/NonUploadingCaliperRunner.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.metrics;
+
+import com.google.caliper.runner.CaliperMain;
+
+class NonUploadingCaliperRunner {
+  private NonUploadingCaliperRunner() {}
+
+  private static String[] concat(String first, String... others) {
+    if (others.length == 0) {
+      return new String[] {first};
+    } else {
+      String[] result = new String[others.length + 1];
+      result[0] = first;
+      System.arraycopy(others, 0, result, 1, others.length);
+      return result;
+    }
+  }
+
+  public static void run(Class<?> c, String[] args) {
+    // Disable result upload; Caliper uploads results to webapp by default, see
+    // https://github.com/google/caliper/issues/356
+    CaliperMain.main(c, concat("-Cresults.upload.options.url=", args));
+  }
+}
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java b/gson/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java
new file mode 100644
index 0000000000000000000000000000000000000000..4151b2abb59aaf2a628e4011154a5410d45ef9b5
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/ParseBenchmark.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2011 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.metrics;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonFactoryBuilder;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.google.caliper.BeforeExperiment;
+import com.google.caliper.Param;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParser;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.CharArrayReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Measure Gson and Jackson parsing and binding performance.
+ *
+ * <p>This benchmark requires that ParseBenchmarkData.zip is on the classpath. That file contains
+ * Twitter feed data, which is representative of what applications will be parsing.
+ */
+public final class ParseBenchmark {
+  @Param Document document;
+  @Param Api api;
+
+  private enum Document {
+    TWEETS(new TypeToken<List<Tweet>>() {}, new TypeReference<List<Tweet>>() {}),
+    READER_SHORT(new TypeToken<Feed>() {}, new TypeReference<Feed>() {}),
+    READER_LONG(new TypeToken<Feed>() {}, new TypeReference<Feed>() {});
+
+    @SuppressWarnings("ImmutableEnumChecker")
+    private final TypeToken<?> gsonType;
+
+    @SuppressWarnings("ImmutableEnumChecker")
+    private final TypeReference<?> jacksonType;
+
+    private Document(TypeToken<?> typeToken, TypeReference<?> typeReference) {
+      this.gsonType = typeToken;
+      this.jacksonType = typeReference;
+    }
+  }
+
+  private enum Api {
+    JACKSON_STREAM {
+      @Override
+      Parser newParser() {
+        return new JacksonStreamParser();
+      }
+    },
+    JACKSON_BIND {
+      @Override
+      Parser newParser() {
+        return new JacksonBindParser();
+      }
+    },
+    GSON_STREAM {
+      @Override
+      Parser newParser() {
+        return new GsonStreamParser();
+      }
+    },
+    GSON_SKIP {
+      @Override
+      Parser newParser() {
+        return new GsonSkipParser();
+      }
+    },
+    GSON_DOM {
+      @Override
+      Parser newParser() {
+        return new GsonDomParser();
+      }
+    },
+    GSON_BIND {
+      @Override
+      Parser newParser() {
+        return new GsonBindParser();
+      }
+    };
+
+    abstract Parser newParser();
+  }
+
+  private char[] text;
+  private Parser parser;
+
+  @BeforeExperiment
+  void setUp() throws Exception {
+    text = resourceToString(document.name() + ".json").toCharArray();
+    parser = api.newParser();
+  }
+
+  public void timeParse(int reps) throws Exception {
+    for (int i = 0; i < reps; i++) {
+      parser.parse(text, document);
+    }
+  }
+
+  private static File getResourceFile(String path) throws Exception {
+    URL url = ParseBenchmark.class.getResource(path);
+    if (url == null) {
+      throw new IllegalArgumentException("Resource " + path + " does not exist");
+    }
+    File file = new File(url.toURI());
+    if (!file.isFile()) {
+      throw new IllegalArgumentException("Resource " + path + " is not a file");
+    }
+    return file;
+  }
+
+  private static String resourceToString(String fileName) throws Exception {
+    ZipFile zipFile = new ZipFile(getResourceFile("/ParseBenchmarkData.zip"));
+    try {
+      ZipEntry zipEntry = zipFile.getEntry(fileName);
+      Reader reader =
+          new InputStreamReader(zipFile.getInputStream(zipEntry), StandardCharsets.UTF_8);
+      char[] buffer = new char[8192];
+      StringWriter writer = new StringWriter();
+      int count;
+      while ((count = reader.read(buffer)) != -1) {
+        writer.write(buffer, 0, count);
+      }
+      reader.close();
+      return writer.toString();
+
+    } finally {
+      zipFile.close();
+    }
+  }
+
+  public static void main(String[] args) throws Exception {
+    NonUploadingCaliperRunner.run(ParseBenchmark.class, args);
+  }
+
+  interface Parser {
+    void parse(char[] data, Document document) throws Exception;
+  }
+
+  private static class GsonStreamParser implements Parser {
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      JsonReader jsonReader = new JsonReader(new CharArrayReader(data));
+      readToken(jsonReader);
+      jsonReader.close();
+    }
+
+    private static void readToken(JsonReader reader) throws IOException {
+      while (true) {
+        switch (reader.peek()) {
+          case BEGIN_ARRAY:
+            reader.beginArray();
+            break;
+          case END_ARRAY:
+            reader.endArray();
+            break;
+          case BEGIN_OBJECT:
+            reader.beginObject();
+            break;
+          case END_OBJECT:
+            reader.endObject();
+            break;
+          case NAME:
+            reader.nextName();
+            break;
+          case BOOLEAN:
+            reader.nextBoolean();
+            break;
+          case NULL:
+            reader.nextNull();
+            break;
+          case NUMBER:
+            reader.nextLong();
+            break;
+          case STRING:
+            reader.nextString();
+            break;
+          case END_DOCUMENT:
+            return;
+        }
+      }
+    }
+  }
+
+  private static class GsonSkipParser implements Parser {
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      JsonReader jsonReader = new JsonReader(new CharArrayReader(data));
+      jsonReader.skipValue();
+      jsonReader.close();
+    }
+  }
+
+  private static class JacksonStreamParser implements Parser {
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      JsonFactory jsonFactory =
+          new JsonFactoryBuilder()
+              .configure(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES, false)
+              .build();
+      com.fasterxml.jackson.core.JsonParser jp =
+          jsonFactory.createParser(new CharArrayReader(data));
+      int depth = 0;
+      do {
+        JsonToken token = jp.nextToken();
+        switch (token) {
+          case START_OBJECT:
+          case START_ARRAY:
+            depth++;
+            break;
+          case END_OBJECT:
+          case END_ARRAY:
+            depth--;
+            break;
+          case FIELD_NAME:
+            jp.getCurrentName();
+            break;
+          case VALUE_STRING:
+            jp.getText();
+            break;
+          case VALUE_NUMBER_INT:
+          case VALUE_NUMBER_FLOAT:
+            jp.getLongValue();
+            break;
+          case VALUE_TRUE:
+          case VALUE_FALSE:
+            jp.getBooleanValue();
+            break;
+          case VALUE_NULL:
+            // Do nothing; nextToken() will advance in stream
+            break;
+          default:
+            throw new IllegalArgumentException("Unexpected token " + token);
+        }
+      } while (depth > 0);
+      jp.close();
+    }
+  }
+
+  private static class GsonDomParser implements Parser {
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      JsonParser.parseReader(new CharArrayReader(data));
+    }
+  }
+
+  private static class GsonBindParser implements Parser {
+    private static final Gson gson =
+        new GsonBuilder().setDateFormat("EEE MMM dd HH:mm:ss Z yyyy").create();
+
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      gson.fromJson(new CharArrayReader(data), document.gsonType);
+    }
+  }
+
+  private static class JacksonBindParser implements Parser {
+    private static final ObjectMapper mapper;
+
+    static {
+      mapper =
+          JsonMapper.builder()
+              .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+              .configure(MapperFeature.AUTO_DETECT_FIELDS, true)
+              .build();
+      mapper.setDateFormat(new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy", Locale.ENGLISH));
+    }
+
+    @Override
+    public void parse(char[] data, Document document) throws Exception {
+      mapper.readValue(new CharArrayReader(data), document.jacksonType);
+    }
+  }
+
+  @SuppressWarnings("MemberName")
+  static class Tweet {
+    @JsonProperty String coordinates;
+    @JsonProperty boolean favorited;
+    @JsonProperty Date created_at;
+    @JsonProperty boolean truncated;
+    @JsonProperty Tweet retweeted_status;
+    @JsonProperty String id_str;
+    @JsonProperty String in_reply_to_id_str;
+    @JsonProperty String contributors;
+    @JsonProperty String text;
+    @JsonProperty long id;
+    @JsonProperty String retweet_count;
+    @JsonProperty String in_reply_to_status_id_str;
+    @JsonProperty Object geo;
+    @JsonProperty boolean retweeted;
+    @JsonProperty String in_reply_to_user_id;
+    @JsonProperty String in_reply_to_screen_name;
+    @JsonProperty Object place;
+    @JsonProperty User user;
+    @JsonProperty String source;
+    @JsonProperty String in_reply_to_user_id_str;
+  }
+
+  @SuppressWarnings("MemberName")
+  static class User {
+    @JsonProperty String name;
+    @JsonProperty String profile_sidebar_border_color;
+    @JsonProperty boolean profile_background_tile;
+    @JsonProperty String profile_sidebar_fill_color;
+    @JsonProperty Date created_at;
+    @JsonProperty String location;
+    @JsonProperty String profile_image_url;
+    @JsonProperty boolean follow_request_sent;
+    @JsonProperty String profile_link_color;
+    @JsonProperty boolean is_translator;
+    @JsonProperty String id_str;
+    @JsonProperty int favourites_count;
+    @JsonProperty boolean contributors_enabled;
+    @JsonProperty String url;
+    @JsonProperty boolean default_profile;
+    @JsonProperty long utc_offset;
+    @JsonProperty long id;
+    @JsonProperty boolean profile_use_background_image;
+    @JsonProperty int listed_count;
+    @JsonProperty String lang;
+
+    @JsonProperty("protected")
+    @SerializedName("protected")
+    boolean isProtected;
+
+    @JsonProperty int followers_count;
+    @JsonProperty String profile_text_color;
+    @JsonProperty String profile_background_color;
+    @JsonProperty String time_zone;
+    @JsonProperty String description;
+    @JsonProperty boolean notifications;
+    @JsonProperty boolean geo_enabled;
+    @JsonProperty boolean verified;
+    @JsonProperty String profile_background_image_url;
+    @JsonProperty boolean default_profile_image;
+    @JsonProperty int friends_count;
+    @JsonProperty int statuses_count;
+    @JsonProperty String screen_name;
+    @JsonProperty boolean following;
+    @JsonProperty boolean show_all_inline_media;
+  }
+
+  static class Feed {
+    @JsonProperty String id;
+    @JsonProperty String title;
+    @JsonProperty String description;
+
+    @JsonProperty("alternate")
+    @SerializedName("alternate")
+    List<Link> alternates;
+
+    @JsonProperty long updated;
+    @JsonProperty List<Item> items;
+
+    @Override
+    public String toString() {
+      StringBuilder result =
+          new StringBuilder()
+              .append(id)
+              .append('\n')
+              .append(title)
+              .append('\n')
+              .append(description)
+              .append('\n')
+              .append(alternates)
+              .append('\n')
+              .append(updated);
+      int i = 1;
+      for (Item item : items) {
+        result.append(i++).append(": ").append(item).append("\n\n");
+      }
+      return result.toString();
+    }
+  }
+
+  static class Link {
+    @JsonProperty String href;
+
+    @Override
+    public String toString() {
+      return href;
+    }
+  }
+
+  static class Item {
+    @JsonProperty List<String> categories;
+    @JsonProperty String title;
+    @JsonProperty long published;
+    @JsonProperty long updated;
+
+    @JsonProperty("alternate")
+    @SerializedName("alternate")
+    List<Link> alternates;
+
+    @JsonProperty Content content;
+    @JsonProperty String author;
+    @JsonProperty List<ReaderUser> likingUsers;
+
+    @Override
+    public String toString() {
+      return title
+          + "\nauthor: "
+          + author
+          + "\npublished: "
+          + published
+          + "\nupdated: "
+          + updated
+          + "\n"
+          + content
+          + "\nliking users: "
+          + likingUsers
+          + "\nalternates: "
+          + alternates
+          + "\ncategories: "
+          + categories;
+    }
+  }
+
+  static class Content {
+    @JsonProperty String content;
+
+    @Override
+    public String toString() {
+      return content;
+    }
+  }
+
+  static class ReaderUser {
+    @JsonProperty String userId;
+
+    @Override
+    public String toString() {
+      return userId;
+    }
+  }
+}
diff --git a/gson/metrics/src/main/java/com/google/gson/metrics/SerializationBenchmark.java b/gson/metrics/src/main/java/com/google/gson/metrics/SerializationBenchmark.java
new file mode 100644
index 0000000000000000000000000000000000000000..372e0d9546a7d588f557260b99e6b25dbbe66e72
--- /dev/null
+++ b/gson/metrics/src/main/java/com/google/gson/metrics/SerializationBenchmark.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 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.metrics;
+
+import com.google.caliper.BeforeExperiment;
+import com.google.caliper.Param;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Caliper based micro benchmarks for Gson serialization
+ *
+ * @author Inderjeet Singh
+ * @author Jesse Wilson
+ * @author Joel Leitch
+ */
+public class SerializationBenchmark {
+
+  private Gson gson;
+  private BagOfPrimitives bag;
+  @Param private boolean pretty;
+
+  public static void main(String[] args) {
+    NonUploadingCaliperRunner.run(SerializationBenchmark.class, args);
+  }
+
+  @BeforeExperiment
+  void setUp() throws Exception {
+    this.gson = pretty ? new GsonBuilder().setPrettyPrinting().create() : new Gson();
+    this.bag = new BagOfPrimitives(10L, 1, false, "foo");
+  }
+
+  public void timeObjectSerialization(int reps) {
+    for (int i = 0; i < reps; ++i) {
+      gson.toJson(bag);
+    }
+  }
+}
diff --git a/gson/metrics/src/main/resources/ParseBenchmarkData.zip b/gson/metrics/src/main/resources/ParseBenchmarkData.zip
new file mode 100644
index 0000000000000000000000000000000000000000..58e08bb504761c1f54900a98d6e3a0d2ac41be83
Binary files /dev/null and b/gson/metrics/src/main/resources/ParseBenchmarkData.zip differ
diff --git a/gson/pom.xml b/gson/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..29511746eb7c9c5c37f9e90c76e6c6dc22279a33
--- /dev/null
+++ b/gson/pom.xml
@@ -0,0 +1,588 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2015 Google LLC
+
+  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.
+-->
+<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" child.project.url.inherit.append.path="false">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>com.google.code.gson</groupId>
+  <artifactId>gson-parent</artifactId>
+  <version>2.10.2-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Gson Parent</name>
+  <description>Gson JSON library</description>
+  <url>https://github.com/google/gson</url>
+
+  <modules>
+    <module>gson</module>
+    <module>graal-native-image-test</module>
+    <module>shrinker-test</module>
+    <module>extras</module>
+    <module>metrics</module>
+    <module>proto</module>
+  </modules>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <maven.compiler.release>7</maven.compiler.release>
+    <maven.compiler.testRelease>11</maven.compiler.testRelease>
+
+    <!-- Make the build reproducible, see https://maven.apache.org/guides/mini/guide-reproducible-builds.html -->
+    <!-- Automatically updated by Maven Release Plugin -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+  </properties>
+
+  <!-- These attributes specify that the URLs should be inherited by the modules as is, to avoid constructing
+    invalid URLs, see also https://maven.apache.org/ref/3.9.1/maven-model-builder/index.html#inheritance-assembly -->
+  <scm child.scm.url.inherit.append.path="false" child.scm.connection.inherit.append.path="false" child.scm.developerConnection.inherit.append.path="false">
+    <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>HEAD</tag>
+  </scm>
+
+  <developers>
+    <developer>
+      <id>google</id>
+      <organization>Google</organization>
+      <organizationUrl>https://www.google.com</organizationUrl>
+    </developer>
+  </developers>
+
+  <issueManagement>
+    <system>GitHub Issues</system>
+    <url>https://github.com/google/gson/issues</url>
+  </issueManagement>
+
+  <licenses>
+    <license>
+      <name>Apache-2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </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>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.13.2</version>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.truth</groupId>
+        <artifactId>truth</artifactId>
+        <version>1.3.0</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <version>3.4.1</version>
+        <executions>
+          <execution>
+            <id>enforce-versions</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <requireMavenVersion>
+                  <!-- Usage of `.mvn/jvm.config` for Error Prone requires at least Maven 3.3.1 -->
+                  <version>[3.3.1,)</version>
+                </requireMavenVersion>
+
+                <!-- Enforce that correct JDK version is used to avoid cryptic build errors -->
+                <requireJavaVersion>
+                  <!-- Other plugins of this build require at least JDK 11 -->
+                  <!-- Fail fast when building with JDK > 17 because it causes build failures,
+                    see https://github.com/google/gson/issues/2501 -->
+                  <version>[11,18)</version>
+                </requireJavaVersion>
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+
+
+      <!-- Spotless plugin: keeps the code formatted following the google-java-styleguide -->
+      <plugin>
+        <groupId>com.diffplug.spotless</groupId>
+        <artifactId>spotless-maven-plugin</artifactId>
+        <version>2.43.0</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>check</goal>
+            </goals>
+          </execution>
+        </executions>
+        <!-- Note: The configuration here is not specific to the `<execution>` above to allow users to run
+          `mvn spotless:apply` from the command line, using the same configuration -->
+        <configuration>
+          <!-- Perform some basic formatting for non-Java code -->
+          <formats>
+            <format>
+              <includes>
+                <include>*.md</include>
+                <include>*.xml</include>
+                <include>.github/**/*.yml</include>
+                <include>.gitignore</include>
+              </includes>
+              <!-- For Markdown files removing trailing whitespace causes issues for hard line breaks,
+                which use two trailing spaces. However, the trailing spaces are difficult to notice anyway;
+                prefer a trailing `\` instead of two spaces. -->
+              <trimTrailingWhitespace />
+              <endWithNewline />
+              <indent>
+                <spaces>true</spaces>
+                <!-- This seems to mostly (or only?) affect the suggested fix in case code contains tabs -->
+                <spacesPerTab>2</spacesPerTab>
+              </indent>
+            </format>
+          </formats>
+
+          <java>
+            <excludes>
+              <!-- Exclude classes which need Java 17 for compilation; Google Java Format internally relies on javac,
+                so formatting will fail if build is executed with JDK 11 -->
+              <exclude>src/test/java/com/google/gson/functional/Java17RecordTest.java</exclude>
+              <exclude>src/test/java/com/google/gson/native_test/Java17RecordReflectionTest.java</exclude>
+            </excludes>
+            <googleJavaFormat>
+              <style>GOOGLE</style>
+              <reflowLongStrings>true</reflowLongStrings>
+              <reorderImports>true</reorderImports>
+              <formatJavadoc>true</formatJavadoc>
+            </googleJavaFormat>
+            <formatAnnotations />     <!-- Puts type annotations immediately before types. -->
+          </java>
+        </configuration>
+      </plugin>
+
+      <!-- Attaches a `.buildinfo` file which contains information for reproducing the build,
+        such as OS, JDK version, ...
+        Since this is a multi-module Maven project, only one aggregated file will be created for
+        the last module, see the note on https://maven.apache.org/plugins/maven-artifact-plugin/usage.html#recording-buildinfo-file -->
+      <!-- The other goals of this plugin are run by the GitHub workflow to verify that
+        the build is reproducible (see `artifact:...` usage in the workflow) -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-artifact-plugin</artifactId>
+        <version>3.5.0</version>
+        <executions>
+          <execution>
+            <goals>
+              <!-- This logs a warning about `source.scm.tag=HEAD`, but this can be ignored;
+                during release Maven Release Plugin temporarily changes the `source.scm.tag`
+                value to the actual Git tag, which will then not cause a warning -->
+              <goal>buildinfo</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.12.1</version>
+          <configuration>
+            <showWarnings>true</showWarnings>
+            <showDeprecation>true</showDeprecation>
+            <failOnWarning>true</failOnWarning>
+            <compilerArgs>
+              <!-- Args related to Error Prone, see: https://errorprone.info/docs/installation#maven -->
+              <arg>-XDcompilePolicy=simple</arg>
+              <arg>-Xplugin:ErrorProne
+                -XepExcludedPaths:.*/generated-test-sources/protobuf/.*
+                -Xep:NotJavadoc:OFF <!-- Triggered by local class. -->
+                <!-- Increase severity from 'suggestion' to 'warning' so that the user has to fix
+                  found issues, and they are not overlooked
+                  TODO: Does not work properly yet, see https://github.com/google/error-prone/issues/4206,
+                        so for now have to manually set them to `:WARN` -->
+                -XepAllSuggestionsAsWarnings
+                <!-- Enable some experimental checks which are disabled by default
+                  In case they cause issues or are unreliable turn them off by adding `:OFF`,
+                  and add a comment mentioning why they were disabled -->
+                -Xep:AnnotationPosition <!-- required by style guide -->
+                -Xep:AssertFalse
+                -Xep:ClassName <!-- required by style guide -->
+                -Xep:ClassNamedLikeTypeParameter:WARN
+                -Xep:ComparisonContractViolated
+                -Xep:ConstantField:WARN <!-- required by style guide -->
+                -Xep:DepAnn
+                -Xep:DifferentNameButSame
+                -Xep:EmptyIf
+                -Xep:EqualsBrokenForNull
+                -Xep:ForEachIterable:WARN
+                -Xep:FunctionalInterfaceClash
+                -Xep:InitializeInline
+                -Xep:InterfaceWithOnlyStatics
+                -Xep:LambdaFunctionalInterface:WARN <!-- only relevant for test code at the moment, which uses Java 11 -->
+                -Xep:LongLiteralLowerCaseSuffix <!-- required by style guide -->
+                -Xep:MemberName <!-- required by style guide -->
+                -Xep:MissingBraces:WARN
+                -Xep:MissingDefault <!-- required by style guide -->
+                -Xep:MixedArrayDimensions:WARN <!-- required by style guide -->
+                -Xep:MultiVariableDeclaration:WARN <!-- required by style guide -->
+                -Xep:MultipleTopLevelClasses:WARN <!-- required by style guide -->
+                -Xep:NonCanonicalStaticMemberImport
+                -Xep:NonFinalStaticField
+                -Xep:PackageLocation:WARN
+                -Xep:PrimitiveArrayPassedToVarargsMethod
+                -Xep:PrivateConstructorForUtilityClass:WARN
+                -Xep:RemoveUnusedImports:WARN
+                -Xep:StatementSwitchToExpressionSwitch:OFF <!-- disabled: requires Java 14 -->
+                -Xep:StaticQualifiedUsingExpression <!-- required by style guide -->
+                -Xep:SwitchDefault:WARN
+                -Xep:SystemExitOutsideMain
+                -Xep:SystemOut
+                -Xep:TestExceptionChecker
+                -Xep:ThrowSpecificExceptions:OFF <!-- disabled: Gson has no proper exception hierarchy yet, see https://github.com/google/gson/issues/2359 -->
+                -Xep:TryFailRefactoring:OFF <!-- disabled: there are too many tests which violate this -->
+                -Xep:TypeParameterNaming:WARN <!-- required by style guide -->
+                -Xep:UnescapedEntity
+                -Xep:UngroupedOverloads:WARN <!-- required by style guide -->
+                -Xep:UnnecessarilyFullyQualified
+                -Xep:UnnecessarilyUsedValue
+                -Xep:UnnecessaryAnonymousClass:OFF <!-- disabled: requires Java 8 -->
+                -Xep:UnnecessaryBoxedVariable:WARN
+                -Xep:UnnecessaryDefaultInEnumSwitch
+                -Xep:UnnecessaryFinal:OFF <!-- disabled: requires Java 8 -->
+                -Xep:UnnecessaryStaticImport:WARN <!-- required by style guide -->
+                -Xep:UnusedException
+                -Xep:UrlInSee
+                -Xep:UseCorrectAssertInTests
+                -Xep:UseEnumSwitch:WARN
+                -Xep:WildcardImport:WARN <!-- required by style guide -->
+                -Xep:YodaCondition
+              </arg>
+              <!-- 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>
+            <annotationProcessorPaths>
+              <path>
+                <groupId>com.google.errorprone</groupId>
+                <artifactId>error_prone_core</artifactId>
+                <version>2.24.1</version>
+              </path>
+            </annotationProcessorPaths>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>3.6.3</version>
+          <configuration>
+            <!-- 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
+              use a newer Java version than the one used for building this project -->
+            <detectJavaApiLink>false</detectJavaApiLink>
+            <links>
+              <link>https://docs.oracle.com/en/java/javase/11/docs/api/</link>
+              <link>https://errorprone.info/api/latest/</link>
+            </links>
+            <!-- Disable detection of offline links between Maven modules:
+              (1) Only `gson` module is published, so for other modules Javadoc links don't
+              matter much at the moment; (2) The derived URL for the modules is based on
+              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>
+            <failOnWarnings>true</failOnWarnings>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>3.2.5</version>
+          </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>3.3.0</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>3.1.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>3.3.0</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>3.1.0</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <version>3.0.1</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 `verify` to make sure tests still pass with new version number;
+              also seems to be necessary because without `package`, goals fail for modules depending
+              on each other; possibly same issue as https://issues.apache.org/jira/browse/MRELEASE-271 -->
+            <preparationGoals>
+              clean verify
+              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" />
+                    <!-- Don't add (unrelated) not yet tracked files -->
+                    <arg value="--update" />
+                    <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.18.3</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>
+
+        <!-- Plugin for checking compatibility with Android API -->
+        <!-- Note: For now this is not part of a normal Maven build but instead executed only by a
+          GitHub workflow because the Animal Sniffer signature files use Java Serialization, so they
+          could in theory contain malicious data (in case we don't fully trust the author) -->
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>animal-sniffer-maven-plugin</artifactId>
+          <version>1.23</version>
+          <executions>
+            <execution>
+              <id>check-android-compatibility</id>
+              <goals>
+                <goal>check</goal>
+              </goals>
+              <configuration>
+                <signature>
+                  <!-- Note: In case Android compatibility impedes Gson development too much in the
+                    future, could consider switching to https://github.com/open-toast/gummy-bears
+                    which accounts for Android desugaring and might allow usage of more Java classes -->
+                  <groupId>net.sf.androidscents.signature</groupId>
+                  <artifactId>android-api-level-21</artifactId>
+                  <version>5.0.1_r2</version>
+                </signature>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <profiles>
+    <!-- Disable Error Prone in Java 15 -->
+    <profile>
+      <id>jdk15</id>
+      <activation>
+        <jdk>15</jdk>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+             <compilerArgs combine.self="override">
+               <compilerArg>-Xlint:all,-options</compilerArg>
+             </compilerArgs>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <!-- Slightly adjust build to support building with JDK >= 21
+      However, by default this will intentionally be blocked by the Maven Enforcer Plugin defined above
+      and must be explicitly circumvented to avoid building non-release Gson artifacts by accident -->
+    <profile>
+      <id>jdk21+</id>
+      <activation>
+        <jdk>[21,)</jdk>
+      </activation>
+      <properties>
+        <!-- JDK 21 does not support Java 7 as release, must use at least Java 8 -->
+        <maven.compiler.release>8</maven.compiler.release>
+      </properties>
+    </profile>
+
+    <!-- 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/gson/proto/.gitignore b/gson/proto/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..f44578ac2bd7b45db957692b8d949b8f023e9493
--- /dev/null
+++ b/gson/proto/.gitignore
@@ -0,0 +1 @@
+src/main/java/com/google/gson/protobuf/generated/
diff --git a/gson/proto/README.md b/gson/proto/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..c6f7906a68b6a9d62407766d07aeed34ed696e5f
--- /dev/null
+++ b/gson/proto/README.md
@@ -0,0 +1,7 @@
+# proto
+
+This Maven module contains the source code for a JSON serializer and deserializer for
+[Protocol Buffers (protobuf)](https://developers.google.com/protocol-buffers/docs/javatutorial)
+messages.
+
+The artifacts created by this module are currently not deployed to Maven Central.
diff --git a/gson/proto/pom.xml b/gson/proto/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..99583c399ff3bd24482ed9c253ec801b74da245a
--- /dev/null
+++ b/gson/proto/pom.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2011 Google LLC
+
+  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.
+-->
+<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/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>proto</artifactId>
+  <name>Gson Protobuf Support</name>
+  <description>Gson support for Protobufs</description>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+
+    <protobufVersion>3.25.2</protobufVersion>
+  </properties>
+
+  <licenses>
+    <license>
+      <name>Apache-2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+    </license>
+  </licenses>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${project.parent.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <version>${protobufVersion}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <version>33.0.0-jre</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <finalName>gson-proto</finalName>
+    <!-- Setup based on https://quarkus.io/guides/grpc-getting-started#generating-java-files-from-proto-with-protobuf-maven-plugin -->
+    <extensions>
+      <extension>
+        <groupId>kr.motd.maven</groupId>
+        <artifactId>os-maven-plugin</artifactId>
+        <version>1.7.1</version>
+      </extension>
+    </extensions>
+
+    <plugins>
+      <plugin>
+        <groupId>org.xolstice.maven.plugins</groupId>
+        <artifactId>protobuf-maven-plugin</artifactId>
+        <version>0.6.1</version>
+        <configuration>
+          <protocArtifact>com.google.protobuf:protoc:${protobufVersion}:exe:${os.detected.classifier}</protocArtifact>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>test-compile</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <configuration>
+            <!-- Not deployed -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <developers>
+    <developer>
+      <name>Inderjeet Singh</name>
+      <organization>Google Inc.</organization>
+    </developer>
+  </developers>
+</project>
diff --git a/gson/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java b/gson/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b872f188581696aa475a001692703ebda4fa64e
--- /dev/null
+++ b/gson/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2010 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.protobuf;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.collect.MapMaker;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.protobuf.DescriptorProtos.EnumValueOptions;
+import com.google.protobuf.DescriptorProtos.FieldOptions;
+import com.google.protobuf.Descriptors.Descriptor;
+import com.google.protobuf.Descriptors.EnumDescriptor;
+import com.google.protobuf.Descriptors.EnumValueDescriptor;
+import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.DynamicMessage;
+import com.google.protobuf.Extension;
+import com.google.protobuf.Message;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * GSON type adapter for protocol buffers that knows how to serialize enums either by using their
+ * values or their names, and also supports custom proto field names.
+ *
+ * <p>You can specify which case representation is used for the proto fields when writing/reading
+ * the JSON payload by calling {@link Builder#setFieldNameSerializationFormat(CaseFormat,
+ * CaseFormat)}.
+ *
+ * <p>An example of default serialization/deserialization using custom proto field names is shown
+ * below:
+ *
+ * <pre>
+ * message MyMessage {
+ *   // Will be serialized as 'osBuildID' instead of the default 'osBuildId'.
+ *   string os_build_id = 1 [(serialized_name) = "osBuildID"];
+ * }
+ * </pre>
+ *
+ * @author Inderjeet Singh
+ * @author Emmanuel Cron
+ * @author Stanley Wang
+ */
+public class ProtoTypeAdapter implements JsonSerializer<Message>, JsonDeserializer<Message> {
+  /** Determines how enum <u>values</u> should be serialized. */
+  public enum EnumSerialization {
+    /**
+     * Serializes and deserializes enum values using their <b>number</b>. When this is used, custom
+     * value names set on enums are ignored.
+     */
+    NUMBER,
+    /** Serializes and deserializes enum values using their <b>name</b>. */
+    NAME;
+  }
+
+  /** Builder for {@link ProtoTypeAdapter}s. */
+  public static class Builder {
+    private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
+    private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
+    private EnumSerialization enumSerialization;
+    private CaseFormat protoFormat;
+    private CaseFormat jsonFormat;
+
+    private Builder(
+        EnumSerialization enumSerialization,
+        CaseFormat fromFieldNameFormat,
+        CaseFormat toFieldNameFormat) {
+      this.serializedNameExtensions = new HashSet<>();
+      this.serializedEnumValueExtensions = new HashSet<>();
+      setEnumSerialization(enumSerialization);
+      setFieldNameSerializationFormat(fromFieldNameFormat, toFieldNameFormat);
+    }
+
+    @CanIgnoreReturnValue
+    public Builder setEnumSerialization(EnumSerialization enumSerialization) {
+      this.enumSerialization = requireNonNull(enumSerialization);
+      return this;
+    }
+
+    /**
+     * Sets the field names serialization format. The first parameter defines how to read the format
+     * of the proto field names you are converting to JSON. The second parameter defines which
+     * format to use when serializing them.
+     *
+     * <p>For example, if you use the following parameters: {@link CaseFormat#LOWER_UNDERSCORE},
+     * {@link CaseFormat#LOWER_CAMEL}, the following conversion will occur:
+     *
+     * <pre>{@code
+     * PROTO     <->  JSON
+     * my_field       myField
+     * foo            foo
+     * n__id_ct       nIdCt
+     * }</pre>
+     */
+    @CanIgnoreReturnValue
+    public Builder setFieldNameSerializationFormat(
+        CaseFormat fromFieldNameFormat, CaseFormat toFieldNameFormat) {
+      this.protoFormat = fromFieldNameFormat;
+      this.jsonFormat = toFieldNameFormat;
+      return this;
+    }
+
+    /**
+     * Adds a field proto annotation that, when set, overrides the default field name
+     * serialization/deserialization. For example, if you add the '{@code serialized_name}'
+     * annotation and you define a field in your proto like the one below:
+     *
+     * <pre>
+     * string client_app_id = 1 [(serialized_name) = "appId"];
+     * </pre>
+     *
+     * ...the adapter will serialize the field using '{@code appId}' instead of the default ' {@code
+     * clientAppId}'. This lets you customize the name serialization of any proto field.
+     */
+    @CanIgnoreReturnValue
+    public Builder addSerializedNameExtension(
+        Extension<FieldOptions, String> serializedNameExtension) {
+      serializedNameExtensions.add(requireNonNull(serializedNameExtension));
+      return this;
+    }
+
+    /**
+     * Adds an enum value proto annotation that, when set, overrides the default <b>enum</b> value
+     * serialization/deserialization of this adapter. For example, if you add the ' {@code
+     * serialized_value}' annotation and you define an enum in your proto like the one below:
+     *
+     * <pre>
+     * enum MyEnum {
+     *   UNKNOWN = 0;
+     *   CLIENT_APP_ID = 1 [(serialized_value) = "APP_ID"];
+     *   TWO = 2 [(serialized_value) = "2"];
+     * }
+     * </pre>
+     *
+     * ...the adapter will serialize the value {@code CLIENT_APP_ID} as "{@code APP_ID}" and the
+     * value {@code TWO} as "{@code 2}". This works for both serialization and deserialization.
+     *
+     * <p>Note that you need to set the enum serialization of this adapter to {@link
+     * EnumSerialization#NAME}, otherwise these annotations will be ignored.
+     */
+    @CanIgnoreReturnValue
+    public Builder addSerializedEnumValueExtension(
+        Extension<EnumValueOptions, String> serializedEnumValueExtension) {
+      serializedEnumValueExtensions.add(requireNonNull(serializedEnumValueExtension));
+      return this;
+    }
+
+    public ProtoTypeAdapter build() {
+      return new ProtoTypeAdapter(
+          enumSerialization,
+          protoFormat,
+          jsonFormat,
+          serializedNameExtensions,
+          serializedEnumValueExtensions);
+    }
+  }
+
+  /**
+   * Creates a new {@link ProtoTypeAdapter} builder, defaulting enum serialization to {@link
+   * EnumSerialization#NAME} and converting field serialization from {@link
+   * CaseFormat#LOWER_UNDERSCORE} to {@link CaseFormat#LOWER_CAMEL}.
+   */
+  public static Builder newBuilder() {
+    return new Builder(EnumSerialization.NAME, CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_CAMEL);
+  }
+
+  private static final FieldDescriptor.Type ENUM_TYPE = FieldDescriptor.Type.ENUM;
+
+  private static final ConcurrentMap<String, ConcurrentMap<Class<?>, Method>> mapOfMapOfMethods =
+      new MapMaker().makeMap();
+
+  private final EnumSerialization enumSerialization;
+  private final CaseFormat protoFormat;
+  private final CaseFormat jsonFormat;
+  private final Set<Extension<FieldOptions, String>> serializedNameExtensions;
+  private final Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions;
+
+  private ProtoTypeAdapter(
+      EnumSerialization enumSerialization,
+      CaseFormat protoFormat,
+      CaseFormat jsonFormat,
+      Set<Extension<FieldOptions, String>> serializedNameExtensions,
+      Set<Extension<EnumValueOptions, String>> serializedEnumValueExtensions) {
+    this.enumSerialization = enumSerialization;
+    this.protoFormat = protoFormat;
+    this.jsonFormat = jsonFormat;
+    this.serializedNameExtensions = serializedNameExtensions;
+    this.serializedEnumValueExtensions = serializedEnumValueExtensions;
+  }
+
+  @Override
+  public JsonElement serialize(Message src, Type typeOfSrc, JsonSerializationContext context) {
+    JsonObject ret = new JsonObject();
+    final Map<FieldDescriptor, Object> fields = src.getAllFields();
+
+    for (Map.Entry<FieldDescriptor, Object> fieldPair : fields.entrySet()) {
+      final FieldDescriptor desc = fieldPair.getKey();
+      String name = getCustSerializedName(desc.getOptions(), desc.getName());
+
+      if (desc.getType() == ENUM_TYPE) {
+        // Enum collections are also returned as ENUM_TYPE
+        if (fieldPair.getValue() instanceof Collection) {
+          // Build the array to avoid infinite loop
+          JsonArray array = new JsonArray();
+          @SuppressWarnings("unchecked")
+          Collection<EnumValueDescriptor> enumDescs =
+              (Collection<EnumValueDescriptor>) fieldPair.getValue();
+          for (EnumValueDescriptor enumDesc : enumDescs) {
+            array.add(context.serialize(getEnumValue(enumDesc)));
+            ret.add(name, array);
+          }
+        } else {
+          EnumValueDescriptor enumDesc = ((EnumValueDescriptor) fieldPair.getValue());
+          ret.add(name, context.serialize(getEnumValue(enumDesc)));
+        }
+      } else {
+        ret.add(name, context.serialize(fieldPair.getValue()));
+      }
+    }
+    return ret;
+  }
+
+  @Override
+  public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+      throws JsonParseException {
+    try {
+      JsonObject jsonObject = json.getAsJsonObject();
+      @SuppressWarnings("unchecked")
+      Class<? extends Message> protoClass = (Class<? extends Message>) typeOfT;
+
+      if (DynamicMessage.class.isAssignableFrom(protoClass)) {
+        throw new IllegalStateException("only generated messages are supported");
+      }
+
+      // Invoke the ProtoClass.newBuilder() method
+      Message.Builder protoBuilder =
+          (Message.Builder) getCachedMethod(protoClass, "newBuilder").invoke(null);
+
+      Message defaultInstance =
+          (Message) getCachedMethod(protoClass, "getDefaultInstance").invoke(null);
+
+      Descriptor protoDescriptor =
+          (Descriptor) getCachedMethod(protoClass, "getDescriptor").invoke(null);
+      // Call setters on all of the available fields
+      for (FieldDescriptor fieldDescriptor : protoDescriptor.getFields()) {
+        String jsonFieldName =
+            getCustSerializedName(fieldDescriptor.getOptions(), fieldDescriptor.getName());
+
+        JsonElement jsonElement = jsonObject.get(jsonFieldName);
+        if (jsonElement != null && !jsonElement.isJsonNull()) {
+          // Do not reuse jsonFieldName here, it might have a custom value
+          Object fieldValue;
+          if (fieldDescriptor.getType() == ENUM_TYPE) {
+            if (jsonElement.isJsonArray()) {
+              // Handling array
+              Collection<EnumValueDescriptor> enumCollection =
+                  new ArrayList<>(jsonElement.getAsJsonArray().size());
+              for (JsonElement element : jsonElement.getAsJsonArray()) {
+                enumCollection.add(
+                    findValueByNameAndExtension(fieldDescriptor.getEnumType(), element));
+              }
+              fieldValue = enumCollection;
+            } else {
+              // No array, just a plain value
+              fieldValue = findValueByNameAndExtension(fieldDescriptor.getEnumType(), jsonElement);
+            }
+            protoBuilder.setField(fieldDescriptor, fieldValue);
+          } else if (fieldDescriptor.isRepeated()) {
+            // If the type is an array, then we have to grab the type from the class.
+            // protobuf java field names are always lower camel case
+            String protoArrayFieldName =
+                protoFormat.to(CaseFormat.LOWER_CAMEL, fieldDescriptor.getName()) + "_";
+            Field protoArrayField = protoClass.getDeclaredField(protoArrayFieldName);
+            Type protoArrayFieldType = protoArrayField.getGenericType();
+            fieldValue = context.deserialize(jsonElement, protoArrayFieldType);
+            protoBuilder.setField(fieldDescriptor, fieldValue);
+          } else {
+            Object field = defaultInstance.getField(fieldDescriptor);
+            fieldValue = context.deserialize(jsonElement, field.getClass());
+            protoBuilder.setField(fieldDescriptor, fieldValue);
+          }
+        }
+      }
+      return protoBuilder.build();
+    } catch (Exception e) {
+      throw new JsonParseException("Error while parsing proto", e);
+    }
+  }
+
+  /**
+   * Retrieves the custom field name from the given options, and if not found, returns the specified
+   * default name.
+   */
+  private String getCustSerializedName(FieldOptions options, String defaultName) {
+    for (Extension<FieldOptions, String> extension : serializedNameExtensions) {
+      if (options.hasExtension(extension)) {
+        return options.getExtension(extension);
+      }
+    }
+    return protoFormat.to(jsonFormat, defaultName);
+  }
+
+  /**
+   * Retrieves the custom enum value name from the given options, and if not found, returns the
+   * specified default value.
+   */
+  private String getCustSerializedEnumValue(EnumValueOptions options, String defaultValue) {
+    for (Extension<EnumValueOptions, String> extension : serializedEnumValueExtensions) {
+      if (options.hasExtension(extension)) {
+        return options.getExtension(extension);
+      }
+    }
+    return defaultValue;
+  }
+
+  /**
+   * Returns the enum value to use for serialization, depending on the value of {@link
+   * EnumSerialization} that was given to this adapter.
+   */
+  private Object getEnumValue(EnumValueDescriptor enumDesc) {
+    if (enumSerialization == EnumSerialization.NAME) {
+      return getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName());
+    } else {
+      return enumDesc.getNumber();
+    }
+  }
+
+  /**
+   * Finds an enum value in the given {@link EnumDescriptor} that matches the given JSON element,
+   * either by name if the current adapter is using {@link EnumSerialization#NAME}, otherwise by
+   * number. If matching by name, it uses the extension value if it is defined, otherwise it uses
+   * its default value.
+   *
+   * @throws IllegalArgumentException if a matching name/number was not found
+   */
+  private EnumValueDescriptor findValueByNameAndExtension(
+      EnumDescriptor desc, JsonElement jsonElement) {
+    if (enumSerialization == EnumSerialization.NAME) {
+      // With enum name
+      for (EnumValueDescriptor enumDesc : desc.getValues()) {
+        String enumValue = getCustSerializedEnumValue(enumDesc.getOptions(), enumDesc.getName());
+        if (enumValue.equals(jsonElement.getAsString())) {
+          return enumDesc;
+        }
+      }
+      throw new IllegalArgumentException(
+          String.format("Unrecognized enum name: %s", jsonElement.getAsString()));
+    } else {
+      // With enum value
+      EnumValueDescriptor fieldValue = desc.findValueByNumber(jsonElement.getAsInt());
+      if (fieldValue == null) {
+        throw new IllegalArgumentException(
+            String.format("Unrecognized enum value: %d", jsonElement.getAsInt()));
+      }
+      return fieldValue;
+    }
+  }
+
+  private static Method getCachedMethod(
+      Class<?> clazz, String methodName, Class<?>... methodParamTypes)
+      throws NoSuchMethodException {
+    ConcurrentMap<Class<?>, Method> mapOfMethods = mapOfMapOfMethods.get(methodName);
+    if (mapOfMethods == null) {
+      mapOfMethods = new MapMaker().makeMap();
+      ConcurrentMap<Class<?>, Method> previous =
+          mapOfMapOfMethods.putIfAbsent(methodName, mapOfMethods);
+      mapOfMethods = previous == null ? mapOfMethods : previous;
+    }
+
+    Method method = mapOfMethods.get(clazz);
+    if (method == null) {
+      method = clazz.getMethod(methodName, methodParamTypes);
+      mapOfMethods.putIfAbsent(clazz, method);
+      // NB: it doesn't matter which method we return in the event of a race.
+    }
+    return method;
+  }
+}
diff --git a/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithAnnotationsTest.java b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithAnnotationsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c56ade3ad37dfba1d670525f8ed697aa644c76d7
--- /dev/null
+++ b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithAnnotationsTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2010 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.protobuf.functional;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.base.CaseFormat;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.protobuf.ProtoTypeAdapter;
+import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
+import com.google.gson.protobuf.generated.Annotations;
+import com.google.gson.protobuf.generated.Bag.OuterMessage;
+import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations;
+import com.google.gson.protobuf.generated.Bag.ProtoWithAnnotations.InnerMessage;
+import com.google.protobuf.GeneratedMessageV3;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for protocol buffers using annotations for field names and enum values.
+ *
+ * @author Emmanuel Cron
+ */
+public class ProtosWithAnnotationsTest {
+  private Gson gson;
+  private Gson gsonWithEnumNumbers;
+  private Gson gsonWithLowerHyphen;
+
+  @Before
+  public void setUp() throws Exception {
+    ProtoTypeAdapter.Builder protoTypeAdapter =
+        ProtoTypeAdapter.newBuilder()
+            .setEnumSerialization(EnumSerialization.NAME)
+            .addSerializedNameExtension(Annotations.serializedName)
+            .addSerializedEnumValueExtension(Annotations.serializedValue);
+    gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(GeneratedMessageV3.class, protoTypeAdapter.build())
+            .create();
+    gsonWithEnumNumbers =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                GeneratedMessageV3.class,
+                protoTypeAdapter.setEnumSerialization(EnumSerialization.NUMBER).build())
+            .create();
+    gsonWithLowerHyphen =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                GeneratedMessageV3.class,
+                protoTypeAdapter
+                    .setFieldNameSerializationFormat(
+                        CaseFormat.LOWER_UNDERSCORE, CaseFormat.LOWER_HYPHEN)
+                    .build())
+            .create();
+  }
+
+  @Test
+  public void testProtoWithAnnotations_deserialize() {
+    String json =
+        String.format(
+            "{  %n"
+                + "   \"id\":\"41e5e7fd6065d101b97018a465ffff01\",%n"
+                + "   \"expiration_date\":{  %n"
+                + "      \"month\":\"12\",%n"
+                + "      \"year\":\"2017\",%n"
+                + "      \"timeStamp\":\"9864653135687\",%n"
+                + "      \"countryCode5f55\":\"en_US\"%n"
+                + "   },%n"
+                // Don't define innerMessage1
+                + "   \"innerMessage2\":{  %n"
+                // Set a number as a string; it should work
+                + "      \"nIdCt\":\"98798465\",%n"
+                + "      \"content\":\"text/plain\",%n"
+                + "      \"$binary_data$\":[  %n"
+                + "         {  %n"
+                + "            \"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\",%n"
+                // Don't define width
+                + "            \"height\":665%n"
+                + "         },%n"
+                + "         {  %n"
+                // Define as an int; it should work
+                + "            \"data\":65,%n"
+                + "            \"width\":-56684%n"
+                // Don't define height
+                + "         }%n"
+                + "      ]%n"
+                + "   },%n"
+                // Define a bunch of non recognizable data
+                + "   \"non_existing\":\"foobar\",%n"
+                + "   \"stillNot\":{  %n"
+                + "      \"bunch\":\"of_useless data\"%n"
+                + "   }%n"
+                + "}");
+    ProtoWithAnnotations proto = gson.fromJson(json, ProtoWithAnnotations.class);
+    assertThat(proto.getId()).isEqualTo("41e5e7fd6065d101b97018a465ffff01");
+    assertThat(proto.getOuterMessage())
+        .isEqualTo(
+            OuterMessage.newBuilder()
+                .setMonth(12)
+                .setYear(2017)
+                .setLongTimestamp(9864653135687L)
+                .setCountryCode5F55("en_US")
+                .build());
+    assertThat(proto.hasInnerMessage1()).isFalse();
+    assertThat(proto.getInnerMessage2())
+        .isEqualTo(
+            InnerMessage.newBuilder()
+                .setNIdCt(98798465)
+                .setContent(InnerMessage.Type.TEXT)
+                .addData(
+                    InnerMessage.Data.newBuilder()
+                        .setData("OFIN8e9fhwoeh8((⁹8efywoih")
+                        .setHeight(665))
+                .addData(InnerMessage.Data.newBuilder().setData("65").setWidth(-56684))
+                .build());
+
+    String rebuilt = gson.toJson(proto);
+    assertThat(rebuilt)
+        .isEqualTo(
+            "{"
+                + "\"id\":\"41e5e7fd6065d101b97018a465ffff01\","
+                + "\"expiration_date\":{"
+                + "\"month\":12,"
+                + "\"year\":2017,"
+                + "\"timeStamp\":9864653135687,"
+                + "\"countryCode5f55\":\"en_US\""
+                + "},"
+                + "\"innerMessage2\":{"
+                + "\"nIdCt\":98798465,"
+                + "\"content\":\"text/plain\","
+                + "\"$binary_data$\":["
+                + "{"
+                + "\"data\":\"OFIN8e9fhwoeh8((⁹8efywoih\","
+                + "\"height\":665"
+                + "},"
+                + "{"
+                + "\"data\":\"65\","
+                + "\"width\":-56684"
+                + "}]}}");
+  }
+
+  @Test
+  public void testProtoWithAnnotations_deserializeUnknownEnumValue() {
+    String json = String.format("{  %n" + "   \"content\":\"UNKNOWN\"%n" + "}");
+    InnerMessage proto = gson.fromJson(json, InnerMessage.class);
+    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
+  }
+
+  @Test
+  public void testProtoWithAnnotations_deserializeUnrecognizedEnumValue() {
+    String json = String.format("{  %n" + "   \"content\":\"UNRECOGNIZED\"%n" + "}");
+    try {
+      gson.fromJson(json, InnerMessage.class);
+      assertWithMessage("Should have thrown").fail();
+    } catch (JsonParseException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testProtoWithAnnotations_deserializeWithEnumNumbers() {
+    String json = String.format("{  %n" + "   \"content\":\"0\"%n" + "}");
+    InnerMessage proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
+    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.UNKNOWN);
+    String rebuilt = gsonWithEnumNumbers.toJson(proto);
+    assertThat(rebuilt).isEqualTo("{\"content\":0}");
+
+    json = String.format("{  %n" + "   \"content\":\"2\"%n" + "}");
+    proto = gsonWithEnumNumbers.fromJson(json, InnerMessage.class);
+    assertThat(proto.getContent()).isEqualTo(InnerMessage.Type.IMAGE);
+    rebuilt = gsonWithEnumNumbers.toJson(proto);
+    assertThat(rebuilt).isEqualTo("{\"content\":2}");
+  }
+
+  @Test
+  public void testProtoWithAnnotations_serialize() {
+    ProtoWithAnnotations proto =
+        ProtoWithAnnotations.newBuilder()
+            .setId("09f3j20839h032y0329hf30932h0nffn")
+            .setOuterMessage(
+                OuterMessage.newBuilder()
+                    .setMonth(14)
+                    .setYear(6650)
+                    .setLongTimestamp(468406876880768L))
+            .setInnerMessage1(
+                InnerMessage.newBuilder()
+                    .setNIdCt(12)
+                    .setContent(InnerMessage.Type.IMAGE)
+                    .addData(InnerMessage.Data.newBuilder().setData("data$$").setWidth(200))
+                    .addData(InnerMessage.Data.newBuilder().setHeight(56)))
+            .build();
+
+    String json = gsonWithLowerHyphen.toJson(proto);
+    assertThat(json)
+        .isEqualTo(
+            "{\"id\":\"09f3j20839h032y0329hf30932h0nffn\","
+                + "\"expiration_date\":{"
+                + "\"month\":14,"
+                + "\"year\":6650,"
+                + "\"timeStamp\":468406876880768"
+                + "},"
+                // This field should be using hyphens
+                + "\"inner-message-1\":{"
+                + "\"n--id-ct\":12,"
+                + "\"content\":2,"
+                + "\"$binary_data$\":["
+                + "{"
+                + "\"data\":\"data$$\","
+                + "\"width\":200"
+                + "},"
+                + "{"
+                + "\"height\":56"
+                + "}]"
+                + "}"
+                + "}");
+
+    ProtoWithAnnotations rebuilt = gsonWithLowerHyphen.fromJson(json, ProtoWithAnnotations.class);
+    assertThat(rebuilt).isEqualTo(proto);
+  }
+}
diff --git a/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithComplexAndRepeatedFieldsTest.java b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithComplexAndRepeatedFieldsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1c3bc1c891cb9ac6088fb9a67959012eac32f51c
--- /dev/null
+++ b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithComplexAndRepeatedFieldsTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 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.protobuf.functional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.CaseFormat;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.protobuf.ProtoTypeAdapter;
+import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
+import com.google.gson.protobuf.generated.Bag.ProtoWithDifferentCaseFormat;
+import com.google.gson.protobuf.generated.Bag.ProtoWithRepeatedFields;
+import com.google.gson.protobuf.generated.Bag.SimpleProto;
+import com.google.protobuf.GeneratedMessageV3;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Functional tests for protocol buffers using complex and repeated fields
+ *
+ * @author Inderjeet Singh
+ */
+public class ProtosWithComplexAndRepeatedFieldsTest {
+  private Gson gson;
+  private Gson upperCamelGson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                GeneratedMessageV3.class,
+                ProtoTypeAdapter.newBuilder()
+                    .setEnumSerialization(EnumSerialization.NUMBER)
+                    .build())
+            .create();
+    upperCamelGson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                GeneratedMessageV3.class,
+                ProtoTypeAdapter.newBuilder()
+                    .setFieldNameSerializationFormat(
+                        CaseFormat.LOWER_UNDERSCORE, CaseFormat.UPPER_CAMEL)
+                    .build())
+            .create();
+  }
+
+  @Test
+  public void testSerializeRepeatedFields() {
+    ProtoWithRepeatedFields proto =
+        ProtoWithRepeatedFields.newBuilder()
+            .addNumbers(2)
+            .addNumbers(3)
+            .addSimples(SimpleProto.newBuilder().setMsg("foo").build())
+            .addSimples(SimpleProto.newBuilder().setCount(3).build())
+            .build();
+    String json = gson.toJson(proto);
+    assertTrue(json.contains("[2,3]"));
+    assertTrue(json.contains("foo"));
+    assertTrue(json.contains("count"));
+  }
+
+  @Test
+  public void testDeserializeRepeatedFieldsProto() {
+    String json = "{numbers:[4,6],simples:[{msg:'bar'},{count:7}]}";
+    ProtoWithRepeatedFields proto = gson.fromJson(json, ProtoWithRepeatedFields.class);
+    assertEquals(4, proto.getNumbers(0));
+    assertEquals(6, proto.getNumbers(1));
+    assertEquals("bar", proto.getSimples(0).getMsg());
+    assertEquals(7, proto.getSimples(1).getCount());
+  }
+
+  @Test
+  public void testSerializeDifferentCaseFormat() {
+    final ProtoWithDifferentCaseFormat proto =
+        ProtoWithDifferentCaseFormat.newBuilder()
+            .setAnotherField("foo")
+            .addNameThatTestsCaseFormat("bar")
+            .build();
+    final JsonObject json = upperCamelGson.toJsonTree(proto).getAsJsonObject();
+    assertEquals("foo", json.get("AnotherField").getAsString());
+    assertEquals("bar", json.get("NameThatTestsCaseFormat").getAsJsonArray().get(0).getAsString());
+  }
+
+  @Test
+  public void testDeserializeDifferentCaseFormat() {
+    final String json = "{NameThatTestsCaseFormat:['bar'],AnotherField:'foo'}";
+    ProtoWithDifferentCaseFormat proto =
+        upperCamelGson.fromJson(json, ProtoWithDifferentCaseFormat.class);
+    assertEquals("foo", proto.getAnotherField());
+    assertEquals("bar", proto.getNameThatTestsCaseFormat(0));
+  }
+}
diff --git a/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithPrimitiveTypesTest.java b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithPrimitiveTypesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..57ab2df7ef9b9fa80a2d5918ff6f885a8674aa3c
--- /dev/null
+++ b/gson/proto/src/test/java/com/google/gson/protobuf/functional/ProtosWithPrimitiveTypesTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.protobuf.functional;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.protobuf.ProtoTypeAdapter;
+import com.google.gson.protobuf.ProtoTypeAdapter.EnumSerialization;
+import com.google.gson.protobuf.generated.Bag.SimpleProto;
+import com.google.protobuf.GeneratedMessageV3;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ProtosWithPrimitiveTypesTest {
+  private Gson gson;
+
+  @Before
+  public void setUp() throws Exception {
+    gson =
+        new GsonBuilder()
+            .registerTypeHierarchyAdapter(
+                GeneratedMessageV3.class,
+                ProtoTypeAdapter.newBuilder()
+                    .setEnumSerialization(EnumSerialization.NUMBER)
+                    .build())
+            .create();
+  }
+
+  @Test
+  public void testSerializeEmptyProto() {
+    SimpleProto proto = SimpleProto.newBuilder().build();
+    String json = gson.toJson(proto);
+    assertEquals("{}", json);
+  }
+
+  @Test
+  public void testDeserializeEmptyProto() {
+    SimpleProto proto = gson.fromJson("{}", SimpleProto.class);
+    assertFalse(proto.hasCount());
+    assertFalse(proto.hasMsg());
+  }
+
+  @Test
+  public void testSerializeProto() {
+    SimpleProto proto = SimpleProto.newBuilder().setCount(3).setMsg("foo").build();
+    String json = gson.toJson(proto);
+    assertTrue(json.contains("\"msg\":\"foo\""));
+    assertTrue(json.contains("\"count\":3"));
+  }
+
+  @Test
+  public void testDeserializeProto() {
+    SimpleProto proto = gson.fromJson("{msg:'foo',count:3}", SimpleProto.class);
+    assertEquals("foo", proto.getMsg());
+    assertEquals(3, proto.getCount());
+  }
+
+  @Test
+  public void testDeserializeWithExplicitNullValue() {
+    SimpleProto proto = gson.fromJson("{msg:'foo',count:null}", SimpleProto.class);
+    assertEquals("foo", proto.getMsg());
+    assertEquals(0, proto.getCount());
+  }
+}
diff --git a/gson/proto/src/test/proto/annotations.proto b/gson/proto/src/test/proto/annotations.proto
new file mode 100644
index 0000000000000000000000000000000000000000..53b727a6661903a44e153b63cb42d754b3b333d8
--- /dev/null
+++ b/gson/proto/src/test/proto/annotations.proto
@@ -0,0 +1,32 @@
+//
+// Copyright (C) 2010 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.
+//
+
+syntax = "proto2";
+
+package google.gson.protobuf.generated;
+option java_package = "com.google.gson.protobuf.generated";
+
+import "google/protobuf/descriptor.proto";
+
+extend google.protobuf.FieldOptions {
+  // Indicates a field name that overrides the default for serialization
+  optional string serialized_name = 92066888;
+}
+
+extend google.protobuf.EnumValueOptions {
+  // Indicates a field value that overrides the default for serialization
+  optional string serialized_value = 92066888;
+}
diff --git a/gson/proto/src/test/proto/bag.proto b/gson/proto/src/test/proto/bag.proto
new file mode 100644
index 0000000000000000000000000000000000000000..3e4769e2a8ca64b8a227732f797bcb91afce0c1b
--- /dev/null
+++ b/gson/proto/src/test/proto/bag.proto
@@ -0,0 +1,72 @@
+//
+// Copyright (C) 2010 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.
+//
+
+syntax = "proto2";
+
+package google.gson.protobuf.generated;
+option java_package = "com.google.gson.protobuf.generated";
+
+import "annotations.proto";
+
+message SimpleProto {
+  optional string msg = 1;
+  optional int32 count = 2;
+}
+
+message ProtoWithDifferentCaseFormat {
+  repeated string name_that_tests_case_format = 1;
+  optional string another_field = 2;
+}
+
+message ProtoWithRepeatedFields {
+  repeated int64 numbers = 1;
+  repeated SimpleProto simples = 2;
+  optional string name = 3;
+}
+
+// -- A more complex message with annotations and nested protos
+
+message OuterMessage {
+  optional int32 month = 1;
+  optional int32 year = 2;
+  optional int64 long_timestamp = 3 [(serialized_name) = "timeStamp"];
+  optional string country_code_5f55 = 4;
+}
+
+message ProtoWithAnnotations {
+  optional string id = 1;
+  optional OuterMessage outer_message = 2 [(serialized_name) = "expiration_date"];
+
+  message InnerMessage {
+    optional int32 n__id_ct = 1;
+
+    enum Type {
+      UNKNOWN = 0;
+      TEXT = 1 [(serialized_value) = "text/plain"];
+      IMAGE = 2 [(serialized_value) = "image/png"];
+    }
+    optional Type content = 2;
+
+    message Data {
+      optional string data = 1;
+      optional int32 width = 2;
+      optional int32 height = 3;
+    }
+    repeated Data data = 3 [(serialized_name) = "$binary_data$"];
+  }
+  optional InnerMessage inner_message_1 = 3;
+  optional InnerMessage inner_message_2 = 4;
+}
\ No newline at end of file
diff --git a/gson/shrinker-test/README.md b/gson/shrinker-test/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f9b674d143e81e37ec47815f7ee47e6c68f6f43b
--- /dev/null
+++ b/gson/shrinker-test/README.md
@@ -0,0 +1,9 @@
+# shrinker-test
+
+This Maven module contains integration tests which check the behavior of Gson when used in combination with code shrinking and obfuscation tools, such as ProGuard or R8.
+
+The code which is shrunken is under `src/main/java`; it should not contain any important assertions in case the code shrinking tools affect these assertions in any way. The test code under `src/test/java` executes the shrunken and obfuscated JAR and verifies that it behaves as expected.
+
+The tests might be a bit brittle, especially the R8 test setup. Future ProGuard and R8 versions might cause the tests to behave differently. In case tests fail the ProGuard and R8 mapping files created in the `target` directory can help with debugging. If necessary rewrite tests or even remove them if they cannot be implemented anymore for newer ProGuard or R8 versions.
+
+**Important:** Because execution of the code shrinking tools is performed during the Maven build, trying to directly run the integration tests from the IDE might not work, or might use stale results if you changed the configuration in between. Run `mvn clean verify` before trying to run the integration tests from the IDE.
diff --git a/gson/shrinker-test/common.pro b/gson/shrinker-test/common.pro
new file mode 100644
index 0000000000000000000000000000000000000000..9995925e9dea4490e1377719697f54c02ad8f3c2
--- /dev/null
+++ b/gson/shrinker-test/common.pro
@@ -0,0 +1,44 @@
+### Common rules for ProGuard and R8
+### Should only contains rules needed specifically for the integration test;
+### any general rules which are relevant for all users should not be here but in `META-INF/proguard` of Gson
+
+-allowaccessmodification
+
+# On Windows mixed case class names might cause problems
+-dontusemixedcaseclassnames
+
+# Ignore notes about duplicate JDK classes
+-dontnote module-info,jdk.internal.**
+
+
+# Keep test entrypoints
+-keep class com.example.Main {
+  public static void runTests(...);
+}
+-keep class com.example.NoSerializedNameMain {
+  public static java.lang.String runTestNoArgsConstructor();
+  public static java.lang.String runTestNoJdkUnsafe();
+  public static java.lang.String runTestHasArgsConstructor();
+}
+
+
+### Test data setup
+
+# Keep fields without annotations which should be preserved
+-keepclassmembers class com.example.ClassWithNamedFields {
+  !transient <fields>;
+}
+
+-keepclassmembernames class com.example.ClassWithExposeAnnotation {
+  <fields>;
+}
+-keepclassmembernames class com.example.ClassWithJsonAdapterAnnotation {
+  ** f;
+}
+-keepclassmembernames class com.example.ClassWithVersionAnnotations {
+  <fields>;
+}
+
+# Keep the name of the class to allow using reflection to check if this class still exists
+# after shrinking
+-keepnames class com.example.UnusedClass
diff --git a/gson/shrinker-test/pom.xml b/gson/shrinker-test/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6e47eefa7960aeef1d244891b5c0d63e5960d6ce
--- /dev/null
+++ b/gson/shrinker-test/pom.xml
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2023 Google Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<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>com.google.code.gson</groupId>
+    <artifactId>gson-parent</artifactId>
+    <version>2.10.2-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>shrinker-test</artifactId>
+
+  <properties>
+    <!-- Make the build reproducible, see root `pom.xml` -->
+    <!-- This is duplicated here because that is recommended by `artifact:check-buildplan` -->
+    <project.build.outputTimestamp>2023-01-01T00:00:00Z</project.build.outputTimestamp>
+
+    <maven.compiler.release>8</maven.compiler.release>
+  </properties>
+
+  <pluginRepositories>
+    <!-- R8 currently only exists in Google Maven repository -->
+    <pluginRepository>
+      <id>google</id>
+      <url>https://maven.google.com</url>
+    </pluginRepository>
+  </pluginRepositories>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${project.parent.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>com.github.siom79.japicmp</groupId>
+          <artifactId>japicmp-maven-plugin</artifactId>
+          <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.codehaus.mojo</groupId>
+          <artifactId>animal-sniffer-maven-plugin</artifactId>
+          <configuration>
+            <!-- This module is not supposed to be consumed as library, so no need to check used classes -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <configuration>
+            <!-- Not deployed -->
+            <skip>true</skip>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <plugins>
+      <!-- Process JAR with ProGuard -->
+      <plugin>
+        <groupId>com.github.wvengen</groupId>
+        <artifactId>proguard-maven-plugin</artifactId>
+        <version>2.6.0</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>proguard</goal>
+            </goals>
+          </execution>
+        </executions>
+        <!-- Upgrades ProGuard to version newer than the one included by plugin by default -->
+        <dependencies>
+          <dependency>
+            <groupId>com.guardsquare</groupId>
+            <artifactId>proguard-base</artifactId>
+            <version>7.4.1</version>
+          </dependency>
+          <dependency>
+            <groupId>com.guardsquare</groupId>
+            <artifactId>proguard-core</artifactId>
+            <version>9.1.1</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <obfuscate>true</obfuscate>
+          <proguardInclude>${project.basedir}/proguard.pro</proguardInclude>
+          <options>
+            <!-- Hacky solution to make ProGuard use the library rules file; only the Android plugin of ProGuard
+              seems to consider it automatically at the moment, see https://github.com/Guardsquare/proguard/issues/337
+              However, R8 defined further below always considers it automatically -->
+            <option>-include</option><option>${project.basedir}/../gson/src/main/resources/META-INF/proguard/gson.pro</option>
+          </options>
+          <libs>
+            <lib>${java.home}/jmods/java.base.jmod</lib>
+            <!-- Used by Gson for optional SQL types support -->
+            <lib>${java.home}/jmods/java.sql.jmod</lib>
+            <!-- Used by transitive Error Prone annotations dependency -->
+            <lib>${java.home}/jmods/java.compiler.jmod</lib>
+          </libs>
+          <!-- Include dependencies in the final JAR -->
+          <includeDependencyInjar>true</includeDependencyInjar>
+          <outjar>proguard-output.jar</outjar>
+        </configuration>
+      </plugin>
+
+      <!-- Prepare a JAR with dependencies for R8 -->
+      <!-- Once there is a proper R8 Maven plugin in the future, prefer that and provide
+        dependencies as additional input JARs there instead of using the Shade plugin -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.5.1</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <createDependencyReducedPom>false</createDependencyReducedPom>
+              <!-- Replace the main JAR -->
+              <shadedArtifactAttached>false</shadedArtifactAttached>
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <!-- Ignore duplicate files in dependencies -->
+                    <exclude>META-INF/MANIFEST.MF</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <!-- Process JAR with R8; currently has no dedicated plugin so use Exec Maven Plugin instead -->
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>3.1.1</version>
+        <executions>
+          <execution>
+            <id>r8</id>
+            <phase>package</phase>
+            <goals>
+              <goal>java</goal>
+            </goals>
+            <configuration>
+              <!-- R8 runs as standalone JAR, does not need any of the project classes -->
+              <addOutputToClasspath>false</addOutputToClasspath>
+              <includeProjectDependencies>false</includeProjectDependencies>
+              <!-- R8 is specified as plugin dependency, see further below -->
+              <includePluginDependencies>true</includePluginDependencies>
+              <executableDependency>
+                <!-- Uses R8 dependency declared below -->
+                <groupId>com.android.tools</groupId>
+                <artifactId>r8</artifactId>
+              </executableDependency>
+              <!-- See https://r8.googlesource.com/r8/+/refs/heads/main/README.md#running-r8 -->
+              <!-- Without `pg-compat` argument this acts like "full mode", see
+                https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode -->
+              <mainClass>com.android.tools.r8.R8</mainClass>
+              <arguments>
+                <argument>--release</argument>
+                <!-- Produce Java class files instead of Android DEX files -->
+                <argument>--classfile</argument>
+                <argument>--lib</argument><argument>${java.home}</argument>
+                <argument>--pg-conf</argument><argument>${project.basedir}/r8.pro</argument>
+                <!-- Create mapping file to make debugging test failures easier -->
+                <argument>--pg-map-output</argument><argument>${project.build.directory}/r8_map.txt</argument>
+                <argument>--output</argument><argument>${project.build.directory}/r8-output.jar</argument>
+                <argument>${project.build.directory}/${project.build.finalName}.jar</argument>
+              </arguments>
+            </configuration>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <!-- R8 dependency used above -->
+            <!-- Note: For some reason Maven shows the warning "Missing POM for com.android.tools:r8:jar",
+              but it appears that can be ignored -->
+            <groupId>com.android.tools</groupId>
+            <artifactId>r8</artifactId>
+            <version>8.2.42</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+
+
+      <!-- Run integration tests to verify shrunken JAR behavior -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <version>3.2.5</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gson/shrinker-test/proguard.pro b/gson/shrinker-test/proguard.pro
new file mode 100644
index 0000000000000000000000000000000000000000..7922bc0967678c0ef8fcdf91fdc9c1daab9cc962
--- /dev/null
+++ b/gson/shrinker-test/proguard.pro
@@ -0,0 +1,17 @@
+# Include common rules
+-include common.pro
+
+### ProGuard specific rules
+
+# Unlike R8, ProGuard does not perform aggressive optimization which makes classes abstract,
+# therefore for ProGuard can successfully perform deserialization, and for that need to
+# preserve the field names
+-keepclassmembernames class com.example.NoSerializedNameMain$TestClassNoArgsConstructor {
+  <fields>;
+}
+-keepclassmembernames class com.example.NoSerializedNameMain$TestClassNotAbstract {
+  <fields>;
+}
+-keepclassmembernames class com.example.NoSerializedNameMain$TestClassHasArgsConstructor {
+  <fields>;
+}
diff --git a/gson/shrinker-test/r8.pro b/gson/shrinker-test/r8.pro
new file mode 100644
index 0000000000000000000000000000000000000000..642334d56f0988188b975391d40be877f2ebb7dd
--- /dev/null
+++ b/gson/shrinker-test/r8.pro
@@ -0,0 +1,24 @@
+# Include common rules
+-include common.pro
+
+### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard
+### See https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode
+
+# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to
+# preserve the generic signature
+-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass
+-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass
+
+# Don't obfuscate class name, to check it in exception message
+-keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassNoArgsConstructor
+-keep,allowshrinking,allowoptimization class com.example.NoSerializedNameMain$TestClassHasArgsConstructor
+
+# This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract
+-keep class com.example.NoSerializedNameMain$TestClassNotAbstract {
+  @com.google.gson.annotations.SerializedName <fields>;
+}
+
+# Keep enum constants which are not explicitly used in code
+-keepclassmembers class com.example.EnumClass {
+  ** SECOND;
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithAdapter.java b/gson/shrinker-test/src/main/java/com/example/ClassWithAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa7f08da6bf293f9ca70f77a03a3e352454f6ab9
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithAdapter.java
@@ -0,0 +1,44 @@
+package com.example;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+@JsonAdapter(ClassWithAdapter.Adapter.class)
+public class ClassWithAdapter {
+  static class Adapter extends TypeAdapter<ClassWithAdapter> {
+    @Override
+    public ClassWithAdapter read(JsonReader in) throws IOException {
+      in.beginObject();
+      String name = in.nextName();
+      if (!name.equals("custom")) {
+        throw new IllegalArgumentException("Unexpected name: " + name);
+      }
+      int i = in.nextInt();
+      in.endObject();
+
+      return new ClassWithAdapter(i);
+    }
+
+    @Override
+    public void write(JsonWriter out, ClassWithAdapter value) throws IOException {
+      out.beginObject();
+      out.name("custom");
+      out.value(value.i);
+      out.endObject();
+    }
+  }
+
+  public Integer i;
+
+  public ClassWithAdapter(int i) {
+    this.i = i;
+  }
+
+  @Override
+  public String toString() {
+    return "ClassWithAdapter[" + i + "]";
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java b/gson/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java
new file mode 100644
index 0000000000000000000000000000000000000000..b35cf374ca6bda59a018341fc5e53fe614f34e31
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java
@@ -0,0 +1,10 @@
+package com.example;
+
+import com.google.gson.annotations.Expose;
+
+/** Uses {@link Expose} annotation. */
+public class ClassWithExposeAnnotation {
+  @Expose int i;
+
+  int i2;
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java b/gson/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..855516b71d4b8b47ac15d71afd45961a0704aaed
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithHasArgsConstructor.java
@@ -0,0 +1,14 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+/** Class without no-args constructor, but with field annotated with {@link SerializedName}. */
+public class ClassWithHasArgsConstructor {
+  @SerializedName("myField")
+  public int i;
+
+  // Specify explicit constructor with args to remove implicit no-args default constructor
+  public ClassWithHasArgsConstructor(int i) {
+    this.i = i;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/gson/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java
new file mode 100644
index 0000000000000000000000000000000000000000..aab1ef6112bff2d0b35b437acecfd69f2ca7e64c
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java
@@ -0,0 +1,137 @@
+package com.example;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+/** Uses {@link JsonAdapter} annotation on fields. */
+public class ClassWithJsonAdapterAnnotation {
+  // For this field don't use @SerializedName and ignore it for deserialization
+  // Has custom ProGuard rule to keep the field name
+  @JsonAdapter(value = Adapter.class, nullSafe = false)
+  DummyClass f;
+
+  @SerializedName("f1")
+  @JsonAdapter(Adapter.class)
+  DummyClass f1;
+
+  @SerializedName("f2")
+  @JsonAdapter(Factory.class)
+  DummyClass f2;
+
+  @SerializedName("f3")
+  @JsonAdapter(Serializer.class)
+  DummyClass f3;
+
+  @SerializedName("f4")
+  @JsonAdapter(Deserializer.class)
+  DummyClass f4;
+
+  public ClassWithJsonAdapterAnnotation() {}
+
+  // Note: R8 seems to make this constructor the no-args constructor and initialize fields
+  // by default; currently this is not visible in the deserialization test because the JSON data
+  // contains values for all fields; but it is noticeable once the JSON data is missing fields
+  public ClassWithJsonAdapterAnnotation(int i1, int i2, int i3, int i4) {
+    f1 = new DummyClass(Integer.toString(i1));
+    f2 = new DummyClass(Integer.toString(i2));
+    f3 = new DummyClass(Integer.toString(i3));
+    f4 = new DummyClass(Integer.toString(i4));
+
+    // Note: Deliberately don't initialize field `f` here to not refer to it anywhere in code
+  }
+
+  @Override
+  public String toString() {
+    return "ClassWithJsonAdapterAnnotation[f1="
+        + f1
+        + ", f2="
+        + f2
+        + ", f3="
+        + f3
+        + ", f4="
+        + f4
+        + "]";
+  }
+
+  static class Adapter extends TypeAdapter<DummyClass> {
+    @Override
+    public DummyClass read(JsonReader in) throws IOException {
+      return new DummyClass("adapter-" + in.nextInt());
+    }
+
+    @Override
+    public void write(JsonWriter out, DummyClass value) throws IOException {
+      out.value("adapter-" + value);
+    }
+  }
+
+  static class Factory implements TypeAdapterFactory {
+    @Override
+    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+      // the code below is not type-safe, but does not matter for this test
+      @SuppressWarnings("unchecked")
+      TypeAdapter<T> r =
+          (TypeAdapter<T>)
+              new TypeAdapter<DummyClass>() {
+                @Override
+                public DummyClass read(JsonReader in) throws IOException {
+                  return new DummyClass("factory-" + in.nextInt());
+                }
+
+                @Override
+                public void write(JsonWriter out, DummyClass value) throws IOException {
+                  out.value("factory-" + value.s);
+                }
+              };
+
+      return r;
+    }
+  }
+
+  static class Serializer implements JsonSerializer<DummyClass> {
+    @Override
+    public JsonElement serialize(DummyClass src, Type typeOfSrc, JsonSerializationContext context) {
+      return new JsonPrimitive("serializer-" + src.s);
+    }
+  }
+
+  static class Deserializer implements JsonDeserializer<DummyClass> {
+    @Override
+    public DummyClass deserialize(
+        JsonElement json, Type typeOfT, JsonDeserializationContext context)
+        throws JsonParseException {
+      return new DummyClass("deserializer-" + json.getAsInt());
+    }
+  }
+
+  // Use this separate class mainly to work around incorrect delegation behavior for JsonSerializer
+  // and JsonDeserializer used with @JsonAdapter, see https://github.com/google/gson/issues/1783
+  static class DummyClass {
+    @SerializedName("s")
+    String s;
+
+    DummyClass(String s) {
+      this.s = s;
+    }
+
+    @Override
+    public String toString() {
+      return s;
+    }
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java b/gson/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a68da9c25538cd13257da9a094a4123bc34c5ca
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java
@@ -0,0 +1,10 @@
+package com.example;
+
+public class ClassWithNamedFields {
+  public int myField;
+  public short notAccessedField = -1;
+
+  public ClassWithNamedFields(int i) {
+    myField = i;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java b/gson/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..5222dff918932ba55d55d43ebe2ddd8c0dc65f97
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithNoArgsConstructor.java
@@ -0,0 +1,13 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+/** Class with no-args constructor and with field annotated with {@link SerializedName}. */
+public class ClassWithNoArgsConstructor {
+  @SerializedName("myField")
+  public int i;
+
+  public ClassWithNoArgsConstructor() {
+    i = -3;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java b/gson/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce982215cab00f76298720fdda8bb6c630c09856
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java
@@ -0,0 +1,15 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+public class ClassWithSerializedName {
+  @SerializedName("myField")
+  public int i;
+
+  @SerializedName("notAccessed")
+  public short notAccessedField = -1;
+
+  public ClassWithSerializedName(int i) {
+    this.i = i;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java b/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..5e9cd6d20dec5f3cb7a7dbe8527adb7838998be1
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedHasArgsConstructor.java
@@ -0,0 +1,18 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Class without no-args constructor, but with field annotated with {@link SerializedName}. The
+ * constructor should not be used in the code, but this shouldn't lead to R8 concluding that values
+ * of the type are not constructible and therefore must be null.
+ */
+public class ClassWithUnreferencedHasArgsConstructor {
+  @SerializedName("myField")
+  public int i;
+
+  // Specify explicit constructor with args to remove implicit no-args default constructor
+  public ClassWithUnreferencedHasArgsConstructor(int i) {
+    this.i = i;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java b/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java
new file mode 100644
index 0000000000000000000000000000000000000000..dba9deb69c928fc5704b14697e621c91d3afbd0b
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithUnreferencedNoArgsConstructor.java
@@ -0,0 +1,17 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Class with no-args constructor and with field annotated with {@link SerializedName}. The
+ * constructor should not be used in the code, but this shouldn't lead to R8 concluding that values
+ * of the type are not constructible and therefore must be null.
+ */
+public class ClassWithUnreferencedNoArgsConstructor {
+  @SerializedName("myField")
+  public int i;
+
+  public ClassWithUnreferencedNoArgsConstructor() {
+    i = -3;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java b/gson/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java
new file mode 100644
index 0000000000000000000000000000000000000000..28b37c755df51b52f7a19071ad2965a13517c7d2
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java
@@ -0,0 +1,19 @@
+package com.example;
+
+import com.google.gson.annotations.Since;
+import com.google.gson.annotations.Until;
+
+/** Uses {@link Since} and {@link Until} annotations. */
+public class ClassWithVersionAnnotations {
+  @Since(1)
+  int i1;
+
+  @Until(1) // will be ignored with GsonBuilder.setVersion(1)
+  int i2;
+
+  @Since(2) // will be ignored with GsonBuilder.setVersion(1)
+  int i3;
+
+  @Until(2)
+  int i4;
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/EnumClass.java b/gson/shrinker-test/src/main/java/com/example/EnumClass.java
new file mode 100644
index 0000000000000000000000000000000000000000..36688887bb251a623042b08c2e83d062b5eec158
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/EnumClass.java
@@ -0,0 +1,6 @@
+package com.example;
+
+public enum EnumClass {
+  FIRST,
+  SECOND
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java b/gson/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java
new file mode 100644
index 0000000000000000000000000000000000000000..a127a8be1377f829258f1adb7a589fba3f27bfc8
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java
@@ -0,0 +1,10 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+public enum EnumClassWithSerializedName {
+  @SerializedName("one")
+  FIRST,
+  @SerializedName("two")
+  SECOND
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/GenericClasses.java b/gson/shrinker-test/src/main/java/com/example/GenericClasses.java
new file mode 100644
index 0000000000000000000000000000000000000000..10d8659f18c741bc9b013eddd45296c35d264fed
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/GenericClasses.java
@@ -0,0 +1,68 @@
+package com.example;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
+
+public class GenericClasses {
+  private GenericClasses() {}
+
+  static class GenericClass<T> {
+    @SerializedName("t")
+    T t;
+
+    @Override
+    public String toString() {
+      return "{t=" + t + "}";
+    }
+  }
+
+  static class UsingGenericClass {
+    @SerializedName("g")
+    GenericClass<DummyClass> g;
+
+    @Override
+    public String toString() {
+      return "{g=" + g + "}";
+    }
+  }
+
+  static class GenericUsingGenericClass<T> {
+    @SerializedName("g")
+    GenericClass<T> g;
+
+    @Override
+    public String toString() {
+      return "{g=" + g + "}";
+    }
+  }
+
+  @JsonAdapter(DummyClass.Adapter.class)
+  static class DummyClass {
+    String s;
+
+    DummyClass(String s) {
+      this.s = s;
+    }
+
+    @Override
+    public String toString() {
+      return s;
+    }
+
+    static class Adapter extends TypeAdapter<DummyClass> {
+      @Override
+      public DummyClass read(JsonReader in) throws IOException {
+        return new DummyClass("read-" + in.nextInt());
+      }
+
+      @Override
+      public void write(JsonWriter out, DummyClass value) throws IOException {
+        throw new UnsupportedOperationException();
+      }
+    }
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/Main.java b/gson/shrinker-test/src/main/java/com/example/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..745200a026478e39a7d9003870d0d112758c93d3
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/Main.java
@@ -0,0 +1,293 @@
+package com.example;
+
+import static com.example.TestExecutor.same;
+
+import com.example.GenericClasses.DummyClass;
+import com.example.GenericClasses.GenericClass;
+import com.example.GenericClasses.GenericUsingGenericClass;
+import com.example.GenericClasses.UsingGenericClass;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+public class Main {
+  private Main() {}
+
+  /**
+   * Main entrypoint, called by {@code ShrinkingIT.test()}.
+   *
+   * <p>To be safe let all tests put their output to the consumer and let integration test verify
+   * it; don't perform any relevant assertions in this code because code shrinkers could affect it.
+   *
+   * @param outputConsumer consumes the test output: {@code name, content} pairs
+   */
+  public static void runTests(BiConsumer<String, String> outputConsumer) {
+    // Create the TypeToken instances on demand because creation of them can fail when
+    // generic signatures were erased
+    testTypeTokenWriteRead(
+        outputConsumer, "anonymous", () -> new TypeToken<List<ClassWithAdapter>>() {});
+    testTypeTokenWriteRead(
+        outputConsumer,
+        "manual",
+        () -> TypeToken.getParameterized(List.class, ClassWithAdapter.class));
+
+    testNamedFields(outputConsumer);
+    testSerializedName(outputConsumer);
+
+    testConstructorNoArgs(outputConsumer);
+    testConstructorHasArgs(outputConsumer);
+    testUnreferencedConstructorNoArgs(outputConsumer);
+    testUnreferencedConstructorHasArgs(outputConsumer);
+
+    testNoJdkUnsafe(outputConsumer);
+
+    testEnum(outputConsumer);
+    testEnumSerializedName(outputConsumer);
+
+    testExposeAnnotation(outputConsumer);
+    testVersionAnnotations(outputConsumer);
+    testJsonAdapterAnnotation(outputConsumer);
+
+    testGenericClasses(outputConsumer);
+  }
+
+  private static void testTypeTokenWriteRead(
+      BiConsumer<String, String> outputConsumer,
+      String description,
+      Supplier<TypeToken<?>> typeTokenSupplier) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+    TestExecutor.run(
+        outputConsumer,
+        "Write: TypeToken " + description,
+        () ->
+            gson.toJson(Arrays.asList(new ClassWithAdapter(1)), typeTokenSupplier.get().getType()));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: TypeToken " + description,
+        () -> {
+          Object deserialized = gson.fromJson("[{\"custom\": 3}]", typeTokenSupplier.get());
+          return deserialized.toString();
+        });
+  }
+
+  /**
+   * Calls {@link Gson#toJson}, but (hopefully) in a way which prevents code shrinkers from
+   * understanding that reflection is used for {@code obj}.
+   */
+  private static String toJson(Gson gson, Object obj) {
+    return gson.toJson(same(obj));
+  }
+
+  /**
+   * Calls {@link Gson#fromJson}, but (hopefully) in a way which prevents code shrinkers from
+   * understanding that reflection is used for {@code c}.
+   */
+  private static <T> T fromJson(Gson gson, String json, Class<T> c) {
+    return gson.fromJson(json, same(c));
+  }
+
+  private static void testNamedFields(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer, "Write: Named fields", () -> toJson(gson, new ClassWithNamedFields(2)));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Named fields",
+        () -> {
+          ClassWithNamedFields deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithNamedFields.class);
+          return Integer.toString(deserialized.myField);
+        });
+  }
+
+  private static void testSerializedName(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: SerializedName",
+        () -> toJson(gson, new ClassWithSerializedName(2)));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: SerializedName",
+        () -> {
+          ClassWithSerializedName deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithSerializedName.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testConstructorNoArgs(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: No args constructor",
+        () -> toJson(gson, new ClassWithNoArgsConstructor()));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: No args constructor; initial constructor value",
+        () -> {
+          ClassWithNoArgsConstructor deserialized =
+              fromJson(gson, "{}", ClassWithNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+    TestExecutor.run(
+        outputConsumer,
+        "Read: No args constructor; custom value",
+        () -> {
+          ClassWithNoArgsConstructor deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testConstructorHasArgs(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: Constructor with args",
+        () -> toJson(gson, new ClassWithHasArgsConstructor(2)));
+    // This most likely relies on JDK Unsafe (unless the shrinker rewrites the constructor in some
+    // way)
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Constructor with args",
+        () -> {
+          ClassWithHasArgsConstructor deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithHasArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testUnreferencedConstructorNoArgs(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    // No write because we're not referencing this class's constructor.
+
+    // This runs the no-args constructor.
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Unreferenced no args constructor; initial constructor value",
+        () -> {
+          ClassWithUnreferencedNoArgsConstructor deserialized =
+              fromJson(gson, "{}", ClassWithUnreferencedNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Unreferenced no args constructor; custom value",
+        () -> {
+          ClassWithUnreferencedNoArgsConstructor deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithUnreferencedNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testUnreferencedConstructorHasArgs(
+      BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    // No write because we're not referencing this class's constructor.
+
+    // This most likely relies on JDK Unsafe (unless the shrinker rewrites the constructor in some
+    // way)
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Unreferenced constructor with args",
+        () -> {
+          ClassWithUnreferencedHasArgsConstructor deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithUnreferencedHasArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testNoJdkUnsafe(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().disableJdkUnsafe().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Read: No JDK Unsafe; initial constructor value",
+        () -> {
+          ClassWithNoArgsConstructor deserialized =
+              fromJson(gson, "{}", ClassWithNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+    TestExecutor.run(
+        outputConsumer,
+        "Read: No JDK Unsafe; custom value",
+        () -> {
+          ClassWithNoArgsConstructor deserialized =
+              fromJson(gson, "{\"myField\": 3}", ClassWithNoArgsConstructor.class);
+          return Integer.toString(deserialized.i);
+        });
+  }
+
+  private static void testEnum(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(outputConsumer, "Write: Enum", () -> toJson(gson, EnumClass.FIRST));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Enum",
+        () -> fromJson(gson, "\"SECOND\"", EnumClass.class).toString());
+  }
+
+  private static void testEnumSerializedName(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: Enum SerializedName",
+        () -> toJson(gson, EnumClassWithSerializedName.FIRST));
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Enum SerializedName",
+        () -> fromJson(gson, "\"two\"", EnumClassWithSerializedName.class).toString());
+  }
+
+  private static void testExposeAnnotation(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
+    TestExecutor.run(
+        outputConsumer, "Write: @Expose", () -> toJson(gson, new ClassWithExposeAnnotation()));
+  }
+
+  private static void testVersionAnnotations(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setVersion(1).create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: Version annotations",
+        () -> toJson(gson, new ClassWithVersionAnnotations()));
+  }
+
+  private static void testJsonAdapterAnnotation(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new GsonBuilder().setPrettyPrinting().create();
+    TestExecutor.run(
+        outputConsumer,
+        "Write: JsonAdapter on fields",
+        () -> toJson(gson, new ClassWithJsonAdapterAnnotation(1, 2, 3, 4)));
+
+    String json = "{\"f1\": 1, \"f2\": 2, \"f3\": {\"s\": \"3\"}, \"f4\": 4}";
+    TestExecutor.run(
+        outputConsumer,
+        "Read: JsonAdapter on fields",
+        () -> fromJson(gson, json, ClassWithJsonAdapterAnnotation.class).toString());
+  }
+
+  private static void testGenericClasses(BiConsumer<String, String> outputConsumer) {
+    Gson gson = new Gson();
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Generic TypeToken",
+        () -> gson.fromJson("{\"t\": 1}", new TypeToken<GenericClass<DummyClass>>() {}).toString());
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Using Generic",
+        () -> fromJson(gson, "{\"g\": {\"t\": 1}}", UsingGenericClass.class).toString());
+    TestExecutor.run(
+        outputConsumer,
+        "Read: Using Generic TypeToken",
+        () ->
+            gson.fromJson(
+                    "{\"g\": {\"t\": 1}}", new TypeToken<GenericUsingGenericClass<DummyClass>>() {})
+                .toString());
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java b/gson/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java
new file mode 100644
index 0000000000000000000000000000000000000000..33aa320a28c4179529921971930b9de95c131f21
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/NoSerializedNameMain.java
@@ -0,0 +1,59 @@
+package com.example;
+
+import static com.example.TestExecutor.same;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Covers cases of classes which don't use {@code @SerializedName} on their fields, and are
+ * therefore not matched by the default {@code gson.pro} rules.
+ */
+public class NoSerializedNameMain {
+  private NoSerializedNameMain() {}
+
+  static class TestClassNoArgsConstructor {
+    // Has a no-args default constructor.
+    public String s;
+  }
+
+  // R8 test rule in r8.pro for this class still removes no-args constructor, but doesn't make class
+  // abstract
+  static class TestClassNotAbstract {
+    public String s;
+  }
+
+  static class TestClassHasArgsConstructor {
+    public String s;
+
+    // Specify explicit constructor with args to remove implicit no-args default constructor
+    public TestClassHasArgsConstructor(String s) {
+      this.s = s;
+    }
+  }
+
+  /** Main entrypoint, called by {@code ShrinkingIT.testNoSerializedName_NoArgsConstructor()}. */
+  public static String runTestNoArgsConstructor() {
+    TestClassNoArgsConstructor deserialized =
+        new Gson().fromJson("{\"s\":\"value\"}", same(TestClassNoArgsConstructor.class));
+    return deserialized.s;
+  }
+
+  /**
+   * Main entrypoint, called by {@code
+   * ShrinkingIT.testNoSerializedName_NoArgsConstructorNoJdkUnsafe()}.
+   */
+  public static String runTestNoJdkUnsafe() {
+    Gson gson = new GsonBuilder().disableJdkUnsafe().create();
+    TestClassNotAbstract deserialized =
+        gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class));
+    return deserialized.s;
+  }
+
+  /** Main entrypoint, called by {@code ShrinkingIT.testNoSerializedName_HasArgsConstructor()}. */
+  public static String runTestHasArgsConstructor() {
+    TestClassHasArgsConstructor deserialized =
+        new Gson().fromJson("{\"s\":\"value\"}", same(TestClassHasArgsConstructor.class));
+    return deserialized.s;
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/TestExecutor.java b/gson/shrinker-test/src/main/java/com/example/TestExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..c5f38227dbff56359bd99895fd72546fe29b70ad
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/TestExecutor.java
@@ -0,0 +1,36 @@
+package com.example;
+
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+public class TestExecutor {
+  private TestExecutor() {}
+
+  /**
+   * Helper method for running individual tests. In case of an exception wraps it and includes the
+   * {@code name} of the test to make debugging issues with the obfuscated JARs a bit easier.
+   */
+  public static void run(
+      BiConsumer<String, String> outputConsumer, String name, Supplier<String> resultSupplier) {
+    String result;
+    try {
+      result = resultSupplier.get();
+    } catch (Throwable t) {
+      throw new RuntimeException("Test failed: " + name, t);
+    }
+    outputConsumer.accept(name, result);
+  }
+
+  /**
+   * Returns {@code t}, but in a way which (hopefully) prevents code shrinkers from simplifying
+   * this.
+   */
+  public static <T> T same(T t) {
+    // This is essentially `return t`, but contains some redundant code to try
+    // prevent the code shrinkers from simplifying this
+    return Optional.of(t)
+        .map(v -> Optional.of(v).get())
+        .orElseThrow(() -> new AssertionError("unreachable"));
+  }
+}
diff --git a/gson/shrinker-test/src/main/java/com/example/UnusedClass.java b/gson/shrinker-test/src/main/java/com/example/UnusedClass.java
new file mode 100644
index 0000000000000000000000000000000000000000..39216d59e845b1c21cf278237488086e00e096de
--- /dev/null
+++ b/gson/shrinker-test/src/main/java/com/example/UnusedClass.java
@@ -0,0 +1,16 @@
+package com.example;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Class with no-args constructor and field annotated with {@code @SerializedName}, but which is not
+ * actually used anywhere in the code.
+ *
+ * <p>The default ProGuard rules should not keep this class.
+ */
+public class UnusedClass {
+  public UnusedClass() {}
+
+  @SerializedName("i")
+  public int i;
+}
diff --git a/gson/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/gson/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java
new file mode 100644
index 0000000000000000000000000000000000000000..09084a163d4cb04623fba19d025d9ae5984252e7
--- /dev/null
+++ b/gson/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2023 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.gson.it;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.example.UnusedClass;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/** Integration test verifying behavior of shrunken and obfuscated JARs. */
+@RunWith(Parameterized.class)
+public class ShrinkingIT {
+  // These JAR files are prepared by the Maven build
+  public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar");
+  public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar");
+
+  @Parameters(name = "{index}: {0}")
+  public static List<Path> jarsToTest() {
+    return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH);
+  }
+
+  @Parameter public Path jarToTest;
+
+  @Before
+  public void verifyJarExists() {
+    if (!Files.isRegularFile(jarToTest)) {
+      fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`");
+    }
+  }
+
+  @FunctionalInterface
+  interface TestAction {
+    void run(Class<?> c) throws Exception;
+  }
+
+  private void runTest(String className, TestAction testAction) throws Exception {
+    // Use bootstrap class loader; load all custom classes from JAR and not
+    // from dependencies of this test
+    ClassLoader classLoader = null;
+
+    // Load the shrunken and obfuscated JARs with a separate class loader, then load
+    // the main test class from it and let the test action invoke its test methods
+    try (URLClassLoader loader =
+        new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) {
+      Class<?> c = loader.loadClass(className);
+      testAction.run(c);
+    }
+  }
+
+  @Test
+  public void test() throws Exception {
+    StringBuilder output = new StringBuilder();
+
+    runTest(
+        "com.example.Main",
+        c -> {
+          Method m = c.getMethod("runTests", BiConsumer.class);
+          m.invoke(
+              null,
+              (BiConsumer<String, String>)
+                  (name, content) -> output.append(name + "\n" + content + "\n===\n"));
+        });
+
+    assertThat(output.toString())
+        .isEqualTo(
+            String.join(
+                "\n",
+                "Write: TypeToken anonymous",
+                "[",
+                "  {",
+                "    \"custom\": 1",
+                "  }",
+                "]",
+                "===",
+                "Read: TypeToken anonymous",
+                "[ClassWithAdapter[3]]",
+                "===",
+                "Write: TypeToken manual",
+                "[",
+                "  {",
+                "    \"custom\": 1",
+                "  }",
+                "]",
+                "===",
+                "Read: TypeToken manual",
+                "[ClassWithAdapter[3]]",
+                "===",
+                "Write: Named fields",
+                "{",
+                "  \"myField\": 2,",
+                "  \"notAccessedField\": -1",
+                "}",
+                "===",
+                "Read: Named fields",
+                "3",
+                "===",
+                "Write: SerializedName",
+                "{",
+                "  \"myField\": 2,",
+                "  \"notAccessed\": -1",
+                "}",
+                "===",
+                "Read: SerializedName",
+                "3",
+                "===",
+                "Write: No args constructor",
+                "{",
+                "  \"myField\": -3",
+                "}",
+                "===",
+                "Read: No args constructor; initial constructor value",
+                "-3",
+                "===",
+                "Read: No args constructor; custom value",
+                "3",
+                "===",
+                "Write: Constructor with args",
+                "{",
+                "  \"myField\": 2",
+                "}",
+                "===",
+                "Read: Constructor with args",
+                "3",
+                "===",
+                "Read: Unreferenced no args constructor; initial constructor value",
+                "-3",
+                "===",
+                "Read: Unreferenced no args constructor; custom value",
+                "3",
+                "===",
+                "Read: Unreferenced constructor with args",
+                "3",
+                "===",
+                "Read: No JDK Unsafe; initial constructor value",
+                "-3",
+                "===",
+                "Read: No JDK Unsafe; custom value",
+                "3",
+                "===",
+                "Write: Enum",
+                "\"FIRST\"",
+                "===",
+                "Read: Enum",
+                "SECOND",
+                "===",
+                "Write: Enum SerializedName",
+                "\"one\"",
+                "===",
+                "Read: Enum SerializedName",
+                "SECOND",
+                "===",
+                "Write: @Expose",
+                "{\"i\":0}",
+                "===",
+                "Write: Version annotations",
+                "{\"i1\":0,\"i4\":0}",
+                "===",
+                "Write: JsonAdapter on fields",
+                "{",
+                "  \"f\": \"adapter-null\",",
+                "  \"f1\": \"adapter-1\",",
+                "  \"f2\": \"factory-2\",",
+                "  \"f3\": \"serializer-3\",",
+                // For f4 only a JsonDeserializer is registered, so serialization falls back to
+                // reflection
+                "  \"f4\": {",
+                "    \"s\": \"4\"",
+                "  }",
+                "}",
+                "===",
+                "Read: JsonAdapter on fields",
+                // For f3 only a JsonSerializer is registered, so for deserialization value is read
+                // as is using reflection
+                "ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3,"
+                    + " f4=deserializer-4]",
+                "===",
+                "Read: Generic TypeToken",
+                "{t=read-1}",
+                "===",
+                "Read: Using Generic",
+                "{g={t=read-1}}",
+                "===",
+                "Read: Using Generic TypeToken",
+                "{g={t=read-1}}",
+                "===",
+                ""));
+  }
+
+  @Test
+  public void testNoSerializedName_NoArgsConstructor() throws Exception {
+    runTest(
+        "com.example.NoSerializedNameMain",
+        c -> {
+          Method m = c.getMethod("runTestNoArgsConstructor");
+
+          if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
+            Object result = m.invoke(null);
+            assertThat(result).isEqualTo("value");
+          } else {
+            // R8 performs more aggressive optimizations
+            Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
+            assertThat(e)
+                .hasCauseThat()
+                .hasMessageThat()
+                .isEqualTo(
+                    "Abstract classes can't be instantiated! Adjust the R8 configuration or"
+                        + " register an InstanceCreator or a TypeAdapter for this type. Class name:"
+                        + " com.example.NoSerializedNameMain$TestClassNoArgsConstructor\n"
+                        + "See https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
+          }
+        });
+  }
+
+  @Test
+  public void testNoSerializedName_NoArgsConstructorNoJdkUnsafe() throws Exception {
+    runTest(
+        "com.example.NoSerializedNameMain",
+        c -> {
+          Method m = c.getMethod("runTestNoJdkUnsafe");
+
+          if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
+            Object result = m.invoke(null);
+            assertThat(result).isEqualTo("value");
+          } else {
+            // R8 performs more aggressive optimizations
+            Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
+            assertThat(e)
+                .hasCauseThat()
+                .hasMessageThat()
+                .isEqualTo(
+                    "Unable to create instance of class"
+                        + " com.example.NoSerializedNameMain$TestClassNotAbstract; usage of JDK"
+                        + " Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for"
+                        + " this type, adding a no-args constructor, or enabling usage of JDK"
+                        + " Unsafe may fix this problem. Or adjust your R8 configuration to keep"
+                        + " the no-args constructor of the class.");
+          }
+        });
+  }
+
+  @Test
+  public void testNoSerializedName_HasArgsConstructor() throws Exception {
+    runTest(
+        "com.example.NoSerializedNameMain",
+        c -> {
+          Method m = c.getMethod("runTestHasArgsConstructor");
+
+          if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
+            Object result = m.invoke(null);
+            assertThat(result).isEqualTo("value");
+          } else {
+            // R8 performs more aggressive optimizations
+            Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
+            assertThat(e)
+                .hasCauseThat()
+                .hasMessageThat()
+                .isEqualTo(
+                    "Abstract classes can't be instantiated! Adjust the R8 configuration or"
+                        + " register an InstanceCreator or a TypeAdapter for this type. Class name:"
+                        + " com.example.NoSerializedNameMain$TestClassHasArgsConstructor\n"
+                        + "See https://github.com/google/gson/blob/main/Troubleshooting.md#r8-abstract-class");
+          }
+        });
+  }
+
+  @Test
+  public void testUnusedClassRemoved() throws Exception {
+    // For some reason this test only works for R8 but not for ProGuard; ProGuard keeps the unused
+    // class
+    assumeTrue(jarToTest.equals(R8_RESULT_PATH));
+
+    String className = UnusedClass.class.getName();
+    ClassNotFoundException e =
+        assertThrows(
+            ClassNotFoundException.class,
+            () -> {
+              runTest(
+                  className,
+                  c -> {
+                    fail("Class should have been removed during shrinking: " + c);
+                  });
+            });
+    assertThat(e).hasMessageThat().contains(className);
+  }
+}