Library / SDK
jhalterman/typetools avatar
jhalterman/typetools

TypeTools, resolving Java generics that the compiler erases

Tools for working with generic types

626 stars96 forksJavaApache-2.0

At a glance

What is it?
TypeTools is a zero-dependency Java library that recovers the generic type information the compiler discards, so a base class can learn at runtime that it is dealing with Long identifiers rather than Serializable ones. It works from fields, methods, lambdas and method references, with an Android caveat and a JVM flag requirement on Java 22 and later.
Who is it for?
TypeTools suits a Java developer building a generic base class that has to act on its type arguments at runtime, most commonly a generic DAO or a mapper that needs the concrete identifier type without a hand-written type token. It does not suit a project that can be written with plain generics, and it does not suit an Android build that needs lambda type resolution, because that depends on a constant pool the Android runtime does not expose.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The type information the compiler throws away

TypeTools is described as a simple, zero-dependency library for working with types, supporting Java 1.6 and later as well as Android. The problem it addresses is stated as one of the sore points with Java.

The specific gap is that Java's generics do not provide a way to resolve or reify the type information for a given class. A generic base class such as `GenericDAO<T, ID extends Serializable>` knows at compile time that it has two parameters, but at runtime the type variables are erased, and a base class that wants to know whether its identifier is a `Long` or a `UUID` has no standard way to find out.

TypeTools fills that gap by fully resolving generic type information declared on any class, interface, lambda expression or method. The `TypeResolver` class is the entry point, and the README lists the core methods: `reify` with a context class, `reify` with a generic type, `resolveRawArguments`, `resolveRawArgument`, `resolveGenericType` and `resolveRawClass`.

The distinction between the two families of method is worth understanding. The `resolve` family gives you the raw class of a type argument, so a `Class<?>`. The `reify` family gives you back a full `Type`, which for a nested generic argument is a `ParameterizedType` rather than a plain class, and that is what lets you walk into `List<Integer>` and get at the `Integer`.

The audience is narrow and technical: developers writing generic infrastructure in Java, most often persistence layers and reflection-heavy frameworks. The zero-dependency claim and the Java 1.6 floor are what make it usable inside a codebase that cannot take on a dependency tree, and the Android support is what makes it usable on a mobile project.

The library is published to Maven Central as `net.jodah:typetools`, with the javadoc hosted separately, and the repository carries a CHANGELOG, a SECURITY.md, a CONTRIBUTING.md and a bin/ directory.

Resolving arguments from a subtype

The core use case is the simplest to state: given a base type and a subtype, what are the type arguments?

java
interface Foo<T1, T2> {}
class Bar implements Foo<Integer, String> {}

Class<?>[] typeArgs = TypeResolver.resolveRawArguments(Foo.class, Bar.class);

assert typeArgs[0] == Integer.class;
assert typeArgs[1] == String.class;

The resolver walks the type hierarchy of `Bar`, finds the point at which `Foo<Integer, String>` is declared, and substitutes the concrete arguments. This is the operation that makes a generic DAO work, and the README gives it as the typical use case.

The generic DAO example in the documentation is worth reading because it shows the pattern the library exists for.

java
class Device {}
class Router extends Device {}

class GenericDAO<T, ID extends Serializable> {
  protected Class<T> persistentClass;
  protected Class<ID> idClass;

  private GenericDAO() {
    Class<?>[] typeArguments = TypeResolver.resolveRawArguments(GenericDAO.class, getClass());
    this.persistentClass = (Class<T>) typeArguments[0];
    this.idClass = (Class<ID>) typeArguments[1];
  }
}

class DeviceDAO<T extends Device> extends GenericDAO<T, Long> {}
class RouterDAO extends DeviceDAO<Router> {}

The resolution happens in the base class constructor, using `getClass()` to find the concrete runtime type. `RouterDAO` extends `DeviceDAO<Router>` extends `GenericDAO<T, Long>`, so the base class can discover at construction time that its persistent class is `Router` and its identifier class is `Long`, and the assertions confirm it.

Note the unchecked casts in the constructor. The library hands back `Class<?>` values and the base class asserts them into `Class<T>` and `Class<ID>` without a runtime check. That is a deliberate trade: the type system cannot verify it, so the correctness of the cast depends on the hierarchy actually being consistent, which is true by construction in normal use and is the part a reviewer should check if the hierarchy is built by unusual means.

The README also notes the layer supertype pattern from Martin Fowler's catalog as a common use case, since layer supertypes often carry type parameters that subclasses populate.

Lambdas and method references, where the JVM helps and does not

TypeTools can also recover type arguments from a lambda expression, which is a genuinely unusual capability because a lambda's class carries no generic signature in the usual sense.

