Skip to content

Resolve forwarded type arguments across POJO hierarchy edges#2014

Open
vbabanin wants to merge 10 commits into
mongodb:mainfrom
vbabanin:JAVA-6065
Open

Resolve forwarded type arguments across POJO hierarchy edges#2014
vbabanin wants to merge 10 commits into
mongodb:mainfrom
vbabanin:JAVA-6065

Conversation

@vbabanin

@vbabanin vbabanin commented Jul 13, 2026

Copy link
Copy Markdown
Member

Problem

After JAVA-3815, encoding a POJO that uses the following hierarchy shape fails:

public abstract class AbstractModel<V> implements InterfaceModel<V> {
    private V value;
    public V getValue() { return value; }
    public void setValue(V v) { this.value = v; }
}

public class ConcreteModel extends AbstractModel<Map<String, Object>> {}

Encoding a ConcreteModel instance throws:

CodecConfigurationException: Can't find a codec for class java.lang.Object

The value property - which should resolve to Map<String, Object> - is being resolved to Object instead.

Root cause

The underlying problem - cachePropertyTypeData unconditionally overwriting a property's resolution on later visits - already existed with superclass chains (JAVA-5110). Same root cause, different symptom: there, a forwarding chain like C extends B<String>, B<T> extends A<T>, A<K> resolves K to Object because the TypeVariable is never substituted. Users with that shape have been hitting it since at least 2023.

JAVA-3815 added interface scanning, which exposed the same overwrite problem for a new set of users: because AbstractModel<V> implements InterfaceModel<V>, the same property is now visited twice - first from AbstractModel with the correct concrete TypeData, then from InterfaceModel with stale TypeData. The second visit silently overwrote the first, surfacing the regression for users who had never changed their code.

Walk trace for ConcreteModel:

Step Class being added parentClassTypeData passed
1 ConcreteModel null (root)
2 AbstractModel TypeData(ConcreteModel, [Map<String,Object>]) - correct
3 InterfaceModel (via AbstractModel's interfaces) TypeData(AbstractModel) - no type params

When the walker scans InterfaceModel, it finds getValue() again and calls cachePropertyTypeData("value", ..., TypeData(AbstractModel)). This overwrites the correct resolution (Map<String,Object>) with TypeData(Object).

Fix

TypeData.newInstance - new composing overload that accepts the current class's type parameters and resolved TypeData as context. When a type argument in an extends/implements clause is a TypeVariable, it is substituted against that context rather than erased to Object. Nested ParameterizedType arguments (e.g. List<T>) are handled recursively.

PojoBuilderHelper.getClassHierarchy - both the superclass edge and per-interface edge now use the composing TypeData.newInstance, so TypeVariables in extends/implements clauses are substituted against the current class's resolved type arguments rather than erased to Object.

JAVA-6065
JAVA-5110
JAVA-6138

Adds substitution-aware TypeData.newInstance and wires PojoBuilderHelper
to use it at both superclass and interface edges. Closes JAVA-5110 and
JAVA-6138.

This comment was marked as outdated.

@rozza

rozza commented Jul 13, 2026

Copy link
Copy Markdown
Member

@vbabanin I shouldn't have pushed my changes here (reverted) I have created a PR for additions to this branch: vbabanin#2

Initial thoughts are this looks good and I prefer over #2015 as its much more robust.

rozza and others added 8 commits July 13, 2026 22:45
Holder/NonGeneric fields must be private (VisibilityModifier); they are
read via reflection so accessors are unnecessary. Also drop the unused
Holder.scalar field.

JAVA-6065
resolveTypeArgument previously fell through to Object for a WildcardType,
so a wildcard nested inside a forwarded supertype argument (e.g.
extends Base<List<? extends Number>>) lost its bound. Resolve it to its
upper bound, mirroring getNestedTypeData, so a type variable inside the
bound is still substituted against the current context.

Adds a TypeData unit test for the bound resolution and a discriminated
POJO round-trip (ForwardingWildcardModel) that fails without the fix.

JAVA-6065
The two newInstance overloads duplicated near-identical type-argument
walking. resolveTypeArgument is a strict superset of getNestedTypeData
(with an empty context, type variables erase to Object exactly as
before), and it also fixes a latent ClassCastException when a wildcard's
upper bound is itself parameterized (e.g. ? extends List<String>).
Delegate the two-arg newInstance to it and remove getNestedTypeData.

JAVA-6065
@vbabanin

Copy link
Copy Markdown
Member Author

Thank you, @rozza! I cherry-picked your suggested changes onto this branch.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Comment thread bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java Outdated
Comment thread bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.

@vbabanin vbabanin marked this pull request as ready for review July 16, 2026 06:37
@vbabanin vbabanin requested a review from a team as a code owner July 16, 2026 06:37
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

Successfully merging this pull request may close these issues.

3 participants