diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1677a18..ef1b23d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,16 +4,20 @@ on: [push, pull_request] jobs: build: + name: "Build on JDK ${{ matrix.java }}" + strategy: + matrix: + java: [ 11, 17 ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 + - name: "Set up JDK ${{ matrix.java }}" uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '11' + java-version: ${{ matrix.java }} cache: 'maven' - name: Build with Maven # This also runs javadoc:jar to detect any issues with the Javadoc generated during release - run: mvn --batch-mode --update-snapshots verify javadoc:jar + run: mvn --batch-mode --update-snapshots --no-transfer-progress verify javadoc:jar diff --git a/.github/workflows/check-api-compatibility.yml b/.github/workflows/check-api-compatibility.yml index aa70782b..a4465764 100644 --- a/.github/workflows/check-api-compatibility.yml +++ b/.github/workflows/check-api-compatibility.yml @@ -24,9 +24,9 @@ jobs: run: | cd gson-old-japicmp # Set dummy version - mvn --batch-mode org.codehaus.mojo:versions-maven-plugin:2.11.0:set -DnewVersion=JAPICMP-OLD + 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 install -DskipTests + mvn --batch-mode --no-transfer-progress install -DskipTests - name: Checkout new version uses: actions/checkout@v3 @@ -34,7 +34,7 @@ jobs: - name: Check API compatibility id: check-compatibility run: | - mvn --batch-mode --fail-at-end package japicmp:cmp -DskipTests + mvn --batch-mode --fail-at-end --no-transfer-progress package japicmp:cmp -DskipTests - name: Upload API differences artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 10df305f..01d95bdf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Can replace this with github/codeql-action/autobuild action to run complete build - name: Compile sources run: | - mvn compile --batch-mode + mvn compile --batch-mode --no-transfer-progress - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index b0790fcd..7810fa88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,38 @@ Change Log ========== +## 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 (#1912) -* Added parsing support for enum that has overridden toString() method (#1950) -* Removed support for building Gson with Gradle (#2081) -* Removed obsolete `codegen` hierarchy (#2099) -* Add support for reflection access filter (#1905) -* Improve `TypeToken` creation validation (#2072) -* Add explicit support for `float` in `JsonWriter` (#2130, #2132) -* Fail when parsing invalid local date (#2134) + 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. @@ -19,52 +40,52 @@ Also many small improvements to javadoc. **The minimum supported Java version changes from 6 to 7.** -* Change target Java version to 7 (#2043) -* Put `module-info.class` into Multi-Release JAR folder (#2013) -* Improve error message when abstract class cannot be constructed (#1814) -* Support EnumMap deserialization (#2071) -* Add LazilyParsedNumber default adapter (#2060) -* Fix JsonReader.hasNext() returning true at end of document (#2061) +* 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 (#2063) -* Add `GsonBuilder.disableJdkUnsafe()` (#1904) -* Add `UPPER_CASE_WITH_UNDERSCORES` in FieldNamingPolicy (#2024) -* Fix failing to serialize Collection or Map with inaccessible constructor (#1902) -* Improve TreeTypeAdapter thread-safety (#1976) -* Fix `Gson.newJsonWriter` ignoring lenient and HTML-safe setting (#1989) -* Delete unused LinkedHashTreeMap (#1992) -* Make default adapters stricter; improve exception messages (#2000) -* Fix `FieldNamingPolicy.upperCaseFirstLetter` uppercasing non-letter (#2004) + 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 (#1993). -* Deprecate `Gson.excluder()` exposing internal `Excluder` class (#1986). -* Prevent Java deserialization of internal classes (#1991). -* Improve number strategy implementation (#1987). -* Fix LongSerializationPolicy null handling being inconsistent with Gson (#1990). -* Support arbitrary Number implementation for Object and Number deserialization (#1290). -* Bump proguard-maven-plugin from 2.4.0 to 2.5.1 (#1980). -* Don't exclude static local classes (#1969). -* Fix `RuntimeTypeAdapterFactory` depending on internal `Streams` class (#1959). -* Improve Maven build (#1964). -* Make dependency on `java.sql` optional (#1707). +* 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 (#1390). -* Better behaviour with Java 9+ and `Unsafe` if there is a security manager (#1712). -* `EnumTypeAdapter` now works better when ProGuard has obfuscated enum fields (#1495). +* 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 (#1693). -* Fixed `IllegalStateException` in `JsonTreeWriter` (#1592). -* Added `JsonArray.isEmpty()` (#1640). -* Added new test cases (#1638). -* Fixed OSGi metadata generation to work on JavaSE < 9 (#1603). +* 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) diff --git a/README.md b/README.md index 995d446a..54598ec9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ There are a few open-source projects that can convert Java objects to JSON. Howe Gradle: ```gradle dependencies { - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10' } ``` @@ -54,7 +54,7 @@ Maven: com.google.code.gson gson - 2.9.1 + 2.10 ``` diff --git a/ReleaseProcess.md b/ReleaseProcess.md index 6e2b923d..eaa0e7c5 100644 --- a/ReleaseProcess.md +++ b/ReleaseProcess.md @@ -6,7 +6,6 @@ The following is a step-by-step procedure for releasing a new version of Google- 1. Ensure all changelists are code-reviewed and have +1 1. `cd gson` to the parent directory; ensure there are no open files and all changes are committed. 1. Run `mvn release:clean` -1. Do a dry run: `mvn release:prepare -DdryRun=true` 1. Start the release: `mvn release:prepare` - Answer questions: usually the defaults are fine. 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. @@ -18,9 +17,13 @@ The following is a step-by-step procedure for releasing a new version of Google- 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). diff --git a/UserGuide.md b/UserGuide.md index 12b53351..b82bd725 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -76,7 +76,7 @@ The Gson instance does not maintain any state while invoking JSON operations. So ```gradle dependencies { - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10' } ``` @@ -90,7 +90,7 @@ To use Gson with Maven2/3, you can use the Gson version available in Maven Centr com.google.code.gson gson - 2.9.1 + 2.10 compile @@ -225,7 +225,9 @@ Collection ints = Arrays.asList(1,2,3,4,5); String json = gson.toJson(ints); // ==> json is [1,2,3,4,5] // Deserialization -Type collectionType = new TypeToken>(){}.getType(); +TypeToken> collectionType = new TypeToken>(){}; +// 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 ints2 = gson.fromJson(json, collectionType); // ==> ints2 is same as ints ``` @@ -263,10 +265,12 @@ For deserialization Gson uses the `read` method of the `TypeAdapter` registered ```java Gson gson = new Gson(); -Type mapType = new TypeToken>(){}.getType(); +TypeToken> mapType = new TypeToken>(){}; 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 stringMap = gson.fromJson(json, mapType); // ==> stringMap is {key=value} ``` diff --git a/extras/pom.xml b/extras/pom.xml index 152f0644..e19851cb 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ io.gitlab.jfronny gson-parent - 2.9.2-SNAPSHOT + 2.11-SNAPSHOT gson-extras diff --git a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 502ad4ec..87b522f0 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -91,7 +91,7 @@ import java.util.Map; * Both the type field name ({@code "type"}) and the type labels ({@code * "Rectangle"}) are configurable. * - *

Registering Types

+ *

Registering Types

* 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.
   {@code
@@ -119,7 +119,7 @@ import java.util.Map;
  *       .registerSubtype(Diamond.class);
  * }
* - *

Serialization and deserialization

+ *

Serialization and deserialization

* In order to serialize and deserialize a polymorphic object, * you must specify the base type explicitly. *
   {@code
@@ -158,7 +158,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
   public static  RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) {
     return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
   }
-  
+
   /**
    * Creates a new runtime type adapter using for {@code baseType} using {@code
    * typeFieldName} as the type field name. Type field names are case sensitive.
@@ -244,7 +244,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
         } else {
             labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
         }
-        
+
         if (labelJsonElement == null) {
           throw new JsonParseException("cannot deserialize " + baseType
               + " because it does not define a field named " + typeFieldName);
@@ -282,7 +282,7 @@ public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory {
               + " because it already defines a field named " + typeFieldName);
         }
         clone.add(typeFieldName, new JsonPrimitive(label));
-        
+
         for (Map.Entry e : jsonObject.entrySet()) {
           clone.add(e.getKey(), e.getValue());
         }
diff --git a/gson/pom.xml b/gson/pom.xml
index 2dd465aa..d8c0bb7a 100644
--- a/gson/pom.xml
+++ b/gson/pom.xml
@@ -4,7 +4,7 @@
   
     io.gitlab.jfronny
     gson-parent
-    2.9.2-SNAPSHOT
+    2.11-SNAPSHOT
   
 
   gson
@@ -17,6 +17,10 @@
     
   
 
+  
+    **/Java17*
+  
+
   
     
       junit
@@ -60,7 +64,23 @@
               
             
           
+          
+            default-testCompile
+            test-compile
+            
+              testCompile
+            
+            
+              
+                ${excludeTestCompilation}
+              
+            
+          
         
+          
+              8
+              8
+          
       
       
         biz.aQute.bnd
@@ -119,4 +139,16 @@
       
     
   
+  
+    
+      JDK17
+      
+        [17,)
+      
+      
+        17
+        
+      
+    
+  
 
diff --git a/gson/src/main/java/com/google/gson/FieldNamingPolicy.java b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
index a4fa7c27..cd42f42c 100644
--- a/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
+++ b/gson/src/main/java/com/google/gson/FieldNamingPolicy.java
@@ -86,6 +86,8 @@ public enum FieldNamingPolicy implements FieldNamingStrategy {
    *   
  • aStringField ---> A_STRING_FIELD
  • *
  • aURL ---> A_U_R_L
  • * + * + * @since 2.9.0 */ UPPER_CASE_WITH_UNDERSCORES() { @Override public String translateName(Field f) { @@ -125,7 +127,8 @@ public enum FieldNamingPolicy implements FieldNamingStrategy { * Using dashes in JavaScript is not recommended since dash is also used for a minus sign in * expressions. This requires that a field named with dashes is always accessed as a quoted * property like {@code myobject['my-field']}. Accessing it as an object field - * {@code myobject.my-field} will result in an unintended javascript expression. + * {@code myobject.my-field} will result in an unintended JavaScript expression. + * * @since 1.4 */ LOWER_CASE_WITH_DASHES() { @@ -148,8 +151,9 @@ public enum FieldNamingPolicy implements FieldNamingStrategy { * Using dots in JavaScript is not recommended since dot is also used for a member sign in * expressions. This requires that a field named with dots is always accessed as a quoted * property like {@code myobject['my.field']}. Accessing it as an object field - * {@code myobject.my.field} will result in an unintended javascript expression. - * @since 2.8 + * {@code myobject.my.field} will result in an unintended JavaScript expression. + * + * @since 2.8.4 */ LOWER_CASE_WITH_DOTS() { @Override public String translateName(Field f) { diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index ad427d2e..b9638d27 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -27,6 +27,7 @@ import com.google.gson.internal.bind.MapTypeAdapterFactory; import com.google.gson.internal.bind.NumberTypeAdapter; import com.google.gson.internal.bind.ObjectTypeAdapter; import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.internal.sql.SqlTypesSupport; import com.google.gson.reflect.TypeToken; @@ -71,26 +72,32 @@ import java.util.concurrent.atomic.AtomicLongArray; *
      * Gson gson = new Gson(); // Or use new GsonBuilder().create();
      * MyType target = new MyType();
    - * String json = gson.toJson(target); // serializes target to Json
    + * String json = gson.toJson(target); // serializes target to JSON
      * MyType target2 = gson.fromJson(json, MyType.class); // deserializes json into target2
      * 
    * - *

    If the object that your are serializing/deserializing is a {@code ParameterizedType} - * (i.e. contains at least one type parameter and may be an array) then you must use the - * {@link #toJson(Object, Type)} or {@link #fromJson(String, Type)} method. Here is an - * example for serializing and deserializing a {@code ParameterizedType}: - * + *

    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}) 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: *

    - * Type listType = new TypeToken<List<String>>() {}.getType();
    - * List<String> target = new LinkedList<String>();
    - * target.add("blah");
    + * TypeToken<List<MyType>> listType = new TypeToken<List<MyType>>() {};
    + * List<MyType> target = new LinkedList<MyType>();
    + * target.add(new MyType(1, "abc"));
      *
      * Gson gson = new Gson();
    - * String json = gson.toJson(target, listType);
    - * List<String> target2 = gson.fromJson(json, listType);
    + * // For serialization you normally do not have to specify the type, Gson will use
    + * // the runtime type of the objects, however you can also specify it explicitly
    + * String json = gson.toJson(target, listType.getType());
    + *
    + * // But for deserialization you have to specify the type
    + * List<MyType> target2 = gson.fromJson(json, listType);
      * 
    * - *

    See the Gson User Guide + *

    See the Gson User Guide * for a more complete set of examples.

    * *

    Lenient JSON handling

    @@ -120,7 +127,7 @@ import java.util.concurrent.atomic.AtomicLongArray; * to make sure there is no trailing data * * - * @see com.google.gson.reflect.TypeToken + * @see TypeToken * * @author Inderjeet Singh * @author Joel Leitch @@ -192,9 +199,9 @@ public final class Gson { * through {@link GsonBuilder#excludeFieldsWithoutExposeAnnotation()}. *
  • 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)}.
  • - *
  • The default field naming policy for the output Json is same as in Java. So, a Java class + *
  • The default field naming policy for the output JSON is same as in Java. So, a Java class * field versionNumber will be output as "versionNumber" in - * Json. The same rules are applied for mapping incoming Json to the Java classes. You can + * JSON. The same rules are applied for mapping incoming JSON to the Java classes. You can * change this policy through {@link GsonBuilder#setFieldNamingPolicy(FieldNamingPolicy)}.
  • *
  • By default, Gson excludes transient or static fields from * consideration for serialization and deserialization. You can change this behavior through @@ -321,6 +328,7 @@ public final class Gson { * instance. * * @return a GsonBuilder instance. + * @since 2.8.3 */ public GsonBuilder newBuilder() { return new GsonBuilder(this); @@ -374,7 +382,7 @@ public final class Gson { } double doubleValue = value.doubleValue(); checkValidFloatingPoint(doubleValue); - out.value(value); + out.value(doubleValue); } }; } @@ -398,7 +406,10 @@ public final class Gson { } float floatValue = value.floatValue(); checkValidFloatingPoint(floatValue); - out.value(value); + // For backward compatibility don't call `JsonWriter.value(float)` because that method has + // been newly added and not all custom JsonWriter implementations might override it yet + Number floatNumber = value instanceof Float ? value : floatValue; + out.value(floatNumber); } }; } @@ -619,13 +630,15 @@ public final class Gson { * {@link JsonElement}s. This method should be used when the specified object is not a generic * type. This method uses {@link Class#getClass()} to get the type for the specified object, but * the {@code getClass()} loses the generic type information because of the Type Erasure feature - * of Java. Note that this method works fine if the any of the object fields are of generic type, + * of Java. Note that this method works fine if any of the object fields are of generic type, * just the object itself should not be of a generic type. If the object is of generic type, use * {@link #toJsonTree(Object, Type)} instead. * - * @param src the object for which Json representation is to be created setting for Gson - * @return Json representation of {@code src}. + * @param src the object for which JSON representation is to be created + * @return JSON representation of {@code src}. * @since 1.4 + * + * @see #toJsonTree(Object, Type) */ public JsonElement toJsonTree(Object src) { if (src == null) { @@ -649,6 +662,8 @@ public final class Gson { *
  • * @return Json representation of {@code src} * @since 1.4 + * + * @see #toJsonTree(Object) */ public JsonElement toJsonTree(Object src, Type typeOfSrc) { JsonTreeWriter writer = new JsonTreeWriter(); @@ -657,17 +672,20 @@ public final class Gson { } /** - * This method serializes the specified object into its equivalent Json representation. + * This method serializes the specified object into its equivalent JSON representation. * This method should be used when the specified object is not a generic type. This method uses * {@link Class#getClass()} to get the type for the specified object, but the * {@code getClass()} loses the generic type information because of the Type Erasure feature - * of Java. Note that this method works fine if the any of the object fields are of generic type, + * of Java. Note that this method works fine if any of the object fields are of generic type, * just the object itself should not be of a generic type. If the object is of generic type, use * {@link #toJson(Object, Type)} instead. If you want to write out the object to a * {@link Writer}, use {@link #toJson(Object, Appendable)} instead. * - * @param src the object for which Json representation is to be created setting for Gson + * @param src the object for which JSON representation is to be created * @return Json representation of {@code src}. + * + * @see #toJson(Object, Appendable) + * @see #toJson(Object, Type) */ public String toJson(Object src) { if (src == null) { @@ -678,7 +696,7 @@ public final class Gson { /** * This method serializes the specified object, including those of generic types, into its - * equivalent Json representation. This method must be used if the specified object is a generic + * equivalent JSON representation. This method must be used if the specified object is a generic * type. For non-generic objects, use {@link #toJson(Object)} instead. If you want to write out * the object to a {@link Appendable}, use {@link #toJson(Object, Type, Appendable)} instead. * @@ -689,7 +707,10 @@ public final class Gson { *
        * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType();
        * 
    - * @return Json representation of {@code src} + * @return JSON representation of {@code src} + * + * @see #toJson(Object, Type, Appendable) + * @see #toJson(Object) */ public String toJson(Object src, Type typeOfSrc) { StringWriter writer = new StringWriter(); @@ -698,18 +719,22 @@ public final class Gson { } /** - * This method serializes the specified object into its equivalent Json representation. + * This method serializes the specified object into its equivalent JSON representation and + * writes it to the writer. * This method should be used when the specified object is not a generic type. This method uses * {@link Class#getClass()} to get the type for the specified object, but the * {@code getClass()} loses the generic type information because of the Type Erasure feature - * of Java. Note that this method works fine if the any of the object fields are of generic type, + * of Java. Note that this method works fine if any of the object fields are of generic type, * just the object itself should not be of a generic type. If the object is of generic type, use * {@link #toJson(Object, Type, Appendable)} instead. * - * @param src the object for which Json representation is to be created setting for Gson - * @param writer Writer to which the Json representation needs to be written + * @param src the object for which JSON representation is to be created + * @param writer Writer to which the JSON representation needs to be written * @throws JsonIOException if there was a problem writing to the writer * @since 1.2 + * + * @see #toJson(Object) + * @see #toJson(Object, Type, Appendable) */ public void toJson(Object src, Appendable writer) throws JsonIOException { if (src != null) { @@ -721,8 +746,9 @@ public final class Gson { /** * This method serializes the specified object, including those of generic types, into its - * equivalent Json representation. This method must be used if the specified object is a generic - * type. For non-generic objects, use {@link #toJson(Object, Appendable)} instead. + * equivalent JSON representation and writes it to the writer. + * This method must be used if the specified object is a generic type. For non-generic objects, + * use {@link #toJson(Object, Appendable)} instead. * * @param src the object for which JSON representation is to be created * @param typeOfSrc The specific genericized type of src. You can obtain @@ -731,9 +757,12 @@ public final class Gson { *
        * Type typeOfSrc = new TypeToken<Collection<Foo>>(){}.getType();
        * 
    - * @param writer Writer to which the Json representation of src needs to be written. + * @param writer Writer to which the JSON representation of src needs to be written. * @throws JsonIOException if there was a problem writing to the writer * @since 1.2 + * + * @see #toJson(Object, Type) + * @see #toJson(Object, Appendable) */ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOException { try { @@ -802,7 +831,7 @@ public final class Gson { * Writes out the equivalent JSON for a tree of {@link JsonElement}s. * * @param jsonElement root of a tree of {@link JsonElement}s - * @param writer Writer to which the Json representation needs to be written + * @param writer Writer to which the JSON representation needs to be written * @throws JsonIOException if there was a problem writing to the writer * @since 1.4 */ @@ -840,6 +869,7 @@ public final class Gson { jsonWriter.setLenient(lenient); jsonWriter.setOmitQuotes(omitQuotes); jsonWriter.setSerializeNulls(serializeNulls); + jsonWriter.setSerializeSpecialFloatingPointValues(serializeSpecialFloatingPointValues); return jsonWriter; } @@ -854,6 +884,7 @@ public final class Gson { public JsonReader newJsonReader(Reader reader) { JsonReader jsonReader = new JsonReader(reader); jsonReader.setLenient(lenient); + jsonReader.setSerializeSpecialFloatingPointValues(serializeSpecialFloatingPointValues); return jsonReader; } @@ -896,17 +927,17 @@ public final class Gson { } /** - * This method deserializes the specified Json into an object of the specified class. It is not + * This method deserializes the specified JSON into an object of the specified class. It is not * suitable to use if the specified class is a generic type since it will not have the generic * type information because of the Type Erasure feature of Java. Therefore, this method should not * be used if the desired type is a generic type. Note that this method works fine if the any of * the fields of the specified object are generics, just the object itself should not be a * generic type. For the cases when the object is of generic type, invoke - * {@link #fromJson(String, Type)}. If you have the Json in a {@link Reader} instead of + * {@link #fromJson(String, TypeToken)}. If you have the JSON in a {@link Reader} instead of * a String, use {@link #fromJson(Reader, Class)} instead. * - *

    An exception is thrown if the JSON string has multiple top-level JSON elements, - * or if there is trailing data. + *

    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 the type of the desired object * @param json the string from which the object is to be deserialized @@ -915,98 +946,167 @@ public final class Gson { * or if {@code json} is empty. * @throws JsonSyntaxException if json is not a valid representation for an object of type * classOfT + * + * @see #fromJson(Reader, Class) + * @see #fromJson(String, TypeToken) */ public T fromJson(String json, Class classOfT) throws JsonSyntaxException { - Object object = fromJson(json, (Type) classOfT); + T object = fromJson(json, TypeToken.get(classOfT)); return Primitives.wrap(classOfT).cast(object); } /** - * This method deserializes the specified Json into an object of the specified type. This method + * This method deserializes the specified JSON into an object of the specified type. This method * is useful if the specified object is a generic type. For non-generic objects, use - * {@link #fromJson(String, Class)} instead. If you have the Json in a {@link Reader} instead of + * {@link #fromJson(String, Class)} instead. If you have the JSON in a {@link Reader} instead of * a String, use {@link #fromJson(Reader, Type)} instead. * + *

    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. + * *

    An exception is thrown if the JSON string has multiple top-level JSON elements, - * or if there is trailing data. + * or if there is trailing data. Use {@link #fromJson(JsonReader, Type)} if this behavior is + * not desired. * * @param the type of the desired object * @param json the string from which the object is to be deserialized - * @param typeOfT The specific genericized type of src. You can obtain this type by using the - * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * @param typeOfT The specific genericized type of src + * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null} + * or if {@code json} is empty. + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * + * @see #fromJson(Reader, Type) + * @see #fromJson(String, Class) + * @see #fromJson(String, TypeToken) + */ + @SuppressWarnings("unchecked") + public T 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. + * + *

    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 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}, you should use: *

    -   * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType();
    +   * new TypeToken<Collection<Foo>>(){}
        * 
    * @return an object of type T from the string. Returns {@code null} if {@code json} is {@code null} * or if {@code json} is empty. - * @throws JsonParseException if json is not a valid representation for an object of type typeOfT - * @throws JsonSyntaxException if json is not a valid representation for an object of type + * @throws JsonSyntaxException if json is not a valid representation for an object of the type typeOfT + * + * @see #fromJson(Reader, TypeToken) + * @see #fromJson(String, Class) + * @since 2.10 */ - public T fromJson(String json, Type typeOfT) throws JsonSyntaxException { + public T fromJson(String json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { return null; } StringReader reader = new StringReader(json); - @SuppressWarnings("unchecked") - T target = (T) fromJson(reader, typeOfT); - return target; + return fromJson(reader, typeOfT); } /** - * This method deserializes the Json read from the specified reader into an object of the + * This method deserializes the JSON read from the specified reader into an object of the * specified class. It is not suitable to use if the specified class is a generic type since it * will not have the generic type information because of the Type Erasure feature of Java. * Therefore, this method should not be used if the desired type is a generic type. Note that - * this method works fine if the any of the fields of the specified object are generics, just the + * this method works fine if any of the fields of the specified object are generics, just the * object itself should not be a generic type. For the cases when the object is of generic type, - * invoke {@link #fromJson(Reader, Type)}. If you have the Json in a String form instead of a + * invoke {@link #fromJson(Reader, TypeToken)}. If you have the JSON in a String form instead of a * {@link Reader}, use {@link #fromJson(String, Class)} instead. * - *

    An exception is thrown if the JSON data has multiple top-level JSON elements, - * or if there is trailing data. + *

    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 the type of the desired object - * @param json the reader producing the Json from which the object is to be deserialized. + * @param json the reader producing the JSON from which the object is to be deserialized. * @param classOfT the class of T - * @return an object of type T from the string. Returns {@code null} if {@code json} is at EOF. + * @return an object of type T from the Reader. Returns {@code null} if {@code json} is at EOF. * @throws JsonIOException if there was a problem reading from the Reader - * @throws JsonSyntaxException if json is not a valid representation for an object of type + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT * @since 1.2 + * + * @see #fromJson(String, Class) + * @see #fromJson(Reader, TypeToken) */ public T fromJson(Reader json, Class classOfT) throws JsonSyntaxException, JsonIOException { - JsonReader jsonReader = newJsonReader(json); - Object object = fromJson(jsonReader, classOfT); - assertFullConsumption(object, jsonReader); + T object = fromJson(json, TypeToken.get(classOfT)); return Primitives.wrap(classOfT).cast(object); } /** - * This method deserializes the Json read from the specified reader into an object of the + * This method deserializes the JSON read from the specified reader into an object of the * specified type. This method is useful if the specified object is a generic type. For - * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the Json in a + * non-generic objects, use {@link #fromJson(Reader, Class)} instead. If you have the JSON in a * String form instead of a {@link Reader}, use {@link #fromJson(String, Type)} instead. * - *

    An exception is thrown if the JSON data has multiple top-level JSON elements, - * or if there is trailing data. + *

    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. + * + *

    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 the type of the desired object - * @param json the reader producing Json from which the object is to be deserialized - * @param typeOfT The specific genericized type of src. You can obtain this type by using the - * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * @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") + public 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. + * + *

    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 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}, you should use: *

    -   * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType();
    +   * new TypeToken<Collection<Foo>>(){}
        * 
    - * @return an object of type T from the json. Returns {@code null} if {@code json} is at EOF. + * @return an object of type T from the Reader. Returns {@code null} if {@code json} is at EOF. * @throws JsonIOException if there was a problem reading from the Reader - * @throws JsonSyntaxException if json is not a valid representation for an object of type - * @since 1.2 + * @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 fromJson(Reader json, Type typeOfT) throws JsonIOException, JsonSyntaxException { + public T fromJson(Reader json, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { JsonReader jsonReader = newJsonReader(json); - @SuppressWarnings("unchecked") - T object = (T) fromJson(jsonReader, typeOfT); + T object = fromJson(jsonReader, typeOfT); assertFullConsumption(object, jsonReader); return object; } @@ -1023,10 +1123,18 @@ public final class Gson { } } + // fromJson(JsonReader, Class) is unfortunately missing and cannot be added now without breaking + // source compatibility in certain cases, see https://github.com/google/gson/pull/1700#discussion_r973764414 + /** - * Reads the next JSON value from {@code reader} and convert it to an object + * Reads the next JSON value from {@code reader} and converts it to an object * of type {@code typeOfT}. Returns {@code null}, if the {@code reader} is at EOF. - * Since Type is not parameterized by T, this method is type unsafe and should be used carefully. + * + *

    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)}. * *

    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. @@ -1035,19 +1143,59 @@ public final class Gson { * regardless of the lenient mode setting of the provided reader. The lenient mode setting * of the reader is restored once this method returns. * - * @throws JsonIOException if there was a problem writing to the Reader - * @throws JsonSyntaxException if json is not a valid representation for an object of type + * @param the type of the desired object + * @param reader the reader whose next JSON value should be deserialized + * @param typeOfT The specific genericized type of src + * @return an object of type T from the JsonReader. Returns {@code null} if {@code reader} is at EOF. + * @throws JsonIOException if there was a problem reading from the JsonReader + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * + * @see #fromJson(Reader, Type) + * @see #fromJson(JsonReader, TypeToken) */ + @SuppressWarnings("unchecked") public T 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. + * + *

    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. + * + *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, + * regardless of the lenient mode setting of the provided reader. The lenient mode setting + * of the reader is restored once this method returns. + * + * @param 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}, you should use: + *

    +   * new TypeToken<Collection<Foo>>(){}
    +   * 
    + * @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 fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; boolean oldLenient = reader.isLenient(); reader.setLenient(lenient); try { reader.peek(); isEmpty = false; - @SuppressWarnings("unchecked") - TypeToken typeToken = (TypeToken) TypeToken.get(typeOfT); - TypeAdapter typeAdapter = getAdapter(typeToken); + TypeAdapter typeAdapter = getAdapter(typeOfT); T object = typeAdapter.read(reader); return object; } catch (EOFException e) { @@ -1074,55 +1222,89 @@ public final class Gson { } /** - * This method deserializes the Json read from the specified parse tree into an object of the + * This method deserializes the JSON read from the specified parse tree into an object of the * specified type. It is not suitable to use if the specified class is a generic type since it * will not have the generic type information because of the Type Erasure feature of Java. * Therefore, this method should not be used if the desired type is a generic type. Note that - * this method works fine if the any of the fields of the specified object are generics, just the + * this method works fine if any of the fields of the specified object are generics, just the * object itself should not be a generic type. For the cases when the object is of generic type, - * invoke {@link #fromJson(JsonElement, Type)}. + * invoke {@link #fromJson(JsonElement, TypeToken)}. + * * @param the type of the desired object * @param json the root of the parse tree of {@link JsonElement}s from which the object is to * be deserialized * @param classOfT The class of T - * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null} + * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null} * or if {@code json} is empty. - * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * @throws JsonSyntaxException if json is not a valid representation for an object of type classOfT * @since 1.3 + * + * @see #fromJson(Reader, Class) + * @see #fromJson(JsonElement, TypeToken) */ public T fromJson(JsonElement json, Class classOfT) throws JsonSyntaxException { - Object object = fromJson(json, (Type) classOfT); + T object = fromJson(json, TypeToken.get(classOfT)); return Primitives.wrap(classOfT).cast(object); } /** - * This method deserializes the Json read from the specified parse tree into an object of the + * This method deserializes the JSON read from the specified parse tree into an object of the + * specified type. This method is useful if the specified object is a generic type. For + * non-generic objects, use {@link #fromJson(JsonElement, Class)} instead. + * + *

    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 the type of the desired object + * @param json the root of the parse tree of {@link JsonElement}s from which the object is to + * be deserialized + * @param typeOfT The specific genericized type of src + * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null} + * or if {@code json} is empty. + * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT + * @since 1.3 + * + * @see #fromJson(Reader, Type) + * @see #fromJson(JsonElement, Class) + * @see #fromJson(JsonElement, TypeToken) + */ + @SuppressWarnings("unchecked") + public T 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 the type of the desired object * @param json the root of the parse tree of {@link JsonElement}s from which the object is to * be deserialized - * @param typeOfT The specific genericized type of src. You can obtain this type by using the - * {@link com.google.gson.reflect.TypeToken} class. For example, to get the type for + * @param typeOfT The specific genericized type of src. You should create an anonymous subclass of + * {@code TypeToken} with the specific generic type arguments. For example, to get the type for * {@code Collection}, you should use: *

    -   * Type typeOfT = new TypeToken<Collection<Foo>>(){}.getType();
    +   * new TypeToken<Collection<Foo>>(){}
        * 
    - * @return an object of type T from the json. Returns {@code null} if {@code json} is {@code null} + * @return an object of type T from the JSON. Returns {@code null} if {@code json} is {@code null} * or if {@code json} is empty. * @throws JsonSyntaxException if json is not a valid representation for an object of type typeOfT - * @since 1.3 + * + * @see #fromJson(Reader, TypeToken) + * @see #fromJson(JsonElement, Class) + * @since 2.10 */ - @SuppressWarnings("unchecked") - public T fromJson(JsonElement json, Type typeOfT) throws JsonSyntaxException { + public T fromJson(JsonElement json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { return null; } - return (T) fromJson(new JsonTreeReader(json), typeOfT); + return fromJson(new JsonTreeReader(json), typeOfT); } - static class FutureTypeAdapter extends TypeAdapter { + static class FutureTypeAdapter extends SerializationDelegatingTypeAdapter { private TypeAdapter delegate; public void setDelegate(TypeAdapter typeAdapter) { @@ -1132,18 +1314,23 @@ public final class Gson { delegate = typeAdapter; } - @Override public T read(JsonReader in) throws IOException { + private TypeAdapter delegate() { if (delegate == null) { - throw new IllegalStateException(); + throw new IllegalStateException("Delegate has not been set yet"); } - return delegate.read(in); + return delegate; + } + + @Override public TypeAdapter getSerializationDelegate() { + return delegate(); + } + + @Override public T read(JsonReader in) throws IOException { + return delegate().read(in); } @Override public void write(JsonWriter out, T value) throws IOException { - if (delegate == null) { - throw new IllegalStateException(); - } - delegate.write(out, value); + delegate().write(out, value); } } diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 60276abd..03ae514f 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -18,6 +18,8 @@ package com.google.gson; import static com.google.gson.internal.DefaultConfig.*; +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; @@ -137,14 +139,25 @@ public final class GsonBuilder { } /** - * Configures Gson to enable versioning support. + * Configures Gson to enable versioning support. Versioning support works based on the + * annotation types {@link Since} and {@link Until}. It allows including or excluding fields + * and classes based on the specified version. See the documentation of these annotation + * types for more information. * - * @param ignoreVersionsAfter any field or type marked with a version higher than this value - * are ignored during serialization or deserialization. + *

    By default versioning support is disabled and usage of {@code @Since} and {@code @Until} + * has no effect. + * + * @param version the version number to use. * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @throws IllegalArgumentException if the version number is NaN or negative + * @see Since + * @see Until */ - public GsonBuilder setVersion(double ignoreVersionsAfter) { - excluder = excluder.withVersion(ignoreVersionsAfter); + public GsonBuilder setVersion(double version) { + if (Double.isNaN(version) || version < 0.0) { + throw new IllegalArgumentException("Invalid version: " + version); + } + excluder = excluder.withVersion(version); return this; } @@ -215,8 +228,9 @@ public final class GsonBuilder { * on the key; however, when this is called then one of the following cases * apply: * - *

    Maps as JSON objects

    - * For this case, assume that a type adapter is registered to serialize and + *

    Maps as JSON objects + * + *

    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}. @@ -240,11 +254,12 @@ public final class GsonBuilder { * } * } * - *

    Maps as JSON arrays

    - * For this case, assume that a type adapter was NOT registered for some + *

    Maps as JSON arrays + * + *

    For this case, assume that a type adapter was NOT registered for some * {@code Point} class, but rather the default Gson serialization is applied. * In this case, some {@code new Point(2,3)} would serialize as {@code - * {"x":2,"y":5}}. + * {"x":2,"y":3}}. * *

    Given the assumption above, a {@code Map} will be * serialize as an array of arrays (can be viewed as an entry set of pairs). @@ -357,6 +372,10 @@ public final class GsonBuilder { * Configures Gson to apply a specific naming strategy to an object's fields during * serialization and deserialization. * + *

    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 @@ -372,6 +391,7 @@ public final class GsonBuilder { * @param objectToNumberStrategy the actual object-to-number strategy * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @see ToNumberPolicy#DOUBLE The default object-to-number strategy + * @since 2.8.9 */ public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStrategy) { this.objectToNumberStrategy = Objects.requireNonNull(objectToNumberStrategy); @@ -384,6 +404,7 @@ public final class GsonBuilder { * @param numberToNumberStrategy the actual number-to-number strategy * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @see ToNumberPolicy#LAZILY_PARSED_NUMBER The default number-to-number strategy + * @since 2.8.9 */ public GsonBuilder setNumberToNumberStrategy(ToNumberStrategy numberToNumberStrategy) { this.numberToNumberStrategy = Objects.requireNonNull(numberToNumberStrategy); @@ -408,6 +429,10 @@ public final class GsonBuilder { * JSON null is written to output, and when deserialized the JSON value is skipped and * {@code null} is returned. * + *

    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 @@ -622,6 +647,10 @@ public final class GsonBuilder { * is designed to handle a large number of factories, so you should consider registering * them to be at par with registering an individual type adapter. * + *

    The created Gson instance might only use the factory once to create an adapter for + * a specific type and cache the result. It is not guaranteed that the factory will be used + * again every time the type is serialized or deserialized. + * * @since 2.1 */ public GsonBuilder registerTypeAdapterFactory(TypeAdapterFactory factory) { @@ -699,6 +728,7 @@ public final class GsonBuilder { * disabling usage of {@code Unsafe}. * * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 2.9.0 */ public GsonBuilder disableJdkUnsafe() { this.useJdkUnsafe = false; @@ -717,8 +747,13 @@ public final class GsonBuilder { * all classes for which no {@link TypeAdapter} has been registered, and for which no * built-in Gson {@code TypeAdapter} exists. * + *

    The created Gson instance might only use an access filter once for a class or its + * members and cache the result. It is not guaranteed that the filter will be used again + * every time a class or its members are accessed during serialization or deserialization. + * * @param filter filter to add * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + * @since 2.9.1 */ public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) { Objects.requireNonNull(filter); diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java index d861cfb3..370b323f 100644 --- a/gson/src/main/java/com/google/gson/JsonArray.java +++ b/gson/src/main/java/com/google/gson/JsonArray.java @@ -16,6 +16,7 @@ package com.google.gson; +import com.google.gson.internal.NonNullElementWrapperList; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; @@ -28,11 +29,14 @@ import java.util.List; * 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}. * + *

    {@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 { - private final List elements; + private final ArrayList elements; /** * Creates an empty JsonArray. @@ -48,6 +52,7 @@ public final class JsonArray extends JsonElement implements IterableThe {@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 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 diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java index 4b600510..60dac41c 100644 --- a/gson/src/main/java/com/google/gson/JsonObject.java +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -27,6 +27,9 @@ import java.util.Set; * 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}. * + *

    {@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 */ @@ -146,6 +149,7 @@ public final class JsonObject extends JsonElement { * Returns the number of key/value pairs in the object. * * @return the number of key/value pairs in the object. + * @since 2.7 */ public int size() { return members.size(); @@ -207,6 +211,22 @@ public final class JsonObject extends JsonElement { 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. + * + *

    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 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 diff --git a/gson/src/main/java/com/google/gson/JsonParser.java b/gson/src/main/java/com/google/gson/JsonParser.java index d3508c10..5b800420 100644 --- a/gson/src/main/java/com/google/gson/JsonParser.java +++ b/gson/src/main/java/com/google/gson/JsonParser.java @@ -45,6 +45,7 @@ public final class JsonParser { * @param json JSON text * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON * @throws JsonParseException if the specified text is not valid JSON + * @since 2.8.6 */ public static JsonElement parseString(String json) throws JsonSyntaxException { return parseReader(new StringReader(json)); @@ -61,6 +62,7 @@ public final class JsonParser { * @return a parse tree of {@link JsonElement}s corresponding to the specified JSON * @throws JsonParseException if there is an IOException or if the specified * text is not valid JSON + * @since 2.8.6 */ public static JsonElement parseReader(Reader reader) throws JsonIOException, JsonSyntaxException { try { @@ -90,6 +92,7 @@ public final class JsonParser { * * @throws JsonParseException if there is an IOException or if the specified * text is not valid JSON + * @since 2.8.6 */ public static JsonElement parseReader(JsonReader reader) throws JsonIOException, JsonSyntaxException { diff --git a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java index b787ae89..254d2e5d 100644 --- a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java +++ b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java @@ -1,8 +1,7 @@ package com.google.gson; -import java.lang.reflect.AccessibleObject; - import com.google.gson.internal.ReflectionAccessFilterHelper; +import java.lang.reflect.AccessibleObject; /** * Filter for determining whether reflection based serialization and @@ -28,10 +27,13 @@ import com.google.gson.internal.ReflectionAccessFilterHelper; * fields and classes. * * @see GsonBuilder#addReflectionAccessFilter(ReflectionAccessFilter) + * @since 2.9.1 */ public interface ReflectionAccessFilter { /** * Result of a filter check. + * + * @since 2.9.1 */ enum FilterResult { /** diff --git a/gson/src/main/java/com/google/gson/ToNumberPolicy.java b/gson/src/main/java/com/google/gson/ToNumberPolicy.java index bd70213b..86892984 100644 --- a/gson/src/main/java/com/google/gson/ToNumberPolicy.java +++ b/gson/src/main/java/com/google/gson/ToNumberPolicy.java @@ -16,12 +16,11 @@ package com.google.gson; -import java.io.IOException; -import java.math.BigDecimal; - import com.google.gson.internal.LazilyParsedNumber; import com.google.gson.stream.JsonReader; import com.google.gson.stream.MalformedJsonException; +import java.io.IOException; +import java.math.BigDecimal; /** * An enumeration that defines two standard number reading strategies and a couple of @@ -29,6 +28,7 @@ import com.google.gson.stream.MalformedJsonException; * {@link Object} and {@link Number}. * * @see ToNumberStrategy + * @since 2.8.9 */ public enum ToNumberPolicy implements ToNumberStrategy { diff --git a/gson/src/main/java/com/google/gson/ToNumberStrategy.java b/gson/src/main/java/com/google/gson/ToNumberStrategy.java index 3cd84fa5..ae742219 100644 --- a/gson/src/main/java/com/google/gson/ToNumberStrategy.java +++ b/gson/src/main/java/com/google/gson/ToNumberStrategy.java @@ -16,9 +16,8 @@ package com.google.gson; -import java.io.IOException; - import com.google.gson.stream.JsonReader; +import java.io.IOException; /** * A strategy that is used to control how numbers should be deserialized for {@link Object} and {@link Number} @@ -56,6 +55,7 @@ import com.google.gson.stream.JsonReader; * @see ToNumberPolicy * @see GsonBuilder#setObjectToNumberStrategy(ToNumberStrategy) * @see GsonBuilder#setNumberToNumberStrategy(ToNumberStrategy) + * @since 2.8.9 */ public interface ToNumberStrategy { diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index 37f22b8e..98e1668a 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapter.java +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java @@ -30,7 +30,7 @@ import java.io.Writer; /** * Converts Java objects to and from JSON. * - *

    Defining a type's JSON form

    + *

    Defining a type's JSON form

    * By default Gson converts application classes to JSON using its built-in type * adapters. If Gson's default JSON conversion isn't appropriate for a type, * extend this class to customize the conversion. Here's an example of a type @@ -96,7 +96,7 @@ import java.io.Writer; */ // non-Javadoc: // -//

    JSON Conversion

    +//

    JSON Conversion

    //

    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:

       {@code
    @@ -118,6 +118,9 @@ import java.io.Writer;
     //
     public abstract class TypeAdapter {
     
    +  public TypeAdapter() {
    +  }
    +
       /**
        * Writes one JSON value (an array, object, string, number, boolean or null)
        * for {@code value}.
    diff --git a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    index c12429e9..75fdddbf 100644
    --- a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    +++ b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    @@ -22,6 +22,7 @@ import com.google.gson.reflect.TypeToken;
      * Creates type adapters for set of related types. Type adapter factories are
      * most useful when several types share similar structure in their JSON form.
      *
    + * 

    Examples

    *

    Example: Converting enums to lowercase

    * In this example, we implement a factory that creates type adapters for all * enums. The type adapters will write enums in lowercase, despite the fact @@ -90,7 +91,7 @@ import com.google.gson.reflect.TypeToken; * If multiple factories support the same type, the factory registered earlier * takes precedence. * - *

    Example: composing other type adapters

    + *

    Example: Composing other type adapters

    * In this example we implement a factory for Guava's {@code Multiset} * collection type. The factory can be used to create type adapters for * multisets of any element type: the type adapter for {@code diff --git a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java index a01d77a6..d1685759 100644 --- a/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java +++ b/gson/src/main/java/com/google/gson/annotations/JsonAdapter.java @@ -16,6 +16,7 @@ package com.google.gson.annotations; +import com.google.gson.Gson; import com.google.gson.JsonDeserializer; import com.google.gson.JsonSerializer; import com.google.gson.TypeAdapter; @@ -60,7 +61,7 @@ import java.lang.annotation.Target; *
    * * Since User class specified UserJsonAdapter.class in @JsonAdapter annotation, it - * will automatically be invoked to serialize/deserialize User instances.
    + * will automatically be invoked to serialize/deserialize User instances. * *

    Here is an example of how to apply this annotation to a field. *

    @@ -80,9 +81,14 @@ import java.lang.annotation.Target;
      *
      * 

    The class referenced by this annotation must be either a {@link * TypeAdapter} or a {@link TypeAdapterFactory}, or must implement one - * or both of {@link JsonDeserializer} or {@link JsonSerializer}. - * Using {@link TypeAdapterFactory} makes it possible to delegate - * to the enclosing {@code Gson} instance. + * or both of {@link JsonDeserializer} or {@link JsonSerializer}. + * Using {@link TypeAdapterFactory} makes it possible to delegate + * to the enclosing {@link Gson} instance. + * + *

    {@code Gson} instances might cache the adapter they create for + * a {@code @JsonAdapter} annotation. It is not guaranteed that a new + * adapter is created every time the annotated class or field is serialized + * or deserialized. * * @since 2.3 * diff --git a/gson/src/main/java/com/google/gson/annotations/Since.java b/gson/src/main/java/com/google/gson/annotations/Since.java index e23b6ec9..a7e51fc1 100644 --- a/gson/src/main/java/com/google/gson/annotations/Since.java +++ b/gson/src/main/java/com/google/gson/annotations/Since.java @@ -16,6 +16,7 @@ package com.google.gson.annotations; +import com.google.gson.GsonBuilder; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -24,12 +25,11 @@ import java.lang.annotation.Target; /** * An annotation that indicates the version number since a member or a type has been present. - * This annotation is useful to manage versioning of your Json classes for a web-service. + * This annotation is useful to manage versioning of your JSON classes for a web-service. * *

    * This annotation has no effect unless you build {@link com.google.gson.Gson} with a - * {@link com.google.gson.GsonBuilder} and invoke - * {@link com.google.gson.GsonBuilder#setVersion(double)} method. + * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method. * *

    Here is an example of how this annotation is meant to be used:

    *
    @@ -50,14 +50,16 @@ import java.lang.annotation.Target;
      *
      * @author Inderjeet Singh
      * @author Joel Leitch
    + * @see GsonBuilder#setVersion(double)
    + * @see Until
      */
     @Documented
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD, ElementType.TYPE})
     public @interface Since {
       /**
    -   * the value indicating a version number since this member
    -   * or type has been present.
    +   * The value indicating a version number since this member or type has been present.
    +   * The number is inclusive; annotated elements will be included if {@code gsonVersion >= value}.
        */
       double value();
     }
    diff --git a/gson/src/main/java/com/google/gson/annotations/Until.java b/gson/src/main/java/com/google/gson/annotations/Until.java
    index 7c61d104..a5fcabd4 100644
    --- a/gson/src/main/java/com/google/gson/annotations/Until.java
    +++ b/gson/src/main/java/com/google/gson/annotations/Until.java
    @@ -16,6 +16,7 @@
     
     package com.google.gson.annotations;
     
    +import com.google.gson.GsonBuilder;
     import java.lang.annotation.Documented;
     import java.lang.annotation.ElementType;
     import java.lang.annotation.Retention;
    @@ -24,14 +25,13 @@ import java.lang.annotation.Target;
     
     /**
      * An annotation that indicates the version number until a member or a type should be present.
    - * Basically, if Gson is created with a version number that exceeds the value stored in the
    - * {@code Until} annotation then the field will be ignored from the JSON output. This annotation
    - * is useful to manage versioning of your JSON classes for a web-service.
    + * Basically, if Gson is created with a version number that is equal to or exceeds the value
    + * stored in the {@code Until} annotation then the field will be ignored from the JSON output.
    + * This annotation is useful to manage versioning of your JSON classes for a web-service.
      *
      * 

    * This annotation has no effect unless you build {@link com.google.gson.Gson} with a - * {@link com.google.gson.GsonBuilder} and invoke - * {@link com.google.gson.GsonBuilder#setVersion(double)} method. + * {@code GsonBuilder} and invoke the {@link GsonBuilder#setVersion(double)} method. * *

    Here is an example of how this annotation is meant to be used:

    *
    @@ -47,12 +47,14 @@ import java.lang.annotation.Target;
      * methods will use all the fields for serialization and deserialization. However, if you created
      * Gson with {@code Gson gson = new GsonBuilder().setVersion(1.2).create()} then the
      * {@code toJson()} and {@code fromJson()} methods of Gson will exclude the {@code emailAddress}
    - * and {@code password} fields from the example above, because the version number passed to the 
    + * and {@code password} fields from the example above, because the version number passed to the
      * GsonBuilder, {@code 1.2}, exceeds the version number set on the {@code Until} annotation,
      * {@code 1.1}, for those fields.
      *
      * @author Inderjeet Singh
      * @author Joel Leitch
    + * @see GsonBuilder#setVersion(double)
    + * @see Since
      * @since 1.3
      */
     @Documented
    @@ -61,8 +63,8 @@ import java.lang.annotation.Target;
     public @interface Until {
     
       /**
    -   * the value indicating a version number until this member
    -   * or type should be ignored.
    +   * The value indicating a version number until this member or type should be be included.
    +   * The number is exclusive; annotated elements will be included if {@code gsonVersion < value}.
        */
       double value();
     }
    diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
    index 68b2bd64..115a2a09 100644
    --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
    +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java
    @@ -61,6 +61,25 @@ public final class ConstructorConstructor {
         this.reflectionFilters = reflectionFilters;
       }
     
    +  /**
    +   * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers
    +   * return an exception message.
    +   * @param c instance of the class to be checked
    +   * @return if instantiable {@code null}, else a non-{@code null} exception message
    +   */
    +  static String checkInstantiable(Class c) {
    +    int modifiers = c.getModifiers();
    +    if (Modifier.isInterface(modifiers)) {
    +      return "Interfaces can't be instantiated! Register an InstanceCreator "
    +          + "or a TypeAdapter for this type. Interface name: " + c.getName();
    +    }
    +    if (Modifier.isAbstract(modifiers)) {
    +      return "Abstract classes can't be instantiated! Register an InstanceCreator "
    +          + "or a TypeAdapter for this type. Class name: " + c.getName();
    +    }
    +    return null;
    +  }
    +
       public  ObjectConstructor get(TypeToken typeToken) {
         final Type type = typeToken.getType();
         final Class rawType = typeToken.getRawType();
    @@ -110,7 +129,7 @@ public final class ConstructorConstructor {
     
         // Check whether type is instantiable; otherwise ReflectionAccessFilter recommendation
         // of adjusting filter suggested below is irrelevant since it would not solve the problem
    -    final String exceptionMessage = UnsafeAllocator.checkInstantiable(rawType);
    +    final String exceptionMessage = checkInstantiable(rawType);
         if (exceptionMessage != null) {
           return new ObjectConstructor() {
             @Override public T construct() {
    @@ -242,14 +261,17 @@ public final class ConstructorConstructor {
               @SuppressWarnings("unchecked") // T is the same raw type as is requested
               T newInstance = (T) constructor.newInstance();
               return newInstance;
    -        } catch (InstantiationException e) {
    -          // TODO: JsonParseException ?
    -          throw new RuntimeException("Failed to invoke " + constructor + " with no args", e);
    +        }
    +        // Note: InstantiationException should be impossible because check at start of method made sure
    +        //   that class is not abstract
    +        catch (InstantiationException e) {
    +          throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
    +              + " with no args", e);
             } catch (InvocationTargetException e) {
    -          // TODO: don't wrap if cause is unchecked!
    +          // TODO: don't wrap if cause is unchecked?
               // TODO: JsonParseException ?
    -          throw new RuntimeException("Failed to invoke " + constructor + " with no args",
    -              e.getTargetException());
    +          throw new RuntimeException("Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
    +              + " with no args", e.getCause());
             } catch (IllegalAccessException e) {
               throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
             }
    @@ -342,11 +364,10 @@ public final class ConstructorConstructor {
       private  ObjectConstructor newUnsafeAllocator(final Class rawType) {
         if (useJdkUnsafe) {
           return new ObjectConstructor() {
    -        private final UnsafeAllocator unsafeAllocator = UnsafeAllocator.create();
             @Override public T construct() {
               try {
                 @SuppressWarnings("unchecked")
    -            T newInstance = (T) unsafeAllocator.newInstance(rawType);
    +            T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
                 return newInstance;
               } catch (Exception e) {
                 throw new RuntimeException(("Unable to create instance of " + rawType + ". "
    diff --git a/gson/src/main/java/com/google/gson/internal/Excluder.java b/gson/src/main/java/com/google/gson/internal/Excluder.java
    index 8d8a25f4..03bd45cb 100644
    --- a/gson/src/main/java/com/google/gson/internal/Excluder.java
    +++ b/gson/src/main/java/com/google/gson/internal/Excluder.java
    @@ -240,9 +240,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
       private boolean isValidSince(Since annotation) {
         if (annotation != null) {
           double annotationVersion = annotation.value();
    -      if (annotationVersion > version) {
    -        return false;
    -      }
    +      return version >= annotationVersion;
         }
         return true;
       }
    @@ -250,9 +248,7 @@ public final class Excluder implements TypeAdapterFactory, Cloneable {
       private boolean isValidUntil(Until annotation) {
         if (annotation != null) {
           double annotationVersion = annotation.value();
    -      if (annotationVersion <= version) {
    -        return false;
    -      }
    +      return version < annotationVersion;
         }
         return true;
       }
    diff --git a/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
    new file mode 100644
    index 00000000..b3017430
    --- /dev/null
    +++ b/gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java
    @@ -0,0 +1,98 @@
    +package com.google.gson.internal;
    +
    +import java.util.AbstractList;
    +import java.util.ArrayList;
    +import java.util.Collection;
    +import java.util.List;
    +import java.util.Objects;
    +import java.util.RandomAccess;
    +
    +/**
    + * {@link List} which wraps another {@code List} but prevents insertion of
    + * {@code null} elements. Methods which only perform checks with the element
    + * argument (e.g. {@link #contains(Object)}) do not throw exceptions for
    + * {@code null} arguments.
    + */
    +public class NonNullElementWrapperList extends AbstractList implements RandomAccess {
    +  // Explicitly specify ArrayList as type to guarantee that delegate implements RandomAccess
    +  private final ArrayList delegate;
    +
    +  public NonNullElementWrapperList(ArrayList delegate) {
    +    this.delegate = Objects.requireNonNull(delegate);
    +  }
    +
    +  @Override public E get(int index) {
    +    return delegate.get(index);
    +  }
    +
    +  @Override public int size() {
    +    return delegate.size();
    +  }
    +
    +  private E nonNull(E element) {
    +    if (element == null) {
    +      throw new NullPointerException("Element must be non-null");
    +    }
    +    return element;
    +  }
    +
    +  @Override public E set(int index, E element) {
    +    return delegate.set(index, nonNull(element));
    +  }
    +
    +  @Override public void add(int index, E element) {
    +    delegate.add(index, nonNull(element));
    +  }
    +
    +  @Override public E remove(int index) {
    +    return delegate.remove(index);
    +  }
    +
    +  /* The following methods are overridden because their default implementation is inefficient */
    +
    +  @Override public void clear() {
    +    delegate.clear();
    +  }
    +
    +  @Override public boolean remove(Object o) {
    +    return delegate.remove(o);
    +  }
    +
    +  @Override public boolean removeAll(Collection c) {
    +    return delegate.removeAll(c);
    +  }
    +
    +  @Override public boolean retainAll(Collection c) {
    +    return delegate.retainAll(c);
    +  }
    +
    +  @Override public boolean contains(Object o) {
    +    return delegate.contains(o);
    +  }
    +
    +  @Override public int indexOf(Object o) {
    +    return delegate.indexOf(o);
    +  }
    +
    +  @Override public int lastIndexOf(Object o) {
    +    return delegate.lastIndexOf(o);
    +  }
    +
    +  @Override public Object[] toArray() {
    +    return delegate.toArray();
    +  }
    +
    +  @Override public  T[] toArray(T[] a) {
    +    return delegate.toArray(a);
    +  }
    +
    +  @Override public boolean equals(Object o) {
    +    return delegate.equals(o);
    +  }
    +
    +  @Override public int hashCode() {
    +    return delegate.hashCode();
    +  }
    +
    +  // TODO: Once Gson targets Java 8 also override List.sort
    +}
    diff --git a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
    index 429bac6b..fae6f802 100644
    --- a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
    +++ b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java
    @@ -20,7 +20,6 @@ import java.io.ObjectInputStream;
     import java.io.ObjectStreamClass;
     import java.lang.reflect.Field;
     import java.lang.reflect.Method;
    -import java.lang.reflect.Modifier;
     
     /**
      * Do sneaky things to allocate objects without invoking their constructors.
    @@ -31,38 +30,21 @@ import java.lang.reflect.Modifier;
     public abstract class UnsafeAllocator {
       public abstract  T newInstance(Class c) throws Exception;
     
    -  /**
    -   * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers
    -   * return an exception message.
    -   * @param c instance of the class to be checked
    -   * @return if instantiable {@code null}, else a non-{@code null} exception message
    -   */
    -  static String checkInstantiable(Class c) {
    -    int modifiers = c.getModifiers();
    -    if (Modifier.isInterface(modifiers)) {
    -      return "Interfaces can't be instantiated! Register an InstanceCreator "
    -          + "or a TypeAdapter for this type. Interface name: " + c.getName();
    -    }
    -    if (Modifier.isAbstract(modifiers)) {
    -      return "Abstract classes can't be instantiated! Register an InstanceCreator "
    -          + "or a TypeAdapter for this type. Class name: " + c.getName();
    -    }
    -    return null;
    -  }
    -
       /**
        * Asserts that the class is instantiable. This check should have already occurred
        * in {@link ConstructorConstructor}; this check here acts as safeguard since trying
        * to use Unsafe for non-instantiable classes might crash the JVM on some devices.
        */
       private static void assertInstantiable(Class c) {
    -    String exceptionMessage = checkInstantiable(c);
    +    String exceptionMessage = ConstructorConstructor.checkInstantiable(c);
         if (exceptionMessage != null) {
           throw new AssertionError("UnsafeAllocator is used for non-instantiable type: " + exceptionMessage);
         }
       }
     
    -  public static UnsafeAllocator create() {
    +  public static final UnsafeAllocator INSTANCE = create();
    +
    +  private static UnsafeAllocator create() {
         // try JVM
         // public class Unsafe {
         //   public Object allocateInstance(Class type);
    diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
    index 4cd24e2d..88e654c8 100644
    --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
    +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java
    @@ -93,6 +93,7 @@ public final class JsonTreeReader extends JsonReader {
     
       @Override public void endObject() throws IOException {
         expect(JsonToken.END_OBJECT);
    +    pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected
         popStack(); // empty iterator
         popStack(); // object
         if (stackSize > 0) {
    @@ -165,16 +166,20 @@ public final class JsonTreeReader extends JsonReader {
         }
       }
     
    -  @Override public String nextName() throws IOException {
    +  private String nextName(boolean skipName) throws IOException {
         expect(JsonToken.NAME);
         Iterator i = (Iterator) peekStack();
         Map.Entry entry = (Map.Entry) i.next();
         String result = (String) entry.getKey();
    -    pathNames[stackSize - 1] = result;
    +    pathNames[stackSize - 1] = skipName ? "" : 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) {
    @@ -269,17 +274,26 @@ public final class JsonTreeReader extends JsonReader {
       }
     
       @Override public void skipValue() throws IOException {
    -    if (peek() == JsonToken.NAME) {
    -      nextName();
    -      pathNames[stackSize - 2] = "null";
    -    } else {
    -      popStack();
    -      if (stackSize > 0) {
    -        pathNames[stackSize - 1] = "null";
    -      }
    -    }
    -    if (stackSize > 0) {
    -      pathIndices[stackSize - 1]++;
    +    JsonToken peeked = peek();
    +    switch (peeked) {
    +      case NAME:
    +        @SuppressWarnings("unused")
    +        String unused = nextName(true);
    +        break;
    +      case END_ARRAY:
    +        endArray();
    +        throw new IllegalStateException("Attempt to skip led outside its parent");
    +      case END_OBJECT:
    +        endObject();
    +        throw new IllegalStateException("Attempt to skip led outside its parent");
    +      case END_DOCUMENT:
    +        throw new IllegalStateException("Attempt to skip led outside the document");
    +      default:
    +        popStack();
    +        if (stackSize > 0) {
    +          pathIndices[stackSize - 1]++;
    +        }
    +        break;
         }
       }
     
    diff --git a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
    index 166bae0f..78045ccf 100644
    --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
    +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java
    @@ -40,7 +40,7 @@ import java.util.Map;
     /**
      * Adapts maps to either JSON objects or JSON arrays.
      *
    - * 

    Maps as JSON objects

    + *

    Maps as JSON objects

    * For primitive keys or when complex map key serialization is not enabled, this * converts Java {@link Map Maps} to JSON Objects. This requires that map keys * can be serialized as strings; this is insufficient for some key types. For @@ -65,7 +65,7 @@ import java.util.Map; * at com.google.gson.ObjectNavigator.navigateClassFields * ...
    * - *

    Maps as JSON arrays

    + *

    Maps as JSON arrays

    * An alternative approach taken by this type adapter when it is required and * complex map key serialization is enabled is to encode maps as arrays of map * entries. Each map entry is a two element array containing a key and a value. diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 31a44e1a..5ddac50e 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -19,6 +19,7 @@ package com.google.gson.internal.bind; import com.google.gson.FieldNamingStrategy; import com.google.gson.Gson; import com.google.gson.JsonIOException; +import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; import com.google.gson.ReflectionAccessFilter; import com.google.gson.ReflectionAccessFilter.FilterResult; @@ -38,11 +39,18 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -88,44 +96,59 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { List fieldNames = new ArrayList<>(alternates.length + 1); fieldNames.add(serializedName); - for (String alternate : alternates) { - fieldNames.add(alternate); - } + Collections.addAll(fieldNames, alternates); return fieldNames; } - @Override public TypeAdapter create(Gson gson, final TypeToken type) { + @Override + public TypeAdapter create(Gson gson, final TypeToken type) { Class raw = type.getRawType(); if (!Object.class.isAssignableFrom(raw)) { return null; // it's a primitive! } - FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); + FilterResult filterResult = + ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); if (filterResult == FilterResult.BLOCK_ALL) { - throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " - + raw + ". Register a TypeAdapter for this type or adjust the access filter."); + throw new JsonIOException( + "ReflectionAccessFilter does not permit using reflection for " + raw + + ". Register a TypeAdapter for this type or adjust the access filter."); } boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; + // If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false + // on JVMs that do not support records. + if (ReflectionHelper.isRecord(raw)) { + @SuppressWarnings("unchecked") + TypeAdapter adapter = (TypeAdapter) new RecordAdapter<>(raw, + getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible); + return adapter; + } + ObjectConstructor constructor = constructorConstructor.get(type); - return new Adapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible)); + return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false)); } - private static void checkAccessible(Object object, Field field) { - if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) { - throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#" - + field.getName() + "' is not accessible and ReflectionAccessFilter does not " - + "permit making it accessible. Register a TypeAdapter for the declaring type " - + "or adjust the access filter."); + private static void checkAccessible(Object object, M member) { + if (!ReflectionAccessFilterHelper.canAccess(member, Modifier.isStatic(member.getModifiers()) ? null : object)) { + String memberDescription = ReflectionHelper.getAccessibleObjectDescription(member, true); + throw new JsonIOException(memberDescription + " is not accessible and ReflectionAccessFilter does not" + + " permit making it accessible. Register a TypeAdapter for the declaring type, adjust the" + + " access filter or increase the visibility of the element and its declaring type."); } } private ReflectiveTypeAdapterFactory.BoundField createBoundField( - final Gson context, final Field field, final String name, + final Gson context, final Field field, final Method accessor, final String name, final TypeToken fieldType, boolean serialize, boolean deserialize, final boolean blockInaccessible) { + final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); + + 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) { @@ -138,16 +161,32 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { @SuppressWarnings("unchecked") final TypeAdapter typeAdapter = (TypeAdapter) mapped; - return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) { - @Override void write(JsonWriter writer, Object value) + return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) { + @Override void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException { if (!serialized) return; if (blockInaccessible) { - checkAccessible(value, field); + if (accessor == null) { + checkAccessible(source, field); + } else { + // Note: This check might actually be redundant because access check for canonical + // constructor should have failed already + checkAccessible(source, accessor); + } } - Object fieldValue = field.get(value); - if (fieldValue == value) { + Object fieldValue; + if (accessor != null) { + try { + fieldValue = accessor.invoke(source); + } catch (InvocationTargetException e) { + String accessorDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false); + throw new JsonIOException("Accessor " + accessorDescription + " threw exception", e.getCause()); + } + } else { + fieldValue = field.get(source); + } + if (fieldValue == source) { // avoid direct recursion return; } @@ -156,20 +195,38 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType()); t.write(writer, fieldValue); } - @Override void read(JsonReader reader, Object value) + + @Override + void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException { + Object fieldValue = typeAdapter.read(reader); + if (fieldValue == null && isPrimitive) { + throw new JsonParseException("null is not allowed as value for record component '" + fieldName + "'" + + " of primitive type; at path " + reader.getPath()); + } + target[index] = fieldValue; + } + + @Override + void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); if (fieldValue != null || !isPrimitive) { if (blockInaccessible) { - checkAccessible(value, field); + checkAccessible(target, field); + } else if (isStaticFinalField) { + // Reflection does not permit setting value of `static final` field, even after calling `setAccessible` + // Handle this here to avoid causing IllegalAccessException when calling `Field.set` + String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false); + throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription); } - field.set(value, fieldValue); + field.set(target, fieldValue); } } }; } - private Map getBoundFields(Gson context, TypeToken type, Class raw, boolean blockInaccessible) { + private Map getBoundFields(Gson context, TypeToken type, Class raw, + boolean blockInaccessible, boolean isRecord) { Map result = new LinkedHashMap<>(); if (raw.isInterface()) { return result; @@ -184,9 +241,9 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { if (raw != originalRaw && fields.length > 0) { FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); if (filterResult == FilterResult.BLOCK_ALL) { - throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " - + raw + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type " - + "or adjust the access filter."); + throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + raw + + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type" + + " or adjust the access filter."); } blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; } @@ -197,9 +254,36 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { if (!serialize && !deserialize) { continue; } + // The accessor method is only used for records. If the type is a record, we will read out values + // via its accessor method instead of via reflection. This way we will bypass the accessible restrictions + Method accessor = null; + if (isRecord) { + // If there is a static field on a record, there will not be an accessor. Instead we will use the default + // field serialization logic, but for deserialization the field is excluded for simplicity. Note that Gson + // ignores static fields by default, but GsonBuilder.excludeFieldsWithModifiers can overwrite this. + if (Modifier.isStatic(field.getModifiers())) { + deserialize = false; + } else { + accessor = ReflectionHelper.getAccessor(raw, field); + // If blockInaccessible, skip and perform access check later + if (!blockInaccessible) { + ReflectionHelper.makeAccessible(accessor); + } + + // @SerializedName can be placed on accessor method, but it is not supported there + // If field and method have annotation it is not easily possible to determine if accessor method + // is implicit and has inherited annotation, or if it is explicitly declared with custom annotation + if (accessor.getAnnotation(SerializedName.class) != null + && field.getAnnotation(SerializedName.class) == null) { + String methodDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false); + throw new JsonIOException("@SerializedName on " + methodDescription + " is not supported"); + } + } + } // If blockInaccessible, skip and perform access check later - if (!blockInaccessible) { + // For Records if the accessor method is used the field does not have to be made accessible + if (!blockInaccessible && accessor == null) { ReflectionHelper.makeAccessible(field); } Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); @@ -208,7 +292,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { for (int i = 0, size = fieldNames.size(); i < size; ++i) { String name = fieldNames.get(i); if (i != 0) serialize = false; // only serialize the default name - BoundField boundField = createBoundField(context, field, name, + BoundField boundField = createBoundField(context, field, accessor, name, TypeToken.get(fieldType), serialize, deserialize, blockInaccessible); BoundField replaced = result.put(name, boundField); if (previous == null) previous = replaced; @@ -226,56 +310,51 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { static abstract class BoundField { final String name; + /** Name of the underlying field */ + final String fieldName; final boolean serialized; final boolean deserialized; - protected BoundField(String name, boolean serialized, boolean deserialized) { + protected BoundField(String name, String fieldName, boolean serialized, boolean deserialized) { this.name = name; + this.fieldName = fieldName; this.serialized = serialized; this.deserialized = deserialized; } - abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; - abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; + + /** Read this field value from the source, and append its JSON value to the writer */ + abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException; + + /** Read the value into the target array, used to provide constructor arguments for records */ + abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException; + + /** Read the value from the reader, and set it on the corresponding field on target via reflection */ + abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException; } - public static final class Adapter extends TypeAdapter { - private final ObjectConstructor constructor; - private final Map boundFields; + /** + * Base class for Adapters produced by this factory. + * + *

    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 type of objects that this Adapter creates. + * @param type of accumulator used to build the deserialization result. + */ + // This class is public because external projects check for this class with `instanceof` (even though it is internal) + public static abstract class Adapter extends TypeAdapter { + final Map boundFields; - Adapter(ObjectConstructor constructor, Map boundFields) { - this.constructor = constructor; + Adapter(Map boundFields) { this.boundFields = boundFields; } - @Override public T read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - T instance = constructor.construct(); - - try { - in.beginObject(); - while (in.hasNext()) { - String name = in.nextName(); - BoundField field = boundFields.get(name); - if (field == null || !field.deserialized) { - in.skipValue(); - } else { - field.read(in, instance); - } - } - } catch (IllegalStateException e) { - throw new JsonSyntaxException(e); - } catch (IllegalAccessException e) { - throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); - } - in.endObject(); - return instance; - } - - @Override public void write(JsonWriter out, T value) throws IOException { + @Override + public void write(JsonWriter out, T value) throws IOException { if (value == null) { out.nullValue(); return; @@ -291,5 +370,163 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } out.endObject(); } + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + A accumulator = createAccumulator(); + + try { + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + BoundField field = boundFields.get(name); + if (field == null || !field.deserialized) { + in.skipValue(); + } else { + readField(accumulator, in, field); + } + } + } catch (IllegalStateException e) { + throw new JsonSyntaxException(e); + } catch (IllegalAccessException e) { + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); + } + in.endObject(); + return finalize(accumulator); + } + + /** Create the Object that will be used to collect each field value */ + abstract A createAccumulator(); + /** + * Read a single BoundField into the accumulator. The JsonReader will be pointed at the + * start of the value for the BoundField to read from. + */ + abstract void readField(A accumulator, JsonReader in, BoundField field) + throws IllegalAccessException, IOException; + /** Convert the accumulator to a final instance of T. */ + abstract T finalize(A accumulator); + } + + private static final class FieldReflectionAdapter extends Adapter { + private final ObjectConstructor constructor; + + FieldReflectionAdapter(ObjectConstructor constructor, Map boundFields) { + super(boundFields); + this.constructor = constructor; + } + + @Override + T createAccumulator() { + return constructor.construct(); + } + + @Override + void readField(T accumulator, JsonReader in, BoundField field) + throws IllegalAccessException, IOException { + field.readIntoField(in, accumulator); + } + + @Override + T finalize(T accumulator) { + return accumulator; + } + } + + private static final class RecordAdapter extends Adapter { + static final Map, Object> PRIMITIVE_DEFAULTS = primitiveDefaults(); + + // The canonical constructor of the record + private final Constructor 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 componentIndices = new HashMap<>(); + + RecordAdapter(Class raw, Map boundFields, boolean blockInaccessible) { + super(boundFields); + constructor = ReflectionHelper.getCanonicalRecordConstructor(raw); + + if (blockInaccessible) { + checkAccessible(null, constructor); + } else { + // Ensure the constructor is accessible + ReflectionHelper.makeAccessible(constructor); + } + + String[] componentNames = ReflectionHelper.getRecordComponentNames(raw); + for (int i = 0; i < componentNames.length; i++) { + componentIndices.put(componentNames[i], i); + } + Class[] parameterTypes = constructor.getParameterTypes(); + + // We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this, + // we create an Object[] where all primitives are initialized to non-null values. + constructorArgsDefaults = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + // This will correctly be null for non-primitive types: + constructorArgsDefaults[i] = PRIMITIVE_DEFAULTS.get(parameterTypes[i]); + } + } + + private static Map, Object> primitiveDefaults() { + Map, Object> zeroes = new HashMap<>(); + zeroes.put(byte.class, (byte) 0); + zeroes.put(short.class, (short) 0); + zeroes.put(int.class, 0); + zeroes.put(long.class, 0L); + zeroes.put(float.class, 0F); + zeroes.put(double.class, 0D); + zeroes.put(char.class, '\0'); + zeroes.put(boolean.class, false); + return zeroes; + } + + @Override + Object[] createAccumulator() { + return constructorArgsDefaults.clone(); + } + + @Override + void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException { + // Obtain the component index from the name of the field backing it + Integer componentIndex = componentIndices.get(field.fieldName); + if (componentIndex == null) { + throw new IllegalStateException( + "Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + + " for field with name '" + field.fieldName + "'," + + " unable to determine which argument in the constructor the field corresponds" + + " to. This is unexpected behavior, as we expect the RecordComponents to have the" + + " same names as the fields in the Java class, and that the order of the" + + " RecordComponents is the same as the order of the canonical constructor parameters."); + } + field.readIntoArray(in, componentIndex, accumulator); + } + + @Override + T finalize(Object[] accumulator) { + try { + return constructor.newInstance(accumulator); + } catch (IllegalAccessException e) { + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); + } + // Note: InstantiationException should be impossible because record class is not abstract; + // IllegalArgumentException should not be possible unless a bad adapter returns objects of the wrong type + catch (InstantiationException | IllegalArgumentException e) { + throw new RuntimeException( + "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + + " with args " + Arrays.toString(accumulator), e); + } + catch (InvocationTargetException e) { + // TODO: JsonParseException ? + throw new RuntimeException( + "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'" + + " with args " + Arrays.toString(accumulator), e.getCause()); + } + } } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java new file mode 100644 index 00000000..dad4ff11 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java @@ -0,0 +1,14 @@ +package com.google.gson.internal.bind; + +import com.google.gson.TypeAdapter; + +/** + * Type adapter which might delegate serialization to another adapter. + */ +public abstract class SerializationDelegatingTypeAdapter extends TypeAdapter { + /** + * 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 getSerializationDelegate(); +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java index b7e92495..560234c0 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TreeTypeAdapter.java @@ -38,7 +38,7 @@ import java.lang.reflect.Type; * tree adapter may be serialization-only or deserialization-only, this class * has a facility to lookup a delegate type adapter on demand. */ -public final class TreeTypeAdapter extends TypeAdapter { +public final class TreeTypeAdapter extends SerializationDelegatingTypeAdapter { private final JsonSerializer serializer; private final JsonDeserializer deserializer; final Gson gson; @@ -97,6 +97,15 @@ public final class TreeTypeAdapter extends TypeAdapter { : (delegate = gson.getDelegateAdapter(skipPast, 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 getSerializationDelegate() { + return serializer != null ? this : delegate(); + } + /** * Returns a new factory that will match each type against {@code exactType}. */ @@ -169,5 +178,5 @@ public final class TreeTypeAdapter extends TypeAdapter { @Override public R deserialize(JsonElement json, Type typeOfT) throws JsonParseException { return (R) gson.fromJson(json, typeOfT); } - }; + } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java index 6a690919..75a991ea 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapterRuntimeTypeWrapper.java @@ -53,10 +53,12 @@ final class TypeAdapterRuntimeTypeWrapper extends TypeAdapter { if (runtimeType != type) { @SuppressWarnings("unchecked") TypeAdapter runtimeTypeAdapter = (TypeAdapter) context.getAdapter(TypeToken.get(runtimeType)); + // For backward compatibility only check ReflectiveTypeAdapterFactory.Adapter here but not any other + // wrapping adapters, see https://github.com/google/gson/pull/1787#issuecomment-1222175189 if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { // The user registered a type adapter for the runtime type, so we will use that chosen = runtimeTypeAdapter; - } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) { + } else if (!isReflective(delegate)) { // The user registered a type adapter for Base class, so we prefer it over the // reflective type adapter for the runtime type chosen = delegate; @@ -68,12 +70,30 @@ final class TypeAdapterRuntimeTypeWrapper extends TypeAdapter { 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 Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { - if (value != null - && (type == Object.class || type instanceof TypeVariable || type instanceof Class)) { + private static Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { + if (value != null && (type instanceof Class || type instanceof TypeVariable)) { type = value.getClass(); } return type; diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index 84723b17..cb069ae7 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -194,7 +194,11 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + out.value(value.byteValue()); + } } }; @@ -223,7 +227,11 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + out.value(value.shortValue()); + } } }; @@ -245,7 +253,11 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + out.value(value.intValue()); + } } }; public static final TypeAdapterFactory INTEGER_FACTORY @@ -323,7 +335,11 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + out.value(value.longValue()); + } } }; @@ -338,7 +354,14 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + // For backward compatibility don't call `JsonWriter.value(float)` because that method has + // been newly added and not all custom JsonWriter implementations might override it yet + Number floatNumber = value instanceof Float ? value : value.floatValue(); + out.value(floatNumber); + } } }; @@ -353,7 +376,11 @@ public final class TypeAdapters { } @Override public void write(JsonWriter out, Number value) throws IOException { - out.value(value); + if (value == null) { + out.nullValue(); + } else { + out.value(value.doubleValue()); + } } }; diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 97230ff6..ac061212 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -2,39 +2,97 @@ package com.google.gson.internal.reflect; import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; public class ReflectionHelper { - private ReflectionHelper() { } + + private static final RecordHelper RECORD_HELPER; + + static { + RecordHelper instance; + try { + // Try to construct the RecordSupportedHelper, if this fails, records are not supported on this JVM. + instance = new RecordSupportedHelper(); + } catch (NoSuchMethodException e) { + instance = new RecordNotSupportedHelper(); + } + RECORD_HELPER = instance; + } + + private ReflectionHelper() {} /** - * Tries making the field accessible, wrapping any thrown exception in a - * {@link JsonIOException} with descriptive message. + * Internal implementation of making an {@link AccessibleObject} accessible. * - * @param field field to make accessible - * @throws JsonIOException if making the field accessible fails + * @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on. + * @throws JsonIOException if making the object accessible fails */ - public static void makeAccessible(Field field) throws JsonIOException { + public static void makeAccessible(AccessibleObject object) throws JsonIOException { try { - field.setAccessible(true); + object.setAccessible(true); } catch (Exception exception) { - throw new JsonIOException("Failed making field '" + field.getDeclaringClass().getName() + "#" - + field.getName() + "' accessible; either change its visibility or write a custom " - + "TypeAdapter for its declaring type", exception); + String description = getAccessibleObjectDescription(object, false); + throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility" + + " or write a custom TypeAdapter for its declaring type.", exception); } } /** - * Creates a string representation for a constructor. - * E.g.: {@code java.lang.String#String(char[], int, int)} + * Returns a short string describing the {@link AccessibleObject} in a human-readable way. + * The result is normally shorter than {@link AccessibleObject#toString()} because it omits + * modifiers (e.g. {@code final}) and uses simple names for constructor and method parameter + * types. + * + * @param object object to describe + * @param uppercaseFirstLetter whether the first letter of the description should be uppercased */ - private static String constructorToString(Constructor constructor) { - StringBuilder stringBuilder = new StringBuilder(constructor.getDeclaringClass().getName()) - .append('#') - .append(constructor.getDeclaringClass().getSimpleName()) - .append('('); - Class[] parameters = constructor.getParameterTypes(); + public static String getAccessibleObjectDescription(AccessibleObject object, boolean uppercaseFirstLetter) { + String description; + + if (object instanceof Field) { + Field field = (Field) object; + description = "field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'"; + } else if (object instanceof Method) { + Method method = (Method) object; + + StringBuilder methodSignatureBuilder = new StringBuilder(method.getName()); + appendExecutableParameters(method, methodSignatureBuilder); + String methodSignature = methodSignatureBuilder.toString(); + + description = "method '" + method.getDeclaringClass().getName() + "#" + methodSignature + "'"; + } else if (object instanceof Constructor) { + description = "constructor '" + constructorToString((Constructor) object) + "'"; + } else { + description = " " + object.toString(); + } + + if (uppercaseFirstLetter && Character.isLowerCase(description.charAt(0))) { + description = Character.toUpperCase(description.charAt(0)) + description.substring(1); + } + return description; + } + + /** + * Creates a string representation for a constructor. + * E.g.: {@code java.lang.String(char[], int, int)} + */ + public static String constructorToString(Constructor constructor) { + StringBuilder stringBuilder = new StringBuilder(constructor.getDeclaringClass().getName()); + appendExecutableParameters(constructor, stringBuilder); + + return stringBuilder.toString(); + } + + // Note: Ideally parameter type would be java.lang.reflect.Executable, but that was added in Java 8 + private static void appendExecutableParameters(AccessibleObject executable, StringBuilder stringBuilder) { + stringBuilder.append('('); + + Class[] parameters = (executable instanceof Method) + ? ((Method) executable).getParameterTypes() + : ((Constructor) executable).getParameterTypes(); for (int i = 0; i < parameters.length; i++) { if (i > 0) { stringBuilder.append(", "); @@ -42,7 +100,7 @@ public class ReflectionHelper { stringBuilder.append(parameters[i].getSimpleName()); } - return stringBuilder.append(')').toString(); + stringBuilder.append(')'); } /** @@ -58,17 +116,155 @@ public class ReflectionHelper { constructor.setAccessible(true); return null; } catch (Exception exception) { - return "Failed making constructor '" + constructorToString(constructor) + "' accessible; " - + "either change its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: " + return "Failed making constructor '" + constructorToString(constructor) + "' accessible;" + + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for" // Include the message since it might contain more detailed information - + exception.getMessage(); + + " its declaring type: " + exception.getMessage(); } } - public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) { - throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + "). " - + "Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using " - + "ReflectionAccessFilter, report this to the Gson maintainers.", + /** If records are supported on the JVM, this is equivalent to a call to Class.isRecord() */ + public static boolean isRecord(Class raw) { + return RECORD_HELPER.isRecord(raw); + } + + public static String[] getRecordComponentNames(Class raw) { + return RECORD_HELPER.getRecordComponentNames(raw); + } + + /** Looks up the record accessor method that corresponds to the given record field */ + public static Method getAccessor(Class raw, Field field) { + return RECORD_HELPER.getAccessor(raw, field); + } + + public static Constructor getCanonicalRecordConstructor(Class 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 Constructor getCanonicalRecordConstructor(Class raw); + + public abstract Method getAccessor(Class raw, Field field); + } + + private static class RecordSupportedHelper extends RecordHelper { + private final Method isRecord; + private final Method getRecordComponents; + private final Method getName; + private final Method getType; + + private RecordSupportedHelper() throws NoSuchMethodException { + isRecord = Class.class.getMethod("isRecord"); + getRecordComponents = Class.class.getMethod("getRecordComponents"); + // Class java.lang.reflect.RecordComponent + Class classRecordComponent = getRecordComponents.getReturnType().getComponentType(); + getName = classRecordComponent.getMethod("getName"); + getType = classRecordComponent.getMethod("getType"); + } + + @Override + boolean isRecord(Class raw) { + try { + return (boolean) isRecord.invoke(raw); + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + + @Override + String[] getRecordComponentNames(Class raw) { + try { + Object[] recordComponents = (Object[]) getRecordComponents.invoke(raw); + String[] componentNames = new String[recordComponents.length]; + for (int i = 0; i < recordComponents.length; i++) { + componentNames[i] = (String) getName.invoke(recordComponents[i]); + } + return componentNames; + } catch (ReflectiveOperationException e) { + throw createExceptionForRecordReflectionException(e); + } + } + + @Override + public Constructor getCanonicalRecordConstructor(Class 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 + Constructor getCanonicalRecordConstructor(Class raw) { + throw new UnsupportedOperationException( + "Records are not supported on this JVM, this method should not be called"); + } + + @Override + public Method getAccessor(Class raw, Field field) { + throw new UnsupportedOperationException( + "Records are not supported on this JVM, this method should not be called"); + } + } } diff --git a/gson/src/main/java/com/google/gson/reflect/TypeToken.java b/gson/src/main/java/com/google/gson/reflect/TypeToken.java index 547e5f5b..39e81f33 100644 --- a/gson/src/main/java/com/google/gson/reflect/TypeToken.java +++ b/gson/src/main/java/com/google/gson/reflect/TypeToken.java @@ -32,7 +32,7 @@ import java.util.Objects; * runtime. * *

    For example, to create a type literal for {@code List}, you can - * create an empty anonymous inner class: + * create an empty anonymous class: * *

    * {@code TypeToken> list = new TypeToken>() {};} @@ -43,6 +43,11 @@ import java.util.Objects; * might expect, which gives a false sense of type-safety at compilation time * and can lead to an unexpected {@code ClassCastException} at runtime. * + *

    If the type arguments of the parameterized type are only available at + * runtime, for example when you want to create a {@code List} based on + * a {@code Class} representing the element type, the method + * {@link #getParameterized(Type, Type...)} can be used. + * * @author Bob Lee * @author Sven Mawson * @author Jesse Wilson @@ -317,8 +322,17 @@ public class TypeToken { } /** - * Gets type literal for the parameterized type represented by applying {@code typeArguments} to - * {@code rawType}. + * Gets a type literal for the parameterized type represented by applying {@code typeArguments} to + * {@code rawType}. This is mainly intended for situations where the type arguments are not + * available at compile time. The following example shows how a type token for {@code Map} + * can be created: + *

    {@code
    +   * Class keyClass = ...;
    +   * Class valueClass = ...;
    +   * TypeToken mapTypeToken = TypeToken.getParameterized(Map.class, keyClass, valueClass);
    +   * }
    + * As seen here the result is a {@code TypeToken}; this method cannot provide any type safety, + * and care must be taken to pass in the correct number of type arguments. * * @throws IllegalArgumentException * If {@code rawType} is not of type {@code Class}, or if the type arguments are invalid for diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 8d708ef9..7557426f 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -33,7 +33,7 @@ import java.util.Objects; * 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. * - *

    Parsing JSON

    + *

    Parsing JSON

    * To create a recursive descent parser for your own JSON streams, first create * an entry point method that creates a {@code JsonReader}. * @@ -62,7 +62,7 @@ import java.util.Objects; * Null literals can be consumed using either {@link #nextNull()} or {@link * #skipValue()}. * - *

    Example

    + *

    Example

    * Suppose we'd like to parse a stream of messages such as the following:
     {@code
      * [
      *   {
    @@ -161,7 +161,7 @@ import java.util.Objects;
      *     return new User(username, followersCount);
      *   }}
    * - *

    Number Handling

    + *

    Number Handling

    * 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}. @@ -171,7 +171,7 @@ import java.util.Objects; * precision loss, extremely large values should be written and read as strings * in JSON. * - *

    Non-Execute Prefix

    + *

    Non-Execute Prefix

    * Web servers that serve private data using JSON may be vulnerable to
    Cross-site * request forgery attacks. In such an attack, a malicious site gains access @@ -229,6 +229,8 @@ public class JsonReader implements Closeable { /** True to accept non-spec compliant JSON */ private boolean lenient = DefaultConfig.DEFAULT_LENIENT; + private boolean serializeSpecialFloatingPointValues = DefaultConfig.DEFAULT_SPECIALIZE_FLOAT_VALUES; + static final int BUFFER_SIZE = 1024; /** * Use a manual buffer to easily read and unread upcoming characters, and @@ -331,6 +333,7 @@ public class JsonReader implements Closeable { */ public final void setLenient(boolean lenient) { this.lenient = lenient; + if (lenient) serializeSpecialFloatingPointValues = true; } /** @@ -340,6 +343,14 @@ public class JsonReader implements Closeable { return lenient; } + public void setSerializeSpecialFloatingPointValues(boolean serializeSpecialFloatingPointValues) { + this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues; + } + + public boolean isSerializeSpecialFloatingPointValues() { + return serializeSpecialFloatingPointValues; + } + /** * Consumes the next token from the JSON stream and asserts that it is the * beginning of a new array. @@ -788,10 +799,9 @@ public class JsonReader implements Closeable { } /** - * Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and - * consumes it. + * Returns the next token, a {@link JsonToken#NAME property name}, and consumes it. * - * @throws java.io.IOException if the next token in the stream is not a property + * @throws IOException if the next token in the stream is not a property * name. */ public String nextName() throws IOException { @@ -815,7 +825,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#STRING string} value of the next token, + * Returns the {@link JsonToken#STRING string} value of the next token, * consuming it. If the next token is a number, this method will return its * string form. * @@ -851,7 +861,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#BOOLEAN boolean} value of the next token, + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, * consuming it. * * @throws IllegalStateException if the next token is not a boolean or if @@ -895,7 +905,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER double} value of the next token, + * Returns the {@link JsonToken#NUMBER double} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a double using {@link Double#parseDouble(String)}. * @@ -930,7 +940,7 @@ public class JsonReader implements Closeable { peeked = PEEKED_BUFFERED; double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. - if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { + if (!serializeSpecialFloatingPointValues && (Double.isNaN(result) || Double.isInfinite(result))) { throw new MalformedJsonException( "JSON forbids NaN and infinities: " + result + locationString()); } @@ -941,7 +951,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER long} value of the next token, + * Returns the {@link JsonToken#NUMBER long} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a long. If the next token's numeric value cannot be exactly * represented by a Java {@code long}, this method throws. @@ -1174,7 +1184,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER int} value of the next token, + * Returns the {@link JsonToken#NUMBER int} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as an int. If the next token's numeric value cannot be exactly * represented by a Java {@code int}, this method throws. @@ -1238,7 +1248,7 @@ public class JsonReader implements Closeable { } /** - * Closes this JSON reader and the underlying {@link java.io.Reader}. + * Closes this JSON reader and the underlying {@link Reader}. */ @Override public void close() throws IOException { peeked = PEEKED_NONE; @@ -1248,9 +1258,18 @@ public class JsonReader implements Closeable { } /** - * Skips the next value recursively. If it is an object or array, all nested - * elements are skipped. This method is intended for use when the JSON token - * stream contains unrecognized or unhandled values. + * Skips the next value recursively. This method is intended for use when + * the JSON token stream contains unrecognized or unhandled values. + * + *

    The behavior depends on the type of the next JSON token: + *

      + *
    • Start of a JSON array or object: It and all of its nested values are skipped.
    • + *
    • Primitive value (for example a JSON number): The primitive value is skipped.
    • + *
    • 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.
    • + *
    • End of a JSON array or object: Only this end token is skipped and an exception is thrown.
    • + *
    • End of JSON document: Skipping has no effect on the state, but an exception is thrown.
    • + *
    */ public void skipValue() throws IOException { int count = 0; @@ -1260,33 +1279,69 @@ public class JsonReader implements Closeable { p = doPeek(); } - if (p == PEEKED_BEGIN_ARRAY) { - push(JsonScope.EMPTY_ARRAY); - count++; - } else if (p == PEEKED_BEGIN_OBJECT) { - push(JsonScope.EMPTY_OBJECT); - count++; - } else if (p == PEEKED_END_ARRAY) { - stackSize--; - count--; - } else if (p == PEEKED_END_OBJECT) { - stackSize--; - count--; - } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { - skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { - skipQuotedValue('\''); - } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { - skipQuotedValue('"'); - } else if (p == PEEKED_NUMBER) { - pos += peekedNumberLength; + switch (p) { + case PEEKED_BEGIN_ARRAY: + push(JsonScope.EMPTY_ARRAY); + count++; + break; + case PEEKED_BEGIN_OBJECT: + push(JsonScope.EMPTY_OBJECT); + count++; + break; + case PEEKED_END_ARRAY: + stackSize--; + count--; + break; + case PEEKED_END_OBJECT: + // Only update when object end is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected + } + stackSize--; + count--; + break; + case PEEKED_UNQUOTED: + skipUnquotedValue(); + break; + case PEEKED_SINGLE_QUOTED: + skipQuotedValue('\''); + break; + case PEEKED_DOUBLE_QUOTED: + skipQuotedValue('"'); + break; + case PEEKED_UNQUOTED_NAME: + skipUnquotedValue(); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + 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] = ""; + } + 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] = ""; + } + break; + case PEEKED_NUMBER: + pos += peekedNumberLength; + break; + case PEEKED_EOF: + throw new IllegalStateException("Attempt to skip led outside the document"); + // For all other tokens there is nothing to do; token has already been consumed from underlying reader } peeked = PEEKED_NONE; } while (count > 0); - if (count < 0) throw new IllegalStateException("Attempt to skip led outside its parent"); pathIndices[stackSize - 1]++; - pathNames[stackSize - 1] = "null"; + if (count < 0) throw new IllegalStateException("Attempt to skip led outside its parent"); } private void push(int newTop) { @@ -1522,7 +1577,7 @@ public class JsonReader implements Closeable { *
  • For JSON arrays the path points to the index of the previous element.
    * If no element has been consumed yet it uses the index 0 (even if there are no elements).
  • *
  • For JSON objects the path points to the last property, or to the current - * property if its value has not been consumed yet.
  • + * property if its name has already been consumed. * * *

    This method can be useful to add additional context to exception messages @@ -1539,7 +1594,7 @@ public class JsonReader implements Closeable { *

  • For JSON arrays the path points to the index of the next element (even * if there are no further elements).
  • *
  • For JSON objects the path points to the last property, or to the current - * property if its value has not been consumed yet.
  • + * property if its name has already been consumed. * * *

    This method can be useful to add additional context to exception messages diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 387011de..44c7b08b 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -43,7 +43,7 @@ import java.util.regex.Pattern; * literal values (strings, numbers, booleans and nulls) as well as the begin * and end delimiters of objects and arrays. * - *

    Encoding JSON

    + *

    Encoding JSON

    * 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: @@ -59,7 +59,7 @@ import java.util.regex.Pattern; * Finally close the object using {@link #endObject()}. * * - *

    Example

    + *

    Example

    * Suppose we'd like to encode a stream of messages such as the following:
     {@code
      * [
      *   {
    @@ -193,6 +193,8 @@ public class JsonWriter implements Closeable, Flushable {
     
       private boolean lenient = DefaultConfig.DEFAULT_LENIENT;
     
    +  private boolean serializeSpecialFloatingPointValues = DefaultConfig.DEFAULT_SPECIALIZE_FLOAT_VALUES;
    +
       private boolean omitQuotes = DefaultConfig.DEFAULT_OMIT_QUOTES;
     
       private boolean htmlSafe;
    @@ -242,6 +244,7 @@ public class JsonWriter implements Closeable, Flushable {
        */
       public final void setLenient(boolean lenient) {
         this.lenient = lenient;
    +    if (lenient) this.serializeSpecialFloatingPointValues = true;
       }
     
       /**
    @@ -251,6 +254,14 @@ public class JsonWriter implements Closeable, Flushable {
         return lenient;
       }
     
    +  public void setSerializeSpecialFloatingPointValues(boolean serializeSpecialFloatingPointValues) {
    +    this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues;
    +  }
    +
    +  public boolean isSerializeSpecialFloatingPointValues() {
    +    return serializeSpecialFloatingPointValues;
    +  }
    +
       /**
        * Configure this writer to emit JSON that's safe for direct inclusion in HTML
        * and XML documents. This escapes the HTML characters {@code <}, {@code >},
    @@ -490,6 +501,7 @@ public class JsonWriter implements Closeable, Flushable {
        * @return this writer.
        * @throws UnsupportedOperationException if this writer does not support
        *    writing raw JSON values.
    +   * @since 2.4
        */
       public JsonWriter jsonValue(String value) throws IOException {
         if (value == null) {
    @@ -536,6 +548,7 @@ public class JsonWriter implements Closeable, Flushable {
        * Encodes {@code value}.
        *
        * @return this writer.
    +   * @since 2.7
        */
       public JsonWriter value(Boolean value) throws IOException {
         if (value == null) {
    @@ -556,10 +569,11 @@ public class JsonWriter implements Closeable, Flushable {
        * @return this writer.
        * @throws IllegalArgumentException if the value is NaN or Infinity and this writer is not {@link
        *     #setLenient(boolean) lenient}.
    +   * @since 2.9.1
        */
       public JsonWriter value(float value) throws IOException {
         writeDeferredName();
    -    if (!lenient && (Float.isNaN(value) || Float.isInfinite(value))) {
    +    if (!serializeSpecialFloatingPointValues && (Float.isNaN(value) || Float.isInfinite(value))) {
           throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
         }
         beforeValue();
    @@ -578,7 +592,7 @@ public class JsonWriter implements Closeable, Flushable {
        */
       public JsonWriter value(double value) throws IOException {
         writeDeferredName();
    -    if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
    +    if (!serializeSpecialFloatingPointValues && (Double.isNaN(value) || Double.isInfinite(value))) {
           throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
         }
         beforeValue();
    @@ -628,7 +642,7 @@ public class JsonWriter implements Closeable, Flushable {
         writeDeferredName();
         String string = value.toString();
         if (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN")) {
    -      if (!lenient) {
    +      if (!serializeSpecialFloatingPointValues) {
             throw new IllegalArgumentException("Numeric values must be finite, but was " + string);
           }
         } else {
    diff --git a/gson/src/test/java/com/google/gson/GsonBuilderTest.java b/gson/src/test/java/com/google/gson/GsonBuilderTest.java
    index 9a7adbae..e1a013b5 100644
    --- a/gson/src/test/java/com/google/gson/GsonBuilderTest.java
    +++ b/gson/src/test/java/com/google/gson/GsonBuilderTest.java
    @@ -16,20 +16,25 @@
     
     package com.google.gson;
     
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotNull;
    +import static org.junit.Assert.assertNotSame;
    +import static org.junit.Assert.fail;
    +
     import com.google.gson.stream.JsonReader;
     import com.google.gson.stream.JsonWriter;
     import java.io.IOException;
     import java.lang.reflect.Field;
     import java.lang.reflect.Modifier;
     import java.lang.reflect.Type;
    -import junit.framework.TestCase;
    +import org.junit.Test;
     
     /**
      * Unit tests for {@link GsonBuilder}.
      *
      * @author Inderjeet Singh
      */
    -public class GsonBuilderTest extends TestCase {
    +public class GsonBuilderTest {
       private static final TypeAdapter NULL_TYPE_ADAPTER = new TypeAdapter() {
         @Override public void write(JsonWriter out, Object value) {
           throw new AssertionError();
    @@ -39,6 +44,7 @@ public class GsonBuilderTest extends TestCase {
         }
       };
     
    +  @Test
       public void testCreatingMoreThanOnce() {
         GsonBuilder builder = new GsonBuilder();
         Gson gson = builder.create();
    @@ -61,6 +67,7 @@ public class GsonBuilderTest extends TestCase {
        * 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();
    @@ -136,6 +143,7 @@ public class GsonBuilderTest extends TestCase {
         }
       }
     
    +  @Test
       public void testExcludeFieldsWithModifiers() {
         Gson gson = new GsonBuilder()
             .excludeFieldsWithModifiers(Modifier.VOLATILE, Modifier.PRIVATE)
    @@ -151,6 +159,7 @@ public class GsonBuilderTest extends TestCase {
         String d = "d";
       }
     
    +  @Test
       public void testTransientFieldExclusion() {
         Gson gson = new GsonBuilder()
             .excludeFieldsWithModifiers()
    @@ -162,6 +171,7 @@ public class GsonBuilderTest extends TestCase {
         transient String a = "a";
       }
     
    +  @Test
       public void testRegisterTypeAdapterForCoreType() {
         Type[] types = {
             byte.class,
    @@ -176,6 +186,7 @@ public class GsonBuilderTest extends TestCase {
         }
       }
     
    +  @Test
       public void testDisableJdkUnsafe() {
         Gson gson = new GsonBuilder()
             .disableJdkUnsafe()
    @@ -198,4 +209,22 @@ public class GsonBuilderTest extends TestCase {
         public ClassWithoutNoArgsConstructor(String s) {
         }
       }
    +
    +  @Test
    +  public void testSetVersionInvalid() {
    +    GsonBuilder builder = new GsonBuilder();
    +    try {
    +      builder.setVersion(Double.NaN);
    +      fail();
    +    } catch (IllegalArgumentException e) {
    +      assertEquals("Invalid version: NaN", e.getMessage());
    +    }
    +
    +    try {
    +      builder.setVersion(-0.1);
    +      fail();
    +    } catch (IllegalArgumentException e) {
    +      assertEquals("Invalid version: -0.1", e.getMessage());
    +    }
    +  }
     }
    diff --git a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    new file mode 100644
    index 00000000..a1786ce6
    --- /dev/null
    +++ b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java
    @@ -0,0 +1,285 @@
    +package com.google.gson;
    +
    +import static org.junit.Assert.assertArrayEquals;
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertSame;
    +import static org.junit.Assert.assertTrue;
    +import static org.junit.Assert.fail;
    +
    +import com.google.gson.common.MoreAsserts;
    +import java.util.Arrays;
    +import java.util.Collections;
    +import java.util.List;
    +import org.junit.Test;
    +
    +/**
    + * Tests for {@link JsonArray#asList()}.
    + */
    +public class JsonArrayAsListTest {
    +  @Test
    +  public void testGet() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertEquals(new JsonPrimitive(1), list.get(0));
    +
    +    try {
    +      list.get(-1);
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    try {
    +      list.get(2);
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    a.add((JsonElement) null);
    +    assertEquals(JsonNull.INSTANCE, list.get(1));
    +  }
    +
    +  @Test
    +  public void testSize() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertEquals(1, list.size());
    +    list.add(new JsonPrimitive(2));
    +    assertEquals(2, list.size());
    +  }
    +
    +  @Test
    +  public void testSet() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    JsonElement old = list.set(0, new JsonPrimitive(2));
    +    assertEquals(new JsonPrimitive(1), old);
    +    assertEquals(new JsonPrimitive(2), list.get(0));
    +    assertEquals(new JsonPrimitive(2), a.get(0));
    +
    +    try {
    +      list.set(-1, new JsonPrimitive(1));
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    try {
    +      list.set(2, new JsonPrimitive(1));
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    try {
    +      list.set(0, null);
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("Element must be non-null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testAdd() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    list.add(0, new JsonPrimitive(2));
    +    list.add(1, new JsonPrimitive(3));
    +    assertTrue(list.add(new JsonPrimitive(4)));
    +    assertTrue(list.add(JsonNull.INSTANCE));
    +
    +    List expectedList = Arrays.asList(
    +        new JsonPrimitive(2),
    +        new JsonPrimitive(3),
    +        new JsonPrimitive(1),
    +        new JsonPrimitive(4),
    +        JsonNull.INSTANCE
    +    );
    +    assertEquals(expectedList, list);
    +
    +    try {
    +      list.set(-1, new JsonPrimitive(1));
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    try {
    +      list.set(list.size(), new JsonPrimitive(1));
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +
    +    try {
    +      list.add(0, null);
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("Element must be non-null", e.getMessage());
    +    }
    +    try {
    +      list.add(null);
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("Element must be non-null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testAddAll() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    list.addAll(Arrays.asList(new JsonPrimitive(2), new JsonPrimitive(3)));
    +
    +    List expectedList = Arrays.asList(
    +        new JsonPrimitive(1),
    +        new JsonPrimitive(2),
    +        new JsonPrimitive(3)
    +    );
    +    assertEquals(expectedList, list);
    +
    +    try {
    +      list.addAll(0, Collections.singletonList(null));
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("Element must be non-null", e.getMessage());
    +    }
    +    try {
    +      list.addAll(Collections.singletonList(null));
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("Element must be non-null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testRemoveIndex() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertEquals(new JsonPrimitive(1), list.remove(0));
    +    assertEquals(0, list.size());
    +    assertEquals(0, a.size());
    +
    +    try {
    +      list.remove(0);
    +      fail();
    +    } catch (IndexOutOfBoundsException e) {
    +    }
    +  }
    +
    +  @Test
    +  public void testRemoveElement() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertTrue(list.remove(new JsonPrimitive(1)));
    +    assertEquals(0, list.size());
    +    assertEquals(0, a.size());
    +
    +    assertFalse(list.remove(new JsonPrimitive(1)));
    +    assertFalse(list.remove(null));
    +  }
    +
    +  @Test
    +  public void testClear() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    list.clear();
    +    assertEquals(0, list.size());
    +    assertEquals(0, a.size());
    +  }
    +
    +  @Test
    +  public void testContains() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertTrue(list.contains(new JsonPrimitive(1)));
    +    assertFalse(list.contains(new JsonPrimitive(2)));
    +    assertFalse(list.contains(null));
    +
    +    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
    +    boolean containsInt = list.contains(1); // should only contain JsonPrimitive(1)
    +    assertFalse(containsInt);
    +  }
    +
    +  @Test
    +  public void testIndexOf() {
    +    JsonArray a = new JsonArray();
    +    // Add the same value twice to test indexOf vs. lastIndexOf
    +    a.add(1);
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertEquals(0, list.indexOf(new JsonPrimitive(1)));
    +    assertEquals(-1, list.indexOf(new JsonPrimitive(2)));
    +    assertEquals(-1, list.indexOf(null));
    +
    +    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
    +    int indexOfInt = list.indexOf(1); // should only contain JsonPrimitive(1)
    +    assertEquals(-1, indexOfInt);
    +
    +    assertEquals(1, list.lastIndexOf(new JsonPrimitive(1)));
    +    assertEquals(-1, list.lastIndexOf(new JsonPrimitive(2)));
    +    assertEquals(-1, list.lastIndexOf(null));
    +  }
    +
    +  @Test
    +  public void testToArray() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    assertArrayEquals(new Object[] {new JsonPrimitive(1)}, list.toArray());
    +
    +    JsonElement[] array = list.toArray(new JsonElement[0]);
    +    assertArrayEquals(new Object[] {new JsonPrimitive(1)}, array);
    +
    +    array = new JsonElement[1];
    +    assertSame(array, list.toArray(array));
    +    assertArrayEquals(new Object[] {new JsonPrimitive(1)}, array);
    +
    +    array = new JsonElement[] {null, new JsonPrimitive(2)};
    +    assertSame(array, list.toArray(array));
    +    // Should have set existing array element to null
    +    assertArrayEquals(new Object[] {new JsonPrimitive(1), null}, array);
    +  }
    +
    +  @Test
    +  public void testEqualsHashCode() {
    +    JsonArray a = new JsonArray();
    +    a.add(1);
    +
    +    List list = a.asList();
    +    MoreAsserts.assertEqualsAndHashCode(list, Collections.singletonList(new JsonPrimitive(1)));
    +    assertFalse(list.equals(Collections.emptyList()));
    +    assertFalse(list.equals(Collections.singletonList(new JsonPrimitive(2))));
    +  }
    +
    +  /** Verify that {@code JsonArray} updates are visible to view and vice versa */
    +  @Test
    +  public void testViewUpdates() {
    +    JsonArray a = new JsonArray();
    +    List list = a.asList();
    +
    +    a.add(1);
    +    assertEquals(1, list.size());
    +    assertEquals(new JsonPrimitive(1), list.get(0));
    +
    +    list.add(new JsonPrimitive(2));
    +    assertEquals(2, a.size());
    +    assertEquals(new JsonPrimitive(2), a.get(1));
    +  }
    +}
    diff --git a/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/src/test/java/com/google/gson/JsonArrayTest.java
    index 70398460..45070e3f 100644
    --- a/gson/src/test/java/com/google/gson/JsonArrayTest.java
    +++ b/gson/src/test/java/com/google/gson/JsonArrayTest.java
    @@ -16,18 +16,26 @@
     
     package com.google.gson;
     
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertTrue;
    +import static org.junit.Assert.fail;
    +
     import com.google.gson.common.MoreAsserts;
    -import junit.framework.TestCase;
    +import java.math.BigInteger;
    +import org.junit.Test;
     
     /**
      * @author Jesse Wilson
      */
    -public final class JsonArrayTest extends TestCase {
    +public final class JsonArrayTest {
     
    +  @Test
       public void testEqualsOnEmptyArray() {
         MoreAsserts.assertEqualsAndHashCode(new JsonArray(), new JsonArray());
       }
     
    +  @Test
       public void testEqualsNonEmptyArray() {
         JsonArray a = new JsonArray();
         JsonArray b = new JsonArray();
    @@ -50,6 +58,7 @@ public final class JsonArrayTest extends TestCase {
         assertFalse(b.equals(a));
       }
     
    +  @Test
       public void testRemove() {
         JsonArray array = new JsonArray();
         try {
    @@ -67,6 +76,7 @@ public final class JsonArrayTest extends TestCase {
         assertTrue(array.contains(a));
       }
     
    +  @Test
       public void testSet() {
         JsonArray array = new JsonArray();
         try {
    @@ -91,6 +101,7 @@ public final class JsonArrayTest extends TestCase {
         assertEquals(1, array.size());
       }
     
    +  @Test
       public void testDeepCopy() {
         JsonArray original = new JsonArray();
         JsonArray firstEntry = new JsonArray();
    @@ -106,6 +117,7 @@ public final class JsonArrayTest extends TestCase {
         assertEquals(0, copy.get(0).getAsJsonArray().size());
       }
     
    +  @Test
       public void testIsEmpty() {
         JsonArray array = new JsonArray();
         assertTrue(array.isEmpty());
    @@ -118,6 +130,7 @@ public final class JsonArrayTest extends TestCase {
         assertTrue(array.isEmpty());
       }
     
    +  @Test
       public void testFailedGetArrayValues() {
         JsonArray jsonArray = new JsonArray();
         jsonArray.add(JsonParser.parseString("{" + "\"key1\":\"value1\"," + "\"key2\":\"value2\"," + "\"key3\":\"value3\"," + "\"key4\":\"value4\"" + "}"));
    @@ -182,6 +195,7 @@ public final class JsonArrayTest extends TestCase {
         }
       }
     
    +  @Test
       public void testGetAs_WrongArraySize() {
         JsonArray jsonArray = new JsonArray();
         try {
    @@ -200,4 +214,160 @@ public final class JsonArrayTest extends TestCase {
           assertEquals("Array must have size 1, but has size 2", e.getMessage());
         }
       }
    +
    +  @Test
    +  public void testStringPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add("Hello");
    +    jsonArray.add("Goodbye");
    +    jsonArray.add("Thank you");
    +    jsonArray.add((String) null);
    +    jsonArray.add("Yes");
    +
    +    assertEquals("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testIntegerPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    int x = 1;
    +    jsonArray.add(x);
    +
    +    x = 2;
    +    jsonArray.add(x);
    +
    +    x = -3;
    +    jsonArray.add(x);
    +
    +    jsonArray.add((Integer) null);
    +
    +    x = 4;
    +    jsonArray.add(x);
    +
    +    x = 0;
    +    jsonArray.add(x);
    +
    +    assertEquals("[1,2,-3,null,4,0]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testDoublePrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    double x = 1.0;
    +    jsonArray.add(x);
    +
    +    x = 2.13232;
    +    jsonArray.add(x);
    +
    +    x = 0.121;
    +    jsonArray.add(x);
    +
    +    jsonArray.add((Double) null);
    +
    +    x = -0.00234;
    +    jsonArray.add(x);
    +
    +    jsonArray.add((Double) null);
    +
    +    assertEquals("[1.0,2.13232,0.121,null,-0.00234,null]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testBooleanPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add(true);
    +    jsonArray.add(true);
    +    jsonArray.add(false);
    +    jsonArray.add(false);
    +    jsonArray.add((Boolean) null);
    +    jsonArray.add(true);
    +
    +    assertEquals("[true,true,false,false,null,true]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testCharPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add('a');
    +    jsonArray.add('e');
    +    jsonArray.add('i');
    +    jsonArray.add((char) 111);
    +    jsonArray.add((Character) null);
    +    jsonArray.add('u');
    +    jsonArray.add("and sometimes Y");
    +
    +    assertEquals("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testMixedPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add('a');
    +    jsonArray.add("apple");
    +    jsonArray.add(12121);
    +    jsonArray.add((char) 111);
    +
    +    jsonArray.add((Boolean) null);
    +    assertEquals(JsonNull.INSTANCE, jsonArray.get(jsonArray.size() - 1));
    +
    +    jsonArray.add((Character) null);
    +    assertEquals(JsonNull.INSTANCE, jsonArray.get(jsonArray.size() - 1));
    +
    +    jsonArray.add(12.232);
    +    jsonArray.add(BigInteger.valueOf(2323));
    +
    +    assertEquals("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]", jsonArray.toString());
    +  }
    +
    +  @Test
    +  public void testNullPrimitiveAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add((Character) null);
    +    jsonArray.add((Boolean) null);
    +    jsonArray.add((Integer) null);
    +    jsonArray.add((Double) null);
    +    jsonArray.add((Float) null);
    +    jsonArray.add((BigInteger) null);
    +    jsonArray.add((String) null);
    +    jsonArray.add((Boolean) null);
    +    jsonArray.add((Number) null);
    +
    +    assertEquals("[null,null,null,null,null,null,null,null,null]", jsonArray.toString());
    +    for (int i = 0; i < jsonArray.size(); i++) {
    +      // Verify that they are actually a JsonNull and not a Java null
    +      assertEquals(JsonNull.INSTANCE, jsonArray.get(i));
    +    }
    +  }
    +
    +  @Test
    +  public void testNullJsonElementAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +    jsonArray.add((JsonElement) null);
    +    assertEquals(JsonNull.INSTANCE, jsonArray.get(0));
    +  }
    +
    +  @Test
    +  public void testSameAddition() {
    +    JsonArray jsonArray = new JsonArray();
    +
    +    jsonArray.add('a');
    +    jsonArray.add('a');
    +    jsonArray.add(true);
    +    jsonArray.add(true);
    +    jsonArray.add(1212);
    +    jsonArray.add(1212);
    +    jsonArray.add(34.34);
    +    jsonArray.add(34.34);
    +    jsonArray.add((Boolean) null);
    +    jsonArray.add((Boolean) null);
    +
    +    assertEquals("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]", jsonArray.toString());
    +  }
     }
    diff --git a/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
    new file mode 100644
    index 00000000..00a89a6f
    --- /dev/null
    +++ b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java
    @@ -0,0 +1,287 @@
    +package com.google.gson;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertNull;
    +import static org.junit.Assert.assertTrue;
    +import static org.junit.Assert.fail;
    +
    +import com.google.gson.common.MoreAsserts;
    +import java.util.AbstractMap.SimpleEntry;
    +import java.util.ArrayList;
    +import java.util.Arrays;
    +import java.util.Collection;
    +import java.util.Collections;
    +import java.util.HashMap;
    +import java.util.List;
    +import java.util.Map;
    +import java.util.Map.Entry;
    +import java.util.Set;
    +import org.junit.Test;
    +
    +/**
    + * Tests for {@link JsonObject#asMap()}.
    + */
    +public class JsonObjectAsMapTest {
    +  @Test
    +  public void testSize() {
    +    JsonObject o = new JsonObject();
    +    assertEquals(0, o.asMap().size());
    +
    +    o.addProperty("a", 1);
    +    Map map = o.asMap();
    +    assertEquals(1, map.size());
    +
    +    map.clear();
    +    assertEquals(0, map.size());
    +    assertEquals(0, o.size());
    +  }
    +
    +  @Test
    +  public void testContainsKey() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    assertTrue(map.containsKey("a"));
    +    assertFalse(map.containsKey("b"));
    +    assertFalse(map.containsKey(null));
    +  }
    +
    +  @Test
    +  public void testContainsValue() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +    o.add("b", JsonNull.INSTANCE);
    +
    +    Map map = o.asMap();
    +    assertTrue(map.containsValue(new JsonPrimitive(1)));
    +    assertFalse(map.containsValue(new JsonPrimitive(2)));
    +    assertFalse(map.containsValue(null));
    +
    +    @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"})
    +    boolean containsInt = map.containsValue(1); // should only contain JsonPrimitive(1)
    +    assertFalse(containsInt);
    +  }
    +
    +  @Test
    +  public void testGet() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    assertEquals(new JsonPrimitive(1), map.get("a"));
    +    assertNull(map.get("b"));
    +    assertNull(map.get(null));
    +  }
    +
    +  @Test
    +  public void testPut() {
    +    JsonObject o = new JsonObject();
    +    Map map = o.asMap();
    +
    +    assertNull(map.put("a", new JsonPrimitive(1)));
    +    assertEquals(1, map.size());
    +    assertEquals(new JsonPrimitive(1), map.get("a"));
    +
    +    JsonElement old = map.put("a", new JsonPrimitive(2));
    +    assertEquals(new JsonPrimitive(1), old);
    +    assertEquals(1, map.size());
    +    assertEquals(new JsonPrimitive(2), map.get("a"));
    +    assertEquals(new JsonPrimitive(2), o.get("a"));
    +
    +    assertNull(map.put("b", JsonNull.INSTANCE));
    +    assertEquals(JsonNull.INSTANCE, map.get("b"));
    +
    +    try {
    +      map.put(null, new JsonPrimitive(1));
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("key == null", e.getMessage());
    +    }
    +
    +    try {
    +      map.put("a", null);
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("value == null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testRemove() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    assertNull(map.remove("b"));
    +    assertEquals(1, map.size());
    +
    +    JsonElement old = map.remove("a");
    +    assertEquals(new JsonPrimitive(1), old);
    +    assertEquals(0, map.size());
    +
    +    assertNull(map.remove("a"));
    +    assertEquals(0, map.size());
    +    assertEquals(0, o.size());
    +
    +    assertNull(map.remove(null));
    +  }
    +
    +  @Test
    +  public void testPutAll() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map otherMap = new HashMap<>();
    +    otherMap.put("a", new JsonPrimitive(2));
    +    otherMap.put("b", new JsonPrimitive(3));
    +
    +    Map map = o.asMap();
    +    map.putAll(otherMap);
    +    assertEquals(2, map.size());
    +    assertEquals(new JsonPrimitive(2), map.get("a"));
    +    assertEquals(new JsonPrimitive(3), map.get("b"));
    +
    +    try {
    +      map.putAll(Collections.singletonMap(null, new JsonPrimitive(1)));
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("key == null", e.getMessage());
    +    }
    +
    +    try {
    +      map.putAll(Collections.singletonMap("a", null));
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("value == null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testClear() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    map.clear();
    +    assertEquals(0, map.size());
    +    assertEquals(0, o.size());
    +  }
    +
    +  @Test
    +  public void testKeySet() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("b", 1);
    +    o.addProperty("a", 2);
    +
    +    Map map = o.asMap();
    +    Set keySet = map.keySet();
    +    // Should contain keys in same order
    +    assertEquals(Arrays.asList("b", "a"), new ArrayList<>(keySet));
    +
    +    // Key set doesn't support insertions
    +    try {
    +      keySet.add("c");
    +      fail();
    +    } catch (UnsupportedOperationException e) {
    +    }
    +
    +    assertTrue(keySet.remove("a"));
    +    assertEquals(Collections.singleton("b"), map.keySet());
    +    assertEquals(Collections.singleton("b"), o.keySet());
    +  }
    +
    +  @Test
    +  public void testValues() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 2);
    +    o.addProperty("b", 1);
    +
    +    Map map = o.asMap();
    +    Collection values = map.values();
    +    // Should contain values in same order
    +    assertEquals(Arrays.asList(new JsonPrimitive(2), new JsonPrimitive(1)), new ArrayList<>(values));
    +
    +    // Values collection doesn't support insertions
    +    try {
    +      values.add(new JsonPrimitive(3));
    +      fail();
    +    } catch (UnsupportedOperationException e) {
    +    }
    +
    +    assertTrue(values.remove(new JsonPrimitive(2)));
    +    assertEquals(Collections.singletonList(new JsonPrimitive(1)), new ArrayList<>(map.values()));
    +    assertEquals(1, o.size());
    +    assertEquals(new JsonPrimitive(1), o.get("b"));
    +  }
    +
    +  @Test
    +  public void testEntrySet() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("b", 2);
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    Set> entrySet = map.entrySet();
    +
    +    List> expectedEntrySet = Arrays.>asList(
    +        new SimpleEntry<>("b", new JsonPrimitive(2)),
    +        new SimpleEntry<>("a", new JsonPrimitive(1))
    +    );
    +    // Should contain entries in same order
    +    assertEquals(expectedEntrySet, new ArrayList<>(entrySet));
    +
    +    try {
    +      entrySet.add(new SimpleEntry("c", new JsonPrimitive(3)));
    +      fail();
    +    } catch (UnsupportedOperationException e) {
    +    }
    +
    +    assertTrue(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1))));
    +    assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))), map.entrySet());
    +    assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(2))), o.entrySet());
    +
    +    // Should return false because entry has already been removed
    +    assertFalse(entrySet.remove(new SimpleEntry<>("a", new JsonPrimitive(1))));
    +
    +    Entry entry = entrySet.iterator().next();
    +    JsonElement old = entry.setValue(new JsonPrimitive(3));
    +    assertEquals(new JsonPrimitive(2), old);
    +    assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))), map.entrySet());
    +    assertEquals(Collections.singleton(new SimpleEntry<>("b", new JsonPrimitive(3))), o.entrySet());
    +
    +    try {
    +      entry.setValue(null);
    +      fail();
    +    } catch (NullPointerException e) {
    +      assertEquals("value == null", e.getMessage());
    +    }
    +  }
    +
    +  @Test
    +  public void testEqualsHashCode() {
    +    JsonObject o = new JsonObject();
    +    o.addProperty("a", 1);
    +
    +    Map map = o.asMap();
    +    MoreAsserts.assertEqualsAndHashCode(map, Collections.singletonMap("a", new JsonPrimitive(1)));
    +    assertFalse(map.equals(Collections.emptyMap()));
    +    assertFalse(map.equals(Collections.singletonMap("a", new JsonPrimitive(2))));
    +  }
    +
    +  /** Verify that {@code JsonObject} updates are visible to view and vice versa */
    +  @Test
    +  public void testViewUpdates() {
    +    JsonObject o = new JsonObject();
    +    Map map = o.asMap();
    +
    +    o.addProperty("a", 1);
    +    assertEquals(1, map.size());
    +    assertEquals(new JsonPrimitive(1), map.get("a"));
    +
    +    map.put("b", new JsonPrimitive(2));
    +    assertEquals(2, o.size());
    +    assertEquals(new JsonPrimitive(2), o.get("b"));
    +  }
    +}
    diff --git a/gson/src/test/java/com/google/gson/JsonObjectTest.java b/gson/src/test/java/com/google/gson/JsonObjectTest.java
    index d12d12d8..a0109ba8 100644
    --- a/gson/src/test/java/com/google/gson/JsonObjectTest.java
    +++ b/gson/src/test/java/com/google/gson/JsonObjectTest.java
    @@ -16,6 +16,13 @@
     
     package com.google.gson;
     
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertNotNull;
    +import static org.junit.Assert.assertNull;
    +import static org.junit.Assert.assertTrue;
    +import static org.junit.Assert.fail;
    +
     import com.google.gson.common.MoreAsserts;
     import java.util.AbstractMap.SimpleEntry;
     import java.util.ArrayDeque;
    @@ -27,15 +34,16 @@ import java.util.Iterator;
     import java.util.List;
     import java.util.Map.Entry;
     import java.util.Set;
    -import junit.framework.TestCase;
    +import org.junit.Test;
     
     /**
      * Unit test for the {@link JsonObject} class.
      *
      * @author Joel Leitch
      */
    -public class JsonObjectTest extends TestCase {
    +public class JsonObjectTest {
     
    +  @Test
       public void testAddingAndRemovingObjectProperties() throws Exception {
         JsonObject jsonObj = new JsonObject();
         String propertyName = "property";
    @@ -54,6 +62,7 @@ public class JsonObjectTest extends TestCase {
         assertNull(jsonObj.remove(propertyName));
       }
     
    +  @Test
       public void testAddingNullPropertyValue() throws Exception {
         String propertyName = "property";
         JsonObject jsonObj = new JsonObject();
    @@ -66,6 +75,7 @@ public class JsonObjectTest extends TestCase {
         assertTrue(jsonElement.isJsonNull());
       }
     
    +  @Test
       public void testAddingNullOrEmptyPropertyName() throws Exception {
         JsonObject jsonObj = new JsonObject();
         try {
    @@ -77,6 +87,7 @@ public class JsonObjectTest extends TestCase {
         jsonObj.add("   \t", JsonNull.INSTANCE);
       }
     
    +  @Test
       public void testAddingBooleanProperties() throws Exception {
         String propertyName = "property";
         JsonObject jsonObj = new JsonObject();
    @@ -89,6 +100,7 @@ public class JsonObjectTest extends TestCase {
         assertTrue(jsonElement.getAsBoolean());
       }
     
    +  @Test
       public void testAddingStringProperties() throws Exception {
         String propertyName = "property";
         String value = "blah";
    @@ -103,6 +115,7 @@ public class JsonObjectTest extends TestCase {
         assertEquals(value, jsonElement.getAsString());
       }
     
    +  @Test
       public void testAddingCharacterProperties() throws Exception {
         String propertyName = "property";
         char value = 'a';
    @@ -124,6 +137,7 @@ public class JsonObjectTest extends TestCase {
       /**
        * From bug report http://code.google.com/p/google-gson/issues/detail?id=182
        */
    +  @Test
       public void testPropertyWithQuotes() {
         JsonObject jsonObj = new JsonObject();
         jsonObj.add("a\"b", new JsonPrimitive("c\"d"));
    @@ -134,6 +148,7 @@ public class JsonObjectTest extends TestCase {
       /**
        * From issue 227.
        */
    +  @Test
       public void testWritePropertyWithEmptyStringName() {
         JsonObject jsonObj = new JsonObject();
         jsonObj.add("", new JsonPrimitive(true));
    @@ -141,15 +156,18 @@ public class JsonObjectTest extends TestCase {
     
       }
     
    +  @Test
       public void testReadPropertyWithEmptyStringName() {
         JsonObject jsonObj = JsonParser.parseString("{\"\":true}").getAsJsonObject();
         assertEquals(true, jsonObj.get("").getAsBoolean());
       }
     
    +  @Test
       public void testEqualsOnEmptyObject() {
         MoreAsserts.assertEqualsAndHashCode(new JsonObject(), new JsonObject());
       }
     
    +  @Test
       public void testEqualsNonEmptyObject() {
         JsonObject a = new JsonObject();
         JsonObject b = new JsonObject();
    @@ -172,6 +190,7 @@ public class JsonObjectTest extends TestCase {
         assertFalse(b.equals(a));
       }
     
    +  @Test
       public void testEqualsHashCodeIgnoringOrder() {
         JsonObject a = new JsonObject();
         JsonObject b = new JsonObject();
    @@ -188,6 +207,7 @@ public class JsonObjectTest extends TestCase {
         MoreAsserts.assertEqualsAndHashCode(a, b);
       }
     
    +  @Test
       public void testSize() {
         JsonObject o = new JsonObject();
         assertEquals(0, o.size());
    @@ -202,6 +222,7 @@ public class JsonObjectTest extends TestCase {
         assertEquals(1, o.size());
       }
     
    +  @Test
       public void testDeepCopy() {
         JsonObject original = new JsonObject();
         JsonArray firstEntry = new JsonArray();
    @@ -217,6 +238,7 @@ public class JsonObjectTest extends TestCase {
       /**
        * From issue 941
        */
    +  @Test
       public void testKeySet() {
         JsonObject a = new JsonObject();
         assertEquals(0, a.keySet().size());
    @@ -250,6 +272,7 @@ public class JsonObjectTest extends TestCase {
         }
       }
     
    +  @Test
       public void testEntrySet() {
         JsonObject o = new JsonObject();
         assertEquals(0, o.entrySet().size());
    diff --git a/gson/src/test/java/com/google/gson/MixedStreamTest.java b/gson/src/test/java/com/google/gson/MixedStreamTest.java
    index 1d7c85c4..23ed6fa9 100644
    --- a/gson/src/test/java/com/google/gson/MixedStreamTest.java
    +++ b/gson/src/test/java/com/google/gson/MixedStreamTest.java
    @@ -174,7 +174,7 @@ public final class MixedStreamTest extends TestCase {
         } catch (NullPointerException expected) {
         }
         try {
    -      gson.fromJson(new JsonReader(new StringReader("true")), null);
    +      gson.fromJson(new JsonReader(new StringReader("true")), (Type) null);
           fail();
         } catch (NullPointerException expected) {
         }
    diff --git a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
    index d878850e..2b3fbafa 100644
    --- a/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
    +++ b/gson/src/test/java/com/google/gson/VersionExclusionStrategyTest.java
    @@ -16,40 +16,82 @@
     
     package com.google.gson;
     
    +import static org.junit.Assert.assertFalse;
    +import static org.junit.Assert.assertTrue;
    +
     import com.google.gson.annotations.Since;
    +import com.google.gson.annotations.Until;
     import com.google.gson.internal.Excluder;
    -import junit.framework.TestCase;
    +import org.junit.Test;
     
     /**
      * Unit tests for the {@link Excluder} class.
      *
      * @author Joel Leitch
      */
    -public class VersionExclusionStrategyTest extends TestCase {
    +public class VersionExclusionStrategyTest {
       private static final double VERSION = 5.0D;
     
    -  public void testClassAndFieldAreAtSameVersion() throws Exception {
    +  @Test
    +  public void testSameVersion() throws Exception {
         Excluder excluder = Excluder.DEFAULT.withVersion(VERSION);
    -    assertFalse(excluder.excludeClass(MockObject.class, true));
    -    assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
    +    assertFalse(excluder.excludeClass(MockClassSince.class, true));
    +    assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
    +
    +    // Until version is exclusive
    +    assertTrue(excluder.excludeClass(MockClassUntil.class, true));
    +    assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
    +
    +    assertFalse(excluder.excludeClass(MockClassBoth.class, true));
    +    assertFalse(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
       }
     
    -  public void testClassAndFieldAreBehindInVersion() throws Exception {
    -    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 1);
    -    assertFalse(excluder.excludeClass(MockObject.class, true));
    -    assertFalse(excluder.excludeField(MockObject.class.getField("someField"), true));
    +  @Test
    +  public void testNewerVersion() throws Exception {
    +    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION + 5);
    +    assertFalse(excluder.excludeClass(MockClassSince.class, true));
    +    assertFalse(excluder.excludeField(MockClassSince.class.getField("someField"), true));
    +
    +    assertTrue(excluder.excludeClass(MockClassUntil.class, true));
    +    assertTrue(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
    +
    +    assertTrue(excluder.excludeClass(MockClassBoth.class, true));
    +    assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
       }
     
    -  public void testClassAndFieldAreAheadInVersion() throws Exception {
    -    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 1);
    -    assertTrue(excluder.excludeClass(MockObject.class, true));
    -    assertTrue(excluder.excludeField(MockObject.class.getField("someField"), true));
    +  @Test
    +  public void testOlderVersion() throws Exception {
    +    Excluder excluder = Excluder.DEFAULT.withVersion(VERSION - 5);
    +    assertTrue(excluder.excludeClass(MockClassSince.class, true));
    +    assertTrue(excluder.excludeField(MockClassSince.class.getField("someField"), true));
    +
    +    assertFalse(excluder.excludeClass(MockClassUntil.class, true));
    +    assertFalse(excluder.excludeField(MockClassUntil.class.getField("someField"), true));
    +
    +    assertTrue(excluder.excludeClass(MockClassBoth.class, true));
    +    assertTrue(excluder.excludeField(MockClassBoth.class.getField("someField"), true));
       }
     
       @Since(VERSION)
    -  private static class MockObject {
    +  private static class MockClassSince {
     
         @Since(VERSION)
         public final int someField = 0;
       }
    +
    +  @Until(VERSION)
    +  private static class MockClassUntil {
    +
    +    @Until(VERSION)
    +    public final int someField = 0;
    +  }
    +
    +  @Since(VERSION)
    +  @Until(VERSION + 2)
    +  private static class MockClassBoth {
    +
    +    @Since(VERSION)
    +    @Until(VERSION + 2)
    +    public final int someField = 0;
    +  }
     }
    diff --git a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
    index aa6f4ccb..daa7aa48 100644
    --- a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java
    @@ -35,7 +35,7 @@ import junit.framework.TestCase;
      * @author Inderjeet Singh
      */
     public class GsonVersionDiagnosticsTest extends TestCase {
    -  private static final Pattern GSON_VERSION_PATTERN = Pattern.compile("(\\(GSON \\d\\.\\d\\.\\d)(?:[-.][A-Z]+)?\\)$");
    +  private static final Pattern GSON_VERSION_PATTERN = Pattern.compile("(\\(GSON \\d\\.\\d+(\\.\\d)?)(?:[-.][A-Z]+)?\\)$");
     
       private Gson gson;
     
    diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
    new file mode 100644
    index 00000000..5e050eed
    --- /dev/null
    +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
    @@ -0,0 +1,431 @@
    +/*
    + * Copyright (C) 2022 Google Inc.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *      http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +package com.google.gson.functional;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.junit.Assert.assertNotNull;
    +import static org.junit.Assert.assertNull;
    +import static org.junit.Assert.assertSame;
    +import static org.junit.Assert.assertThrows;
    +import static org.junit.Assert.fail;
    +
    +import com.google.gson.ExclusionStrategy;
    +import com.google.gson.FieldAttributes;
    +import com.google.gson.Gson;
    +import com.google.gson.GsonBuilder;
    +import com.google.gson.JsonDeserializationContext;
    +import com.google.gson.JsonDeserializer;
    +import com.google.gson.JsonElement;
    +import com.google.gson.JsonIOException;
    +import com.google.gson.JsonParseException;
    +import com.google.gson.JsonPrimitive;
    +import com.google.gson.JsonSerializationContext;
    +import com.google.gson.JsonSerializer;
    +import com.google.gson.ReflectionAccessFilter.FilterResult;
    +import com.google.gson.TypeAdapter;
    +import com.google.gson.annotations.Expose;
    +import com.google.gson.annotations.JsonAdapter;
    +import com.google.gson.annotations.SerializedName;
    +import com.google.gson.stream.JsonReader;
    +import com.google.gson.stream.JsonWriter;
    +import java.io.IOException;
    +import java.lang.reflect.Type;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.junit.runners.JUnit4;
    +
    +@RunWith(JUnit4.class)
    +public final class Java17RecordTest {
    +  private final Gson gson = new Gson();
    +
    +  @Test
    +  public void testFirstNameIsChosenForSerialization() {
    +    RecordWithCustomNames target = new RecordWithCustomNames("v1", "v2");
    +    // Ensure name1 occurs exactly once, and name2 and name3 don't appear
    +    assertEquals("{\"name\":\"v1\",\"name1\":\"v2\"}", gson.toJson(target));
    +  }
    +
    +  @Test
    +  public void testMultipleNamesDeserializedCorrectly() {
    +    assertEquals("v1", gson.fromJson("{'name':'v1'}", RecordWithCustomNames.class).a);
    +
    +    // Both name1 and name2 gets deserialized to b
    +    assertEquals("v11", gson.fromJson("{'name': 'v1', 'name1':'v11'}", RecordWithCustomNames.class).b);
    +    assertEquals("v2", gson.fromJson("{'name': 'v1', 'name2':'v2'}", RecordWithCustomNames.class).b);
    +    assertEquals("v3", gson.fromJson("{'name': 'v1', 'name3':'v3'}", RecordWithCustomNames.class).b);
    +  }
    +
    +  @Test
    +  public void testMultipleNamesInTheSameString() {
    +    // The last value takes precedence
    +    assertEquals("v3",
    +        gson.fromJson("{'name': 'foo', 'name1':'v1','name2':'v2','name3':'v3'}", RecordWithCustomNames.class).b);
    +  }
    +
    +  private record RecordWithCustomNames(
    +      @SerializedName("name") String a,
    +      @SerializedName(value = "name1", alternate = {"name2", "name3"}) String b) {}
    +
    +  @Test
    +  public void testSerializedNameOnAccessor() {
    +    record LocalRecord(int i) {
    +      @SerializedName("a")
    +      @Override
    +      public int i() {
    +        return i;
    +      }
    +    }
    +
    +    var exception = assertThrows(JsonIOException.class, () -> gson.getAdapter(LocalRecord.class));
    +    assertEquals("@SerializedName on method '" + LocalRecord.class.getName() + "#i()' is not supported",
    +        exception.getMessage());
    +  }
    +
    +  @Test
    +  public void testFieldNamingStrategy() {
    +    record LocalRecord(int i) {}
    +
    +    Gson gson = new GsonBuilder()
    +        .setFieldNamingStrategy(f -> f.getName() + "-custom")
    +        .create();
    +
    +    assertEquals("{\"i-custom\":1}", gson.toJson(new LocalRecord(1)));
    +    assertEquals(new LocalRecord(2), gson.fromJson("{\"i-custom\":2}", LocalRecord.class));
    +  }
    +
    +  @Test
    +  public void testUnknownJsonProperty() {
    +    record LocalRecord(int i) {}
    +
    +    // Unknown property 'x' should be ignored
    +    assertEquals(new LocalRecord(1), gson.fromJson("{\"i\":1,\"x\":2}", LocalRecord.class));
    +  }
    +
    +  @Test
    +  public void testDuplicateJsonProperties() {
    +    record LocalRecord(Integer a, Integer b) {}
    +
    +    String json = "{\"a\":null,\"a\":2,\"b\":1,\"b\":null}";
    +    // Should use value of last occurrence
    +    assertEquals(new LocalRecord(2, null), gson.fromJson(json, LocalRecord.class));
    +  }
    +
    +  @Test
    +  public void testConstructorRuns() {
    +    record LocalRecord(String s) {
    +      LocalRecord {
    +        s = "custom-" + s;
    +      }
    +    }
    +
    +    LocalRecord deserialized = gson.fromJson("{\"s\": null}", LocalRecord.class);
    +    assertEquals(new LocalRecord(null), deserialized);
    +    assertEquals("custom-null", deserialized.s());
    +  }
    +
    +  /** Tests behavior when the canonical constructor throws an exception */
    +  @Test
    +  public void testThrowingConstructor() {
    +    record LocalRecord(String s) {
    +      static final RuntimeException thrownException = new RuntimeException("Custom exception");
    +
    +      @SuppressWarnings("unused")
    +      LocalRecord {
    +        throw thrownException;
    +      }
    +    }
    +
    +    try {
    +      gson.fromJson("{\"s\":\"value\"}", LocalRecord.class);
    +      fail();
    +    }
    +    // TODO: Adjust this once Gson throws more specific exception type
    +    catch (RuntimeException e) {
    +      assertEquals("Failed to invoke constructor '" + LocalRecord.class.getName() + "(String)' with args [value]",
    +          e.getMessage());
    +      assertSame(LocalRecord.thrownException, e.getCause());
    +    }
    +  }
    +
    +  @Test
    +  public void testAccessorIsCalled() {
    +    record LocalRecord(String s) {
    +      @Override
    +      public String s() {
    +        return "accessor-value";
    +      }
    +    }
    +
    +    assertEquals("{\"s\":\"accessor-value\"}", gson.toJson(new LocalRecord(null)));
    +  }
    +
    +  /** Tests behavior when a record accessor method throws an exception */
    +  @Test
    +  public void testThrowingAccessor() {
    +    record LocalRecord(String s) {
    +      static final RuntimeException thrownException = new RuntimeException("Custom exception");
    +
    +      @Override
    +      public String s() {
    +        throw thrownException;
    +      }
    +    }
    +
    +    try {
    +      gson.toJson(new LocalRecord("a"));
    +      fail();
    +    } catch (JsonIOException e) {
    +      assertEquals("Accessor method '" + LocalRecord.class.getName() + "#s()' threw exception",
    +          e.getMessage());
    +      assertSame(LocalRecord.thrownException, e.getCause());
    +    }
    +  }
    +
    +  /** Tests behavior for a record without components */
    +  @Test
    +  public void testEmptyRecord() {
    +    record EmptyRecord() {}
    +
    +    assertEquals("{}", gson.toJson(new EmptyRecord()));
    +    assertEquals(new EmptyRecord(), gson.fromJson("{}", EmptyRecord.class));
    +  }
    +
    +  /**
    +   * Tests behavior when {@code null} is serialized / deserialized as record value;
    +   * basically makes sure the adapter is 'null-safe'
    +   */
    +  @Test
    +  public void testRecordNull() throws IOException {
    +    record LocalRecord(int i) {}
    +
    +    TypeAdapter adapter = gson.getAdapter(LocalRecord.class);
    +    assertEquals("null", adapter.toJson(null));
    +    assertNull(adapter.fromJson("null"));
    +  }
    +
    +  @Test
    +  public void testPrimitiveDefaultValues() {
    +    RecordWithPrimitives expected = new RecordWithPrimitives("s", (byte) 0, (short) 0, 0, 0, 0, 0, '\0', false);
    +    assertEquals(expected, gson.fromJson("{'aString': 's'}", RecordWithPrimitives.class));
    +  }
    +
    +  @Test
    +  public void testPrimitiveJsonNullValue() {
    +    String s = "{'aString': 's', 'aByte': null, 'aShort': 0}";
    +    var e = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
    +    assertEquals("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte",
    +        e.getMessage());
    +  }
    +
    +  /**
    +   * Tests behavior when JSON contains non-null value, but custom adapter returns null
    +   * for primitive component
    +   */
    +  @Test
    +  public void testPrimitiveAdapterNullValue() {
    +    Gson gson = new GsonBuilder()
    +        .registerTypeAdapter(byte.class, new TypeAdapter() {
    +          @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");
    +          }
    +        })
    +        .setLenient()
    +        .create();
    +
    +    String s = "{'aString': 's', 'aByte': 0}";
    +    var exception = assertThrows(JsonParseException.class, () -> gson.fromJson(s, RecordWithPrimitives.class));
    +    assertEquals("null is not allowed as value for record component 'aByte' of primitive type; at path $.aByte",
    +        exception.getMessage());
    +  }
    +
    +  private record RecordWithPrimitives(
    +      String aString, byte aByte, short aShort, int anInt, long aLong, float aFloat, double aDouble, char aChar, boolean aBoolean) {}
    +
    +  /** Tests behavior when value of Object component is missing; should default to null */
    +  @Test
    +  public void testObjectDefaultValue() {
    +    record LocalRecord(String s, int i) {}
    +
    +    assertEquals(new LocalRecord(null, 1), gson.fromJson("{\"i\":1}", LocalRecord.class));
    +  }
    +
    +  /**
    +   * Tests serialization of a record with {@code static} field.
    +   *
    +   * 

    Important: It is not documented that this is officially supported; this + * test just checks the current behavior. + */ + @Test + public void testStaticFieldSerialization() { + // By default Gson should ignore static fields + assertEquals("{}", gson.toJson(new RecordWithStaticField())); + + Gson gson = new GsonBuilder() + // Include static fields + .excludeFieldsWithModifiers(0) + .create(); + + String json = gson.toJson(new RecordWithStaticField()); + assertEquals("{\"s\":\"initial\"}", json); + } + + /** + * Tests deserialization of a record with {@code static} field. + * + *

    Important: It is not documented that this is officially supported; this + * test just checks the current behavior. + */ + @Test + public void testStaticFieldDeserialization() { + // By default Gson should ignore static fields + gson.fromJson("{\"s\":\"custom\"}", RecordWithStaticField.class); + assertEquals("initial", RecordWithStaticField.s); + + Gson gson = new GsonBuilder() + // Include static fields + .excludeFieldsWithModifiers(0) + .create(); + + String oldValue = RecordWithStaticField.s; + try { + RecordWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", RecordWithStaticField.class); + assertNotNull(obj); + // Currently record deserialization always ignores static fields + assertEquals("initial", RecordWithStaticField.s); + } finally { + RecordWithStaticField.s = oldValue; + } + } + + private record RecordWithStaticField() { + static String s = "initial"; + } + + @Test + public void testExposeAnnotation() { + record RecordWithExpose( + @Expose int a, + int b + ) {} + + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + String json = gson.toJson(new RecordWithExpose(1, 2)); + assertEquals("{\"a\":1}", json); + } + + @Test + public void testFieldExclusionStrategy() { + record LocalRecord(int a, int b, double c) {} + + Gson gson = new GsonBuilder() + .setExclusionStrategies(new ExclusionStrategy() { + @Override public boolean shouldSkipField(FieldAttributes f) { + return f.getName().equals("a"); + } + + @Override public boolean shouldSkipClass(Class clazz) { + return clazz == double.class; + } + }) + .create(); + + assertEquals("{\"b\":2}", gson.toJson(new LocalRecord(1, 2, 3.0))); + } + + @Test + public void testJsonAdapterAnnotation() { + record Adapter() implements JsonSerializer, JsonDeserializer { + @Override public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return "deserializer-" + json.getAsString(); + } + + @Override public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer-" + src); + } + } + record LocalRecord( + @JsonAdapter(Adapter.class) String s + ) {} + + assertEquals("{\"s\":\"serializer-a\"}", gson.toJson(new LocalRecord("a"))); + assertEquals(new LocalRecord("deserializer-a"), gson.fromJson("{\"s\":\"a\"}", LocalRecord.class)); + } + + @Test + public void testClassReflectionFilter() { + record Allowed(int a) {} + record Blocked(int b) {} + + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(c -> c == Allowed.class ? FilterResult.ALLOW : FilterResult.BLOCK_ALL) + .create(); + + String json = gson.toJson(new Allowed(1)); + assertEquals("{\"a\":1}", json); + + var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new Blocked(1))); + assertEquals("ReflectionAccessFilter does not permit using reflection for class " + Blocked.class.getName() + + ". Register a TypeAdapter for this type or adjust the access filter.", + exception.getMessage()); + } + + @Test + public void testReflectionFilterBlockInaccessible() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(c -> FilterResult.BLOCK_INACCESSIBLE) + .create(); + + var exception = assertThrows(JsonIOException.class, () -> gson.toJson(new PrivateRecord(1))); + assertEquals("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type.", + exception.getMessage()); + + exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", PrivateRecord.class)); + assertEquals("Constructor 'com.google.gson.functional.Java17RecordTest$PrivateRecord(int)' is not accessible and" + + " ReflectionAccessFilter does not permit making it accessible. Register a TypeAdapter for the declaring" + + " type, adjust the access filter or increase the visibility of the element and its declaring type.", + exception.getMessage()); + + assertEquals("{\"i\":1}", gson.toJson(new PublicRecord(1))); + assertEquals(new PublicRecord(2), gson.fromJson("{\"i\":2}", PublicRecord.class)); + } + + private record PrivateRecord(int i) {} + public record PublicRecord(int i) {} + + /** + * Tests behavior when {@code java.lang.Record} is used as type for serialization + * and deserialization. + */ + @Test + public void testRecordBaseClass() { + record LocalRecord(int i) {} + + assertEquals("{}", gson.toJson(new LocalRecord(1), Record.class)); + + var exception = assertThrows(JsonIOException.class, () -> gson.fromJson("{}", Record.class)); + assertEquals("Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for" + + " this type. Class name: java.lang.Record", + exception.getMessage()); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java b/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java deleted file mode 100644 index 410a0817..00000000 --- a/gson/src/test/java/com/google/gson/functional/JsonArrayTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.gson.functional; - -import com.google.gson.JsonArray; -import java.math.BigInteger; -import junit.framework.TestCase; - -/** - * Functional tests for adding primitives to a JsonArray. - * - * @author Dillon Dixon - */ -public class JsonArrayTest extends TestCase { - - public void testStringPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add("Hello"); - jsonArray.add("Goodbye"); - jsonArray.add("Thank you"); - jsonArray.add((String) null); - jsonArray.add("Yes"); - - assertEquals("[\"Hello\",\"Goodbye\",\"Thank you\",null,\"Yes\"]", jsonArray.toString()); - } - - public void testIntegerPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - int x = 1; - jsonArray.add(x); - - x = 2; - jsonArray.add(x); - - x = -3; - jsonArray.add(x); - - jsonArray.add((Integer) null); - - x = 4; - jsonArray.add(x); - - x = 0; - jsonArray.add(x); - - assertEquals("[1,2,-3,null,4,0]", jsonArray.toString()); - } - - public void testDoublePrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - double x = 1.0; - jsonArray.add(x); - - x = 2.13232; - jsonArray.add(x); - - x = 0.121; - jsonArray.add(x); - - jsonArray.add((Double) null); - - x = -0.00234; - jsonArray.add(x); - - jsonArray.add((Double) null); - - assertEquals("[1.0,2.13232,0.121,null,-0.00234,null]", jsonArray.toString()); - } - - public void testBooleanPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add(true); - jsonArray.add(true); - jsonArray.add(false); - jsonArray.add(false); - jsonArray.add((Boolean) null); - jsonArray.add(true); - - assertEquals("[true,true,false,false,null,true]", jsonArray.toString()); - } - - public void testCharPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add('a'); - jsonArray.add('e'); - jsonArray.add('i'); - jsonArray.add((char) 111); - jsonArray.add((Character) null); - jsonArray.add('u'); - jsonArray.add("and sometimes Y"); - - assertEquals("[\"a\",\"e\",\"i\",\"o\",null,\"u\",\"and sometimes Y\"]", jsonArray.toString()); - } - - public void testMixedPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add('a'); - jsonArray.add("apple"); - jsonArray.add(12121); - jsonArray.add((char) 111); - jsonArray.add((Boolean) null); - jsonArray.add((Character) null); - jsonArray.add(12.232); - jsonArray.add(BigInteger.valueOf(2323)); - - assertEquals("[\"a\",\"apple\",12121,\"o\",null,null,12.232,2323]", jsonArray.toString()); - } - - public void testNullPrimitiveAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add((Character) null); - jsonArray.add((Boolean) null); - jsonArray.add((Integer) null); - jsonArray.add((Double) null); - jsonArray.add((Float) null); - jsonArray.add((BigInteger) null); - jsonArray.add((String) null); - jsonArray.add((Boolean) null); - jsonArray.add((Number) null); - - assertEquals("[null,null,null,null,null,null,null,null,null]", jsonArray.toString()); - } - - public void testSameAddition() { - JsonArray jsonArray = new JsonArray(); - - jsonArray.add('a'); - jsonArray.add('a'); - jsonArray.add(true); - jsonArray.add(true); - jsonArray.add(1212); - jsonArray.add(1212); - jsonArray.add(34.34); - jsonArray.add(34.34); - jsonArray.add((Boolean) null); - jsonArray.add((Boolean) null); - - assertEquals("[\"a\",\"a\",true,true,1212,1212,34.34,34.34,null,null]", jsonArray.toString()); - } -} diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index 0bcbc5d2..9fd5dd50 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -20,6 +20,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; @@ -482,6 +483,16 @@ public class ObjectTest extends TestCase { gson.fromJson(gson.toJson(product), Product.class); } + static final class Department { + public String name = "abc"; + public String code = "123"; + } + + static final class Product { + private List attributes = new ArrayList<>(); + private List departments = new ArrayList<>(); + } + // http://code.google.com/p/google-gson/issues/detail?id=270 public void testDateAsMapObjectField() { HasObjectMap a = new HasObjectMap(); @@ -493,17 +504,92 @@ public class ObjectTest extends TestCase { } } - public class HasObjectMap { + static class HasObjectMap { Map map = new HashMap<>(); } - static final class Department { - public String name = "abc"; - public String code = "123"; + /** + * Tests serialization of a class with {@code static} field. + * + *

    Important: It is not documented that this is officially supported; this + * test just checks the current behavior. + */ + public void testStaticFieldSerialization() { + // By default Gson should ignore static fields + assertEquals("{}", gson.toJson(new ClassWithStaticField())); + + Gson gson = new GsonBuilder() + // Include static fields + .excludeFieldsWithModifiers(0) + .create(); + + String json = gson.toJson(new ClassWithStaticField()); + assertEquals("{\"s\":\"initial\"}", json); + + json = gson.toJson(new ClassWithStaticFinalField()); + assertEquals("{\"s\":\"initial\"}", json); } - static final class Product { - private List attributes = new ArrayList<>(); - private List departments = new ArrayList<>(); + /** + * Tests deserialization of a class with {@code static} field. + * + *

    Important: It is not documented that this is officially supported; this + * test just checks the current behavior. + */ + public void testStaticFieldDeserialization() { + // By default Gson should ignore static fields + gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class); + assertEquals("initial", ClassWithStaticField.s); + + Gson gson = new GsonBuilder() + // Include static fields + .excludeFieldsWithModifiers(0) + .create(); + + String oldValue = ClassWithStaticField.s; + try { + ClassWithStaticField obj = gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticField.class); + assertNotNull(obj); + assertEquals("custom", ClassWithStaticField.s); + } finally { + ClassWithStaticField.s = oldValue; + } + + try { + gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticFinalField.class); + fail(); + } catch (JsonIOException e) { + assertEquals("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'", + e.getMessage()); + } + } + + static class ClassWithStaticField { + static String s = "initial"; + } + + static class ClassWithStaticFinalField { + static final String s = "initial"; + } + + public void testThrowingDefaultConstructor() { + try { + gson.fromJson("{}", ClassWithThrowingConstructor.class); + fail(); + } + // TODO: Adjust this once Gson throws more specific exception type + catch (RuntimeException e) { + assertEquals("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args", + e.getMessage()); + assertSame(ClassWithThrowingConstructor.thrownException, e.getCause()); + } + } + + static class ClassWithThrowingConstructor { + static final RuntimeException thrownException = new RuntimeException("Custom exception"); + + public ClassWithThrowingConstructor() { + throw thrownException; + } } } diff --git a/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java index 49cb0db9..e6168746 100644 --- a/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java +++ b/gson/src/test/java/com/google/gson/functional/ParameterizedTypesTest.java @@ -16,13 +16,19 @@ package com.google.gson.functional; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import com.google.gson.ParameterizedTypeFixtures.MyParameterizedType; import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeAdapter; import com.google.gson.ParameterizedTypeFixtures.MyParameterizedTypeInstanceCreator; import com.google.gson.common.TestTypes.BagOfPrimitives; import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; @@ -32,7 +38,8 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; /** * Functional tests for the serialization and deserialization of parameterized types in Gson. @@ -40,15 +47,15 @@ import junit.framework.TestCase; * @author Inderjeet Singh * @author Joel Leitch */ -public class ParameterizedTypesTest extends TestCase { +public class ParameterizedTypesTest { private Gson gson; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() { gson = new Gson(); } + @Test public void testParameterizedTypesSerialization() throws Exception { MyParameterizedType src = new MyParameterizedType<>(10); Type typeOfSrc = new TypeToken>() {}.getType(); @@ -56,6 +63,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(src.getExpectedJson(), json); } + @Test public void testParameterizedTypeDeserialization() throws Exception { BagOfPrimitives bag = new BagOfPrimitives(); MyParameterizedType expected = new MyParameterizedType<>(bag); @@ -70,6 +78,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(expected, actual); } + @Test public void testTypesWithMultipleParametersSerialization() throws Exception { MultiParameters src = new MultiParameters<>(10, 1.0F, 2.1D, "abc", new BagOfPrimitives()); @@ -81,6 +90,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(expected, json); } + @Test public void testTypesWithMultipleParametersDeserialization() throws Exception { Type typeOfTarget = new TypeToken>() {}.getType(); @@ -93,6 +103,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(expected, target); } + @Test public void testParameterizedTypeWithCustomSerializer() { Type ptIntegerType = new TypeToken>() {}.getType(); Type ptStringType = new TypeToken>() {}.getType(); @@ -109,6 +120,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(MyParameterizedTypeAdapter.getExpectedJson(stringTarget), json); } + @Test public void testParameterizedTypesWithCustomDeserializer() { Type ptIntegerType = new TypeToken>() {}.getType(); Type ptStringType = new TypeToken>() {}.getType(); @@ -130,6 +142,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals("abc", stringTarget.value); } + @Test public void testParameterizedTypesWithWriterSerialization() throws Exception { Writer writer = new StringWriter(); MyParameterizedType src = new MyParameterizedType<>(10); @@ -138,6 +151,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(src.getExpectedJson(), writer.toString()); } + @Test public void testParameterizedTypeWithReaderDeserialization() throws Exception { BagOfPrimitives bag = new BagOfPrimitives(); MyParameterizedType expected = new MyParameterizedType<>(bag); @@ -158,6 +172,7 @@ public class ParameterizedTypesTest extends TestCase { return args; } + @Test public void testVariableTypeFieldsAndGenericArraysSerialization() throws Exception { Integer obj = 0; Integer[] array = { 1, 2, 3 }; @@ -174,6 +189,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(objToSerialize.getExpectedJson(), json); } + @Test public void testVariableTypeFieldsAndGenericArraysDeserialization() throws Exception { Integer obj = 0; Integer[] array = { 1, 2, 3 }; @@ -191,6 +207,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(objAfterDeserialization.getExpectedJson(), json); } + @Test public void testVariableTypeDeserialization() throws Exception { Type typeOfSrc = new TypeToken>() {}.getType(); ObjectWithTypeVariables objToSerialize = @@ -201,6 +218,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(objAfterDeserialization.getExpectedJson(), json); } + @Test public void testVariableTypeArrayDeserialization() throws Exception { Integer[] array = { 1, 2, 3 }; @@ -213,6 +231,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(objAfterDeserialization.getExpectedJson(), json); } + @Test public void testParameterizedTypeWithVariableTypeDeserialization() throws Exception { List list = new ArrayList<>(); list.add(4); @@ -227,6 +246,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(objAfterDeserialization.getExpectedJson(), json); } + @Test public void testParameterizedTypeGenericArraysSerialization() throws Exception { List list = new ArrayList<>(); list.add(1); @@ -240,6 +260,7 @@ public class ParameterizedTypesTest extends TestCase { assertEquals("{\"arrayOfListOfTypeParameters\":[[1,2],[1,2]]}", json); } + @Test public void testParameterizedTypeGenericArraysDeserialization() throws Exception { List list = new ArrayList<>(); list.add(1); @@ -483,6 +504,7 @@ public class ParameterizedTypesTest extends TestCase { int value = 30; } + @Test public void testDeepParameterizedTypeSerialization() { Amount amount = new Amount<>(); String json = gson.toJson(amount); @@ -490,6 +512,7 @@ public class ParameterizedTypesTest extends TestCase { assertTrue(json.contains("30")); } + @Test public void testDeepParameterizedTypeDeserialization() { String json = "{value:30}"; Type type = new TypeToken>() {}.getType(); @@ -497,4 +520,47 @@ public class ParameterizedTypesTest extends TestCase { assertEquals(30, amount.value); } // End: tests to reproduce issue 103 + + private static void assertCorrectlyDeserialized(Object object) { + @SuppressWarnings("unchecked") + List list = (List) object; + assertEquals(1, list.size()); + assertEquals(4, list.get(0).q); + } + + @Test + public void testGsonFromJsonTypeToken() { + TypeToken> typeToken = new TypeToken>() {}; + Type type = typeToken.getType(); + + { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("q", 4); + JsonArray jsonArray = new JsonArray(); + jsonArray.add(jsonObject); + + assertCorrectlyDeserialized(gson.fromJson(jsonArray, typeToken)); + assertCorrectlyDeserialized(gson.fromJson(jsonArray, type)); + } + + String json = "[{\"q\":4}]"; + + { + assertCorrectlyDeserialized(gson.fromJson(json, typeToken)); + assertCorrectlyDeserialized(gson.fromJson(json, type)); + } + + { + assertCorrectlyDeserialized(gson.fromJson(new StringReader(json), typeToken)); + assertCorrectlyDeserialized(gson.fromJson(new StringReader(json), type)); + } + + { + JsonReader reader = new JsonReader(new StringReader(json)); + assertCorrectlyDeserialized(gson.fromJson(reader, typeToken)); + + reader = new JsonReader(new StringReader(json)); + assertCorrectlyDeserialized(gson.fromJson(reader, type)); + } + } } diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java index fc0f6985..13716346 100644 --- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java @@ -60,6 +60,11 @@ public class PrimitiveTest extends TestCase { public void testByteSerialization() { assertEquals("1", gson.toJson(1, byte.class)); assertEquals("1", gson.toJson(1, Byte.class)); + assertEquals(Byte.toString(Byte.MIN_VALUE), gson.toJson(Byte.MIN_VALUE, Byte.class)); + assertEquals(Byte.toString(Byte.MAX_VALUE), gson.toJson(Byte.MAX_VALUE, Byte.class)); + // Should perform narrowing conversion + assertEquals("-128", gson.toJson(128, Byte.class)); + assertEquals("1", gson.toJson(1.5, Byte.class)); } public void testByteDeserialization() { @@ -98,6 +103,13 @@ public class PrimitiveTest extends TestCase { public void testShortSerialization() { assertEquals("1", gson.toJson(1, short.class)); assertEquals("1", gson.toJson(1, Short.class)); + assertEquals(Short.toString(Short.MIN_VALUE), gson.toJson(Short.MIN_VALUE, Short.class)); + assertEquals(Short.toString(Short.MAX_VALUE), gson.toJson(Short.MAX_VALUE, Short.class)); + // Should perform widening conversion + assertEquals("1", gson.toJson((byte) 1, Short.class)); + // Should perform narrowing conversion + assertEquals("-32768", gson.toJson(32768, Short.class)); + assertEquals("1", gson.toJson(1.5, Short.class)); } public void testShortDeserialization() { @@ -133,6 +145,54 @@ public class PrimitiveTest extends TestCase { } } + public void testIntSerialization() { + assertEquals("1", gson.toJson(1, int.class)); + assertEquals("1", gson.toJson(1, Integer.class)); + assertEquals(Integer.toString(Integer.MIN_VALUE), gson.toJson(Integer.MIN_VALUE, Integer.class)); + assertEquals(Integer.toString(Integer.MAX_VALUE), gson.toJson(Integer.MAX_VALUE, Integer.class)); + // Should perform widening conversion + assertEquals("1", gson.toJson((byte) 1, Integer.class)); + // Should perform narrowing conversion + assertEquals("-2147483648", gson.toJson(2147483648L, Integer.class)); + assertEquals("1", gson.toJson(1.5, Integer.class)); + } + + public void testLongSerialization() { + assertEquals("1", gson.toJson(1L, long.class)); + assertEquals("1", gson.toJson(1L, Long.class)); + assertEquals(Long.toString(Long.MIN_VALUE), gson.toJson(Long.MIN_VALUE, Long.class)); + assertEquals(Long.toString(Long.MAX_VALUE), gson.toJson(Long.MAX_VALUE, Long.class)); + // Should perform widening conversion + assertEquals("1", gson.toJson((byte) 1, Long.class)); + // Should perform narrowing conversion + assertEquals("1", gson.toJson(1.5, Long.class)); + } + + public void testFloatSerialization() { + assertEquals("1.5", gson.toJson(1.5f, float.class)); + assertEquals("1.5", gson.toJson(1.5f, Float.class)); + assertEquals(Float.toString(Float.MIN_VALUE), gson.toJson(Float.MIN_VALUE, Float.class)); + assertEquals(Float.toString(Float.MAX_VALUE), gson.toJson(Float.MAX_VALUE, Float.class)); + // Should perform widening conversion + assertEquals("1.0", gson.toJson((byte) 1, Float.class)); + // (This widening conversion is actually lossy) + assertEquals(Float.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Float.class)); + // Should perform narrowing conversion + gson = new GsonBuilder().serializeSpecialFloatingPointValues().create(); + assertEquals("Infinity", gson.toJson(Double.MAX_VALUE, Float.class)); + } + + public void testDoubleSerialization() { + assertEquals("1.5", gson.toJson(1.5, double.class)); + assertEquals("1.5", gson.toJson(1.5, Double.class)); + assertEquals(Double.toString(Double.MIN_VALUE), gson.toJson(Double.MIN_VALUE, Double.class)); + assertEquals(Double.toString(Double.MAX_VALUE), gson.toJson(Double.MAX_VALUE, Double.class)); + // Should perform widening conversion + assertEquals("1.0", gson.toJson((byte) 1, Double.class)); + // (This widening conversion is actually lossy) + assertEquals(Double.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Double.class)); + } + public void testPrimitiveIntegerAutoboxedInASingleElementArraySerialization() { int target[] = {-9332}; assertEquals("[-9332]", gson.toJson(target)); diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java index b2541df3..6c9ab449 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java @@ -2,6 +2,7 @@ package com.google.gson.functional; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeNotNull; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -20,6 +21,7 @@ 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.Type; import java.util.LinkedList; import java.util.List; @@ -39,7 +41,7 @@ public class ReflectionAccessFilterTest { } @Test - public void testBlockInaccessibleJava() { + public void testBlockInaccessibleJava() throws ReflectiveOperationException { Gson gson = new GsonBuilder() .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA) .create(); @@ -51,16 +53,26 @@ public class ReflectionAccessFilterTest { } catch (JsonIOException expected) { // Note: This test is rather brittle and depends on the JDK implementation assertEquals( - "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit " - + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.", + "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit" + + " making it accessible. Register a TypeAdapter for the declaring type, adjust the access" + + " filter or increase the visibility of the element and its declaring type.", expected.getMessage() ); } - // But serialization should succeed for classes with only public fields - // awt is unavailable and this should just work (tm) -// String json = gson.toJson(new Point(1, 2)); -// assertEquals("{\"x\":1,\"y\":2}", json); + + // But serialization should succeed for classes with only public fields. + // Not many JDK classes have mutable public fields, thank goodness, but java.awt.Point does. + Class pointClass = null; + try { + pointClass = Class.forName("java.awt.Point"); + } catch (ClassNotFoundException e) { + } + assumeNotNull(pointClass); + Constructor pointConstructor = pointClass.getConstructor(int.class, int.class); + Object point = pointConstructor.newInstance(1, 2); + String json = gson.toJson(point); + assertEquals("{\"x\":1,\"y\":2}", json); } @Test @@ -74,8 +86,9 @@ public class ReflectionAccessFilterTest { fail("Expected exception; test needs to be run with Java >= 9"); } catch (JsonIOException expected) { assertEquals( - "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit " - + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.", + "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit" + + " making it accessible. Register a TypeAdapter for the declaring type, adjust the access" + + " filter or increase the visibility of the element and its declaring type.", expected.getMessage() ); } @@ -93,8 +106,8 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread. " - + "Register a TypeAdapter for this type or adjust the access filter.", + "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread." + + " Register a TypeAdapter for this type or adjust the access filter.", expected.getMessage() ); } @@ -111,9 +124,9 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "ReflectionAccessFilter does not permit using reflection for class java.io.Reader " - + "(supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass). " - + "Register a TypeAdapter for this type or adjust the access filter.", + "ReflectionAccessFilter does not permit using reflection for class java.io.Reader" + + " (supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass)." + + " Register a TypeAdapter for this type or adjust the access filter.", expected.getMessage() ); } @@ -141,9 +154,10 @@ public class ReflectionAccessFilterTest { fail("Expected exception; test needs to be run with Java >= 9"); } catch (JsonIOException expected) { assertEquals( - "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i' " - + "is not accessible and ReflectionAccessFilter does not permit making it accessible. " - + "Register a TypeAdapter for the declaring type or adjust the access filter.", + "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i'" + + " is not accessible and ReflectionAccessFilter does not permit making it accessible." + + " Register a TypeAdapter for the declaring type, adjust the access filter or increase" + + " the visibility of the element and its declaring type.", expected.getMessage() ); } @@ -183,9 +197,9 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "ReflectionAccessFilter does not permit using reflection for class " - + "com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass. " - + "Register a TypeAdapter for this type or adjust the access filter.", + "ReflectionAccessFilter does not permit using reflection for class" + + " com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass." + + " Register a TypeAdapter for this type or adjust the access filter.", expected.getMessage() ); } @@ -222,9 +236,10 @@ public class ReflectionAccessFilterTest { fail("Expected exception; test needs to be run with Java >= 9"); } catch (JsonIOException expected) { assertEquals( - "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i' " - + "is not accessible and ReflectionAccessFilter does not permit making it accessible. " - + "Register a TypeAdapter for the declaring type or adjust the access filter.", + "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i'" + + " is not accessible and ReflectionAccessFilter does not permit making it accessible." + + " Register a TypeAdapter for the declaring type, adjust the access filter or increase" + + " the visibility of the element and its declaring type.", expected.getMessage() ); } @@ -263,9 +278,9 @@ public class ReflectionAccessFilterTest { fail("Expected exception; test needs to be run with Java >= 9"); } catch (JsonIOException expected) { assertEquals( - "Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor; " - + "constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an " - + "InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.", + "Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor;" + + " constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an" + + " InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.", expected.getMessage() ); } @@ -295,9 +310,9 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor; " - + "ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator " - + "or a TypeAdapter for this type or adjust the access filter to allow using reflection.", + "Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor;" + + " ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator" + + " or a TypeAdapter for this type or adjust the access filter to allow using reflection.", expected.getMessage() ); } @@ -311,7 +326,7 @@ public class ReflectionAccessFilterTest { } @Override public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) throws IOException { throw new AssertionError("Not needed for test"); - }; + } }) .create(); ClassWithoutNoArgsConstructor deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class); @@ -357,8 +372,8 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass. " - + "Register a TypeAdapter for this type or adjust the access filter.", + "ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass." + + " Register a TypeAdapter for this type or adjust the access filter.", expected.getMessage() ); } @@ -417,8 +432,8 @@ public class ReflectionAccessFilterTest { fail(); } catch (JsonIOException expected) { assertEquals( - "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for " - + "this type. Interface name: java.lang.Runnable", + "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for" + + " this type. Interface name: java.lang.Runnable", expected.getMessage() ); } diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java index ce2458cb..3b485581 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java @@ -56,8 +56,8 @@ public class ReflectionAccessTest { fail("Unexpected exception; test has to be run with `--illegal-access=deny`"); } catch (JsonIOException expected) { assertTrue(expected.getMessage().startsWith( - "Failed making constructor 'java.util.Collections$EmptyList#EmptyList()' accessible; " - + "either change its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type" + "Failed making constructor 'java.util.Collections$EmptyList()' accessible;" + + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: " )); } } diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java new file mode 100644 index 00000000..73a01012 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java @@ -0,0 +1,193 @@ +package com.google.gson.functional; + +import static org.junit.Assert.assertEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import org.junit.Test; + +public class TypeAdapterRuntimeTypeWrapperTest { + private static class Base { + } + private static class Subclass extends Base { + @SuppressWarnings("unused") + String f = "test"; + } + private static class Container { + @SuppressWarnings("unused") + Base b = new Subclass(); + } + private static class Deserializer implements JsonDeserializer { + @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() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer"); + } + }) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"serializer\"}", json); + } + + /** + * When only {@link JsonDeserializer} is registered for Base, then on + * serialization should prefer reflective adapter for Subclass since + * Base would use reflective adapter as delegate. + */ + @Test + public void testJsonDeserializer_ReflectiveSerializerDelegate() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } + + /** + * When {@link JsonDeserializer} with custom adapter as delegate is + * registered for Base, then on serialization should prefer custom adapter + * delegate for Base over reflective adapter for Subclass. + */ + @Test + public void testJsonDeserializer_CustomSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register custom delegate + .registerTypeAdapter(Base.class, new TypeAdapter() { + @Override + public Base read(JsonReader in) throws IOException { + throw new UnsupportedOperationException(); + } + @Override + public void write(JsonWriter out, Base value) throws IOException { + out.value("custom delegate"); + } + }) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"custom delegate\"}", json); + } + + /** + * When two (or more) {@link JsonDeserializer}s are registered for Base + * which eventually fall back to reflective adapter as delegate, then on + * serialization should prefer reflective adapter for Subclass. + */ + @Test + public void testJsonDeserializer_ReflectiveTreeSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register delegate which itself falls back to reflective serialization + .registerTypeAdapter(Base.class, new Deserializer()) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } + + /** + * When {@link JsonDeserializer} with {@link JsonSerializer} as delegate + * is registered for Base, then on serialization should prefer + * {@code JsonSerializer} over reflective adapter for Subclass. + */ + @Test + public void testJsonDeserializer_JsonSerializerDelegate() { + Gson gson = new GsonBuilder() + // Register JsonSerializer as delegate + .registerTypeAdapter(Base.class, new JsonSerializer() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("custom delegate"); + } + }) + .registerTypeAdapter(Base.class, new Deserializer()) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":\"custom delegate\"}", json); + } + + /** + * When a {@link JsonDeserializer} is registered for Subclass, and a custom + * {@link JsonSerializer} is registered for Base, then Gson should prefer + * the reflective adapter for Subclass for backward compatibility (see + * https://github.com/google/gson/pull/1787#issuecomment-1222175189) even + * though normally TypeAdapterRuntimeTypeWrapper should prefer the custom + * serializer for Base. + */ + @Test + public void testJsonDeserializer_SubclassBackwardCompatibility() { + Gson gson = new GsonBuilder() + .registerTypeAdapter(Subclass.class, new JsonDeserializer() { + @Override + public Subclass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + throw new AssertionError("not needed for this test"); + } + }) + .registerTypeAdapter(Base.class, new JsonSerializer() { + @Override + public JsonElement serialize(Base src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("base"); + } + }) + .create(); + + String json = gson.toJson(new Container()); + assertEquals("{\"b\":{\"f\":\"test\"}}", json); + } + + private static class CyclicBase { + @SuppressWarnings("unused") + CyclicBase f; + } + + private static class CyclicSub extends CyclicBase { + @SuppressWarnings("unused") + int i; + + public CyclicSub(int i) { + this.i = i; + } + } + + /** + * Tests behavior when the type of a field refers to a type whose adapter is + * currently in the process of being created. For these cases {@link Gson} + * uses a future adapter for the type. That adapter later uses the actual + * adapter as delegate. + */ + @Test + public void testGsonFutureAdapter() { + CyclicBase b = new CyclicBase(); + b.f = new CyclicSub(2); + String json = new Gson().toJson(b); + assertEquals("{\"f\":{\"i\":2}}", json); + } +} diff --git a/gson/src/test/java/com/google/gson/functional/VersioningTest.java b/gson/src/test/java/com/google/gson/functional/VersioningTest.java index 2416fc06..49dabcab 100644 --- a/gson/src/test/java/com/google/gson/functional/VersioningTest.java +++ b/gson/src/test/java/com/google/gson/functional/VersioningTest.java @@ -15,13 +15,17 @@ */ package com.google.gson.functional; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.annotations.Since; import com.google.gson.annotations.Until; import com.google.gson.common.TestTypes.BagOfPrimitives; - -import junit.framework.TestCase; +import org.junit.Test; /** * Functional tests for versioning support in Gson. @@ -29,47 +33,60 @@ import junit.framework.TestCase; * @author Inderjeet Singh * @author Joel Leitch */ -public class VersioningTest extends TestCase { +public class VersioningTest { private static final int A = 0; private static final int B = 1; private static final int C = 2; private static final int D = 3; - private GsonBuilder builder; - - @Override - protected void setUp() throws Exception { - super.setUp(); - builder = new GsonBuilder(); + private static Gson gsonWithVersion(double version) { + return new GsonBuilder().setVersion(version).create(); } + @Test public void testVersionedUntilSerialization() { Version1 target = new Version1(); - Gson gson = builder.setVersion(1.29).create(); + Gson gson = gsonWithVersion(1.29); String json = gson.toJson(target); assertTrue(json.contains("\"a\":" + A)); - gson = builder.setVersion(1.3).create(); + gson = gsonWithVersion(1.3); + json = gson.toJson(target); + assertFalse(json.contains("\"a\":" + A)); + + gson = gsonWithVersion(1.31); json = gson.toJson(target); assertFalse(json.contains("\"a\":" + A)); } + @Test public void testVersionedUntilDeserialization() { - Gson gson = builder.setVersion(1.3).create(); String json = "{\"a\":3,\"b\":4,\"c\":5}"; + + Gson gson = gsonWithVersion(1.29); Version1 version1 = gson.fromJson(json, Version1.class); + assertEquals(3, version1.a); + + gson = gsonWithVersion(1.3); + version1 = gson.fromJson(json, Version1.class); + assertEquals(A, version1.a); + + gson = gsonWithVersion(1.31); + version1 = gson.fromJson(json, Version1.class); assertEquals(A, version1.a); } + @Test public void testVersionedClassesSerialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); String json1 = gson.toJson(new Version1()); String json2 = gson.toJson(new Version1_1()); assertEquals(json1, json2); } + @Test public void testVersionedClassesDeserialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); String json = "{\"a\":3,\"b\":4,\"c\":5}"; Version1 version1 = gson.fromJson(json, Version1.class); assertEquals(3, version1.a); @@ -80,13 +97,15 @@ public class VersioningTest extends TestCase { assertEquals(C, version1_1.c); } + @Test public void testIgnoreLaterVersionClassSerialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); assertEquals("null", gson.toJson(new Version1_2())); } + @Test public void testIgnoreLaterVersionClassDeserialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); String json = "{\"a\":3,\"b\":4,\"c\":5,\"d\":6}"; Version1_2 version1_2 = gson.fromJson(json, Version1_2.class); // Since the class is versioned to be after 1.0, we expect null @@ -94,14 +113,16 @@ public class VersioningTest extends TestCase { assertNull(version1_2); } + @Test public void testVersionedGsonWithUnversionedClassesSerialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); BagOfPrimitives target = new BagOfPrimitives(10, 20, false, "stringValue"); assertEquals(target.getExpectedJson(), gson.toJson(target)); } + @Test public void testVersionedGsonWithUnversionedClassesDeserialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); String json = "{\"longValue\":10,\"intValue\":20,\"booleanValue\":false}"; BagOfPrimitives expected = new BagOfPrimitives(); @@ -112,34 +133,45 @@ public class VersioningTest extends TestCase { assertEquals(expected, actual); } + @Test public void testVersionedGsonMixingSinceAndUntilSerialization() { - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); SinceUntilMixing target = new SinceUntilMixing(); String json = gson.toJson(target); assertFalse(json.contains("\"b\":" + B)); - gson = builder.setVersion(1.2).create(); + gson = gsonWithVersion(1.2); json = gson.toJson(target); assertTrue(json.contains("\"b\":" + B)); - gson = builder.setVersion(1.3).create(); + gson = gsonWithVersion(1.3); + json = gson.toJson(target); + assertFalse(json.contains("\"b\":" + B)); + + gson = gsonWithVersion(1.4); json = gson.toJson(target); assertFalse(json.contains("\"b\":" + B)); } + @Test public void testVersionedGsonMixingSinceAndUntilDeserialization() { String json = "{\"a\":5,\"b\":6}"; - Gson gson = builder.setVersion(1.0).create(); + Gson gson = gsonWithVersion(1.0); SinceUntilMixing result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); assertEquals(B, result.b); - gson = builder.setVersion(1.2).create(); + gson = gsonWithVersion(1.2); result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); assertEquals(6, result.b); - gson = builder.setVersion(1.3).create(); + gson = gsonWithVersion(1.3); + result = gson.fromJson(json, SinceUntilMixing.class); + assertEquals(5, result.a); + assertEquals(B, result.b); + + gson = gsonWithVersion(1.4); result = gson.fromJson(json, SinceUntilMixing.class); assertEquals(5, result.a); assertEquals(B, result.b); diff --git a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java index e3ce147e..54d0a506 100644 --- a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java +++ b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java @@ -37,9 +37,8 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase { * to instantiate an interface */ public void testInterfaceInstantiation() throws Exception { - UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); try { - unsafeAllocator.newInstance(Interface.class); + UnsafeAllocator.INSTANCE.newInstance(Interface.class); fail(); } catch (AssertionError e) { assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type")); @@ -51,9 +50,8 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase { * to instantiate an abstract class */ public void testAbstractClassInstantiation() throws Exception { - UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); try { - unsafeAllocator.newInstance(AbstractClass.class); + UnsafeAllocator.INSTANCE.newInstance(AbstractClass.class); fail(); } catch (AssertionError e) { assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type")); @@ -64,8 +62,7 @@ public final class UnsafeAllocatorInstantiationTest extends TestCase { * Ensure that no exception is thrown when trying to instantiate a concrete class */ public void testConcreteClassInstantiation() throws Exception { - UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); - ConcreteClass instance = unsafeAllocator.newInstance(ConcreteClass.class); + ConcreteClass instance = UnsafeAllocator.INSTANCE.newInstance(ConcreteClass.class); assertNotNull(instance); } } diff --git a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java index 5a63f3cb..c20a3683 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java @@ -22,14 +22,12 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; - import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.JavaVersion; import com.google.gson.internal.bind.DefaultDateTypeAdapter.DateType; import com.google.gson.reflect.TypeToken; - import junit.framework.TestCase; /** @@ -75,6 +73,39 @@ public class DefaultDateTypeAdapterTest extends TestCase { } } + public void testParsingDatesFormattedWithSystemLocale() throws Exception { + // TODO(eamonnmcmanus): fix this test, which fails on JDK 8 and 17 + if (JavaVersion.getMajorJavaVersion() != 11) { + return; + } + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.FRANCE); + try { + String afterYearSep = JavaVersion.isJava9OrLater() ? " à " : " "; + assertParsed(String.format("1 janv. 1970%s00:00:00", afterYearSep), + DateType.DATE.createDefaultsAdapterFactory()); + assertParsed("01/01/70", DateType.DATE.createAdapterFactory(DateFormat.SHORT)); + assertParsed("1 janv. 1970", DateType.DATE.createAdapterFactory(DateFormat.MEDIUM)); + assertParsed("1 janvier 1970", DateType.DATE.createAdapterFactory(DateFormat.LONG)); + assertParsed("01/01/70 00:00", + DateType.DATE.createAdapterFactory(DateFormat.SHORT, DateFormat.SHORT)); + assertParsed(String.format("1 janv. 1970%s00:00:00", afterYearSep), + DateType.DATE.createAdapterFactory(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertParsed(String.format("1 janvier 1970%s00:00:00 UTC", afterYearSep), + DateType.DATE.createAdapterFactory(DateFormat.LONG, DateFormat.LONG)); + assertParsed(JavaVersion.isJava9OrLater() ? (JavaVersion.getMajorJavaVersion() <11 ? + "jeudi 1 janvier 1970 à 00:00:00 Coordinated Universal Time" : + "jeudi 1 janvier 1970 à 00:00:00 Temps universel coordonné") : + "jeudi 1 janvier 1970 00 h 00 UTC", + DateType.DATE.createAdapterFactory(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + public void testParsingDatesFormattedWithUsLocale() throws Exception { TimeZone defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("UTC")); diff --git a/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java new file mode 100644 index 00000000..18984c7b --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java @@ -0,0 +1,81 @@ +package com.google.gson.internal.bind; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.reflect.Java17ReflectionHelperTest; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; +import java.security.Principal; +import org.junit.Before; +import org.junit.Test; + +public class Java17ReflectiveTypeAdapterFactoryTest { + + // The class jdk.net.UnixDomainPrincipal is one of the few Record types that are included in the JDK. + // We use this to test serialization and deserialization of Record classes, so we do not need to + // have record support at the language level for these tests. This class was added in JDK 16. + Class unixDomainPrincipalClass; + + @Before + public void setUp() throws Exception { + unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + } + + // Class for which the normal reflection based adapter is used + private static class DummyClass { + @SuppressWarnings("unused") + public String s; + } + + @Test + public void testCustomAdapterForRecords() { + Gson gson = new Gson(); + TypeAdapter recordAdapter = gson.getAdapter(unixDomainPrincipalClass); + TypeAdapter defaultReflectionAdapter = gson.getAdapter(DummyClass.class); + assertNotEquals(recordAdapter.getClass(), defaultReflectionAdapter.getClass()); + } + + @Test + public void testSerializeRecords() throws ReflectiveOperationException { + Gson gson = + new GsonBuilder() + .registerTypeAdapter(UserPrincipal.class, new PrincipalTypeAdapter<>()) + .registerTypeAdapter(GroupPrincipal.class, new PrincipalTypeAdapter<>()) + .create(); + + UserPrincipal userPrincipal = gson.fromJson("\"user\"", UserPrincipal.class); + GroupPrincipal groupPrincipal = gson.fromJson("\"group\"", GroupPrincipal.class); + Object recordInstance = + unixDomainPrincipalClass + .getDeclaredConstructor(UserPrincipal.class, GroupPrincipal.class) + .newInstance(userPrincipal, groupPrincipal); + String serialized = gson.toJson(recordInstance); + Object deserializedRecordInstance = gson.fromJson(serialized, unixDomainPrincipalClass); + + assertEquals(recordInstance, deserializedRecordInstance); + assertEquals("{\"user\":\"user\",\"group\":\"group\"}", serialized); + } + + private static class PrincipalTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, T principal) throws IOException { + out.value(principal.getName()); + } + + @Override + public T read(JsonReader in) throws IOException { + final String name = in.nextString(); + // This type adapter is only used for Group and User Principal, both of which are implemented by PrincipalImpl. + @SuppressWarnings("unchecked") + T principal = (T) new Java17ReflectionHelperTest.PrincipalImpl(name); + return principal; + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 4e6a218e..2ac32a0e 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -29,12 +29,15 @@ import java.util.Arrays; import java.util.List; import junit.framework.TestCase; +import static org.junit.Assert.assertThrows; + @SuppressWarnings("resource") public class JsonTreeReaderTest extends TestCase { public void testSkipValue_emptyJsonObject() throws IOException { JsonTreeReader in = new JsonTreeReader(new JsonObject()); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); } public void testSkipValue_filledJsonObject() throws IOException { @@ -53,6 +56,46 @@ public class JsonTreeReaderTest extends TestCase { JsonTreeReader in = new JsonTreeReader(jsonObject); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); + } + + public void testSkipValue_name() throws IOException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("a", "value"); + JsonTreeReader in = new JsonTreeReader(jsonObject); + in.beginObject(); + in.skipValue(); + assertEquals(JsonToken.STRING, in.peek()); + assertEquals("$.", in.getPath()); + assertEquals("value", in.nextString()); + } + + public void testSkipValue_afterEndOfDocument() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + assertThrows("Attempt to skip led outside the document", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atArrayEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonArray()); + reader.beginArray(); + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atObjectEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testHasNext_endOfDocument() throws IOException { @@ -92,7 +135,8 @@ public class JsonTreeReaderTest extends TestCase { * {@code JsonReader} must be overridden. */ public void testOverrides() { - List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()"); + List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()", + "setSerializeSpecialFloatingPointValues(boolean)", "isSerializeSpecialFloatingPointValues()"); MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods); } } diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java index 126390b6..19e30f34 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -256,8 +256,12 @@ public final class JsonTreeWriterTest extends TestCase { * methods of {@code JsonWriter} must be overridden. */ public void testOverrides() { - List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()", "setIndent(java.lang.String)", - "setHtmlSafe(boolean)", "isHtmlSafe()", "setSerializeNulls(boolean)", "getSerializeNulls()", "setOmitQuotes(boolean)", "getOmitQuotes()"); + List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()", + "setSerializeSpecialFloatingPointValues(boolean)", "isSerializeSpecialFloatingPointValues()", + "setIndent(java.lang.String)", + "setHtmlSafe(boolean)", "isHtmlSafe()", + "setSerializeNulls(boolean)", "getSerializeNulls()", + "setOmitQuotes(boolean)", "getOmitQuotes()"); MoreAsserts.assertOverridesMethods(JsonWriter.class, JsonTreeWriter.class, ignoredMethods); } } diff --git a/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java new file mode 100644 index 00000000..4d4089e8 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java @@ -0,0 +1,83 @@ +package com.google.gson.internal.reflect; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.UserPrincipal; +import java.util.Objects; +import org.junit.Test; + +public class Java17ReflectionHelperTest { + @Test + public void testJava17Record() throws ClassNotFoundException { + Class unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + // UnixDomainPrincipal is a record + assertTrue(ReflectionHelper.isRecord(unixDomainPrincipalClass)); + // with 2 components + assertArrayEquals( + new String[] {"user", "group"}, + ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)); + // Check canonical constructor + Constructor constructor = + ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass); + assertNotNull(constructor); + assertArrayEquals( + new Class[] {UserPrincipal.class, GroupPrincipal.class}, + constructor.getParameterTypes()); + } + + @Test + public void testJava17RecordAccessors() throws ReflectiveOperationException { + // Create an instance of UnixDomainPrincipal, using our custom implementation of UserPrincipal, + // and GroupPrincipal. Then attempt to access each component of the record using our accessor + // methods. + Class unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + Object unixDomainPrincipal = + ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass) + .newInstance(new PrincipalImpl("user"), new PrincipalImpl("group")); + + String[] componentNames = ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass); + assertTrue(componentNames.length > 0); + + for (String componentName : componentNames) { + Field componentField = unixDomainPrincipalClass.getDeclaredField(componentName); + Method accessor = ReflectionHelper.getAccessor(unixDomainPrincipalClass, componentField); + Object principal = accessor.invoke(unixDomainPrincipal); + + assertEquals(new PrincipalImpl(componentName), principal); + } + } + + /** Implementation of {@link UserPrincipal} and {@link GroupPrincipal} just for record tests. */ + public static class PrincipalImpl implements UserPrincipal, GroupPrincipal { + private final String name; + + public PrincipalImpl(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (o instanceof PrincipalImpl) { + return Objects.equals(name, ((PrincipalImpl) o).name); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/gson/src/test/java/com/google/gson/jf/ReadNullTest.java b/gson/src/test/java/com/google/gson/jf/ReadNullTest.java new file mode 100644 index 00000000..24dc008f --- /dev/null +++ b/gson/src/test/java/com/google/gson/jf/ReadNullTest.java @@ -0,0 +1,24 @@ +package com.google.gson.jf; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import junit.framework.TestCase; + +import java.io.*; + +public class ReadNullTest extends TestCase { + public void testReadNull() throws IOException { + Gson gson = new GsonBuilder().serializeNulls().setLenient().create(); + String exampleFile = "{\n // Yes\n \"value1\": 1024,\n \"value2\": null,\n \"value3\": 10\n}"; + try (Reader r = new StringReader(exampleFile); var jr = gson.newJsonReader(r)) { + jr.beginObject(); + assertEquals("value1", jr.nextName()); + assertEquals(1024, jr.nextInt()); + assertEquals("value2", jr.nextName()); + jr.nextNull(); + assertEquals("value3", jr.nextName()); + assertEquals(10, jr.nextInt()); + jr.endObject(); + } + } +} diff --git a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java index 61e69a38..e0ff0653 100644 --- a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java +++ b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java @@ -162,7 +162,7 @@ public class PerformanceTest extends TestCase { * 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 - public void disable_testByteArrayDeserialization() { + public void disabled_testByteArrayDeserialization() { for (int numElements = 10639296; true; numElements += 16384) { StringBuilder sb = new StringBuilder(numElements*2); sb.append("["); @@ -218,7 +218,7 @@ public class PerformanceTest extends TestCase { System.out.printf("Deserialize classes avg time: %d ms\n", avg); } - public void disable_testLargeObjectSerializationAndDeserialization() { + public void disabled_testLargeObjectSerializationAndDeserialization() { Map largeObject = new HashMap<>(); for (long l = 0; l < 100000; l++) { largeObject.put("field" + l, l); @@ -343,4 +343,4 @@ public class PerformanceTest extends TestCase { this.field = field; } } -} \ No newline at end of file +} diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java index ab802be1..3acc13d5 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -16,6 +16,10 @@ package com.google.gson.stream; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; + import com.google.gson.JsonElement; import com.google.gson.internal.Streams; import com.google.gson.internal.bind.JsonTreeReader; @@ -27,9 +31,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import static org.junit.Assert.assertEquals; -import static org.junit.Assume.assumeTrue; - @SuppressWarnings("resource") @RunWith(Parameterized.class) public class JsonReaderPathTest { @@ -221,12 +222,28 @@ public class JsonReaderPathTest { assertEquals("$[2]", reader.getPath()); } + @Test public void skipArrayEnd() throws IOException { + JsonReader reader = factory.create("[[],1]"); + reader.beginArray(); + reader.beginArray(); + assertEquals("$[0][0]", reader.getPreviousPath()); + assertEquals("$[0][0]", reader.getPath()); + // skip end of array + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals("$[0]", reader.getPreviousPath()); + assertEquals("$[1]", reader.getPath()); + } + @Test public void skipObjectNames() throws IOException { - JsonReader reader = factory.create("{\"a\":1}"); + JsonReader reader = factory.create("{\"a\":[]}"); reader.beginObject(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + + reader.beginArray(); + assertEquals("$.[0]", reader.getPreviousPath()); + assertEquals("$.[0]", reader.getPath()); } @Test public void skipObjectValues() throws IOException { @@ -236,13 +253,27 @@ public class JsonReaderPathTest { assertEquals("$.", reader.getPath()); reader.nextName(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); reader.nextName(); assertEquals("$.b", reader.getPreviousPath()); assertEquals("$.b", reader.getPath()); } + @Test public void skipObjectEnd() throws IOException { + JsonReader reader = factory.create("{\"a\":{},\"b\":2}"); + reader.beginObject(); + reader.nextName(); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + // skip end of object + assertEquals(JsonToken.END_OBJECT, reader.peek()); + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + } + @Test public void skipNestedStructures() throws IOException { JsonReader reader = factory.create("[[1,2,3],4]"); reader.beginArray(); @@ -251,6 +282,17 @@ public class JsonReaderPathTest { assertEquals("$[1]", reader.getPath()); } + @Test public void skipEndOfDocument() throws IOException { + JsonReader reader = factory.create("[]"); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + assertThrows("Attempt to skip led outside the document", IllegalStateException.class, reader::skipValue); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + @Test public void arrayOfObjects() throws IOException { JsonReader reader = factory.create("[{},{},{}]"); reader.beginArray(); @@ -307,6 +349,52 @@ public class JsonReaderPathTest { assertEquals("$", reader.getPath()); } + @Test public void objectOfObjects() throws IOException { + JsonReader reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}"); + reader.beginObject(); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.endObject(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.beginObject(); + assertEquals("$.b.", reader.getPreviousPath()); + assertEquals("$.b.", reader.getPath()); + reader.nextName(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.nextInt(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.endObject(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + public enum Factory { STRING_READER { @Override public JsonReader create(String data) { diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index 17ce4fdf..8aa0bd01 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -16,13 +16,6 @@ package com.google.gson.stream; -import java.io.EOFException; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.Arrays; -import junit.framework.TestCase; - import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; import static com.google.gson.stream.JsonToken.BOOLEAN; @@ -32,6 +25,14 @@ 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 java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Arrays; +import junit.framework.TestCase; @SuppressWarnings("resource") public final class JsonReaderTest extends TestCase { @@ -140,6 +141,35 @@ public final class JsonReaderTest extends TestCase { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipObjectName() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\": 1}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + + public void testSkipObjectNameSingleQuoted() throws IOException { + JsonReader reader = new JsonReader(reader("{'a': 1}")); + reader.setLenient(true); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + + public void testSkipObjectNameUnquoted() throws IOException { + JsonReader reader = new JsonReader(reader("{a: 1}")); + reader.setLenient(true); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + public void testSkipInteger() throws IOException { JsonReader reader = new JsonReader(reader( "{\"a\":123456789,\"b\":-123456789}")); @@ -164,6 +194,34 @@ public final class JsonReaderTest extends TestCase { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipValueAfterEndOfDocument() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + assertThrows("Attempt to skip led outside the document", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtArrayEnd() throws IOException { + JsonReader reader = new JsonReader(reader("[]")); + reader.beginArray(); + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtObjectEnd() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + assertThrows("Attempt to skip led outside its parent", IllegalStateException.class, reader::skipValue); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + public void testHelloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" + diff --git a/metrics/pom.xml b/metrics/pom.xml index 078fa938..9df1c9e4 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ io.gitlab.jfronny gson-parent - 2.9.2-SNAPSHOT + 2.11-SNAPSHOT gson-metrics @@ -32,7 +32,7 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4 + 2.13.4.2 com.google.caliper diff --git a/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java b/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java index dad0d99a..738b5ae4 100644 --- a/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java +++ b/metrics/src/main/java/com/google/gson/metrics/CollectionsDeserializationBenchmark.java @@ -33,14 +33,15 @@ import java.util.List; */ public class CollectionsDeserializationBenchmark { - private static final Type LIST_TYPE = new TypeToken>(){}.getType(); + private static final TypeToken> LIST_TYPE_TOKEN = new TypeToken>(){}; + private static final Type LIST_TYPE = LIST_TYPE_TOKEN.getType(); private Gson gson; private String json; public static void main(String[] args) { NonUploadingCaliperRunner.run(CollectionsDeserializationBenchmark.class, args); } - + @BeforeExperiment void setUp() throws Exception { this.gson = new Gson(); @@ -51,12 +52,12 @@ public class CollectionsDeserializationBenchmark { this.json = gson.toJson(bags, LIST_TYPE); } - /** + /** * Benchmark to measure Gson performance for deserializing an object */ public void timeCollectionsDefault(int reps) { for (int i=0; i() {}, new TypeReference() {}), READER_LONG(new TypeToken() {}, new TypeReference() {}); - private final Type gsonType; + private final TypeToken gsonType; private final TypeReference jacksonType; private Document(TypeToken typeToken, TypeReference typeReference) { - this.gsonType = typeToken.getType(); + this.gsonType = typeToken; this.jacksonType = typeReference; } } diff --git a/pom.xml b/pom.xml index 765676e4..48571084 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.gitlab.jfronny gson-parent - 2.9.2-SNAPSHOT + 2.11-SNAPSHOT pom Gson Parent @@ -31,6 +31,14 @@ HEAD + + + google + Google + https://www.google.com + + + GitHub Issues https://github.com/google/gson/issues @@ -79,7 +87,6 @@ maven-compiler-plugin 3.10.1 - ${javaVersion} true true false @@ -101,6 +108,9 @@ [11,) + + 11 all,-missing false release + + + + clean verify + antrun:run@replace-version-placeholders + antrun:run@replace-old-version-references + antrun:run@git-add-changed + + + maven-antrun-plugin + 3.1.0 + + + + replace-version-placeholders + + run + + + + + + + + + + + + + replace-old-version-references + + run + + + + + + + + + + + + + + + false + + + + + git-add-changed + + run + + + + + + + + + + + + + + com.github.siom79.japicmp diff --git a/proto/pom.xml b/proto/pom.xml index ee507f9e..82d771fd 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ io.gitlab.jfronny gson-parent - 2.9.2-SNAPSHOT + 2.11-SNAPSHOT proto diff --git a/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java b/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java index 3136c58b..9aa166fc 100644 --- a/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java +++ b/proto/src/main/java/com/google/gson/protobuf/ProtoTypeAdapter.java @@ -64,7 +64,6 @@ import java.util.concurrent.ConcurrentMap; * string os_build_id = 1 [(serialized_name) = "osBuildID"]; * } * - *

    * * @author Inderjeet Singh * @author Emmanuel Cron