-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prevent TypeToken
from capturing type variables
#2376
Changes from all commits
febe8f8
9bc4255
d178fe0
fa64d73
37fd5d0
f0648b6
48785e0
63d5831
2324190
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ This guide describes how to troubleshoot common issues when using Gson. | |
See the [user guide](UserGuide.md#collections-examples) for more information. | ||
- When using `TypeToken` prefer the `Gson.fromJson` overloads with `TypeToken` parameter such as [`fromJson(Reader, TypeToken)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#fromJson(java.io.Reader,com.google.gson.reflect.TypeToken)). | ||
The overloads with `Type` parameter do not provide any type-safety guarantees. | ||
- When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken<List<T>>()` (where `T` is a type variable). Due to Java type erasure the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementClass)`. | ||
- When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken<List<T>>()` (where `T` is a type variable). Due to Java [type erasure](https://dev.java/learn/generics/type-erasure/) the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately. | ||
|
||
## <a id="reflection-inaccessible"></a> `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' | ||
|
||
|
@@ -336,3 +336,22 @@ For Android you can add this rule to the `proguard-rules.pro` file, see also the | |
For Android you can alternatively use the [`@Keep` annotation](https://developer.android.com/studio/write/annotations#keep) on the class or constructor you want to keep. That might be easier than having to maintain a custom R8 configuration. | ||
|
||
Note that the latest Gson versions (> 2.10.1) specify a default R8 configuration. If your class is a top-level class or is `static`, has a no-args constructor and its fields are annotated with Gson's [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html), you might not have to perform any additional R8 configuration. | ||
|
||
## <a id="typetoken-type-variable"></a> `IllegalArgumentException`: 'TypeToken type argument must not contain a type variable' | ||
|
||
**Symptom:** An exception with the message 'TypeToken type argument must not contain a type variable' is thrown | ||
|
||
**Reason:** This exception is thrown when you create an anonymous `TypeToken` subclass which captures a type variable, for example `new TypeToken<List<T>>() {}` (where `T` is a type variable). At compile time such code looks safe and you can use the type `List<T>` without any warnings. However, this code is not actually type-safe because at runtime due to [type erasure](https://dev.java/learn/generics/type-erasure/) only the upper bound of the type variable is available. For the previous example that would be `List<Object>`. When using such a `TypeToken` with any Gson methods performing deserialization this would lead to confusing and difficult to debug `ClassCastException`s. For serialization it can in some cases also lead to undesired results. | ||
|
||
Note: Earlier version of Gson unfortunately did not prevent capturing type variables, which caused many users to unwittingly write type-unsafe code. | ||
|
||
**Solution:** | ||
|
||
- Use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementType)` where `elementType` is a type you have to provide separately. | ||
- For Kotlin users: Use [`reified` type parameters](https://kotlinlang.org/docs/inline-functions.html#reified-type-parameters), that means change `<T>` to `<reified T>`, if possible. If you have a chain of functions with type parameters you will probably have to make all of them `reified`. | ||
- If you don't actually use Gson's `TypeToken` for any Gson method, use a general purpose 'type token' implementation provided by a different library instead, for example Guava's [`com.google.common.reflect.TypeToken`](https://javadoc.io/doc/com.google.guava/guava/latest/com/google/common/reflect/TypeToken.html). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not completely sure to what extent the Guava |
||
|
||
For backward compatibility it is possible to restore Gson's old behavior of allowing `TypeToken` to capture type variables by setting the [system property](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/System.html#setProperty(java.lang.String,java.lang.String)) `gson.allowCapturingTypeVariables` to `"true"`, **however**: | ||
|
||
- This does not solve any of the type-safety problems mentioned above; in the long term you should prefer one of the other solutions listed above. This system property might be removed in future Gson versions. | ||
- You should only ever set the property to `"true"`, but never to any other value or manually clear it. Otherwise this might counteract any libraries you are using which might have deliberately set the system property because they rely on its behavior. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ | |
import java.lang.reflect.ParameterizedType; | ||
import java.lang.reflect.Type; | ||
import java.lang.reflect.TypeVariable; | ||
import java.lang.reflect.WildcardType; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
@@ -38,11 +39,12 @@ | |
* <p> | ||
* {@code TypeToken<List<String>> list = new TypeToken<List<String>>() {};} | ||
* | ||
* <p>Capturing a type variable as type argument of a {@code TypeToken} should | ||
* be avoided. Due to type erasure the runtime type of a type variable is not | ||
* available to Gson and therefore it cannot provide the functionality one | ||
* might expect, which gives a false sense of type-safety at compilation time | ||
* and can lead to an unexpected {@code ClassCastException} at runtime. | ||
* <p>Capturing a type variable as type argument of an anonymous {@code TypeToken} | ||
* subclass is not allowed, for example {@code TypeToken<List<T>>}. | ||
* Due to type erasure the runtime type of a type variable is not available | ||
* to Gson and therefore it cannot provide the functionality one might expect. | ||
* This would give a false sense of type-safety at compile time and could | ||
* lead to an unexpected {@code ClassCastException} at runtime. | ||
* | ||
* <p>If the type arguments of the parameterized type are only available at | ||
* runtime, for example when you want to create a {@code List<E>} based on | ||
|
@@ -64,7 +66,14 @@ public class TypeToken<T> { | |
* | ||
* <p>Clients create an empty anonymous subclass. Doing so embeds the type | ||
* parameter in the anonymous class's type hierarchy so we can reconstitute it | ||
* at runtime despite erasure. | ||
* at runtime despite erasure, for example: | ||
* <p> | ||
* {@code new TypeToken<List<String>>() {}} | ||
* | ||
* @throws IllegalArgumentException | ||
* If the anonymous {@code TypeToken} subclass captures a type variable, | ||
* for example {@code TypeToken<List<T>>}. See the {@code TypeToken} | ||
* class documentation for more details. | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
protected TypeToken() { | ||
|
@@ -83,6 +92,10 @@ private TypeToken(Type type) { | |
this.hashCode = this.type.hashCode(); | ||
} | ||
|
||
private static boolean isCapturingTypeVariablesForbidden() { | ||
return !Objects.equals(System.getProperty("gson.allowCapturingTypeVariables"), "true"); | ||
} | ||
|
||
/** | ||
* Verifies that {@code this} is an instance of a direct subclass of TypeToken and | ||
* returns the type argument for {@code T} in {@link $Gson$Types#canonicalize | ||
|
@@ -93,7 +106,12 @@ private Type getTypeTokenTypeArgument() { | |
if (superclass instanceof ParameterizedType) { | ||
ParameterizedType parameterized = (ParameterizedType) superclass; | ||
if (parameterized.getRawType() == TypeToken.class) { | ||
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); | ||
Type typeArgument = $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); | ||
|
||
if (isCapturingTypeVariablesForbidden()) { | ||
verifyNoTypeVariable(typeArgument); | ||
} | ||
return typeArgument; | ||
} | ||
} | ||
// Check for raw TypeToken as superclass | ||
|
@@ -108,6 +126,39 @@ else if (superclass == TypeToken.class) { | |
throw new IllegalStateException("Must only create direct subclasses of TypeToken"); | ||
} | ||
|
||
private static void verifyNoTypeVariable(Type type) { | ||
if (type instanceof TypeVariable) { | ||
TypeVariable<?> typeVariable = (TypeVariable<?>) type; | ||
throw new IllegalArgumentException("TypeToken type argument must not contain a type variable; captured type variable " | ||
+ typeVariable.getName() + " declared by " + typeVariable.getGenericDeclaration() | ||
+ "\nSee " + TroubleshootingGuide.createUrl("typetoken-type-variable")); | ||
} else if (type instanceof GenericArrayType) { | ||
verifyNoTypeVariable(((GenericArrayType) type).getGenericComponentType()); | ||
} else if (type instanceof ParameterizedType) { | ||
ParameterizedType parameterizedType = (ParameterizedType) type; | ||
Type ownerType = parameterizedType.getOwnerType(); | ||
if (ownerType != null) { | ||
verifyNoTypeVariable(ownerType); | ||
} | ||
|
||
for (Type typeArgument : parameterizedType.getActualTypeArguments()) { | ||
verifyNoTypeVariable(typeArgument); | ||
} | ||
} else if (type instanceof WildcardType) { | ||
WildcardType wildcardType = (WildcardType) type; | ||
for (Type bound : wildcardType.getLowerBounds()) { | ||
verifyNoTypeVariable(bound); | ||
} | ||
for (Type bound : wildcardType.getUpperBounds()) { | ||
verifyNoTypeVariable(bound); | ||
} | ||
} else if (type == null) { | ||
// Occurs in Eclipse IDE and certain Java versions (e.g. Java 11.0.18) when capturing type variable | ||
// declared by method of local class, see https://github.com/eclipse-jdt/eclipse.jdt.core/issues/975 | ||
throw new IllegalArgumentException("TypeToken captured `null` as type argument; probably a compiler / runtime bug"); | ||
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is quite an interesting bug, which even occurred even on CI when running with JDK 11, so in the corresponding unit tests it checks for this exception message as well. See also eclipse-jdt/eclipse.jdt.core#975 |
||
} | ||
} | ||
|
||
/** | ||
* Returns the raw (non-generic) type for this type. | ||
*/ | ||
|
@@ -334,7 +385,7 @@ public static <T> TypeToken<T> get(Class<T> type) { | |
* Class<V> valueClass = ...; | ||
* TypeToken<?> mapTypeToken = TypeToken.getParameterized(Map.class, keyClass, valueClass); | ||
* }</pre> | ||
* As seen here the result is a {@code TypeToken<?>}; this method cannot provide any type safety, | ||
* As seen here the result is a {@code TypeToken<?>}; this method cannot provide any type-safety, | ||
* and care must be taken to pass in the correct number of type arguments. | ||
* | ||
* <p>If {@code rawType} is a non-generic class and no type arguments are provided, this method | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refers to https://dev.java here and below because that page contains a good explanation, and also this seems to be the official Java website, even though it is probably not that well known yet because it is still quite new.