java
Function<String, Integer> strToInt = s -> Integer.valueOf(s);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(Function.class, strToInt.getClass());

assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;

The method reference case is the same idea.

java
Comparator<String> comparator = String::compareToIgnoreCase;
Class<?> typeArg = TypeResolver.resolveRawArgument(Comparator.class, comparator.getClass());

assert typeArg == String.class;

The mechanism behind this is the constant pool, and the documentation is explicit about the limits. Lambda type argument resolution is currently supported out of the box for Eclipse Temurin and Azul Zulu 8 through 21. Lambda support depends on the constant pool, which is not available on Android.

That last sentence is the significant constraint, and it is a hard one. If your Android build needs to resolve the type arguments of a lambda, this library cannot do it there. Class-based resolution still works on Android, so the library is usable on that platform, just not for the lambda cases.

The second limit is more subtle and appears in the section on unresolvable lambda type arguments. When resolving type arguments with lambda expressions, only type parameters used in the functional interface's method signature can be resolved. The example uses a custom interface `ExtraFunction<T, R, Z>` that extends `Function<T, R>`, and the third parameter `Z` cannot be recovered, because it does not appear in the method signature the lambda implements. The documentation truncates mid-assertion there, but the rule is clear: the resolver can only see the parameters the JVM actually encodes.

The practical advice is to treat lambda resolution as working for the common case of a standard functional interface, and to test the specific case you care about rather than assuming.

Reifying nested generics and reading fields and methods

The `reify` method goes further than the raw resolution, and it is the one to reach for when your type argument is itself generic.

java
interface Foo<T> {}
class Bar implements Foo<List<Integer>> {}

Type typeArgs = TypeResolver.reify(Foo.class, Bar.class);

ParameterizedType paramType = (ParameterizedType) typeArgs;
Type[] actualTypeArgs = paramType.getActualTypeArguments();
ParameterizedType arg = (ParameterizedType)actualTypeArgs[0];

assert paramType.getRawType() == Foo.class;
assert arg1.getRawType() == List.class;
assert arg1.getActualTypeArguments()[0] == Integer.class;

The extra step is the cast to `ParameterizedType`. `List<Integer>` is not a `Class`, it is a `ParameterizedType`, so to get to the `Integer` you have to walk the type arguments of the argument. The README's snippet has a naming inconsistency in its assertions, using `arg1` where the variable is named `arg`, and the assertions reference `arg1` in the second and third lines. The intent is clear even if the pasted code would not compile as written.

The other direction is reading type parameters off members rather than off the type hierarchy.

java
class Entity<ID extends Serializable> {
  ID id;
  void setId(ID id) {}
}

class SomeEntity extends Entity<Long> {}

Type fieldType = Entity.class.getDeclaredField("id").getGenericType();
Type mutatorType = Entity.class.getDeclaredMethod("setId", Serializable.class).getGenericParameterTypes()[0];

assert TypeResolver.resolveRawClass(fieldType, SomeEntity.class) == Long.class;
assert TypeResolver.resolveRawClass(mutatorType, SomeEntity.class) == Long.class;

Here the generic type is read directly from the reflection API, the `getGenericType()` of the field and the generic parameter type of the setter, and the resolver is given the concrete subclass to fill in the variable. This is the pattern for code that walks a class's members and needs to know their concrete types, which is what serialization and mapping frameworks do.

Note the `getDeclaredMethod` call takes `Serializable.class` as the parameter type, because that is what the erasure of `ID extends Serializable` is. It has to match the erased signature, not the generic one.

The Java 22 warning and the module opens it asks for

On Java 22 and later, the library triggers a warning, and the documentation explains it precisely.

code
WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
WARNING: sun.misc.Unsafe::staticFieldOffset has been called by net.jodah.typetools.TypeResolverUtil22
WARNING: Please consider reporting this to the maintainers of class net.jodah.typetools.TypeResolverUtil22
WARNING: sun.misc.Unsafe::staticFieldOffset will be removed in a future release

The mechanism behind it is that reading the constant pool of a lambda class, which is how the lambda type arguments are recovered, has no supported API on those releases, so the library falls back on `sun.misc.Unsafe` to reach the field offset. `sun.misc.Unsafe` is on a path to removal, and the message says so.

The documentation gives two ways to avoid the warning, and both are about opening modules. The first is three JVM flags.

code
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
--add-opens java.base/jdk.internal.access=ALL-UNNAMED
--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED

The second is the same idea expressed on the module path, granting access to the library's own module instead of to everything.

code
--add-opens java.base/java.lang.reflect=net.jodah.typetools
--add-opens java.base/jdk.internal.access=net.jodah.typetools
--add-opens java.base/jdk.internal.reflect=net.jodah.typetools

