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

Convert GenericType or ResolvedType to JavaType #69

Open
jingkaimori opened this issue Mar 13, 2022 · 19 comments
Open

Convert GenericType or ResolvedType to JavaType #69

jingkaimori opened this issue Mar 13, 2022 · 19 comments

Comments

@jingkaimori
Copy link

Some function like Jackson2JsonRedisSerializer requires JavaType as parameter. When I tried to pass generic type parameter via GenericType object, I find that GenericType can only be converted to ResolvedType, and there is no way from ResolvedType to JavaType.

TypeResolver resolver = new TypeResolver();
GenericType gtype = new GenericType<T>(){}
ResolvedType rtype = resolver.resolve(gtype);
// seems correct but constructType will throw when receiving GenericType!
JavaType javatype= TypeFactory.defaultInstance().constructType(rtype);
@cowtowncoder
Copy link
Member

cowtowncoder commented Mar 14, 2022

Correct: ResolvedType of classmate is not related to JavaType of Jackson.
Unfortunately I do not think it could be made to, either; Classmate definitely should NOT have a dependency to jackson-databind, but same is true for the opposite: Jackson should not have to add dependency to Classmate.

It would definitely be nice to have some means to convert ResolvedType into JavaType, esp. if it could be done without actual class level dependency. But I do not know how this could be achived.

I am open to ideas for supporting some means of conversion, with the constraint that no dependencies may be added across these 2 libraries (Jackson, ClassMate).

@rfox12
Copy link

rfox12 commented Mar 17, 2022

+1 for interoperability between Jackson and Classmate (two solid dependencies for me).

In my case lots of good introspection is performed by Jackson... then repeated by Classmate.

Unless... Is there a way to have Jackson's JavaType tell me about that type's (generic) fields?

@cowtowncoder
Copy link
Member

I'd definitely be +1 for improvements, just not sure where and how (plus, if I had time to directly work on it).

But as to JavaType, it depends: it can tell about generic type parameters of type itself, but not of "contained" things (Fields, Methods etc). For that Jackson has BasicBeanIntrospector and some other types; but those are not easily usable from outside of framework itself (like when handling callback to find (de)serializer and so on).
So what specifically do you have in mind?

@rfox12
Copy link

rfox12 commented Mar 22, 2022

I won't be so bold as to make an ask. After doing some digging on my own I arrived at what you just told me... no easy path. I love and use both libraries and from a purely aspirational perspective I think it would be nice if Classmate was flexible enough so that Jackson used Classmate. But as the saying goes, I'm sure we all have bigger fish to fry. For me (and probably most programs) doing introspection twice (or even five times) on startup doesn't really impact me. Thanks for your contributions!

@cowtowncoder
Copy link
Member

Yeah. The origin story of ClassMate is that it came out of Jackson internals, cleaned up.
Unfortunately this also makes the integration rather difficult: Jackson JavaType is a little bit too tightly coupled to what Jackson does, wrt semantics (think MapType, CollectionType).

In some ways one would probably need to have custom TypeFactory (jackson-databind) that could take ResolvedType -- API/call-wise it might already allow passing it as ResolvedType implements (I think?) java.lang.reflect.Type; but of course there is no logic for conversion.

Such thing -- ClassMateTypeFactory? -- would live in a separate package as I wouldn't really want to add classmate dependency on jackson-databind if just adding sort of new ability, but without getting rid of existing JavaType handling.
It would offer some level of integration, still.
I guess it wouldn't be 100% mandatory to sub-class TypeFactory, however; maybe simpler would be to have something that uses TypeFactory to just construct JavaType... this might be better way to go, although be slightly less convenient to use (being additional converter one needs to call explicitly).

The bigger approach of making Jackson just use ClassMate is something that ultimately would probably be good (despite adding one more dependency), except for one thing: since JavaType is exposed via Jackson APIs, it'd be impossible to do this just as internal rewrite (or not impossible; impractical? herculean undertaking?).

@garretwilson
Copy link

garretwilson commented Oct 4, 2023

Thank you for the ClassMate library. I second the need for a conversion from GenericType/ResolvedType to Jackson's JavaType.

Classmate definitely should NOT have a dependency to jackson-databind, but same is true for the opposite: Jackson should not have to add dependency to Classmate.

True and true, but nothing says a support module published as a separate artifact (e.g. jackson-classmate) can't have a dependency on ClassMate. In fact I think that's an entirely natural and appropriate approach.

