FasterXML / FasterXML/java-classmate

Redesign `ResolvedType` to be a generic interface that does not implement `Type`

Offen
#111 10 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
Vorherrschende Sprache
Java
Sterne
266
Forks
47
Ø Merge
1 Std. 42 Min.
Gemergte PRs (30 T.)
1

Beschreibung

@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` 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](https://github.com/FasterXML/java-classmate/issues/37) (PLOOP properties layer, 2017), [#69](https://github.com/FasterXML/java-classmate/issues/69) (ResolvedType → JavaType conversion), and the draft [PR #103](https://github.com/FasterXML/java-classmate/pull/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:

```java
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`. This forces runtime casts and loses compile-time safety:

```java
Type listType = field.getGenericType(); // List
Class raw = getRawType(listType);
Object result = raw.cast(someValue); // no compile-time safety
```

A generic `ResolvedType` would preserve type safety through the chain:

```java
ResolvedType> listType = resolver.resolve(field);
Class> raw = listType.erasedClass();
List 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:

```java
record User(String id, List 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:

- For simple wrappers: return the stored original `Type`
- For resolution results: synthesize `ParameterizedTypeImpl`, `GenericArrayTypeImpl`, etc. (as PR #103 does)

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

```java
public interface ResolvedType {
Class erasedClass();
List> typeArguments();
Optional> typeArgument(int index);

boolean isParameterized();
boolean isArray();
Optional> arrayComponentType();

Type toType(); // original or synthesized

// Factory methods
static ResolvedType of(Class 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** `` 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.)

Beitragsleitfaden

Für dieses Repository ist kein Beitragsleitfaden indexiert

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.