@cowtowncoder I'm in the middle of creating an abstract database interface that involves transparent storage of objects (initially Java record) with full type conversions. For a corner of the conversion I'm using Jackson ObjectMapper, although I'm not sure about what I will use long-term. In any case I had a chance to dig into my old PLOOP code (described in #37) and how it relates to ResolvedType (see e.g. our discussion in #69). There are several tensions lurking in those discussions, as well as the proposal I made in #76 that GenericType<T> should not really be a java.lang.reflect.Type.
For this current work I just now had a long and in-depth conversation with one of the top LLMs, refreshing my memory about these issues and understanding what it would look like to bring PLOOP up to date and carry it forward using ClassMate. The benefit here is that in our discussion we started with actual, practical, working code that we had just implemented in this database framework. We looked at how our new implementation could be generalized by bringing in PLOOP+ClassMate. We came upon some important insights, which I believe form a synthesis of the tensions I was feeling in those other tickets, especially in light of the new PR #103 which basically tries to reverse a ResolvedType back into a Type. Below is a summary (produced by the LLM) of those insights. I leave a few additional comments at the end.
ClassMate ResolvedType Design Insights
Background
This builds on discussions from #37 (PLOOP properties layer, 2017), #69 (ResolvedType → JavaType conversion), and the draft PR #103 (ResolvedTypeMapper). These issues circle around the same fundamental design tension in ClassMate's type abstraction.
The Core Problem
ResolvedType currently implements java.lang.reflect.Type, which appears to offer seamless interoperability. However, Type has subtype interfaces (ParameterizedType, GenericArrayType, TypeVariable, WildcardType) that code routinely inspects:
Type type = ...;
if (type instanceof ParameterizedType pt) {
Type[] args = pt.getActualTypeArguments();
// process generic arguments
}
ResolvedType doesn't implement these subtypes. Code that receives a ResolvedType through a Type reference and checks instanceof ParameterizedType gets false—silently losing all generic information. This affects Jackson's TypeFactory, Hibernate Validator, and any API that inspects Type structure.
PR #103 addresses this by creating a ResolvedTypeMapper that synthesizes proper ParameterizedType and GenericArrayType implementations from ResolvedType data. This works, but it's an external workaround for what should be built-in behavior.
ResolvedType as a Type Replacement
The insight from working with these APIs is that ResolvedType should be understood as a replacement for Type in application code, not an implementation of it.
Within an application, you'd use ResolvedType everywhere—passing it through property descriptors, converters, serializers. You'd only touch Type at boundaries:
- Input: Converting from
Field.getGenericType(), RecordComponent.getGenericType(), etc.
- Output: Converting to
Type when calling external APIs (Jackson, Hibernate, etc.)
This positions ResolvedType as a richer abstraction that shields application code from Type's design limitations while remaining interoperable.
Why ResolvedType Should Be Generic
Java's reflection types aren't generic—ParameterizedType.getRawType() returns Type, not Type<T>. This forces runtime casts and loses compile-time safety:
Type listType = field.getGenericType(); // List<String>
Class<?> raw = getRawType(listType);
Object result = raw.cast(someValue); // no compile-time safety
A generic ResolvedType<T> would preserve type safety through the chain:
ResolvedType<List<String>> listType = resolver.resolve(field);
Class<List<String>> raw = listType.erasedClass();
List<String> result = raw.cast(someValue); // type-safe
This matters for frameworks passing type information through multiple layers.
Why ResolvedType Should Be an Interface
ClassMate's value is resolving type variables through inheritance hierarchies. But not all contexts need that resolution:
record User(String id, List<String> tags) {}
For records, RecordComponent.getGenericType() returns fully-specified types. ClassMate resolution adds overhead without benefit.
If ResolvedType were an interface, there could be multiple implementations:
- Simple wrapper: Stores the original
Type directly; toType() returns it unchanged
- Resolution result: ClassMate-backed;
toType() synthesizes a Type from resolved data
Both satisfy the same interface. Consumers don't know or care which implementation they receive.
The toType() Bridge
Rather than implementing Type (which fails instanceof checks), ResolvedType should provide an explicit toType() method that returns a proper Type hierarchy:
The synthesized types implement the correct interfaces, so instanceof ParameterizedType works as expected. External APIs receive types they can inspect normally.
This is essentially what PR #103 provides externally. Building it into ResolvedType would make it a first-class capability.
Proposed Design Sketch
public interface ResolvedType<T> {
Class<T> erasedClass();
List<ResolvedType<?>> typeArguments();
Optional<ResolvedType<?>> typeArgument(int index);
boolean isParameterized();
boolean isArray();
Optional<ResolvedType<?>> arrayComponentType();
Type toType(); // original or synthesized
// Factory methods
static <T> ResolvedType<T> of(Class<T> clazz) { ... }
static ResolvedType<?> of(Type type) { ... } // simple wrapper, stores original
static ResolvedType<?> resolve(Type type, Class<?> context) { ... } // full resolution
}
Key design points:
- Interface, not abstract class—allows implementation flexibility
- Generic
<T> parameter—preserves compile-time type safety
- Does not implement
java.lang.reflect.Type—avoids instanceof failures
toType() method—explicit bridge for external API interoperability
- Multiple factory methods—simple wrapping vs. full resolution
Summary
ResolvedType currently occupies an awkward middle ground: implementing Type suggests compatibility, but the subtype contracts aren't satisfied. PR #103 demonstrates that users need proper Type representations for external APIs, but the current design forces external workarounds.
The proposed redesign clarifies ResolvedType's role: a richer, type-safe replacement for Type within application code, with an explicit toType() bridge for interoperability. Making it an interface enables both lightweight wrappers (preserving original types) and full resolution results (synthesizing types), unified under a common abstraction.
This raises lots of questions which we can discuss in the comments. e.g. Is ResolvedType the best name for this new, redesigned thing? Are you interested in going in this direction for a new major ClassMate version?
For the short term in my framework I will create this general, improved ResolvedType interface (probably with a different name), with factories that produce it. The early iterations may have ClassMate and the current ResolvedType serving as backing implementations, depending on how much this helps go forward quickly. Longer term I would hope that this interface on our end could be replaced with a redesigned ClassMate ResolvedType, whatever its name; and/or integrated with PLOOP.
I would also be interested in discussing the possibility of working with you for a new ClassMate that might even have a separate property description facility mentioned in #37 and possibly even used eventually in a new version of Jackson. (With LLMs such large-scale thinking and refactoring suddenly becomes much more feasible, even producing better results than if humans were to painstakingly pore over the code—as long as innovative, design-oriented humans direct and guide the LLMs.)
@cowtowncoder I'm in the middle of creating an abstract database interface that involves transparent storage of objects (initially Java
record) with full type conversions. For a corner of the conversion I'm using JacksonObjectMapper, although I'm not sure about what I will use long-term. In any case I had a chance to dig into my old PLOOP code (described in #37) and how it relates toResolvedType(see e.g. our discussion in #69). There are several tensions lurking in those discussions, as well as the proposal I made in #76 thatGenericType<T>should not really be ajava.lang.reflect.Type.For this current work I just now had a long and in-depth conversation with one of the top LLMs, refreshing my memory about these issues and understanding what it would look like to bring PLOOP up to date and carry it forward using ClassMate. The benefit here is that in our discussion we started with actual, practical, working code that we had just implemented in this database framework. We looked at how our new implementation could be generalized by bringing in PLOOP+ClassMate. We came upon some important insights, which I believe form a synthesis of the tensions I was feeling in those other tickets, especially in light of the new PR #103 which basically tries to reverse a
ResolvedTypeback into aType. Below is a summary (produced by the LLM) of those insights. I leave a few additional comments at the end.ClassMate
ResolvedTypeDesign InsightsBackground
This builds on discussions from #37 (PLOOP properties layer, 2017), #69 (ResolvedType → JavaType conversion), and the draft PR #103 (ResolvedTypeMapper). These issues circle around the same fundamental design tension in ClassMate's type abstraction.
The Core Problem
ResolvedTypecurrently implementsjava.lang.reflect.Type, which appears to offer seamless interoperability. However,Typehas subtype interfaces (ParameterizedType,GenericArrayType,TypeVariable,WildcardType) that code routinely inspects:ResolvedTypedoesn't implement these subtypes. Code that receives aResolvedTypethrough aTypereference and checksinstanceof ParameterizedTypegetsfalse—silently losing all generic information. This affects Jackson'sTypeFactory, Hibernate Validator, and any API that inspectsTypestructure.PR #103 addresses this by creating a
ResolvedTypeMapperthat synthesizes properParameterizedTypeandGenericArrayTypeimplementations fromResolvedTypedata. This works, but it's an external workaround for what should be built-in behavior.ResolvedTypeas aTypeReplacementThe insight from working with these APIs is that
ResolvedTypeshould be understood as a replacement forTypein application code, not an implementation of it.Within an application, you'd use
ResolvedTypeeverywhere—passing it through property descriptors, converters, serializers. You'd only touchTypeat boundaries:Field.getGenericType(),RecordComponent.getGenericType(), etc.Typewhen calling external APIs (Jackson, Hibernate, etc.)This positions
ResolvedTypeas a richer abstraction that shields application code fromType's design limitations while remaining interoperable.Why
ResolvedTypeShould Be GenericJava's reflection types aren't generic—
ParameterizedType.getRawType()returnsType, notType<T>. This forces runtime casts and loses compile-time safety:A generic
ResolvedType<T>would preserve type safety through the chain:This matters for frameworks passing type information through multiple layers.
Why
ResolvedTypeShould Be an InterfaceClassMate's value is resolving type variables through inheritance hierarchies. But not all contexts need that resolution:
For records,
RecordComponent.getGenericType()returns fully-specified types. ClassMate resolution adds overhead without benefit.If
ResolvedTypewere an interface, there could be multiple implementations:Typedirectly;toType()returns it unchangedtoType()synthesizes aTypefrom resolved dataBoth satisfy the same interface. Consumers don't know or care which implementation they receive.
The
toType()BridgeRather than implementing
Type(which failsinstanceofchecks),ResolvedTypeshould provide an explicittoType()method that returns a properTypehierarchy:TypeParameterizedTypeImpl,GenericArrayTypeImpl, etc. (as PR Draft: Issue #69 | Add ResolvedTypeMapper to convert ResolvedType into Type #103 does)The synthesized types implement the correct interfaces, so
instanceof ParameterizedTypeworks as expected. External APIs receive types they can inspect normally.This is essentially what PR #103 provides externally. Building it into
ResolvedTypewould make it a first-class capability.Proposed Design Sketch
Key design points:
<T>parameter—preserves compile-time type safetyjava.lang.reflect.Type—avoidsinstanceoffailurestoType()method—explicit bridge for external API interoperabilitySummary
ResolvedTypecurrently occupies an awkward middle ground: implementingTypesuggests compatibility, but the subtype contracts aren't satisfied. PR #103 demonstrates that users need properTyperepresentations for external APIs, but the current design forces external workarounds.The proposed redesign clarifies
ResolvedType's role: a richer, type-safe replacement forTypewithin application code, with an explicittoType()bridge for interoperability. Making it an interface enables both lightweight wrappers (preserving original types) and full resolution results (synthesizing types), unified under a common abstraction.This raises lots of questions which we can discuss in the comments. e.g. Is
ResolvedTypethe best name for this new, redesigned thing? Are you interested in going in this direction for a new major ClassMate version?For the short term in my framework I will create this general, improved
ResolvedTypeinterface (probably with a different name), with factories that produce it. The early iterations may have ClassMate and the currentResolvedTypeserving as backing implementations, depending on how much this helps go forward quickly. Longer term I would hope that this interface on our end could be replaced with a redesigned ClassMateResolvedType, whatever its name; and/or integrated with PLOOP.I would also be interested in discussing the possibility of working with you for a new ClassMate that might even have a separate property description facility mentioned in #37 and possibly even used eventually in a new version of Jackson. (With LLMs such large-scale thinking and refactoring suddenly becomes much more feasible, even producing better results than if humans were to painstakingly pore over the code—as long as innovative, design-oriented humans direct and guide the LLMs.)