In other words, assuming you have a multimodule Maven project, you just add another aggregated subproject with only these conversion utilities, and give the project an artifact ID of jackson-classmate. That subproject would have a dependency on both jackson-databind and classmate. Everything compiles and deploys together (as separate artifacts of course). Then developers can simply add jackson-classmate to their project if they need the conversion utilities.

@garretwilson
Copy link

garretwilson commented Oct 4, 2023

Actually let me take a step back and address the question itself. I have a GenericType<T> from ClassMate, and I need to call ObjectReader.forType(…). There are several variations of this method, including forType(JavaType valueType) and forType(TypeReference<?> valueTypeRef).

It seems the most direct analog of ClassMate GenericType<?> in Jackson is TypeReference<?>. So I suppose I need to do one of the following:

  • Convert GenericType<?> to TypeReference<?>.
  • Convert GenericType<?> to JavaType (equivalent to TypeReference.getType()).
  • Convert ResolvedType to TypeReference<?>.
  • Convert ResolvedType (from TypeResolver.resolve(GenericType<?>)) to JavaType (equivalent to TypeReference.getType()).

Which one of these is easier—or even possible?

Since I'm calling ObjectReader.forType(…), maybe there is an easier way, because (at the moment) all I care about is getting the correct type of ObjectReader.

I see that forType(TypeReference<?> valueTypeRef) actually calls:

return forType(_config.getTypeFactory().constructType(valueTypeRef.getType()))

That uses the TypeReference.getType(), which actually calls:

Type superClass = getClass().getGenericSuperclass();
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

So (not having tried this), could we construct a JavaType from a GenericType<T> like this?

GenericType<?> genericType = new GenericType<Foo<Bar>>(){}; //just like TypeReference
Type genericTypeSubclassSuperClass = genericType.getClass().getGenericSuperclass();
//TODO do sanity check `genericTypeSubclassSuperClass instanceof Class<?>` if desired
Type unwrappedGenericType = ((ParameterizedType) genericTypeSubclassSuperClass).getActualTypeArguments()[0];
JavaType = _config.getTypeFactory().constructType(unwrappedGenericType);

I'm inferring all of this just from perusing the source code—I haven't tested any of it, and I don't even now if it will compile. but it seems reasonably simple and straightforward

The only mystery at the moment is that I haven't searched enough to find out how to get a _config.getTypeFactory() to use in creating the JavaType, but in the question @jingkaimori indicates that I can use a default TypeFactory to do the same thing. (I wonder if there is a benefit of using the DeserializationConfig of the ObjectReader.) I'll look into this more.

Update: Oh, the DeserializationConfig gets to the ObjectReader from ObjectMapper.getDeserializationConfig(), which is public. So if worst comes to worse, I can use the config from the object mapper. But the point of having an ObjectReader is that I don't even want to have access to the ObjectMapper directly, so let me see if there is something better.

Update: Duh 🤦‍♂️ — the ObjectReader itself has a public getConfig().

So all indications are that I would be able to create a JavaType from a GenericType<?> using any TypeFactory; and in the case of ObjectReader.forType(…) I would use the TypeFactory from the ObjectReader itself. For a general case, you could use TypeFactory.defaultInstance() as in the question, but I highly recommend using a type factory appropriate for the context for which you need the JavaType—at least the one that comes from an existing ObjectMapper.

I'm going to pause work on this for today, but tomorrow I'll test the conversion code above and write unit tests for it. There seems to be no mystery—I would be willing to bet a bagel (to invent an expression) that it will work just fine.

@garretwilson
Copy link

I couldn't wait until tomorrow. 😄 Here's the code:

public static JavaType toJavaType(TypeFactory typeFactory, GenericType<?> genericType) {
  Type genericTypeSubclassSuperClass = genericType.getClass().getGenericSuperclass();
  //TODO do sanity check `genericTypeSubclassSuperClass instanceof Class<?>` if desired
  Type unwrappedGenericType = ((ParameterizedType)genericTypeSubclassSuperClass).getActualTypeArguments()[0];
  return typeFactory.constructType(unwrappedGenericType);
}

Here's the unit test:

@Test
void test() {
  @SuppressWarnings("serial")
  final GenericType<?> genericType = new GenericType<List<String>>() {};
  final TypeReference<?> typeReference = new TypeReference<List<String>>() {};
  final TypeFactory typeFactory = TypeFactory.defaultInstance();
  final JavaType javaTypeFromTypeReference = typeFactory.constructType(typeReference.getType());
  assertThat(toJavaType(typeFactory, genericType), is(javaTypeFromTypeReference));
}

