Repository navigation
Resolve generic types with one resolver for both mappers (plan item 4.4) - #4435
Merged
Merged
Conversation
TypeResolver binds each type variable a class's supertypes declare to the type the hierarchy gives it, by identity rather than by name or position, cached per class with a ClassValue. TypeData, the reflective mapper and critter's PropertyModelGenerator all use it, replacing three separate implementations. Fixes a generic map subclass that reorders its type parameters getting its key and value types swapped, `? super` wildcards losing their bound, type variables nested in a field's type (List<T>) or bound to a parameterized type staying unresolved, critter keeping only plain-class bindings, and critter flattening wildcards in the TypeData it emits.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Lower-bound codec selection breaks valid mixed-value contravariant collections, while the Critter tests can mask generation failures by accepting reflective fallback.
2 open issues
What changed in this PR
This PR centralizes generic-type resolution for Morphia’s reflective and Critter mappers, addressing plan item 4.4 and reducing differences in their type metadata.
Changes:
- Adds a cached resolver for inherited type variables, nested generics, arrays, and wildcards.
- Integrates shared resolution into both mappers and preserves wildcard metadata in generated models.
- Adds regression tests for generic bindings and round trips, and marks the plan item complete.
| File | Description |
|---|---|
design/mapping-game-plan.md |
Marks item 4.4 complete and explains the shared-resolver approach. |
core/src/test/java/dev/morphia/test/mapping/TestGenericResolution.java |
Adds generic-resolution and round-trip coverage. |
core/src/test/java/dev/morphia/test/mapping/codec/pojo/TypeDataTest.java |
Tests inherited interface bindings. |
core/src/main/java/dev/morphia/mapping/conventions/MethodDiscovery.java |
Resolves getter types against the entity. |
core/src/main/java/dev/morphia/mapping/conventions/FieldDiscovery.java |
Resolves field types against the entity. |
core/src/main/java/dev/morphia/mapping/codec/pojo/WildCardTypeData.java |
Exposes wildcard construction and bound direction. |
core/src/main/java/dev/morphia/mapping/codec/pojo/TypeResolver.java |
Implements cached hierarchy bindings and type substitution. |
core/src/main/java/dev/morphia/mapping/codec/pojo/TypeData.java |
Uses shared argument resolution and preserves lower bounds. |
core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java |
Replaces name-based resolution with the shared resolver. |
core/src/main/java/dev/morphia/critter/parser/generator/PropertyModelGenerator.java |
Replaces Critter’s separate resolver. |
core/src/main/java/dev/morphia/critter/parser/generator/GenerationUtils.java |
Emits wildcard metadata in generated models. |
Review effort: Balanced
A List<? super Integer> can hold a List<Object>'s values, so decoding its elements as Integer threw ClassCastException on a mixed list; the wildcard keeps resolving to its upper bound, as before. TestGenericResolution now round-trips such a list, and under critter asserts the model is a CritterEntityModel so a fallback to reflection can't pass unnoticed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Plan item 4.4 (#19, A6): one generic-type resolver for both mappers.
Changes
TypeResolver(internal). For each class it binds every type variable its supertypes declare to the type the hierarchy gives it. The binding is byTypeVariableidentity, not by name or position. It follows superclasses and interfaces, and the bindings are cached per class with aClassValue.resolve(context, type)substitutes those bindings throughParameterizedType,GenericArrayTypeand wildcards. A generic array whose component resolves to a class becomes that array class, soT[]withT = StringisString[], the same as a declaredString[].TypeData.getuses it for the type arguments it reports, replacing the positionalTypeParameters.TypeData.Builder.build()no longer wraps a list that the constructor copies anyway.EntityModel.getTypeData(Type). This replaces the by-nameparameterizationmap,propagateTypesand the 3-arggetTypeData, which only handled a bareT.PropertyModelGeneratorcalls the same resolver and drops its own, which kept only plain-class bindings.GenerationUtils.emitTypeDatanow emitsWildCardTypeDatarather than flattening wildcards.Fixes
class ReorderedMap<V,K> extends HashMap<K,V>, fieldReorderedMap<String,Integer>String, valueInteger: swappedInteger, valueStringBase<T>{T value; List<T> values;},Sub extends Base<List<String>>List<String>/List<Object>; critterObjectforvalueList<String>/List<List<String>>in bothMiddle<T,M> extends Root<List<T>>,Leaf extends Middle<String,Integer>,Root'sT valueList<Object>, critterObjectList<String>in bothArrayBase<T>{T[] array;},StringArray extends ArrayBase<String>Object(array)String[]List<? extends Number>,List<? super Integer>in critterList<Number>,List<Object>List<? extends Number>,List<? extends Object>, as in reflectionTests
TestGenericResolutioncovers each row above, plus getter-based properties, round trips for the reordered map, theList<String>binding and aList<? super Integer>holding a string and an integer, and an unboundT[]. Each assertion runs against whichever mapper the build selects; under critter it also asserts that critter built the model (aCritterEntityModel) instead of falling back to reflection, except for the array entities critter doesn't support yet. The new resolution cases failed before this change with both mappers. Without theemitTypeDatachange, the two wildcard cases still fail under critter.TypeDataTestgains a binding through an interface (Holder<T>) and a subclass of its implementor.generate-test-models, then the tests against the generated models) for these tests.-Dmorphia.mapper=critterand with reflection.Decisions to review
PropertyModelGeneratoralready reads each member's generic type through reflection; onlytypeData(String), which nothing outside tests calls, parses signatures. One resolver keeps the mappers from drifting. Generated models still need no reflection at runtime, since the resolvedTypeDatais emitted as constants.? super Xstill decodes as its upper bound,Object, contrary to the plan, which listed it as a bug. AList<? super Integer>can hold aList<Object>with strings in it, and decoding it asIntegerthrewClassCastException(caught in review; the mixed-list round trip now covers it). The plan entry records this.EntityModel.getTypeData(Class, TypeData, Type)is replaced bygetTypeData(Type). The class is@MorphiaInternaland revapi excludes it.Outer<T>.Inner) still resolves toObject.Foo extends Base<List<Foo>>) still recurse inTypeData.get, as before.Benchmarks
CodecBenchmark, before (masterat 1ef42e8) and after, both mappers,-prof gc. This step only changes howTypeDatais built at mapping time, so the encode/decode path shouldn't move, and allocation doesn't: every B/op is within 1%. The first full run showed reflection 3–12% slower across the board, encode included, and critter 2–7% faster, the kind of uniform shift that means machine noise. Rerunning reflection over three alternating rounds puts every median within ±1.3% (the branch's outliers in round 1 coincide with test runs I had going).reflection: full run
critter: full run
Reflection rerun, 3 alternating rounds (ns/op per round, Δ of medians):