Skip to content
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

Add API for retrieving type arguments of parameterized type #1952

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Marcono1234
Copy link
Collaborator

@Marcono1234 Marcono1234 commented Sep 6, 2021

Purpose

Resolves #333
Probably also resolves #1334

When users write custom type adapters for their generic classes, they need to get the resolved type arguments to get the respective adapters for them. A common example are collection types similar to Map<K, V> or List<E>. Gson internally has logic for retrieving type arguments, but it does not expose this as public API. This leads to users repeatedly writing the same code. Additionally user-written code is sometimes incomplete because it forgets to consider that subtypes might have different type parameters, e.g. StringKeyMap<V> implements Map<String, V>.

This pull request adds one method to TypeToken for simplifying this: TypeToken.getTypeArguments(Class<?> supertype)

Usage example (snippet from a hypothetical Map<K, V> TypeAdapterFactory):

Class<?> rawType = typeToken.getRawType();
if (!Map.class.isAssignableFrom(rawType)) {
  return null;
}

Type[] typeArguments = typeToken.getTypeArguments(Map.class);
Type keyType = typeArguments[0]; // Argument for parameter K of type Map<K, V>
Type valueType = typeArguments[1]; // Argument for parameter V of type Map<K, V>

Implementation

Overlaps in some parts with #1753

The main implementation is based on $Gson$Types#getSupertype which is already used by the Collection and Map type adapter factories. However unlike those factories the getTypeArguments method performs additional resolution by using (transitive) type bounds of type variables and wildcards. This matches more closely how the type behaves when the user programs against its API, e.g. for a raw type of NumberCollection<E extends Number> only Number instances can be added. Similarly getTypeArguments would return Number as type argument.
For type variables it only considers the first bound (and ignores other bounds, if any); otherwise the implementation might become too complex and additionally Gson's TypeToken cannot represent intersection types properly.

To support this properly, TypeToken was changed to report the erasure of the first bound of a TypeVariable as raw type instead of just Object. However, this should most likely affect only very few existing projects, probably mostly cases where raw types were used or type variables were captured using TypeToken (which is error-prone and might be good to disallow in the future, see also #1753).

The implementations of Collection and Map type adapter factories have not been changed to use getTypeArguments to not change their behavior too much; though it might be worth considering this in the future.

Open questions

  • Currently type variables and wildcards are only resolved when returned directly. However, for other types such as parameterized types nested type variables and wildcards are not resolved. For example when the result is List<E> with E being a type variable, then that is not resolved. This assumes that the type adapter for List would then also call getTypeArguments to resolve E, but for the built-in adapters this is currently not the case.
    The question is whether all occurrences of type variables and wildcards should be resolved. If so this should probably become part of $Gson$Types.resolve, activated with a boolean parameter.

@google-cla google-cla bot added the cla: yes label Sep 6, 2021
@Marcono1234 Marcono1234 force-pushed the marcono1234/parameterized-type-resolution-api branch 2 times, most recently from 000021e to 44e6cdc Compare September 7, 2021 23:15
@Marcono1234 Marcono1234 force-pushed the marcono1234/parameterized-type-resolution-api branch from 44e6cdc to df2436f Compare September 10, 2021 19:07
@eamonnmcmanus
Copy link
Member

Gson is in maintenance mode and we are generally reluctant to take on new features. I would say particularly so for something like this that is potentially quite tricky to get right in the details. What I think would be better would be documentation explaining how to achieve tasks like this with existing APIs. In particular, Guava also has a TypeToken API which is much more complete as well as being fully supported. (The use of the same class name in Guava and Gson is a bit irritating, but there it is.)

You can make a Guava TypeToken out of a Gson one like this:

import com.google.gson.reflect.TypeToken;

public class StringKeyMap<V> extends AbstractMap<String, V> {
  private static void test() {
    var gsonTypeToken = new TypeToken<StringKeyMap<Integer>>() {};
    var guavaTypeToken = com.google.common.reflect.TypeToken.of(gsonTypeToken.getType());
    var mapK = Map.class.getTypeParameters()[0];
    var mapV = Map.class.getTypeParameters()[1];
    var stringMapK = guavaTypeToken.resolveType(mapK); // String
    var stringMapV = guavaTypeToken.resolveType(mapV); // Integer
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Extract getContainerElementType() method in $Gson$Types Public APIs for type introspection
2 participants