From 6b9db2e44948d410b2dbede6a4a8667782d6c04b Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 19 Sep 2022 15:47:11 +0200 Subject: [PATCH 01/35] Add Gson.fromJson(..., TypeToken) overloads (#1700) * Add Gson.fromJson(..., TypeToken) overloads Previously only Gson.fromJson(..., Type) existed which is however not type-safe since the generic type parameter T used for the return type is not bound. Since these methods are often used in the form gson.fromJson(..., new TypeToken<...>(){}.getType()) this commit now adds overloads which accept a TypeToken and are therefore more type-safe. Additional changes: - Fixed some grammar mistakes - Added javadoc @see tags - Consistently write "JSON" in uppercase - More precise placement of @SuppressWarnings("unchecked") * Add to Gson.fromJson javadoc that JSON is fully consumed The newly added documentation deliberately does not state which exception is thrown because Gson.assertFullConsumption could throw either a JsonIOException or a JsonSyntaxException. * Remove unnecessary wrapping and unwrapping as TypeToken in Gson.fromJson Since the actual implementation of Gson.fromJson is TypeToken based, the TypeToken variant overloads are now the "main" implementation and the other overloads delegate to them. Previously the Type variant overloads were the "main" implementation which caused `TypeToken.getType()` followed by `TypeToken.get(...)` when the TypeToken variant overloads were used. * Trim source code whitespaces * Fix Gson.fromJson(JsonReader, Class) not casting read Object To be consistent with the other Gson.fromJson(..., Class) overloads the method should cast the result. * Replace User Guide link in Gson documentation * Remove more references to fromJson(..., Type) * Extend documentation for fromJson(JsonReader, ...) * Replace some TypeToken.getType() usages * Address feedback; improve documentation * Remove fromJson(JsonReader, Class) again As noticed during review adding this method is source incompatible. --- UserGuide.md | 4 +- gson/src/main/java/com/google/gson/Gson.java | 360 +++++++++++++----- .../com/google/gson/reflect/TypeToken.java | 20 +- .../java/com/google/gson/MixedStreamTest.java | 2 +- .../functional/ParameterizedTypesTest.java | 76 +++- .../CollectionsDeserializationBenchmark.java | 9 +- .../google/gson/metrics/ParseBenchmark.java | 4 +- 7 files changed, 364 insertions(+), 111 deletions(-) diff --git a/UserGuide.md b/UserGuide.md index 12b53351..2aafb067 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -225,7 +225,7 @@ 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>(){}; Collection ints2 = gson.fromJson(json, collectionType); // ==> ints2 is same as ints ``` @@ -263,7 +263,7 @@ 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 diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 168bc6f1..c3262a6f 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -76,26 +76,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

@@ -125,7 +131,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 @@ -210,9 +216,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 @@ -644,13 +650,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) { @@ -674,6 +682,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(); @@ -682,17 +692,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) { @@ -703,7 +716,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. * @@ -714,7 +727,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(); @@ -723,18 +739,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) { @@ -746,8 +766,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 @@ -756,9 +777,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 { @@ -824,7 +848,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 */ @@ -913,17 +937,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 @@ -932,98 +956,165 @@ 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) */ - 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) */ - 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; } @@ -1040,10 +1131,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. @@ -1052,19 +1151,58 @@ 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) + */ + public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; boolean oldLenient = reader.isLenient(); reader.setLenient(true); 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) { @@ -1091,52 +1229,86 @@ 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) */ - @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 { 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/test/java/com/google/gson/MixedStreamTest.java b/gson/src/test/java/com/google/gson/MixedStreamTest.java index 00eb4bc8..fa16659f 100644 --- a/gson/src/test/java/com/google/gson/MixedStreamTest.java +++ b/gson/src/test/java/com/google/gson/MixedStreamTest.java @@ -174,7 +174,7 @@ public final class MixedStreamTest extends TestCase { } catch (NullPointerException expected) { } try { - gson.fromJson(new JsonReader(new StringReader("true")), null); + gson.fromJson(new JsonReader(new StringReader("true")), (Type) null); fail(); } catch (NullPointerException expected) { } diff --git a/gson/src/test/java/com/google/gson/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/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; } } From 2154e468b490c8a6576db13eca31c9d1afe6bf78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Sep 2022 07:07:43 -0700 Subject: [PATCH 02/35] Bump maven-jar-plugin from 3.2.2 to 3.3.0 (#2195) Bumps [maven-jar-plugin](https://github.com/apache/maven-jar-plugin) from 3.2.2 to 3.3.0. - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.2.2...maven-jar-plugin-3.3.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index be79d03a..3b078351 100644 --- a/pom.xml +++ b/pom.xml @@ -113,7 +113,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.2.2 + 3.3.0 org.apache.maven.plugins From 0864a02e867633cba2daa8ace94eb3a72cc880de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Fri, 23 Sep 2022 14:33:28 -0700 Subject: [PATCH 03/35] Build on JDK 17 as well as 11. (#2198) * Build on JDK 8 and 17 as well as 11. * Remove JDK 8 for now. `DefaultDateTypeAdapterTest` fails. * Tweak javadoc to avoid warnings. Mostly these are about using `

    ` when the previous tag was `

    `, and the like. This previous tag might be implicit (part of what javadoc itself outputs rather than the HTML in doc comments). Apparently JDK 11 puts method javadoc inside `

    ` while JDK 11 puts it inside `

    `. Or something like that. Anyway it doesn't appear to be possible to use `

    ` _or_ `

    ` and please both. --- .github/workflows/build.yml | 8 ++++++-- .../gson/typeadapters/RuntimeTypeAdapterFactory.java | 2 +- gson/src/main/java/com/google/gson/GsonBuilder.java | 10 ++++++---- gson/src/main/java/com/google/gson/TypeAdapter.java | 2 +- .../main/java/com/google/gson/TypeAdapterFactory.java | 2 +- .../main/java/com/google/gson/stream/JsonReader.java | 2 +- .../main/java/com/google/gson/stream/JsonWriter.java | 2 +- .../gson/internal/bind/DefaultDateTypeAdapterTest.java | 6 ++++-- .../com/google/gson/protobuf/ProtoTypeAdapter.java | 1 - 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1677a18..b387983e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,15 +4,19 @@ 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 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..109fc384 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
    diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
    index 8332ccb3..64b91f62 100644
    --- a/gson/src/main/java/com/google/gson/GsonBuilder.java
    +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
    @@ -221,8 +221,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}. @@ -246,8 +247,9 @@ 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}}. diff --git a/gson/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java index 37f22b8e..0c10e222 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 diff --git a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java index c12429e9..60f7b7e2 100644 --- a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java @@ -22,7 +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. * - *

    Example: Converting enums to lowercase

    + *

    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 * that they're defined in {@code CONSTANT_CASE} in the corresponding Java 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 a468d7ed..06fd3baf 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}. * 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 6e978132..7f1ab0ea 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -42,7 +42,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: diff --git a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java index 3d1ec7f7..c20a3683 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/DefaultDateTypeAdapterTest.java @@ -22,14 +22,12 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; - import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.JavaVersion; import com.google.gson.internal.bind.DefaultDateTypeAdapter.DateType; import com.google.gson.reflect.TypeToken; - import junit.framework.TestCase; /** @@ -76,6 +74,10 @@ public class DefaultDateTypeAdapterTest extends TestCase { } public void testParsingDatesFormattedWithSystemLocale() throws Exception { + // TODO(eamonnmcmanus): fix this test, which fails on JDK 8 and 17 + if (JavaVersion.getMajorJavaVersion() != 11) { + return; + } TimeZone defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("UTC")); Locale defaultLocale = Locale.getDefault(); diff --git a/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 From 2591ede59bf7e4cef496a01e82282c88e256cec8 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 25 Sep 2022 02:02:11 +0200 Subject: [PATCH 04/35] Fix incorrect HTML headings in Javadoc (#2200) --- .../gson/typeadapters/RuntimeTypeAdapterFactory.java | 8 ++++---- gson/src/main/java/com/google/gson/TypeAdapter.java | 2 +- .../src/main/java/com/google/gson/TypeAdapterFactory.java | 5 +++-- .../google/gson/internal/bind/MapTypeAdapterFactory.java | 4 ++-- gson/src/main/java/com/google/gson/stream/JsonReader.java | 6 +++--- gson/src/main/java/com/google/gson/stream/JsonWriter.java | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) 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 109fc384..87b522f0 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -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/src/main/java/com/google/gson/TypeAdapter.java b/gson/src/main/java/com/google/gson/TypeAdapter.java
    index 0c10e222..5685cadb 100644
    --- a/gson/src/main/java/com/google/gson/TypeAdapter.java
    +++ b/gson/src/main/java/com/google/gson/TypeAdapter.java
    @@ -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
    diff --git a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    index 60f7b7e2..75fdddbf 100644
    --- a/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    +++ b/gson/src/main/java/com/google/gson/TypeAdapterFactory.java
    @@ -22,7 +22,8 @@ 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.
      *
    - * 

    Example: Converting enums to lowercase

    + *

    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 * that they're defined in {@code CONSTANT_CASE} in the corresponding Java @@ -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/internal/bind/MapTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java index f7c5a554..68ecffb9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/MapTypeAdapterFactory.java @@ -40,7 +40,7 @@ import java.util.Map; /** * Adapts maps to either JSON objects or JSON arrays. * - *

    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/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 06fd3baf..925c50f7 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -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 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 7f1ab0ea..d828d468 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -58,7 +58,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
      * [
      *   {
    
    From 441406cc780fc5384832b48e382a7c899bac21af Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C3=89amonn=20McManus?= 
    Date: Tue, 27 Sep 2022 12:32:40 -0700
    Subject: [PATCH 05/35] Add a currently-failing test for Java records and
     `@Ignore` it. (#2203)
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    * Add a currently-failing test for Java records and `@Ignore` it.
    
    Also do the Maven gymastics required to ensure that this test only runs on Java
    versions ≥17. (It would also work on Java 16, but 17 is all we have in the CI.)
    
    Fix some compilation problems I saw when running locally, which for some reason
    don't show up in the CI.
    
    * Suppress some new lint options that trigger `-Werror`.
    We may fix these later. (Every test will need an explicit constructor!)
    
    * Select Java version with maven.compiler.release and maven.compiler.testRelease.
    
    Use `assumeNotNull` rather than an if-statement.
    
    * Specify 11 for javadoc.
    
    * Restore the @see for AccessibleObject.
    ---
     gson/pom.xml                                  | 29 ++++++++
     .../gson/functional/Java17RecordTest.java     | 70 +++++++++++++++++++
     .../ReflectionAccessFilterTest.java           | 19 +++--
     .../gson/functional/ReflectionAccessTest.java |  1 +
     pom.xml                                       |  7 +-
     5 files changed, 119 insertions(+), 7 deletions(-)
     create mode 100644 gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
    
    diff --git a/gson/pom.xml b/gson/pom.xml
    index 3eddb245..e53251e9 100644
    --- a/gson/pom.xml
    +++ b/gson/pom.xml
    @@ -17,6 +17,10 @@
         
       
     
    +  
    +    **/Java17*
    +  
    +
       
         
           junit
    @@ -60,6 +64,18 @@
                   
                 
               
    +          
    +            default-testCompile
    +            test-compile
    +            
    +              testCompile
    +            
    +            
    +              
    +                ${excludeTestCompilation}
    +              
    +            
    +          
             
           
           
    @@ -222,4 +238,17 @@
           
         
       
    +  
    +    
    +      JDK17
    +      
    +        [17,)
    +      
    +      
    +        17
    +        
    +        ,-exports,-missing-explicit-ctor,-removal
    +      
    +    
    +  
     
    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..345da29e
    --- /dev/null
    +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java
    @@ -0,0 +1,70 @@
    +/*
    + * 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.assertThrows;
    +
    +import com.google.gson.Gson;
    +import com.google.gson.annotations.SerializedName;
    +import java.util.Objects;
    +import org.junit.Ignore;
    +import org.junit.Test;
    +import org.junit.runner.RunWith;
    +import org.junit.runners.JUnit4;
    +
    +@RunWith(JUnit4.class)
    +@Ignore
    +public final class Java17RecordTest {
    +  private final Gson gson = new Gson();
    +
    +  @Test
    +  public void testFirstNameIsChosenForSerialization() {
    +    MyRecord target = new MyRecord("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'}", MyRecord.class).a);
    +
    +    // Both name1 and name2 gets deserialized to b
    +    assertEquals("v11", gson.fromJson("{'name1':'v11'}", MyRecord.class).b);
    +    assertEquals("v2", gson.fromJson("{'name2':'v2'}", MyRecord.class).b);
    +    assertEquals("v3", gson.fromJson("{'name3':'v3'}", MyRecord.class).b);
    +  }
    +
    +  @Test
    +  public void testMultipleNamesInTheSameString() {
    +    // The last value takes precedence
    +    assertEquals("v3", gson.fromJson("{'name1':'v1','name2':'v2','name3':'v3'}", MyRecord.class).b);
    +  }
    +
    +  @Test
    +  public void testConstructorRuns() {
    +    assertThrows(NullPointerException.class,
    +        () -> gson.fromJson("{'name1': null, 'name2': null", MyRecord.class));
    +  }
    +
    +  private static record MyRecord(
    +      @SerializedName("name") String a,
    +      @SerializedName(value = "name1", alternate = {"name2", "name3"}) String b) {
    +    MyRecord {
    +      Objects.requireNonNull(a);
    +    }
    +  }
    +}
    diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
    index 775baf9f..8814b377 100644
    --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java
    @@ -2,6 +2,7 @@ package com.google.gson.functional;
     
     import static org.junit.Assert.assertEquals;
     import static org.junit.Assert.fail;
    +import static org.junit.Assume.assumeNotNull;
     
     import com.google.gson.Gson;
     import com.google.gson.GsonBuilder;
    @@ -17,10 +18,10 @@ import com.google.gson.TypeAdapter;
     import com.google.gson.internal.ConstructorConstructor;
     import com.google.gson.stream.JsonReader;
     import com.google.gson.stream.JsonWriter;
    -import java.awt.Point;
     import java.io.File;
     import java.io.IOException;
     import java.io.Reader;
    +import java.lang.reflect.Constructor;
     import java.lang.reflect.Type;
     import java.util.LinkedList;
     import java.util.List;
    @@ -40,7 +41,7 @@ public class ReflectionAccessFilterTest {
       }
     
       @Test
    -  public void testBlockInaccessibleJava() {
    +  public void testBlockInaccessibleJava() throws ReflectiveOperationException {
         Gson gson = new GsonBuilder()
           .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA)
           .create();
    @@ -58,8 +59,18 @@ public class ReflectionAccessFilterTest {
           );
         }
     
    -    // But serialization should succeed for classes with only public fields
    -    String json = gson.toJson(new Point(1, 2));
    +
    +    // But serialization should succeed for classes with only public fields.
    +    // Not many JDK classes have mutable public fields, thank goodness, but java.awt.Point does.
    +    Class pointClass = null;
    +    try {
    +      pointClass = Class.forName("java.awt.Point");
    +    } catch (ClassNotFoundException e) {
    +    }
    +    assumeNotNull(pointClass);
    +    Constructor pointConstructor = pointClass.getConstructor(int.class, int.class);
    +    Object point = pointConstructor.newInstance(1, 2);
    +    String json = gson.toJson(point);
         assertEquals("{\"x\":1,\"y\":2}", json);
       }
     
    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 7b7dc303..71e293e8 100644
    --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java
    @@ -37,6 +37,7 @@ public class ReflectionAccessTest {
       }
     
       @Test
    +  @SuppressWarnings("removal") // java.lang.SecurityManager
       public void testRestrictiveSecurityManager() throws Exception {
         // Must use separate class loader, otherwise permission is not checked, see Class.getDeclaredFields()
         Class clazz = loadClassWithDifferentClassLoader(ClassWithPrivateMembers.class);
    diff --git a/pom.xml b/pom.xml
    index 3b078351..ecb68860 100644
    --- a/pom.xml
    +++ b/pom.xml
    @@ -21,7 +21,8 @@
     
       
         UTF-8
    -    7
    +    7
    +    
       
     
       
    @@ -70,14 +71,13 @@
               maven-compiler-plugin
               3.10.1
               
    -            ${javaVersion}
                 true
                 true
                 true
                 
                   
    -              -Xlint:all,-options
    +              -Xlint:all,-options${extraLintSuppressions}
                 
                 
                   [11,)
    @@ -92,6 +92,7 @@
                 
                   [11,)
                 
    +            11
                 
                 all,-missing
                 
    -              -Xlint:all,-options${extraLintSuppressions}
    +              -Xlint:all,-options
                 
                 
                   [11,)
    @@ -92,6 +91,8 @@
                 
                   [11,)
                 
    +            
                 11
                 
                 all,-missing
    
    From b777ae5216c6fde9ab7bca8d6cfca630ffe12b4f Mon Sep 17 00:00:00 2001
    From: Marcono1234 
    Date: Wed, 28 Sep 2022 08:05:49 +0200
    Subject: [PATCH 07/35] Disable Maven transfer progress for GitHub workflows
     (#2206)
    
    Disables the download transfer progress which is shown when Maven downloads
    (or uploads) artifacts which are not available in the local repository.
    This download progress can be quite verbose and is normally not that relevant.
    ---
     .github/workflows/build.yml                   | 2 +-
     .github/workflows/check-api-compatibility.yml | 6 +++---
     .github/workflows/codeql-analysis.yml         | 2 +-
     3 files changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
    index b387983e..ef1b23d0 100644
    --- a/.github/workflows/build.yml
    +++ b/.github/workflows/build.yml
    @@ -20,4 +20,4 @@ jobs:
               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
    
    From 9c9cafcf9d732206171da0e5da549c97476a2aa2 Mon Sep 17 00:00:00 2001
    From: kb1000 
    Date: Fri, 30 Sep 2022 00:59:47 +0200
    Subject: [PATCH 08/35] Only create one UnsafeAllocator instance (#2196)
    
    * Only create one UnsafeAllocator instance
    
    * Move checkInstantiable to ConstructorConstructor
    ---
     .../gson/internal/ConstructorConstructor.java | 24 ++++++++++++++---
     .../google/gson/internal/UnsafeAllocator.java | 26 +++----------------
     .../UnsafeAllocatorInstantiationTest.java     |  9 +++----
     3 files changed, 28 insertions(+), 31 deletions(-)
    
    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..da330c30 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() {
    @@ -342,11 +361,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/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/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);
       }
     }
    
    From ea7ab7cd520199fdb4a8d47fe462a4a0357a58bb Mon Sep 17 00:00:00 2001
    From: Marcono1234 
    Date: Fri, 30 Sep 2022 16:57:02 +0200
    Subject: [PATCH 09/35] Mention in User Guide alternative for Gson versions
     without fromJson(..., TypeToken) (#2209)
    
    ---
     UserGuide.md | 4 ++++
     1 file changed, 4 insertions(+)
    
    diff --git a/UserGuide.md b/UserGuide.md
    index 2aafb067..49ac54d4 100644
    --- a/UserGuide.md
    +++ b/UserGuide.md
    @@ -226,6 +226,8 @@ String json = gson.toJson(ints);  // ==> json is [1,2,3,4,5]
     
     // Deserialization
     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
     ```
    @@ -267,6 +269,8 @@ 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}
     ```
    
    From 47668fad57e23dacc5482e40a276a93850cec2cf Mon Sep 17 00:00:00 2001
    From: BillyGalbreath 
    Date: Sat, 1 Oct 2022 17:57:16 -0500
    Subject: [PATCH 10/35] Fix typo in GsonBuilder Javadoc (#2213)
    
    ---
     gson/src/main/java/com/google/gson/GsonBuilder.java | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
    index 64b91f62..94f2138f 100644
    --- a/gson/src/main/java/com/google/gson/GsonBuilder.java
    +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
    @@ -252,7 +252,7 @@ public final class GsonBuilder {
        * 

    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). From 28609089faa747f2ad5730281c14093ab40d6fda Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 2 Oct 2022 00:58:26 +0200 Subject: [PATCH 11/35] Add Javadoc since tags for previously added elements (#2211) --- .../main/java/com/google/gson/FieldNamingPolicy.java | 10 +++++++--- gson/src/main/java/com/google/gson/Gson.java | 1 + gson/src/main/java/com/google/gson/GsonBuilder.java | 4 ++++ gson/src/main/java/com/google/gson/JsonArray.java | 6 ++++++ gson/src/main/java/com/google/gson/JsonObject.java | 1 + gson/src/main/java/com/google/gson/JsonParser.java | 3 +++ .../java/com/google/gson/ReflectionAccessFilter.java | 6 ++++-- gson/src/main/java/com/google/gson/ToNumberPolicy.java | 6 +++--- .../main/java/com/google/gson/ToNumberStrategy.java | 4 ++-- .../main/java/com/google/gson/stream/JsonWriter.java | 3 +++ 10 files changed, 34 insertions(+), 10 deletions(-) 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 c3262a6f..afdfc31f 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -343,6 +343,7 @@ public final class Gson { * instance. * * @return a GsonBuilder instance. + * @since 2.8.3 */ public GsonBuilder newBuilder() { return new GsonBuilder(this); diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 94f2138f..38c7e909 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -364,6 +364,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); @@ -376,6 +377,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); @@ -682,6 +684,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; @@ -702,6 +705,7 @@ public final class GsonBuilder { * * @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..5a0c77e8 100644 --- a/gson/src/main/java/com/google/gson/JsonArray.java +++ b/gson/src/main/java/com/google/gson/JsonArray.java @@ -48,6 +48,7 @@ public final class JsonArray extends JsonElement implements Iterable Date: Mon, 3 Oct 2022 01:38:43 +0200 Subject: [PATCH 12/35] Improve versioning support documentation and validate version (#2214) --- .../java/com/google/gson/GsonBuilder.java | 23 ++++-- .../com/google/gson/annotations/Since.java | 12 +-- .../com/google/gson/annotations/Until.java | 18 +++-- .../com/google/gson/internal/Excluder.java | 8 +- .../java/com/google/gson/GsonBuilderTest.java | 33 +++++++- .../gson/VersionExclusionStrategyTest.java | 70 ++++++++++++---- .../gson/functional/VersioningTest.java | 80 +++++++++++++------ 7 files changed, 180 insertions(+), 64 deletions(-) diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 38c7e909..50757b3b 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -28,6 +28,8 @@ import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS; import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES; import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE; +import com.google.gson.annotations.Since; +import com.google.gson.annotations.Until; import com.google.gson.internal.$Gson$Preconditions; import com.google.gson.internal.Excluder; import com.google.gson.internal.bind.DefaultDateTypeAdapter; @@ -143,14 +145,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; } 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/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/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/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/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);
    
    From 3e3266cf48f132928225e1561a6ae4cb5503d08f Mon Sep 17 00:00:00 2001
    From: Marcono1234 
    Date: Tue, 4 Oct 2022 16:22:26 +0200
    Subject: [PATCH 13/35] Perform numeric conversion for primitive numeric type
     adapters (#2158)
    
    * Perform numeric conversion for primitive numeric type adapters
    
    This should probably not be visible to the user unless they use the
    non-typesafe `Gson.toJson(Object, Type)` where unrelated number types can
    be used, or when malformed generic containers are used. For example a
    `List` containing a Float.
    
    This change also has the advantage of avoiding `JsonWriter.value(Number)`
    for primitive type adapters. That method has some overhead because it needs
    to make sure that the value is a valid JSON number. However, for primitive
    numbers this check is redundant.
    
    * Don't call `JsonWriter.value(float)` for backward compatibility
    
    * Fix typo in comments
    ---
     gson/src/main/java/com/google/gson/Gson.java  |  7 ++-
     .../gson/internal/bind/TypeAdapters.java      | 39 ++++++++++--
     .../google/gson/functional/PrimitiveTest.java | 60 +++++++++++++++++++
     3 files changed, 98 insertions(+), 8 deletions(-)
    
    diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
    index afdfc31f..bb3e2c77 100644
    --- a/gson/src/main/java/com/google/gson/Gson.java
    +++ b/gson/src/main/java/com/google/gson/Gson.java
    @@ -406,7 +406,7 @@ public final class Gson {
             }
             double doubleValue = value.doubleValue();
             checkValidFloatingPoint(doubleValue);
    -        out.value(value);
    +        out.value(doubleValue);
           }
         };
       }
    @@ -430,7 +430,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);
           }
         };
       }
    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/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
    index 6d74cc29..c4c25f00 100644
    --- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
    +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java
    @@ -64,6 +64,11 @@ public class PrimitiveTest extends TestCase {
       public void testByteSerialization() {
         assertEquals("1", gson.toJson(1, byte.class));
         assertEquals("1", gson.toJson(1, Byte.class));
    +    assertEquals(Byte.toString(Byte.MIN_VALUE), gson.toJson(Byte.MIN_VALUE, Byte.class));
    +    assertEquals(Byte.toString(Byte.MAX_VALUE), gson.toJson(Byte.MAX_VALUE, Byte.class));
    +    // Should perform narrowing conversion
    +    assertEquals("-128", gson.toJson(128, Byte.class));
    +    assertEquals("1", gson.toJson(1.5, Byte.class));
       }
     
       public void testByteDeserialization() {
    @@ -102,6 +107,13 @@ public class PrimitiveTest extends TestCase {
       public void testShortSerialization() {
         assertEquals("1", gson.toJson(1, short.class));
         assertEquals("1", gson.toJson(1, Short.class));
    +    assertEquals(Short.toString(Short.MIN_VALUE), gson.toJson(Short.MIN_VALUE, Short.class));
    +    assertEquals(Short.toString(Short.MAX_VALUE), gson.toJson(Short.MAX_VALUE, Short.class));
    +    // Should perform widening conversion
    +    assertEquals("1", gson.toJson((byte) 1, Short.class));
    +    // Should perform narrowing conversion
    +    assertEquals("-32768", gson.toJson(32768, Short.class));
    +    assertEquals("1", gson.toJson(1.5, Short.class));
       }
     
       public void testShortDeserialization() {
    @@ -137,6 +149,54 @@ public class PrimitiveTest extends TestCase {
         }
       }
     
    +  public void testIntSerialization() {
    +    assertEquals("1", gson.toJson(1, int.class));
    +    assertEquals("1", gson.toJson(1, Integer.class));
    +    assertEquals(Integer.toString(Integer.MIN_VALUE), gson.toJson(Integer.MIN_VALUE, Integer.class));
    +    assertEquals(Integer.toString(Integer.MAX_VALUE), gson.toJson(Integer.MAX_VALUE, Integer.class));
    +    // Should perform widening conversion
    +    assertEquals("1", gson.toJson((byte) 1, Integer.class));
    +    // Should perform narrowing conversion
    +    assertEquals("-2147483648", gson.toJson(2147483648L, Integer.class));
    +    assertEquals("1", gson.toJson(1.5, Integer.class));
    +  }
    +
    +  public void testLongSerialization() {
    +    assertEquals("1", gson.toJson(1L, long.class));
    +    assertEquals("1", gson.toJson(1L, Long.class));
    +    assertEquals(Long.toString(Long.MIN_VALUE), gson.toJson(Long.MIN_VALUE, Long.class));
    +    assertEquals(Long.toString(Long.MAX_VALUE), gson.toJson(Long.MAX_VALUE, Long.class));
    +    // Should perform widening conversion
    +    assertEquals("1", gson.toJson((byte) 1, Long.class));
    +    // Should perform narrowing conversion
    +    assertEquals("1", gson.toJson(1.5, Long.class));
    +  }
    +
    +  public void testFloatSerialization() {
    +    assertEquals("1.5", gson.toJson(1.5f, float.class));
    +    assertEquals("1.5", gson.toJson(1.5f, Float.class));
    +    assertEquals(Float.toString(Float.MIN_VALUE), gson.toJson(Float.MIN_VALUE, Float.class));
    +    assertEquals(Float.toString(Float.MAX_VALUE), gson.toJson(Float.MAX_VALUE, Float.class));
    +    // Should perform widening conversion
    +    assertEquals("1.0", gson.toJson((byte) 1, Float.class));
    +    // (This widening conversion is actually lossy)
    +    assertEquals(Float.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Float.class));
    +    // Should perform narrowing conversion
    +    gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
    +    assertEquals("Infinity", gson.toJson(Double.MAX_VALUE, Float.class));
    +  }
    +
    +  public void testDoubleSerialization() {
    +    assertEquals("1.5", gson.toJson(1.5, double.class));
    +    assertEquals("1.5", gson.toJson(1.5, Double.class));
    +    assertEquals(Double.toString(Double.MIN_VALUE), gson.toJson(Double.MIN_VALUE, Double.class));
    +    assertEquals(Double.toString(Double.MAX_VALUE), gson.toJson(Double.MAX_VALUE, Double.class));
    +    // Should perform widening conversion
    +    assertEquals("1.0", gson.toJson((byte) 1, Double.class));
    +    // (This widening conversion is actually lossy)
    +    assertEquals(Double.toString(Long.MAX_VALUE - 10L), gson.toJson(Long.MAX_VALUE - 10L, Double.class));
    +  }
    +
       public void testPrimitiveIntegerAutoboxedInASingleElementArraySerialization() {
         int target[] = {-9332};
         assertEquals("[-9332]", gson.toJson(target));
    
    From e614e71ee43ca7bc1cb466bd1eaf4d85499900d9 Mon Sep 17 00:00:00 2001
    From: Marcono1234 
    Date: Tue, 4 Oct 2022 18:01:55 +0200
    Subject: [PATCH 14/35] Clarify in documentation that Gson might cache strategy
     results (#2215)
    
    * Clarify in documentation that Gson might cache strategy results
    
    * Improve wording; mention that adapter factory results are cached as well
    ---
     .../main/java/com/google/gson/GsonBuilder.java   | 16 ++++++++++++++++
     .../com/google/gson/annotations/JsonAdapter.java | 14 ++++++++++----
     2 files changed, 26 insertions(+), 4 deletions(-)
    
    diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
    index 50757b3b..8b04430f 100644
    --- a/gson/src/main/java/com/google/gson/GsonBuilder.java
    +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
    @@ -362,6 +362,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 @@ -415,6 +419,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 @@ -620,6 +628,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) { @@ -716,6 +728,10 @@ 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 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 * From 5269701679295d8143ec578e25f68fe25594f46a Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 10 Oct 2022 16:51:36 +0200 Subject: [PATCH 15/35] Improve `JsonReader.skipValue()` (#2062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix JsonReader.skipValue() not behaving properly at end of document JsonReader implementation erroneously reset `peeked` to PEEKED_NONE; JsonTreeReader threw ArrayIndexOutOfBoundsException. * Fix JsonReader.skipValue() not behaving properly at end of array and object For JsonReader this caused an IllegalStateException (in the past it caused JsonReader to get stuck in an infinite loop); for JsonTreeReader it only popped the empty iterator but not the JsonArray or JsonObject, which caused peek() to again report END_ARRAY or END_OBJECT. * Only have JsonReader.skipValue() overwrite path name when name was skipped This improves the JSON path when the value for a property was skipped and before the subsequent property (or the end of the object) getPath() is called. * Address feedback; improve test coverage Co-authored-by: Éamonn McManus --- .../gson/internal/bind/JsonTreeReader.java | 40 ++++-- .../com/google/gson/stream/JsonReader.java | 118 ++++++++++++------ .../internal/bind/JsonTreeReaderTest.java | 41 ++++++ .../gson/stream/JsonReaderPathTest.java | 103 +++++++++++++-- .../google/gson/stream/JsonReaderTest.java | 71 +++++++++-- 5 files changed, 309 insertions(+), 64 deletions(-) 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..e47c57c7 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: + nextName(true); + break; + case END_ARRAY: + endArray(); + break; + case END_OBJECT: + endObject(); + break; + case END_DOCUMENT: + // Do nothing + break; + default: + popStack(); + if (stackSize > 0) { + pathIndices[stackSize - 1]++; + } + break; } } diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 925c50f7..ed6bab97 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -777,10 +777,9 @@ public class JsonReader implements Closeable { } /** - * Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and - * consumes it. + * Returns the next token, a {@link JsonToken#NAME property name}, and consumes it. * - * @throws java.io.IOException if the next token in the stream is not a property + * @throws IOException if the next token in the stream is not a property * name. */ public String nextName() throws IOException { @@ -804,7 +803,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#STRING string} value of the next token, + * Returns the {@link JsonToken#STRING string} value of the next token, * consuming it. If the next token is a number, this method will return its * string form. * @@ -840,7 +839,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#BOOLEAN boolean} value of the next token, + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, * consuming it. * * @throws IllegalStateException if the next token is not a boolean or if @@ -884,7 +883,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 +929,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER long} value of the next token, + * Returns the {@link JsonToken#NUMBER long} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a long. If the next token's numeric value cannot be exactly * represented by a Java {@code long}, this method throws. @@ -1163,7 +1162,7 @@ public class JsonReader implements Closeable { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER int} value of the next token, + * Returns the {@link JsonToken#NUMBER int} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as an int. If the next token's numeric value cannot be exactly * represented by a Java {@code int}, this method throws. @@ -1223,7 +1222,7 @@ public class JsonReader implements Closeable { } /** - * Closes this JSON reader and the underlying {@link java.io.Reader}. + * Closes this JSON reader and the underlying {@link Reader}. */ @Override public void close() throws IOException { peeked = PEEKED_NONE; @@ -1233,9 +1232,19 @@ public class JsonReader implements Closeable { } /** - * Skips the next value recursively. If it is an object or array, all nested - * elements are skipped. This method is intended for use when the JSON token - * stream contains unrecognized or unhandled values. + * Skips the next value recursively. This method is intended for use when + * the JSON token stream contains unrecognized or unhandled values. + * + *

    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.
    • + *
    • End of JSON document: Skipping has no effect, the next token continues to be the + * end of the document.
    • + *
    */ public void skipValue() throws IOException { int count = 0; @@ -1245,32 +1254,69 @@ public class JsonReader implements Closeable { p = doPeek(); } - if (p == PEEKED_BEGIN_ARRAY) { - push(JsonScope.EMPTY_ARRAY); - count++; - } else if (p == PEEKED_BEGIN_OBJECT) { - push(JsonScope.EMPTY_OBJECT); - count++; - } else if (p == PEEKED_END_ARRAY) { - stackSize--; - count--; - } else if (p == PEEKED_END_OBJECT) { - stackSize--; - count--; - } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { - skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { - skipQuotedValue('\''); - } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { - skipQuotedValue('"'); - } else if (p == PEEKED_NUMBER) { - pos += peekedNumberLength; + switch (p) { + case PEEKED_BEGIN_ARRAY: + push(JsonScope.EMPTY_ARRAY); + count++; + break; + case PEEKED_BEGIN_OBJECT: + push(JsonScope.EMPTY_OBJECT); + count++; + break; + case PEEKED_END_ARRAY: + stackSize--; + count--; + break; + case PEEKED_END_OBJECT: + // Only update when object end is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected + } + stackSize--; + count--; + break; + case PEEKED_UNQUOTED: + skipUnquotedValue(); + break; + case PEEKED_SINGLE_QUOTED: + skipQuotedValue('\''); + break; + case PEEKED_DOUBLE_QUOTED: + skipQuotedValue('"'); + break; + case PEEKED_UNQUOTED_NAME: + skipUnquotedValue(); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + 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: + // Do nothing + return; + // For all other tokens there is nothing to do; token has already been consumed from underlying reader } peeked = PEEKED_NONE; - } while (count != 0); + } while (count > 0); pathIndices[stackSize - 1]++; - pathNames[stackSize - 1] = "null"; } private void push(int newTop) { @@ -1505,7 +1551,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 @@ -1522,7 +1568,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/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 4e6a218e..767d63bd 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -35,6 +35,7 @@ public class JsonTreeReaderTest extends TestCase { JsonTreeReader in = new JsonTreeReader(new JsonObject()); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); } public void testSkipValue_filledJsonObject() throws IOException { @@ -53,6 +54,46 @@ public class JsonTreeReaderTest extends TestCase { JsonTreeReader in = new JsonTreeReader(jsonObject); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); + } + + public void testSkipValue_name() throws IOException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("a", "value"); + JsonTreeReader in = new JsonTreeReader(jsonObject); + in.beginObject(); + in.skipValue(); + assertEquals(JsonToken.STRING, in.peek()); + assertEquals("$.", in.getPath()); + assertEquals("value", in.nextString()); + } + + public void testSkipValue_afterEndOfDocument() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atArrayEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonArray()); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atObjectEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testHasNext_endOfDocument() throws IOException { diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java index ab802be1..a755bd83 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -16,6 +16,9 @@ package com.google.gson.stream; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + import com.google.gson.JsonElement; import com.google.gson.internal.Streams; import com.google.gson.internal.bind.JsonTreeReader; @@ -27,9 +30,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import static org.junit.Assert.assertEquals; -import static org.junit.Assume.assumeTrue; - @SuppressWarnings("resource") @RunWith(Parameterized.class) public class JsonReaderPathTest { @@ -221,12 +221,27 @@ public class JsonReaderPathTest { assertEquals("$[2]", reader.getPath()); } + @Test public void skipArrayEnd() throws IOException { + JsonReader reader = factory.create("[[],1]"); + reader.beginArray(); + reader.beginArray(); + assertEquals("$[0][0]", reader.getPreviousPath()); + assertEquals("$[0][0]", reader.getPath()); + reader.skipValue(); // skip end of array + assertEquals("$[0]", reader.getPreviousPath()); + assertEquals("$[1]", reader.getPath()); + } + @Test public void skipObjectNames() throws IOException { - JsonReader reader = factory.create("{\"a\":1}"); + JsonReader reader = factory.create("{\"a\":[]}"); reader.beginObject(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + + reader.beginArray(); + assertEquals("$.[0]", reader.getPreviousPath()); + assertEquals("$.[0]", reader.getPath()); } @Test public void skipObjectValues() throws IOException { @@ -236,13 +251,25 @@ public class JsonReaderPathTest { assertEquals("$.", reader.getPath()); reader.nextName(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); reader.nextName(); assertEquals("$.b", reader.getPreviousPath()); assertEquals("$.b", reader.getPath()); } + @Test public void skipObjectEnd() throws IOException { + JsonReader reader = factory.create("{\"a\":{},\"b\":2}"); + reader.beginObject(); + reader.nextName(); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + reader.skipValue(); // skip end of object + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + } + @Test public void skipNestedStructures() throws IOException { JsonReader reader = factory.create("[[1,2,3],4]"); reader.beginArray(); @@ -251,6 +278,20 @@ public class JsonReaderPathTest { assertEquals("$[1]", reader.getPath()); } + @Test public void skipEndOfDocument() throws IOException { + JsonReader reader = factory.create("[]"); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + @Test public void arrayOfObjects() throws IOException { JsonReader reader = factory.create("[{},{},{}]"); reader.beginArray(); @@ -307,6 +348,52 @@ public class JsonReaderPathTest { assertEquals("$", reader.getPath()); } + @Test public void objectOfObjects() throws IOException { + JsonReader reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}"); + reader.beginObject(); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.endObject(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.beginObject(); + assertEquals("$.b.", reader.getPreviousPath()); + assertEquals("$.b.", reader.getPath()); + reader.nextName(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.nextInt(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.endObject(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + public enum Factory { STRING_READER { @Override public JsonReader create(String data) { diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index 7ec5e462..faaa87a2 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -16,13 +16,6 @@ package com.google.gson.stream; -import java.io.EOFException; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.Arrays; -import junit.framework.TestCase; - import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; import static com.google.gson.stream.JsonToken.BOOLEAN; @@ -33,6 +26,13 @@ import static com.google.gson.stream.JsonToken.NULL; import static com.google.gson.stream.JsonToken.NUMBER; import static com.google.gson.stream.JsonToken.STRING; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Arrays; +import junit.framework.TestCase; + @SuppressWarnings("resource") public final class JsonReaderTest extends TestCase { public void testReadArray() throws IOException { @@ -140,6 +140,35 @@ public final class JsonReaderTest extends TestCase { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipObjectName() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\": 1}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", 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 +193,34 @@ public final class JsonReaderTest extends TestCase { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipValueAfterEndOfDocument() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtArrayEnd() throws IOException { + JsonReader reader = new JsonReader(reader("[]")); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtObjectEnd() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + public void testHelloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" + From 8451c1fa63f5198daf38498869836e85c85bdd08 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Tue, 11 Oct 2022 01:10:48 +0200 Subject: [PATCH 16/35] Fix TypeAdapterRuntimeTypeWrapper not detecting reflective TreeTypeAdapter and FutureTypeAdapter (#1787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix TypeAdapterRuntimeTypeWrapper not detecting reflective TreeTypeAdapter Previously on serialization TypeAdapterRuntimeTypeWrapper preferred a TreeTypeAdapter without `serializer` which falls back to the reflective adapter. This behavior was incorrect because it caused the reflective adapter for a Base class to be used for serialization (indirectly as TreeTypeAdapter delegate) instead of using the reflective adapter for a Subclass extending Base. * Address review feedback * Convert TypeAdapterRuntimeTypeWrapperTest to JUnit 4 test * Prefer wrapped reflective adapter for serialization of subclass * Detect reflective adapter used as delegate for Gson.FutureTypeAdapter * Tiny style tweak. Co-authored-by: Éamonn McManus --- gson/src/main/java/com/google/gson/Gson.java | 22 +- .../SerializationDelegatingTypeAdapter.java | 14 ++ .../gson/internal/bind/TreeTypeAdapter.java | 13 +- .../bind/TypeAdapterRuntimeTypeWrapper.java | 28 ++- .../TypeAdapterRuntimeTypeWrapperTest.java | 193 ++++++++++++++++++ 5 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/internal/bind/SerializationDelegatingTypeAdapter.java create mode 100644 gson/src/test/java/com/google/gson/functional/TypeAdapterRuntimeTypeWrapperTest.java diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index bb3e2c77..22071a17 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -32,6 +32,7 @@ import com.google.gson.internal.bind.MapTypeAdapterFactory; import com.google.gson.internal.bind.NumberTypeAdapter; import com.google.gson.internal.bind.ObjectTypeAdapter; import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.internal.bind.SerializationDelegatingTypeAdapter; import com.google.gson.internal.bind.TypeAdapters; import com.google.gson.internal.sql.SqlTypesSupport; import com.google.gson.reflect.TypeToken; @@ -1315,7 +1316,7 @@ public final class Gson { return fromJson(new JsonTreeReader(json), typeOfT); } - static class FutureTypeAdapter extends TypeAdapter { + static class FutureTypeAdapter extends SerializationDelegatingTypeAdapter { private TypeAdapter delegate; public void setDelegate(TypeAdapter typeAdapter) { @@ -1325,18 +1326,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/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/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); + } +} From a0dc7bfdddfe488510edde8d8abb0727743394c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Undheim?= Date: Tue, 11 Oct 2022 18:13:49 +0200 Subject: [PATCH 17/35] Support Java Records when present in JVM. (#2201) * Support Java Records when present in JVM. Fixes google/gson#1794 Added support in the ReflectionHelper to detect if a class is a record on the JVM (via reflection), and if so, we will create a special RecordAdapter to deserialize records, using the canoncial constructor. The ReflectionTypeAdapterFactory had to be refactored a bit to support this. The Adapter class inside the factory is now abstract, with concrete implementations for normal field reflection and for Records. The common code is in the Adapter, with each implementation deserializing values into an intermediary object. For the FieldReflectionAdapter, the intermediary is actually the final result, and field access is used to write to fields as before. For the RecordAdapter the intermediary is the Object[] to pass to the Record constructor. * Fixed comments from @Marcono1234 Also updated so that we now use the record accessor method to read out values from a record, so that direct field access is not necessary. Also added some tests, that should only execute on Java versions with record support, and be ignored for other JVMs * Fixed additional comments from @Marcono1234 * Made Adapter in ReflectiveTypeAdapterFactory public Fix comment from @eamonnmcmanus --- .../bind/ReflectiveTypeAdapterFactory.java | 280 ++++++++++++++---- .../internal/reflect/ReflectionHelper.java | 197 +++++++++++- .../ReflectiveTypeAdapterFactoryTest.java | 83 ++++++ .../reflect/ReflectionHelperTest.java | 90 ++++++ 4 files changed, 584 insertions(+), 66 deletions(-) create mode 100644 gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java create mode 100644 gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java 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..5caeb107 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 @@ -38,12 +38,17 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; +import java.lang.reflect.Array; +import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -94,22 +99,32 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { 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)) { + return new RecordAdapter<>(raw, getBoundFields(gson, type, raw, true, true)); + } + 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) { @@ -122,7 +137,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } 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()); @@ -138,16 +153,18 @@ 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) - throws IOException, IllegalAccessException { + return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) { + @Override void write(JsonWriter writer, Object source) + throws IOException, ReflectiveOperationException { if (!serialized) return; - if (blockInaccessible) { - checkAccessible(value, field); + if (blockInaccessible && accessor == null) { + checkAccessible(source, field); } - Object fieldValue = field.get(value); - if (fieldValue == value) { + Object fieldValue = (accessor != null) + ? accessor.invoke(source) + : field.get(source); + if (fieldValue == source) { // avoid direct recursion return; } @@ -156,20 +173,31 @@ 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 { + Object fieldValue = typeAdapter.read(reader); + if (fieldValue != null || !isPrimitive) { + 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); } - 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; @@ -197,8 +225,19 @@ 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 + // If there is a static field on a record, there will not be an accessor. Instead we will use the default + // field logic for dealing with statics. + Method accessor = null; + if (isRecord && !Modifier.isStatic(field.getModifiers())) { + accessor = ReflectionHelper.getAccessor(raw, field); + } - // If blockInaccessible, skip and perform access check later + // If blockInaccessible, skip and perform access check later. When constructing a BoundedField for a Record + // field, blockInaccessible is always true, thus makeAccessible will never get called. This is not an issue + // though, as we will use the accessor method instead for reading record fields, and the constructor for + // writing fields. if (!blockInaccessible) { ReflectionHelper.makeAccessible(field); } @@ -208,7 +247,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 +265,50 @@ 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, ReflectiveOperationException; + + /** 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; + + /** 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. + */ + public static abstract class Adapter extends TypeAdapter { + protected final Map boundFields; - Adapter(ObjectConstructor constructor, Map boundFields) { - this.constructor = constructor; + protected 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; @@ -288,8 +321,143 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); + } catch (ReflectiveOperationException e) { + throw ReflectionHelper.createExceptionForRecordReflectionException(e); } 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 BoundedField 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 { + // The actual record constructor. + 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) { + super(boundFields); + this.constructor = ReflectionHelper.getCanonicalRecordConstructor(raw); + // Ensure the constructor is accessible + ReflectionHelper.makeAccessible(this.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++) { + if (parameterTypes[i].isPrimitive()) { + // Voodoo magic, we create a new instance of this primitive type using reflection via an + // array. The array has 1 element, that of course will be initialized to the primitives + // default value. We then retrieve this value back from the array to get the properly + // initialized default value for the primitve type. + constructorArgsDefaults[i] = Array.get(Array.newInstance(parameterTypes[i], 1), 0); + } + } + } + + @Override + Object[] createAccumulator() { + return constructorArgsDefaults.clone(); + } + + @Override + void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException { + Integer fieldIndex = componentIndices.get(field.fieldName); + if (fieldIndex == null) { + throw new IllegalStateException( + "Could not find the index in the constructor " + + constructor + + " for field with name " + + field.name + + ", unable to determine which argument in the constructor the field corresponds" + + " to. This is unexpected behaviour, 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 arguments."); + } + field.readIntoArray(in, fieldIndex, accumulator); + } + + @Override + @SuppressWarnings("unchecked") + T finalize(Object[] accumulator) { + try { + return (T) constructor.newInstance(accumulator); + } catch (ReflectiveOperationException e) { + throw new RuntimeException( + "Failed to invoke " + constructor + " with args " + Arrays.toString(accumulator), e); + } + } } } 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..f55b30f5 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,26 +2,64 @@ package com.google.gson.internal.reflect; import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; + +import java.lang.reflect.*; 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. + * Tries making the field accessible, wrapping any thrown exception in a {@link JsonIOException} + * with descriptive message. * * @param field field to make accessible * @throws JsonIOException if making the field accessible fails */ public static void makeAccessible(Field field) throws JsonIOException { + makeAccessible("field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'", field); + } + + /** + * Tries making the constructor accessible, wrapping any thrown exception in a {@link JsonIOException} + * with descriptive message. + * + * @param constructor constructor to make accessible + * @throws JsonIOException if making the constructor accessible fails + */ + public static void makeAccessible(Constructor constructor) throws JsonIOException { + makeAccessible( + "constructor " + constructor + " in " + constructor.getDeclaringClass().getName(), + constructor + ); + } + + /** + * Internal implementation of making an {@link AccessibleObject} accessible. + * + * @param description describe what we are attempting to make accessible + * @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on. + * @throws JsonIOException if making the object accessible fails + */ + private static void makeAccessible(String description, 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); + throw new JsonIOException("Failed making " + description + "' accessible; either change its visibility " + + "or write a custom TypeAdapter for its declaring type", exception); } } @@ -65,10 +103,149 @@ public class ReflectionHelper { } } - public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) { + /** If records are supported on the JVM, this is equivalent to a call to Class.isRecord() */ + public static boolean isRecord(Class raw) { + return RECORD_HELPER.isRecord(raw); + } + + public static String[] getRecordComponentNames(Class raw) { + return RECORD_HELPER.getRecordComponentNames(raw); + } + + /** Looks up the record accessor method that corresponds to the given record field */ + public static Method getAccessor(Class raw, Field field) { + return RECORD_HELPER.getAccessor(raw, field); + } + + public static 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); } + + + public 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 exists in the JVM. This exception is unexpected behaviour.", + 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 final Method getAccessor; + + private RecordSupportedHelper() throws NoSuchMethodException { + isRecord = Class.class.getMethod("isRecord"); + getRecordComponents = Class.class.getMethod("getRecordComponents"); + Class recordComponentType = getRecordComponents.getReturnType().getComponentType(); + getName = recordComponentType.getMethod("getName"); + getType = recordComponentType.getMethod("getType"); + getAccessor = recordComponentType.getMethod("getAccessor"); + } + + @Override + boolean isRecord(Class raw) { + try { + return Boolean.class.cast(isRecord.invoke(raw)).booleanValue(); + } 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/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java new file mode 100644 index 00000000..08c92f80 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java @@ -0,0 +1,83 @@ +package com.google.gson.internal.bind; + +import static org.junit.Assert.*; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.reflect.ReflectionHelperTest; +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.AssumptionViolatedException; +import org.junit.Before; +import org.junit.Test; + +public class ReflectiveTypeAdapterFactoryTest { + + // 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 { + try { + Class.forName("java.lang.Record"); + unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); + } catch (ClassNotFoundException e) { + // Records not supported, ignore + throw new AssumptionViolatedException("java.lang.Record not supported"); + } + } + + @Test + public void testCustomAdapterForRecords() { + Gson gson = new Gson(); + TypeAdapter recordAdapter = gson.getAdapter(unixDomainPrincipalClass); + TypeAdapter defaultReflectionAdapter = gson.getAdapter(UserPrincipal.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 ReflectionHelperTest.PrincipalImpl(name); + return principal; + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java new file mode 100644 index 00000000..7d0c9833 --- /dev/null +++ b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java @@ -0,0 +1,90 @@ +package com.google.gson.internal.reflect; + +import static org.junit.Assert.*; + +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.AssumptionViolatedException; +import org.junit.Before; +import org.junit.Test; + +public class ReflectionHelperTest { + + @Before + public void setUp() throws Exception { + try { + Class.forName("java.lang.Record"); + } catch (ClassNotFoundException e) { + // Records not supported, ignore + throw new AssumptionViolatedException("java.lang.Record not supported"); + } + } + + @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")); + for (String componentName : + ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)) { + 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 (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PrincipalImpl principal = (PrincipalImpl) o; + return Objects.equals(name, principal.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} From 86c35bba3091afe70f38598b04ba89b7e8f539f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Wed, 12 Oct 2022 17:24:36 -0400 Subject: [PATCH 18/35] Small adjustments to the new record code. (#2219) * Small adjustments to the new record code. * Replace wildcard imports with single imports. * Enable `Java17RecordTest` and fix its many previously-hidden problems. * Use a `Map` to get primitive zero values rather than a potentially-expensive reflective trick. * Apply some automated code fixes. * Address review comments. --- .../bind/ReflectiveTypeAdapterFactory.java | 29 ++++++++----- .../internal/reflect/ReflectionHelper.java | 10 ++--- .../gson/functional/Java17RecordTest.java | 43 +++++++++++++------ .../ReflectiveTypeAdapterFactoryTest.java | 3 +- .../reflect/ReflectionHelperTest.java | 13 +++--- 5 files changed, 63 insertions(+), 35 deletions(-) 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 5caeb107..3da14f23 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 @@ -43,7 +43,6 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; -import java.lang.reflect.Array; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; @@ -93,9 +92,7 @@ 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; } @@ -394,6 +391,8 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } private static final class RecordAdapter extends Adapter { + static Map, Object> PRIMITIVE_DEFAULTS = primitiveDefaults(); + // The actual record constructor. private final Constructor constructor; // Array of arguments to the constructor, initialized with default values for primitives @@ -417,16 +416,24 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { // 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++) { - if (parameterTypes[i].isPrimitive()) { - // Voodoo magic, we create a new instance of this primitive type using reflection via an - // array. The array has 1 element, that of course will be initialized to the primitives - // default value. We then retrieve this value back from the array to get the properly - // initialized default value for the primitve type. - constructorArgsDefaults[i] = Array.get(Array.newInstance(parameterTypes[i], 1), 0); - } + // 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(); 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 f55b30f5..80df515a 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,8 +2,10 @@ package com.google.gson.internal.reflect; import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; - -import java.lang.reflect.*; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; public class ReflectionHelper { @@ -158,7 +160,6 @@ public class ReflectionHelper { private final Method getRecordComponents; private final Method getName; private final Method getType; - private final Method getAccessor; private RecordSupportedHelper() throws NoSuchMethodException { isRecord = Class.class.getMethod("isRecord"); @@ -166,13 +167,12 @@ public class ReflectionHelper { Class recordComponentType = getRecordComponents.getReturnType().getComponentType(); getName = recordComponentType.getMethod("getName"); getType = recordComponentType.getMethod("getType"); - getAccessor = recordComponentType.getMethod("getAccessor"); } @Override boolean isRecord(Class raw) { try { - return Boolean.class.cast(isRecord.invoke(raw)).booleanValue(); + return (boolean) isRecord.invoke(raw); } catch (ReflectiveOperationException e) { throw createExceptionForRecordReflectionException(e); } diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java index 95166d82..023bec30 100644 --- a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java @@ -16,18 +16,17 @@ package com.google.gson.functional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; import java.util.Objects; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -@Ignore // Disabled until record support is added public final class Java17RecordTest { private final Gson gson = new Gson(); @@ -35,36 +34,54 @@ public final class Java17RecordTest { public void testFirstNameIsChosenForSerialization() { MyRecord target = new MyRecord("v1", "v2"); // Ensure name1 occurs exactly once, and name2 and name3 don't appear - assertEquals("{\"name\":\"v1\",\"name1\":\"v2\"}", gson.toJson(target)); + assertEquals("{\"name\":\"modified-v1\",\"name1\":\"v2\"}", gson.toJson(target)); } @Test public void testMultipleNamesDeserializedCorrectly() { - assertEquals("v1", gson.fromJson("{'name':'v1'}", MyRecord.class).a); + assertEquals("modified-v1", gson.fromJson("{'name':'v1'}", MyRecord.class).a); // Both name1 and name2 gets deserialized to b - assertEquals("v11", gson.fromJson("{'name1':'v11'}", MyRecord.class).b); - assertEquals("v2", gson.fromJson("{'name2':'v2'}", MyRecord.class).b); - assertEquals("v3", gson.fromJson("{'name3':'v3'}", MyRecord.class).b); + assertEquals("v11", gson.fromJson("{'name': 'v1', 'name1':'v11'}", MyRecord.class).b); + assertEquals("v2", gson.fromJson("{'name': 'v1', 'name2':'v2'}", MyRecord.class).b); + assertEquals("v3", gson.fromJson("{'name': 'v1', 'name3':'v3'}", MyRecord.class).b); } @Test public void testMultipleNamesInTheSameString() { // The last value takes precedence - assertEquals("v3", gson.fromJson("{'name1':'v1','name2':'v2','name3':'v3'}", MyRecord.class).b); + assertEquals("v3", + gson.fromJson("{'name': 'foo', 'name1':'v1','name2':'v2','name3':'v3'}", MyRecord.class).b); } @Test public void testConstructorRuns() { - assertThrows(NullPointerException.class, - () -> gson.fromJson("{'name1': null, 'name2': null", MyRecord.class)); + assertEquals(new MyRecord(null, null), + gson.fromJson("{'name1': null, 'name2': null}", MyRecord.class)); } - private static record MyRecord( + @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 testPrimitiveNullValues() { + RecordWithPrimitives expected = new RecordWithPrimitives("s", (byte) 0, (short) 0, 0, 0, 0, 0, '\0', false); + // TODO(eamonnmcmanus): consider forbidding null for primitives + String s = "{'aString': 's', 'aByte': null, 'aShort': null, 'anInt': null, 'aLong': null, 'aFloat': null, 'aDouble': null, 'aChar': null, 'aBoolean': null}"; + assertEquals(expected, gson.fromJson(s, RecordWithPrimitives.class)); + } + + public record MyRecord( @SerializedName("name") String a, @SerializedName(value = "name1", alternate = {"name2", "name3"}) String b) { - MyRecord { - Objects.requireNonNull(a); + public MyRecord { + a = "modified-" + a; } } + + public record RecordWithPrimitives( + String aString, byte aByte, short aShort, int anInt, long aLong, float aFloat, double aDouble, char aChar, boolean aBoolean) {} } diff --git a/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java index 08c92f80..8ee15aa0 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java @@ -1,6 +1,7 @@ package com.google.gson.internal.bind; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import com.google.gson.Gson; import com.google.gson.GsonBuilder; diff --git a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java index 7d0c9833..f5c827e8 100644 --- a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java +++ b/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java @@ -1,6 +1,9 @@ package com.google.gson.internal.reflect; -import static org.junit.Assert.*; +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; @@ -76,10 +79,10 @@ public class ReflectionHelperTest { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PrincipalImpl principal = (PrincipalImpl) o; - return Objects.equals(name, principal.name); + if (o instanceof PrincipalImpl) { + return Objects.equals(name, ((PrincipalImpl) o).name); + } + return false; } @Override From 243f5e11262d580fa21f045d35d75af1b2b6221d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:19:08 -0700 Subject: [PATCH 19/35] Bump jackson-databind from 2.13.4 to 2.13.4.1 (#2222) Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.4 to 2.13.4.1. - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- metrics/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/pom.xml b/metrics/pom.xml index 40f17c8f..d1fa4427 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -32,7 +32,7 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4 + 2.13.4.1 com.google.caliper From c2458bf876ef52a4489af937f7dd7c2709b74fb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Oct 2022 13:28:29 -0700 Subject: [PATCH 20/35] Bump jackson-databind from 2.13.4.1 to 2.13.4.2 (#2223) Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.4.1 to 2.13.4.2. - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- metrics/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/pom.xml b/metrics/pom.xml index d1fa4427..48d943de 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -32,7 +32,7 @@ com.fasterxml.jackson.core jackson-databind - 2.13.4.1 + 2.13.4.2 com.google.caliper From 954d526af4ad9e58872e7a64f92e749421d1cdf5 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 16 Oct 2022 21:30:49 +0200 Subject: [PATCH 21/35] Add `JsonArray.asList` and `JsonObject.asMap` view methods (#2225) * Add `JsonArray.asList` and `JsonObject.asMap` view methods * Address review comments --- .../main/java/com/google/gson/JsonArray.java | 20 +- .../main/java/com/google/gson/JsonObject.java | 18 ++ .../internal/NonNullElementWrapperList.java | 98 ++++++ .../com/google/gson/JsonArrayAsListTest.java | 285 +++++++++++++++++ .../java/com/google/gson/JsonArrayTest.java | 174 ++++++++++- .../com/google/gson/JsonObjectAsMapTest.java | 287 ++++++++++++++++++ .../java/com/google/gson/JsonObjectTest.java | 27 +- .../google/gson/functional/JsonArrayTest.java | 161 ---------- 8 files changed, 904 insertions(+), 166 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/internal/NonNullElementWrapperList.java create mode 100644 gson/src/test/java/com/google/gson/JsonArrayAsListTest.java create mode 100644 gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java delete mode 100644 gson/src/test/java/com/google/gson/functional/JsonArrayTest.java diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java index 5a0c77e8..e9ce580c 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. @@ -393,6 +397,20 @@ 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 + */ + 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 428861a6..0c36ef24 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 */ @@ -208,6 +211,21 @@ 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 + */ + 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/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/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java new file mode 100644 index 00000000..36d671f9 --- /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") + 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") + 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..7f175389 --- /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") + 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/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()); - } -} From 66d9621ce87c04a5167ee04097694093b13b514c Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sat, 22 Oct 2022 18:01:56 +0200 Subject: [PATCH 22/35] Adjust Record adapter and extend test coverage (#2224) * Adjust Record adapter and extend test coverage * Address review feedback * Make constructor string more concise * Add tests for Gson default behavior for static fields * Improve exception for deserializing static final field Previously it would report "Unexpected IllegalAccessException occurred..." due to the uncaught IllegalAccessException. * Improve handling of exception thrown by accessor Such an exception is not 'unexpected' (which was claimed by the previous exception handling) because user code could throw it. * Improve constructor invocation exception handling and add tests --- .../gson/internal/ConstructorConstructor.java | 15 +- .../bind/ReflectiveTypeAdapterFactory.java | 182 +++++--- .../internal/reflect/ReflectionHelper.java | 125 +++--- .../gson/functional/Java17RecordTest.java | 387 +++++++++++++++++- .../google/gson/functional/ObjectTest.java | 100 ++++- .../ReflectionAccessFilterTest.java | 62 +-- .../gson/functional/ReflectionAccessTest.java | 4 +- ...va17ReflectiveTypeAdapterFactoryTest.java} | 29 +- ...t.java => Java17ReflectionHelperTest.java} | 22 +- 9 files changed, 715 insertions(+), 211 deletions(-) rename gson/src/test/java/com/google/gson/internal/bind/{ReflectiveTypeAdapterFactoryTest.java => Java17ReflectiveTypeAdapterFactoryTest.java} (77%) rename gson/src/test/java/com/google/gson/internal/reflect/{ReflectionHelperTest.java => Java17ReflectionHelperTest.java} (84%) 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 da330c30..115a2a09 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -261,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); } 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 3da14f23..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,16 +39,19 @@ 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.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -108,8 +112,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); if (filterResult == FilterResult.BLOCK_ALL) { throw new JsonIOException( - "ReflectionAccessFilter does not permit using reflection for " - + raw + "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; @@ -117,19 +120,22 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { // 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)) { - return new RecordAdapter<>(raw, getBoundFields(gson, type, raw, true, true)); + @SuppressWarnings("unchecked") + TypeAdapter adapter = (TypeAdapter) new RecordAdapter<>(raw, + getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible); + return adapter; } ObjectConstructor constructor = constructorConstructor.get(type); 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."); } } @@ -137,7 +143,12 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { 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) { @@ -152,15 +163,29 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { final TypeAdapter typeAdapter = (TypeAdapter) mapped; return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) { @Override void write(JsonWriter writer, Object source) - throws IOException, ReflectiveOperationException { + throws IOException, IllegalAccessException { if (!serialized) return; - if (blockInaccessible && accessor == null) { - checkAccessible(source, field); + if (blockInaccessible) { + if (accessor == null) { + checkAccessible(source, field); + } else { + // Note: This check might actually be redundant because access check for canonical + // constructor should have failed already + checkAccessible(source, accessor); + } } - Object fieldValue = (accessor != null) - ? accessor.invoke(source) - : field.get(source); + 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; @@ -172,11 +197,13 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } @Override - void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException { + void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException { Object fieldValue = typeAdapter.read(reader); - if (fieldValue != null || !isPrimitive) { - target[index] = fieldValue; + 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 @@ -186,6 +213,11 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { if (fieldValue != null || !isPrimitive) { if (blockInaccessible) { checkAccessible(target, field); + } else if (isStaticFinalField) { + // Reflection does not permit setting value of `static final` field, even after calling `setAccessible` + // Handle this here to avoid causing IllegalAccessException when calling `Field.set` + String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false); + throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription); } field.set(target, fieldValue); } @@ -209,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; } @@ -224,18 +256,34 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } // 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 - // If there is a static field on a record, there will not be an accessor. Instead we will use the default - // field logic for dealing with statics. Method accessor = null; - if (isRecord && !Modifier.isStatic(field.getModifiers())) { - accessor = ReflectionHelper.getAccessor(raw, field); + 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. When constructing a BoundedField for a Record - // field, blockInaccessible is always true, thus makeAccessible will never get called. This is not an issue - // though, as we will use the accessor method instead for reading record fields, and the constructor for - // writing fields. - if (!blockInaccessible) { + // If blockInaccessible, skip and perform access check later + // For Records if the accessor method is used the field does not have to be made accessible + if (!blockInaccessible && accessor == null) { ReflectionHelper.makeAccessible(field); } Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); @@ -275,10 +323,10 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } /** Read this field value from the source, and append its JSON value to the writer */ - abstract void write(JsonWriter writer, Object source) throws IOException, ReflectiveOperationException; + 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; + 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; @@ -297,10 +345,11 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { * @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 { - protected final Map boundFields; + final Map boundFields; - protected Adapter(Map boundFields) { + Adapter(Map boundFields) { this.boundFields = boundFields; } @@ -318,8 +367,6 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } } catch (IllegalAccessException e) { throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); - } catch (ReflectiveOperationException e) { - throw ReflectionHelper.createExceptionForRecordReflectionException(e); } out.endObject(); } @@ -356,7 +403,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { /** Create the Object that will be used to collect each field value */ abstract A createAccumulator(); /** - * Read a single BoundedField into the accumulator. The JsonReader will be pointed at the + * 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) @@ -391,20 +438,25 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { } private static final class RecordAdapter extends Adapter { - static Map, Object> PRIMITIVE_DEFAULTS = primitiveDefaults(); + static final Map, Object> PRIMITIVE_DEFAULTS = primitiveDefaults(); - // The actual record constructor. - private final Constructor constructor; + // 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) { + RecordAdapter(Class raw, Map boundFields, boolean blockInaccessible) { super(boundFields); - this.constructor = ReflectionHelper.getCanonicalRecordConstructor(raw); - // Ensure the constructor is accessible - ReflectionHelper.makeAccessible(this.constructor); + 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++) { @@ -441,29 +493,39 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { @Override void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException { - Integer fieldIndex = componentIndices.get(field.fieldName); - if (fieldIndex == null) { + // 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 " - + constructor - + " for field with name " - + field.name - + ", unable to determine which argument in the constructor the field corresponds" - + " to. This is unexpected behaviour, as we expect the RecordComponents to have the" + "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 arguments."); + + " RecordComponents is the same as the order of the canonical constructor parameters."); } - field.readIntoArray(in, fieldIndex, accumulator); + field.readIntoArray(in, componentIndex, accumulator); } @Override - @SuppressWarnings("unchecked") T finalize(Object[] accumulator) { try { - return (T) constructor.newInstance(accumulator); - } catch (ReflectiveOperationException e) { + 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 + " with args " + Arrays.toString(accumulator), e); + "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/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 80df515a..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 @@ -24,57 +24,75 @@ public class ReflectionHelper { private ReflectionHelper() {} - /** - * Tries making the field accessible, wrapping any thrown exception in a {@link JsonIOException} - * with descriptive message. - * - * @param field field to make accessible - * @throws JsonIOException if making the field accessible fails - */ - public static void makeAccessible(Field field) throws JsonIOException { - makeAccessible("field '" + field.getDeclaringClass().getName() + "#" + field.getName() + "'", field); - } - - /** - * Tries making the constructor accessible, wrapping any thrown exception in a {@link JsonIOException} - * with descriptive message. - * - * @param constructor constructor to make accessible - * @throws JsonIOException if making the constructor accessible fails - */ - public static void makeAccessible(Constructor constructor) throws JsonIOException { - makeAccessible( - "constructor " + constructor + " in " + constructor.getDeclaringClass().getName(), - constructor - ); - } - /** * Internal implementation of making an {@link AccessibleObject} accessible. * - * @param description describe what we are attempting to make accessible * @param object the object that {@link AccessibleObject#setAccessible(boolean)} should be called on. * @throws JsonIOException if making the object accessible fails */ - private static void makeAccessible(String description, AccessibleObject object) throws JsonIOException { + public static void makeAccessible(AccessibleObject object) throws JsonIOException { try { object.setAccessible(true); } catch (Exception exception) { - throw new JsonIOException("Failed making " + description + "' 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(", "); @@ -82,7 +100,7 @@ public class ReflectionHelper { stringBuilder.append(parameters[i].getSimpleName()); } - return stringBuilder.append(')').toString(); + stringBuilder.append(')'); } /** @@ -98,10 +116,10 @@ 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(); } } @@ -125,20 +143,20 @@ public class ReflectionHelper { 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.", + 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); } - public static RuntimeException createExceptionForRecordReflectionException( + 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 exists in the JVM. This exception is unexpected behaviour.", + 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); } @@ -164,9 +182,10 @@ public class ReflectionHelper { private RecordSupportedHelper() throws NoSuchMethodException { isRecord = Class.class.getMethod("isRecord"); getRecordComponents = Class.class.getMethod("getRecordComponents"); - Class recordComponentType = getRecordComponents.getReturnType().getComponentType(); - getName = recordComponentType.getMethod("getName"); - getType = recordComponentType.getMethod("getType"); + // Class java.lang.reflect.RecordComponent + Class classRecordComponent = getRecordComponents.getReturnType().getComponentType(); + getName = classRecordComponent.getMethod("getName"); + getType = classRecordComponent.getMethod("getType"); } @Override diff --git a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java index 023bec30..a172f5a4 100644 --- a/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java +++ b/gson/src/test/java/com/google/gson/functional/Java17RecordTest.java @@ -17,11 +17,32 @@ 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 java.util.Objects; +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; @@ -32,32 +53,167 @@ public final class Java17RecordTest { @Test public void testFirstNameIsChosenForSerialization() { - MyRecord target = new MyRecord("v1", "v2"); + RecordWithCustomNames target = new RecordWithCustomNames("v1", "v2"); // Ensure name1 occurs exactly once, and name2 and name3 don't appear - assertEquals("{\"name\":\"modified-v1\",\"name1\":\"v2\"}", gson.toJson(target)); + assertEquals("{\"name\":\"v1\",\"name1\":\"v2\"}", gson.toJson(target)); } @Test public void testMultipleNamesDeserializedCorrectly() { - assertEquals("modified-v1", gson.fromJson("{'name':'v1'}", MyRecord.class).a); + 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'}", MyRecord.class).b); - assertEquals("v2", gson.fromJson("{'name': 'v1', 'name2':'v2'}", MyRecord.class).b); - assertEquals("v3", gson.fromJson("{'name': 'v1', 'name3':'v3'}", MyRecord.class).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'}", MyRecord.class).b); + 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() { - assertEquals(new MyRecord(null, null), - gson.fromJson("{'name1': null, 'name2': null}", MyRecord.class)); + 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 @@ -67,21 +223,208 @@ public final class Java17RecordTest { } @Test - public void testPrimitiveNullValues() { - RecordWithPrimitives expected = new RecordWithPrimitives("s", (byte) 0, (short) 0, 0, 0, 0, 0, '\0', false); - // TODO(eamonnmcmanus): consider forbidding null for primitives - String s = "{'aString': 's', 'aByte': null, 'aShort': null, 'anInt': null, 'aLong': null, 'aFloat': null, 'aDouble': null, 'aChar': null, 'aBoolean': null}"; - assertEquals(expected, gson.fromJson(s, RecordWithPrimitives.class)); + 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()); } - public record MyRecord( - @SerializedName("name") String a, - @SerializedName(value = "name1", alternate = {"name2", "name3"}) String b) { - public MyRecord { - a = "modified-" + a; + /** + * 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"); + } + }) + .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; } } - public record RecordWithPrimitives( - String aString, byte aByte, short aShort, int anInt, long aLong, float aFloat, double aDouble, char aChar, boolean aBoolean) {} + 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/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index de384879..bed5b598 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -20,6 +20,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; @@ -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/ReflectionAccessFilterTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java index 8814b377..6c9ab449 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java @@ -53,8 +53,9 @@ 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() ); } @@ -85,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() ); } @@ -104,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() ); } @@ -122,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() ); } @@ -152,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() ); } @@ -194,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() ); } @@ -233,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() ); } @@ -274,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() ); } @@ -306,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() ); } @@ -322,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); @@ -368,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() ); } @@ -428,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 cdf56852..02649c5f 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java @@ -119,8 +119,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/internal/bind/ReflectiveTypeAdapterFactoryTest.java b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java similarity index 77% rename from gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java rename to gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java index 8ee15aa0..18984c7b 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactoryTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/Java17ReflectiveTypeAdapterFactoryTest.java @@ -6,42 +6,39 @@ 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.ReflectionHelperTest; +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.AssumptionViolatedException; import org.junit.Before; import org.junit.Test; -public class ReflectiveTypeAdapterFactoryTest { +public class Java17ReflectiveTypeAdapterFactoryTest { - // The class jdk.net.UnixDomainPrincipal is one of the few Record types that are included in the - // JDK. + // 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. + // 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 { - try { - Class.forName("java.lang.Record"); - unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); - } catch (ClassNotFoundException e) { - // Records not supported, ignore - throw new AssumptionViolatedException("java.lang.Record not supported"); - } + 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(UserPrincipal.class); + TypeAdapter defaultReflectionAdapter = gson.getAdapter(DummyClass.class); assertNotEquals(recordAdapter.getClass(), defaultReflectionAdapter.getClass()); } @@ -77,7 +74,7 @@ public class ReflectiveTypeAdapterFactoryTest { 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 ReflectionHelperTest.PrincipalImpl(name); + T principal = (T) new Java17ReflectionHelperTest.PrincipalImpl(name); return principal; } } diff --git a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java similarity index 84% rename from gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java rename to gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java index f5c827e8..4d4089e8 100644 --- a/gson/src/test/java/com/google/gson/internal/reflect/ReflectionHelperTest.java +++ b/gson/src/test/java/com/google/gson/internal/reflect/Java17ReflectionHelperTest.java @@ -11,22 +11,9 @@ import java.lang.reflect.Method; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.UserPrincipal; import java.util.Objects; -import org.junit.AssumptionViolatedException; -import org.junit.Before; import org.junit.Test; -public class ReflectionHelperTest { - - @Before - public void setUp() throws Exception { - try { - Class.forName("java.lang.Record"); - } catch (ClassNotFoundException e) { - // Records not supported, ignore - throw new AssumptionViolatedException("java.lang.Record not supported"); - } - } - +public class Java17ReflectionHelperTest { @Test public void testJava17Record() throws ClassNotFoundException { Class unixDomainPrincipalClass = Class.forName("jdk.net.UnixDomainPrincipal"); @@ -54,8 +41,11 @@ public class ReflectionHelperTest { Object unixDomainPrincipal = ReflectionHelper.getCanonicalRecordConstructor(unixDomainPrincipalClass) .newInstance(new PrincipalImpl("user"), new PrincipalImpl("group")); - for (String componentName : - ReflectionHelper.getRecordComponentNames(unixDomainPrincipalClass)) { + + 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); From 9578583effe9c6acc1296e856eeb81bfd75581d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 10:28:25 -0700 Subject: [PATCH 23/35] Small tweaks to fix Error Prone warnings. (#2227) * Small tweaks to fix Error Prone warnings. * Add another suppression. --- .../java/com/google/gson/internal/bind/JsonTreeReader.java | 2 +- gson/src/test/java/com/google/gson/JsonArrayAsListTest.java | 4 ++-- gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 e47c57c7..81c3363c 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -277,7 +277,7 @@ public final class JsonTreeReader extends JsonReader { JsonToken peeked = peek(); switch (peeked) { case NAME: - nextName(true); + String unused = nextName(true); break; case END_ARRAY: endArray(); diff --git a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java index 36d671f9..a1786ce6 100644 --- a/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java +++ b/gson/src/test/java/com/google/gson/JsonArrayAsListTest.java @@ -210,7 +210,7 @@ public class JsonArrayAsListTest { assertFalse(list.contains(new JsonPrimitive(2))); assertFalse(list.contains(null)); - @SuppressWarnings("unlikely-arg-type") + @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"}) boolean containsInt = list.contains(1); // should only contain JsonPrimitive(1) assertFalse(containsInt); } @@ -227,7 +227,7 @@ public class JsonArrayAsListTest { assertEquals(-1, list.indexOf(new JsonPrimitive(2))); assertEquals(-1, list.indexOf(null)); - @SuppressWarnings("unlikely-arg-type") + @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"}) int indexOfInt = list.indexOf(1); // should only contain JsonPrimitive(1) assertEquals(-1, indexOfInt); diff --git a/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java index 7f175389..00a89a6f 100644 --- a/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java +++ b/gson/src/test/java/com/google/gson/JsonObjectAsMapTest.java @@ -59,7 +59,7 @@ public class JsonObjectAsMapTest { assertFalse(map.containsValue(new JsonPrimitive(2))); assertFalse(map.containsValue(null)); - @SuppressWarnings("unlikely-arg-type") + @SuppressWarnings({"unlikely-arg-type", "CollectionIncompatibleType"}) boolean containsInt = map.containsValue(1); // should only contain JsonPrimitive(1) assertFalse(containsInt); } From 4f948dd482eaec60273696274401f2c275079cda Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Tue, 25 Oct 2022 02:26:43 +0200 Subject: [PATCH 24/35] Automatically replace version references on `release:prepare` (#2212) * Automatically replace version references on `release:prepare` * Specify encoding and improve placeholder replacements * Add `since $next-version$` for `JsonArray.asList` and `JsonObject.asMap` --- ReleaseProcess.md | 5 +- gson/src/main/java/com/google/gson/Gson.java | 5 +- .../main/java/com/google/gson/JsonArray.java | 1 + .../main/java/com/google/gson/JsonObject.java | 1 + pom.xml | 72 +++++++++++++++++++ 5 files changed, 82 insertions(+), 2 deletions(-) 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/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 22071a17..7e0cce21 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -1024,6 +1024,7 @@ public final class Gson { * * @see #fromJson(Reader, TypeToken) * @see #fromJson(String, Class) + * @since $next-version$ */ public T fromJson(String json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { @@ -1116,6 +1117,7 @@ public final class Gson { * * @see #fromJson(String, TypeToken) * @see #fromJson(Reader, Class) + * @since $next-version$ */ public T fromJson(Reader json, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { JsonReader jsonReader = newJsonReader(json); @@ -1199,6 +1201,7 @@ public final class Gson { * * @see #fromJson(Reader, TypeToken) * @see #fromJson(JsonReader, Type) + * @since $next-version$ */ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; @@ -1304,10 +1307,10 @@ public final class Gson { * @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 $next-version$ */ public T fromJson(JsonElement json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java index e9ce580c..fadc7664 100644 --- a/gson/src/main/java/com/google/gson/JsonArray.java +++ b/gson/src/main/java/com/google/gson/JsonArray.java @@ -406,6 +406,7 @@ public final class JsonArray extends JsonElement implements Iterable asList() { return new NonNullElementWrapperList<>(elements); diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java index 0c36ef24..d1c6b30b 100644 --- a/gson/src/main/java/com/google/gson/JsonObject.java +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -220,6 +220,7 @@ public final class JsonObject extends JsonElement { * Use {@link JsonNull} for JSON null values. * * @return mutable {@code Map} view + * @since $next-version$ */ public Map asMap() { // It is safe to expose the underlying map because it disallows null keys and values diff --git a/pom.xml b/pom.xml index b7327676..140ab0d3 100644 --- a/pom.xml +++ b/pom.xml @@ -136,8 +136,80 @@ false release + + + + package -DskipTests + 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 From 9efdfad33c6d717764f2b3a5214cf71bdea83c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:33:34 -0700 Subject: [PATCH 25/35] [maven-release-plugin] prepare release gson-parent-2.10 --- README.md | 4 ++-- UserGuide.md | 4 ++-- extras/pom.xml | 2 +- gson/pom.xml | 4 ++-- gson/src/main/java/com/google/gson/Gson.java | 8 ++++---- gson/src/main/java/com/google/gson/JsonArray.java | 2 +- gson/src/main/java/com/google/gson/JsonObject.java | 2 +- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0e785781..dddaf95f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ There are a few open-source projects that can convert Java objects to JSON. Howe Gradle: ```gradle dependencies { - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10' } ``` @@ -28,7 +28,7 @@ Maven: com.google.code.gson gson - 2.9.1 + 2.10 ``` diff --git a/UserGuide.md b/UserGuide.md index 49ac54d4..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 diff --git a/extras/pom.xml b/extras/pom.xml index 7e5c87ca..533f78fa 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 3861a935..277ab598 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson @@ -246,7 +246,7 @@ 17 - + diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 7e0cce21..262cd175 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -1024,7 +1024,7 @@ public final class Gson { * * @see #fromJson(Reader, TypeToken) * @see #fromJson(String, Class) - * @since $next-version$ + * @since 2.10 */ public T fromJson(String json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { @@ -1117,7 +1117,7 @@ public final class Gson { * * @see #fromJson(String, TypeToken) * @see #fromJson(Reader, Class) - * @since $next-version$ + * @since 2.10 */ public T fromJson(Reader json, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { JsonReader jsonReader = newJsonReader(json); @@ -1201,7 +1201,7 @@ public final class Gson { * * @see #fromJson(Reader, TypeToken) * @see #fromJson(JsonReader, Type) - * @since $next-version$ + * @since 2.10 */ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; @@ -1310,7 +1310,7 @@ public final class Gson { * * @see #fromJson(Reader, TypeToken) * @see #fromJson(JsonElement, Class) - * @since $next-version$ + * @since 2.10 */ public T fromJson(JsonElement json, TypeToken typeOfT) throws JsonSyntaxException { if (json == null) { diff --git a/gson/src/main/java/com/google/gson/JsonArray.java b/gson/src/main/java/com/google/gson/JsonArray.java index fadc7664..370b323f 100644 --- a/gson/src/main/java/com/google/gson/JsonArray.java +++ b/gson/src/main/java/com/google/gson/JsonArray.java @@ -406,7 +406,7 @@ public final class JsonArray extends JsonElement implements Iterable asList() { return new NonNullElementWrapperList<>(elements); diff --git a/gson/src/main/java/com/google/gson/JsonObject.java b/gson/src/main/java/com/google/gson/JsonObject.java index d1c6b30b..60dac41c 100644 --- a/gson/src/main/java/com/google/gson/JsonObject.java +++ b/gson/src/main/java/com/google/gson/JsonObject.java @@ -220,7 +220,7 @@ public final class JsonObject extends JsonElement { * Use {@link JsonNull} for JSON null values. * * @return mutable {@code Map} view - * @since $next-version$ + * @since 2.10 */ public Map asMap() { // It is safe to expose the underlying map because it disallows null keys and values diff --git a/metrics/pom.xml b/metrics/pom.xml index 48d943de..ca904248 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson-metrics diff --git a/pom.xml b/pom.xml index 140ab0d3..536e591d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - HEAD + gson-parent-2.10 diff --git a/proto/pom.xml b/proto/pom.xml index 02b1ef0d..ff920604 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 proto From c7544a0e864e4ad4a458a3313da3cef2f84e7b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:33:36 -0700 Subject: [PATCH 26/35] [maven-release-plugin] prepare for next development iteration --- extras/pom.xml | 2 +- gson/pom.xml | 2 +- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index 533f78fa..a1851711 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 277ab598..4e7b88a6 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson diff --git a/metrics/pom.xml b/metrics/pom.xml index ca904248..b411d92e 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-metrics diff --git a/pom.xml b/pom.xml index 536e591d..ab58ab37 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - gson-parent-2.10 + HEAD diff --git a/proto/pom.xml b/proto/pom.xml index ff920604..8ed8728f 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT proto From 7bca5c4061726164e8b68f2496118b7b5243eefe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:42:45 -0700 Subject: [PATCH 27/35] [maven-release-plugin] rollback the release of gson-parent-2.10 --- extras/pom.xml | 2 +- gson/pom.xml | 4 ++-- metrics/pom.xml | 2 +- pom.xml | 2 +- proto/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index a1851711..7e5c87ca 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.9.2-SNAPSHOT gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 4e7b88a6..3861a935 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.9.2-SNAPSHOT gson @@ -246,7 +246,7 @@ 17 - + diff --git a/metrics/pom.xml b/metrics/pom.xml index b411d92e..48d943de 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.9.2-SNAPSHOT gson-metrics diff --git a/pom.xml b/pom.xml index ab58ab37..140ab0d3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.9.2-SNAPSHOT pom Gson Parent diff --git a/proto/pom.xml b/proto/pom.xml index 8ed8728f..02b1ef0d 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.9.2-SNAPSHOT proto From 4705518e12fe205b9c880b14fd15efa6aae4a5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:47:38 -0700 Subject: [PATCH 28/35] Revise the version regex in `GsonVersionDiagnosticsTest`. (#2228) Before we can release 2.10 we must support two-digit components. Additionally, there's no reason to require a patch number (2.10.0 rather than 2.10). --- .../com/google/gson/functional/GsonVersionDiagnosticsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 87e9ee5511021a918bf957cf56a9142b4efa82b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:48:55 -0700 Subject: [PATCH 29/35] [maven-release-plugin] prepare release gson-parent-2.10 --- extras/pom.xml | 2 +- gson/pom.xml | 4 ++-- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index 7e5c87ca..533f78fa 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 3861a935..277ab598 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson @@ -246,7 +246,7 @@ 17 - + diff --git a/metrics/pom.xml b/metrics/pom.xml index 48d943de..ca904248 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 gson-metrics diff --git a/pom.xml b/pom.xml index 140ab0d3..536e591d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - HEAD + gson-parent-2.10 diff --git a/proto/pom.xml b/proto/pom.xml index 02b1ef0d..ff920604 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.9.2-SNAPSHOT + 2.10 proto From 79c27dd885eeacbb41e13e86d83fa7ca85afd3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 17:48:57 -0700 Subject: [PATCH 30/35] [maven-release-plugin] prepare for next development iteration --- extras/pom.xml | 2 +- gson/pom.xml | 2 +- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index 533f78fa..a1851711 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 277ab598..4e7b88a6 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson diff --git a/metrics/pom.xml b/metrics/pom.xml index ca904248..b411d92e 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-metrics diff --git a/pom.xml b/pom.xml index 536e591d..ab58ab37 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - gson-parent-2.10 + HEAD diff --git a/proto/pom.xml b/proto/pom.xml index ff920604..8ed8728f 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT proto From 7ca36c5e67350808e82acf8ddf56b5ef1b21b96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 18:06:21 -0700 Subject: [PATCH 31/35] Add a `` section to the main `pom.xml`. --- pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pom.xml b/pom.xml index ab58ab37..0b8b66d7 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,13 @@ HEAD + + + Google + http://www.google.com + + + GitHub Issues https://github.com/google/gson/issues From dd92e49b279f335006433148e673fdfb2c387074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 18:06:55 -0700 Subject: [PATCH 32/35] [maven-release-plugin] prepare release gson-parent-2.10 --- extras/pom.xml | 2 +- gson/pom.xml | 2 +- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index a1851711..533f78fa 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.10 gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 4e7b88a6..277ab598 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.10 gson diff --git a/metrics/pom.xml b/metrics/pom.xml index b411d92e..ca904248 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.10 gson-metrics diff --git a/pom.xml b/pom.xml index 0b8b66d7..155d98dd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.10 pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - HEAD + gson-parent-2.10 diff --git a/proto/pom.xml b/proto/pom.xml index 8ed8728f..ff920604 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.11-SNAPSHOT + 2.10 proto From 763e69a33117fcfc2b527aee7cda5866cc747b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 24 Oct 2022 18:06:57 -0700 Subject: [PATCH 33/35] [maven-release-plugin] prepare for next development iteration --- extras/pom.xml | 2 +- gson/pom.xml | 2 +- metrics/pom.xml | 2 +- pom.xml | 4 ++-- proto/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extras/pom.xml b/extras/pom.xml index 533f78fa..a1851711 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-extras diff --git a/gson/pom.xml b/gson/pom.xml index 277ab598..4e7b88a6 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -4,7 +4,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson diff --git a/metrics/pom.xml b/metrics/pom.xml index ca904248..b411d92e 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -3,7 +3,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT gson-metrics diff --git a/pom.xml b/pom.xml index 155d98dd..0b8b66d7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT pom Gson Parent @@ -28,7 +28,7 @@ https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git - gson-parent-2.10 + HEAD diff --git a/proto/pom.xml b/proto/pom.xml index ff920604..8ed8728f 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -6,7 +6,7 @@ com.google.code.gson gson-parent - 2.10 + 2.11-SNAPSHOT proto From dd6635dc61626dde42423b61e6427abd946f35cb Mon Sep 17 00:00:00 2001 From: Snowhite Date: Sat, 29 Oct 2022 16:15:43 +0800 Subject: [PATCH 34/35] Making consistent prefixs in PerformanceTest (#1760) * Making consistent prefixs in PerformanceTest change some "disable_" to "disabled_" * Update PerformanceTest.java --- .../test/java/com/google/gson/metrics/PerformanceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 +} From ff96296eeab00269962476c322b7d109851e0253 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 30 Oct 2022 12:01:20 +0100 Subject: [PATCH 35/35] Add 2.10 changes to CHANGELOG; minor release follow-ups (#2229) * Add 2.10 changes to CHANGELOG; minor release follow-ups * Use GitHub URLs in CHANGELOG GitHub automatically displays them only as short reference with link and additionally shows a preview when hovering over them. * Add `id` to pom.xml developer entry Otherwise BND plugin shows a warning. * Run unit tests during release preparation * Move git option before pathspec --- CHANGELOG.md | 105 +++++++++++------- .../gson/internal/bind/JsonTreeReader.java | 1 + pom.xml | 12 +- 3 files changed, 72 insertions(+), 46 deletions(-) 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/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java index 81c3363c..47e70e68 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 @@ -277,6 +277,7 @@ public final class JsonTreeReader extends JsonReader { JsonToken peeked = peek(); switch (peeked) { case NAME: + @SuppressWarnings("unused") String unused = nextName(true); break; case END_ARRAY: diff --git a/pom.xml b/pom.xml index 0b8b66d7..c75937ad 100644 --- a/pom.xml +++ b/pom.xml @@ -33,8 +33,9 @@ + google Google - http://www.google.com + https://www.google.com @@ -144,10 +145,11 @@ false release - + - package -DskipTests + clean verify antrun:run@replace-version-placeholders antrun:run@replace-old-version-references antrun:run@git-add-changed @@ -210,6 +212,8 @@ + +