Smuggling Generic Types into the Runtime
How type tokens use anonymous subclasses and reflection to recover erased generic types
The problem type tokens solve
Java generics normally lose type information at runtime because of type erasure. However, libraries like Jackson and Guava use a clever trick called type tokens to recover generic type information using anonymous subclasses and reflection.
The trick works because Java preserves generic type information in class metadata, even though it erases it from variables and objects.
Suppose you want to deserialize JSON into a generic type:
List<User> users = mapper.readValue(json, List.class);
This fails because the runtime only sees List, not List<User>. Jackson cannot know what element type the list should contain.
The type token trick
Jackson solves this using TypeReference:
List<User> users =
mapper.readValue(json, new TypeReference<List<User>>() {});
The {} creates an anonymous subclass. Because it is a real class, the JVM stores the full generic type in its metadata. Jackson can then retrieve the type using reflection.
What Java stores internally
Even though generics are erased for objects, class definitions retain their generic signature. The anonymous class actually looks like TypeReference<List<User>> internally. The JVM keeps that information in the class file.
Jackson can inspect it like this:
Type type = getClass().getGenericSuperclass();
Which returns something like TypeReference<List<User>>. From that it extracts List<User>.
Simplified implementation of a type token
Here is a minimal example of how a library might implement this idea:
import java.lang.reflect.*;
abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
Type superclass = getClass().getGenericSuperclass();
ParameterizedType parameterized = (ParameterizedType) superclass;
this.type = parameterized.getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
Usage:
TypeToken<List<String>> token =
new TypeToken<List<String>>() {};
System.out.println(token.getType());
// Output: java.util.List<java.lang.String>
The program recovers the erased generic type.
Why the anonymous class is required
This does not work:
TypeToken<List<String>> token = new TypeToken<>(); // erased -- no type info
The compiler erases the generic type for a direct instantiation.
But this works:
new TypeToken<List<String>>() {}
Because the compiler generates a new class like:
// Generated by compiler
class TypeToken$1 extends TypeToken<List<String>> { }
That class contains the full type information in its bytecode Signature attribute.
Proving it: what if TypeToken were concrete?
Our TypeToken is abstract, so you cannot even instantiate it directly. But what if it were concrete? Would the generic type survive? We can test this with a non-abstract version:
public class ConcreteTypeToken<T> {
private final Type type;
public ConcreteTypeToken() {
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof ParameterizedType parameterized) {
this.type = parameterized.getActualTypeArguments()[0];
} else {
// superclass is raw Object -- no ParameterizedType, no generic info to extract
this.type = superclass;
}
}
public Type getType() { return type; }
}
Now compare the two ways of creating one:
// Direct instantiation -- no subclass
ConcreteTypeToken<List<String>> erased = new ConcreteTypeToken<>();
// Anonymous subclass -- the {} smuggles the type through
ConcreteTypeToken<List<String>> preserved = new ConcreteTypeToken<List<String>>() {};
Output:
Direct instantiation:
genericSuperclass: class java.lang.Object // raw Object, not a ParameterizedType
getType(): class java.lang.Object // nothing to extract
Anonymous subclass:
genericSuperclass: ConcreteTypeToken<java.util.List<java.lang.String>>
getType(): java.util.List<java.lang.String> // preserved
Without {}, there is no subclass. getGenericSuperclass() returns raw Object - it is not a ParameterizedType, so there is no generic signature to extract. The type parameter List<String> exists only in the source code and is erased during compilation.
With {}, the compiler generates a new class that extends ConcreteTypeToken<List<String>>. That inheritance relationship is baked into the class file’s Signature attribute, and reflection can read it back at runtime. This is the smuggling mechanism – the generic type hitches a ride inside the subclass metadata.
Guava’s TypeToken
Google’s Guava library provides a full implementation:
TypeToken<List<String>> token =
new TypeToken<List<String>>() {};
token.getType(); // java.util.List<java.lang.String>
token.isSubtypeOf(List.class); // true
Guava’s implementation handles complex cases like Map<String, List<Integer>> and List<? extends Number>.
Why this works despite erasure
Erasure removes generics from objects, but class metadata still contains them. The bytecode stores a Signature attribute:
Signature:
Ljava/lang/Object;
Ljava/util/List<Ljava/lang/String;>;
Libraries exploit this metadata through reflection.
Real libraries using type tokens
| Category | Libraries |
|---|---|
| JSON | Jackson (TypeReference), Gson (TypeToken) |
| Utility | Guava (TypeToken), Spring Framework |
Gson example:
Type type = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
Mental model
Normal generics – type is lost:
List<String> --> runtime sees --> List
Type token trick – type is preserved:
new TypeToken<List<String>>() {}
--> anonymous subclass created
--> JVM stores generic signature in class metadata
--> reflection extracts it at runtime
You effectively smuggle generic type information through class metadata.
Summary
Type tokens work by:
- Creating an anonymous subclass
- The JVM stores its generic superclass signature in bytecode
- Reflection extracts the type via
getGenericSuperclass() - Libraries reconstruct the erased generic type
Java’s designers never intended generics to be used this way, but developers discovered that class metadata, reflection, and anonymous subclasses could work together to recover what erasure takes away.
I like stepping through the code so here it is.
You could just use C#
C# does not need the type token trick because it uses more reified generics – generic type parameters are preserved at runtime by the CLR. List<string> is a distinct type from List<int> all the way down to the JIT-compiled machine code.
The Jackson deserialization problem from earlier:
// Java -- fails, runtime only sees List
List<User> users = mapper.readValue(json, List.class);
// Java -- workaround via type token
List<User> users =
mapper.readValue(json, new TypeReference<List<User>>() {});
In C#, the equivalent just works:
// C# -- no trick needed, runtime knows the type
List<User> users = JsonSerializer.Deserialize<List<User>>(json);
You can also inspect generic types directly:
typeof(List<string>).GetGenericArguments()[0] // returns typeof(string)
No anonymous subclasses, no getGenericSuperclass(), no reflection hacks.
A good example of how a design decision for backward compatibility in Java ripples into patterns that developers have to learn and libraries have to implement.