I'll create another convenience method:

public static ObjectReader forType(ObjectReader objectReader, TypeFactory typeFactory, GenericType<?> genericType)

This method will simply delegate to the method above, passing objectReader.getConfig.getTypeFactory() so that we know we're using the correct type factory, and then delegate to ObjectReader.forType(JavaType valueType).

I haven't yet tried the resulting ObjectReader, but since forType(TypeReference<?> valueTypeRef) creates a JavaType exactly as I do in the unit test, I can't imagine how it couldn't work.

@garretwilson
Copy link

garretwilson commented Oct 5, 2023

(Note that I'm not sure why GenericType decided to implement Serializable. This means I'll have to suppress a compiler warning about the lack of a serialVersionUID every time I do something like new GenericType<String>(){}, which is like … basically every single time a use it. 😢 I guess I'll open a separate ticket for that. Update: I opened #73 for that issue.)

@garretwilson
Copy link

garretwilson commented Oct 5, 2023

I had another thought. You'll note that the conversion logic I gave above to convert ClassMate GenericType<?> to Jackson JavaType will actually work with any "Super Type Token", to use Neil Gafter's term. Therefore instead of adding a jackson-classmate library just for conversions, you could actually add a generalized method in Jackson to work with any such type; e.g. in Jackson ObjectReader:

public ObjectReader forType(JavaType valueType) { // already exists
  …
}

public ObjectReader forType(TypeReference<?> valueTypeRef) { // already exists
  …
}

public ObjectReader forSuperTypeToken(Object superTypeToken) { //new method
  Type superTypeTokenSuperClass = superTypeToken.getClass().getGenericSuperclass();
  //TODO do sanity check `superTypeTokenSuperClass instanceof Class<?>` if desired
  Type unwrappedType = ((ParameterizedType)superTypeTokenSuperClass).getActualTypeArguments()[0];
  return forType(_config.getTypeFactory().constructType(unwrappedType);
}

This should work not only for ClassMate's GenericType, but for Spring's ParameterizedTypeReference, Guava's TypeToken, and all the classes from other libraries that follow the same pattern.

Of course you would want to add appropriate guard code to ensure the Object passed conforms to what you'd expect for this pattern.

You may decide for whatever reason you don't want to add this sort of feature to Jackson. I'm just pointing out it's possible.

@cowtowncoder
Copy link
Member

Agreed on many points: yes, something could definitely depend on ClassMate and Jackson-databind. Similarly, ability to use "generalized" super token pattern seems like an interesting idea.

My main concern really (beyond avoiding wrong kind of dependencies) has to do with API design. And I think that instead of adding super-type token into ObjectMapper or ObjectReader (and presumably ObjectWriter too), more natural place would be TypeFactory, which could resolve these into JavaType, for use anywhere.
But I can then see the challenge of users actually finding this functionality...

Still, even if something else were to be added in other places, I would love to get a PR for adding resolution method (with simple validation as suggested that there's 1-and-only-1 type parameter, and super type is Object) added in TypeFactory.
I think method name should reflect "super type" somewhere given that nominal type, Object, is so... "generic" (no pun intended).
I also wonder if it'd be awkward to even require that class name itself contains "Type". Just as additional sanity check.

@garretwilson
Copy link

And I think that instead of adding super-type token into ObjectMapper or ObjectReader (and presumably ObjectWriter too), more natural place would be TypeFactory, which could resolve these into JavaType, for use anywhere. But I can then see the challenge of users actually finding this functionality...

I'm not so worried about users finding the conversion code in TypeFactory; my bigger worry is the developer using the wrong TypeFactory e.g. trying to create an ObjectReader from an existing one for a specific GenericType<?>.

In other words, I agree that the conversion code should go in TypeFactory (if we decide we want to do a general toJavaType(…) method from an Object super type token), but I also think that another ObjectReader.forSuperTypeToken(…) should be added that will delegate to the TypeFactory, using the correct TypeFactory from the ObjectReader. (The same goes for other locations I'm sure.

I wouldn't mind doing this. I'm in the middle of some intense coding for another project, though, so I won't get to it immediately.

@garretwilson
Copy link

garretwilson commented Oct 5, 2023

A comment here for whoever gets time to address this: I see this note in Jackson TypeReference:

        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class<?>) { // sanity check, should never happen
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        }

I think the point here is that we expect the generic super type not to be just a plain class, but some parameterized type that contains type information. But I think if checks were added below specifically for that, those would be more direct and include the check here. I'm referring to:

        /* 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect
         *   it is possible to make it fail?
         *   But let's deal with specific
         *   case when we know an actual use case, and thereby suitable
         *   workarounds for valid case(s) and/or error to throw
         *   on invalid one(s).
         */
        _type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

Thus the improved, pretty code looks like this:

/**
 * Converts a ClassMate "generic type" to a Jackson "Java type" using the supplied type factory.
 * @param typeFactory The type factory for creating Jackson types.
 * @param genericType The ClassMate generic type holder.
 * @return A Jackson Java type token instance.
 * @throws IllegalArgumentException if the given type is not a direct subclass of {@link GenericType} providing a single generic type parameter.
 */
public static JavaType toJavaType(@Nonnull final TypeFactory typeFactory, @Nonnull final GenericType<?> genericType) {
  final Type genericTypeSubclassSuperClass = genericType.getClass().getGenericSuperclass();
  if(genericTypeSubclassSuperClass instanceof ParameterizedType parameterizedType) {
    checkArgument(GenericType.class.equals(parameterizedType.getRawType()), "Type token must be immediate subclass of `%s`.",
        GenericType.class.getSimpleName());
    final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
    if(actualTypeArguments.length == 1) {
      return typeFactory.constructType(actualTypeArguments[0]);
    }
  }
  throw new IllegalArgumentException("Type token must provide a single generic type argument.");
}

This is code specific to GenericType, using Java 17 and my own preconditions utility. But it can be modified to work with any "super type token" just by removing the argument check for GenericType.class.equals(parameterizedType.getRawType().

@cowtowncoder
Copy link
Member

Right, Java 17 cannot be used yet.

Point about ObjectReader makes sense, although given that method should not be static at all (for this reason), maybe it's not quite as likely users use wrong instance (but what do I know).

I would still start with TypeFactory adding member method, fwtw.

@garretwilson
Copy link

garretwilson commented Oct 10, 2023

I would still start with TypeFactory adding member method, fwtw.

The other issue in my mind is naming; if we have a method that takes a general "super type token" (such as GenericType<?> or ParameterizedType<?>), what do we call it? Above I had been using "super type token", e.g. ObjectReader.forSuperTypeToken(…), using Neil Gafter's term, but that term is sort of awkward and confusing. Upon reading his article again, I realize that "super type token" is really a specialized technique for the general "type token" technique we have been using for eons with Class<?> as the token. This technique is a type token technique using super types holding the token.

So one approach for the naming would be to simply call it a "type token", and have code that detects whether we are passing a Class<?> or a type token "wrapper" such as GenericType<?>. (In fact Guava even calls theirs a TypeToken<?>.) Thus we would have a method like ObjectReader.forTypeToken(…) and there would be a couple of lines saying if(typeToken instanceof Class<?>) … else //unwrap the super type token. The naming becomes clearer and the method becomes even more general.

It would look like this:

/**
 * Returns a type represented by a type token: either a {@link Class}; or a direct subclass of some parameterized type (a <dfn>super type token</dfn>), where
 * the super class parameterized type represents the type.
 * @param typeToken The type token: either a {@link Class} or a super type token such as <code>com.fasterxml.classmate.GenericType<T></code>, Spring
 *          <code>org.springframework.core.ParameterizedTypeReference<T>, or Guava <code>com.google.common.reflect.TypeToken<T></code>.
 * @return The type represented by the type token.
 * @see <a href="https://gafter.blogspot.com/2006/12/super-type-tokens.html">Super Type Tokens</a>
 * @throws IllegalArgumentException if the given object is not a {@link Class}; or a direct subclass of a class providing a single generic type parameter.
 */
public static Type typeTokenToType(@Nonnull final Object typeToken) {
  if(typeToken instanceof Class<?> clazz) { //a class as a type token is already the type
    return clazz;
  }
  //super type token
  final Type superTypeTokenSuperClass = typeToken.getClass().getGenericSuperclass();
  if(superTypeTokenSuperClass instanceof ParameterizedType parameterizedType) {
    final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
    if(actualTypeArguments.length == 1) {
      return actualTypeArguments[0];
    }
  }
  throw new IllegalArgumentException("Type token must be an instance of `Class`, or have a super class providing a single generic type argument.");
}

That's the generalized "convert type token to Type" method. (The typeToken instanceof Class<?> clazz can easily be split into two lines for lower Java versions, of course.)

I'll likely add a method like this to my PLOOP library, which we discussed years ago and which will soon come back to life.

For Jackson a new TypeFactory method might look like this:

public JavaType constructTypeFromTypeToken(final Object typeToken) {
  return constructType(typeTokenToType(typeToken));
}

Well that was easy!

And finally the convenience method for ObjectReader might look like this:

public ObjectReader forTypeToken(final Object typeToken) {
  return forType(getConfig().getTypeFactory().constructTypeFromTypeToken(typeToken));
}

Then all the following would work:

  • myObjectReader.forTypeToken(String.class)
  • myObjectReader.forTypeToken(new GenericType<List<Foo>>(){})
  • myObjectReader.forTypeToken(new ParameterizedTypeReference<List<Foo>>(){})
  • … (all those other libraries using similar super type token types)

(I realize that at some point we'll need to open a ticket in Jackson to add this. I can add it when I get time; I'm trying to juggle several things at once as I work on my own project which is using this.)

@garretwilson
Copy link

garretwilson commented Oct 10, 2023

Let me come full circle back to the original question. I've already fully explained how to convert from a ClassMate GenericType<?> to a Jackson JavaType by extracting the Type from the GenericType<?> and using Jackson TypeFactory.constructType(Type) on the extracted Type.

But what about converting from ResolvedType as @jingkaimori originally asked? In my case, I might decide I want to convert all types to ClassMate ResolvedType to pass around, and only occasionally call Jackson. ResolvedType seems more appropriate to use for general passing across several methods; GenericType<?> is only a (necessary) hack to get the ball rolling. The problem, as Jackson knows nothing about ResolvedType, is that ResolvedType seems to have thrown away the original Type!

Let me explain it a different way. If I find a Method, I can find the Type (maybe a ParameterizedType for List<String> for example) of its first parameter using method.getGenericParameterTypes()[0]. I assume then that I can construct a Jackson JavaType for the first parameter using typeFactory.constructType(method.getGenericParameterTypes()[0]). But if instead of saving the Type for the first method parameter, what if I immediately resolve it using new TypeResolver.resolve(method.getGenericParameterTypes()[0])? This gives me a ResolvedType—but what happened to the original Type representing the method parameter type? As far as I can tell it has been discarded, so that I can no longer call typeFactory.constructType(resolvedType), as @jingkaimori pointed out.

If only there were a ResolvedType.getType() or something that would give me back the original type—or some (reasonably short) algorithm to reconstruct it.

If there is not, after I pull out my PLOOP library and dust it off, I may improve its TypeInfo<T> class to store, not only the ClassMate ResolvedType, but also the original Type that was used as input to TypeResolver. This will give me a convenient TypeInfo<T> type holder to pass around, providing me acess to ResolvedType but also giving me the original Type in case I need to use it with Jackson.

@cowtowncoder
Copy link
Member

I agree that converting from ResolvedType to JavaType would be much more useful than "super type token" (for which names that I can think of would include "type reference", "super-type reference", "type wrapper").

But I am not sure integration for ResolvedType -> JavaType would nicely fit into either library as per earlier discussion.
However, it could definitely make a reasonable stand-alone library at first.
And maybe, just maybe, one could make it some kind of extension of TypeFactory... either with existing extension point (TypeModifier? maybe not), or, more likely, adding new one. I doubt I'll have time to focus on that, but -- as always! -- happy to help others as sidekick if they want to give it a try.

Does this make sense? Going with PLOOP probably makes sense then.

@garretwilson
Copy link

Does this make sense? Going with PLOOP probably makes sense then.

Sure, sounds fine. The biggest thing I was wanting to confirm here (besides thinking out loud) is that there wasn't a ResolvedType -> Type I was missing somewhere. And I think it's clear now there isn't. Thanks for the response.

In the meantime, if you get a chance to work on any ClassMate things, I would say #74 and #75 should get the highest priority. That way it would be easier for me and/or others to jump in and help with other tickets if needed.

@cowtowncoder
Copy link
Member

The biggest thing I was wanting to confirm here (besides thinking out loud) is that there wasn't a ResolvedType -> Type I was missing somewhere.

Correct: there isn't. I am not a fan of java.lang.reflect.Type. Although TBH I have occasionally wondered about possibly trying to use it for conversions -- it might (but might not) work. At least that type and all subtypes are pure interfaces so with enough work... maybe?

I'll see if I find time to tackle issues mentioned.

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

No branches or pull requests

4 participants