The module-path form is the better one to use in a build, because it grants the opens to the library rather than to all unnamed code, so a wider classpath does not silently widen access to internal JDK packages. The `ALL-UNNAMED` form is fine for a local run.

There is also a caching switch, which matters if you resolve types in a hot path. By default, type variable information for each resolved type is weakly cached by the `TypeResolver`.

java
TypeResolver.enableCache();
TypeResolver.disableCache();

Weakly cached means the cache does not keep a resolved type alive, so a program that resolves many types does not leak, at the cost of recomputation when entries are collected. That default is the right one for most programs, and the switches exist for the cases where you know the working set is small and fixed, or where you would rather not pay the cache bookkeeping at all.

What you get, what it costs, and where the edges are

TypeTools does one thing and does it without pulling anything else in. That is the whole proposition, and it is a reasonable one for a library that sits in the reflection layer of an application.

The case for it is narrow and specific. If you are writing a generic base class that has to behave differently depending on its type arguments, you have three options: write type tokens by hand, use a library like this, or use the language's own answer. Java's own answer is that it does not have one, which is the gap the project names. The zero-dependency claim means adding it does not perturb your dependency tree or your classpath conflict analysis, and the Java 1.6 floor means it drops into an old codebase without a version negotiation.

The limits are documented, and three of them matter. Lambda resolution needs a constant pool, so it is unavailable on Android and only covers type parameters that appear in the functional interface's method signature. The `sun.misc.Unsafe` path used on Java 22 and later depends on a JDK internal that the warning says will be removed, so there is a future in which this needs a different implementation. And the resolver can only report what the class hierarchy and the reflection metadata actually carry, so a type that was never written down anywhere cannot be recovered.

The `reify` method also has a sharp edge worth understanding: it returns a `Type`, and the caller has to cast through `ParameterizedType` to get at nested arguments. The README's own example has a variable naming slip in the assertions, which is a small sign that this path is less exercised in the documentation than the raw resolution path.

For a library at version 0.6.3 with three releases on the same day in October 2026 and a push on 2026-10-20, the project is being worked on. The licence is Apache-2.0, with a LICENSE.txt at the root of the repository, which permits use in commercial and closed-source products with the notice retained, and there is a SECURITY.md for reporting a problem privately.

Editorial conclusion

TypeTools suits a Java developer building a generic base class that has to act on its type arguments at runtime, most commonly a generic DAO or a mapper that needs the concrete identifier type without a hand-written type token. It does not suit a project that can be written with plain generics, and it does not suit an Android build that needs lambda type resolution, because that depends on a constant pool the Android runtime does not expose. Verify first that the type information you need is reachable through the class, field or method you plan to resolve, that your Android build does not depend on lambda argument resolution, and that you add the documented module opens on Java 22 and later so the tool does not trip the sun.misc.Unsafe deprecation warning.

Frequently asked questions

What problem does TypeTools solve in Java?

Java's generics do not provide a way to resolve or reify the type information for a given class, because the type variables are erased. TypeTools resolves generic type information declared on any class, interface, lambda expression or method, so a generic base class can discover its concrete type arguments at runtime.

How do I add TypeTools to a Maven project?

The library is published to Maven Central as net.jodah:typetools, with the javadoc hosted separately. The main entry point is the TypeResolver class, whose methods include reify, resolveRawArguments, resolveRawArgument, resolveGenericType and resolveRawClass.

Does TypeTools work on Android?

Yes for class-based resolution, but the documentation states that lambda support depends on the constant pool, which is not available on Android, so lambda type argument resolution is unavailable there. Lambda resolution is supported out of the box on Eclipse Temurin and Azul Zulu 8 through 21.

What is the sun.misc.Unsafe warning on Java 22 and how do I avoid it?

On Java 22 and later the library warns that a terminally deprecated sun.misc.Unsafe method has been called and will be removed in a future release. The documentation says to avoid it with module opens, either the ALL-UNNAMED flags for java.base/java.lang.reflect, jdk.internal.access and jdk.internal.reflect, or the same opens granted to the net.jodah.typetools module on the module path.

What is the difference between reify and resolveRawArguments in TypeTools?

The resolve family returns the raw class of a type argument as a Class, while reify returns a full Type. The difference matters for nested generics, where the argument is a ParameterizedType such as List<Integer> and the caller has to cast and walk the type arguments to reach the Integer.

Official sources

  1. Issues
  2. jhalterman/typetools on GitHub
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jhalterman-typetools.svg)](https://hysenlabs.com/projects/jhalterman-typetools)

Was this useful?