From 55e8113fd6b88592f83b9386c6fda23757da0c00 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:43:31 +0100 Subject: [PATCH 1/3] Test fixes for checkstyle 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 --- .../src/test/unit/org/bson/codecs/pojo/TypeDataTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java index 78100a8464..67bbcf0e5d 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -182,13 +182,12 @@ public void newInstanceWithEmptyTypeParamsReturnsRawTypeData() throws NoSuchFiel } private static class Holder { - T scalar; - List list; - List concrete; - Map nestedMap; + private List list; + private List concrete; + private Map nestedMap; } private static class NonGeneric { - String s; + private String s; } } From 6f3322d43fbe697c170808062567e8a4ae65e2ab Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:33:23 +0100 Subject: [PATCH 2/3] Resolve nested bounded wildcards in forwarded supertype type arguments resolveTypeArgument previously fell through to Object for a WildcardType, so a wildcard nested inside a forwarded supertype argument (e.g. extends Base>) 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 --- .../main/org/bson/codecs/pojo/TypeData.java | 11 +++++-- .../bson/codecs/pojo/PojoRoundTripTest.java | 8 +++++ .../org/bson/codecs/pojo/TypeDataTest.java | 16 ++++++++++ .../entities/ForwardingWildcardModel.java | 29 +++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index 77511b11d5..e8cfd6aca7 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -110,10 +110,15 @@ private static TypeData resolveTypeArgument(final Type type, return TypeData.builder(Object.class).build(); } else if (type instanceof Class) { return TypeData.builder((Class) type).build(); + } else if (type instanceof WildcardType) { + // A wildcard cannot be the top-level type argument of an extends/implements clause (JLS §8.1.4, + // §8.1.5), but it can appear nested inside one (e.g. extends Base>). + // Resolve it to its upper bound, mirroring getNestedTypeData, so any type variable inside the + // bound is still substituted against the current context. + return resolveTypeArgument(((WildcardType) type).getUpperBounds()[0], currentClassTypeParameters, + currentClassTypeData); } else { - // WildcardType is intentionally not handled: wildcards cannot appear as type arguments - // in superclass or interface declarations (JLS §8.1.4, §8.1.5), so this method is - // never called with a WildcardType from getClassHierarchy. Falls through to Object below. + // Any other Type (e.g. GenericArrayType) is erased to Object. return TypeData.builder(Object.class).build(); } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java b/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java index aa702b44c6..798f0781f1 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java @@ -34,6 +34,7 @@ import org.bson.codecs.pojo.entities.ForwardingInterfaceModel; import org.bson.codecs.pojo.entities.ForwardingModel; import org.bson.codecs.pojo.entities.ForwardingNestedModel; +import org.bson.codecs.pojo.entities.ForwardingWildcardModel; import org.bson.codecs.pojo.entities.GenericHolderModel; import org.bson.codecs.pojo.entities.GenericTreeModel; import org.bson.codecs.pojo.entities.InterfaceBasedModel; @@ -549,6 +550,13 @@ private static List testCases() { new ForwardingNestedModel(asList("a", "b", "c")), getPojoCodecProviderBuilder(ForwardingNestedModel.class), "{'value': ['a', 'b', 'c']}")); + data.add(new TestData("Forwarding nested wildcard chain resolves to List", + new ForwardingWildcardModel(asList(getShapeModelCircle(), getShapeModelRectangle())), + getPojoCodecProviderBuilder(ForwardingWildcardModel.class, ShapeModelAbstract.class, + ShapeModelCircle.class, ShapeModelRectangle.class), + "{'value': [{'_t': 'org.bson.codecs.pojo.entities.ShapeModelCircle', 'color': 'orange', 'radius': 4.2}, " + + "{'_t': 'org.bson.codecs.pojo.entities.ShapeModelRectangle', 'color': 'green', 'width': 22.1, " + + "'height': 105.0}]}")); return data; } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java index 67bbcf0e5d..e3455ea5e8 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -181,10 +181,26 @@ public void newInstanceWithEmptyTypeParamsReturnsRawTypeData() throws NoSuchFiel assertEquals(expected, actual); } + @Test + public void newInstanceResolvesBoundedWildcardToUpperBound() throws NoSuchFieldException { + Field boundedWildcardField = Holder.class.getDeclaredField("boundedWildcard"); + Type boundedWildcardGenericType = boundedWildcardField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + TypeData currentResolved = TypeData.builder(Holder.class) + .addTypeParameter(TypeData.builder(String.class).build()).build(); + + TypeData expected = TypeData.builder(List.class) + .addTypeParameter(TypeData.builder(Number.class).build()).build(); + TypeData actual = TypeData.newInstance(boundedWildcardGenericType, List.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + private static class Holder { private List list; private List concrete; private Map nestedMap; + private List boundedWildcard; } private static class NonGeneric { diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java new file mode 100644 index 0000000000..1a68e08f86 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java @@ -0,0 +1,29 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs.pojo.entities; + +import java.util.List; + +public class ForwardingWildcardModel extends ForwardingNestedBase> { + + public ForwardingWildcardModel() { + } + + public ForwardingWildcardModel(final List value) { + super(value); + } +} From ef6e31d347369e7ed647dbb602839a6c463b68eb Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:38:05 +0100 Subject: [PATCH 3/3] Unify TypeData type-argument resolution into resolveTypeArgument 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). Delegate the two-arg newInstance to it and remove getNestedTypeData. JAVA-6065 --- .../main/org/bson/codecs/pojo/TypeData.java | 34 +++---------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index e8cfd6aca7..c669a8e655 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -61,14 +61,8 @@ public static TypeData newInstance(final Field field) { } public static TypeData newInstance(final Type genericType, final Class clazz) { - TypeData.Builder builder = TypeData.builder(clazz); - if (genericType instanceof ParameterizedType) { - ParameterizedType pType = (ParameterizedType) genericType; - for (Type argType : pType.getActualTypeArguments()) { - getNestedTypeData(builder, argType); - } - } - return builder.build(); + // No enclosing class context: type variables have nothing to resolve against and erase to Object. + return newInstance(genericType, clazz, Collections.>emptyList(), null); } static TypeData newInstance(final Type genericParentType, final Class parentClass, @@ -112,9 +106,9 @@ private static TypeData resolveTypeArgument(final Type type, return TypeData.builder((Class) type).build(); } else if (type instanceof WildcardType) { // A wildcard cannot be the top-level type argument of an extends/implements clause (JLS §8.1.4, - // §8.1.5), but it can appear nested inside one (e.g. extends Base>). - // Resolve it to its upper bound, mirroring getNestedTypeData, so any type variable inside the - // bound is still substituted against the current context. + // §8.1.5), but it can appear nested inside one (e.g. extends Base>) or in a + // field/method generic type. Resolve it to its upper bound so any type variable inside the bound is + // still substituted against the current context. return resolveTypeArgument(((WildcardType) type).getUpperBounds()[0], currentClassTypeParameters, currentClassTypeData); } else { @@ -123,24 +117,6 @@ private static TypeData resolveTypeArgument(final Type type, } } - @SuppressWarnings({"unchecked", "rawtypes"}) - private static void getNestedTypeData(final TypeData.Builder builder, final Type type) { - if (type instanceof ParameterizedType) { - ParameterizedType pType = (ParameterizedType) type; - TypeData.Builder paramBuilder = TypeData.builder((Class) pType.getRawType()); - for (Type argType : pType.getActualTypeArguments()) { - getNestedTypeData(paramBuilder, argType); - } - builder.addTypeParameter(paramBuilder.build()); - } else if (type instanceof WildcardType) { - builder.addTypeParameter(TypeData.builder((Class) ((WildcardType) type).getUpperBounds()[0]).build()); - } else if (type instanceof TypeVariable) { - builder.addTypeParameter(TypeData.builder(Object.class).build()); - } else if (type instanceof Class) { - builder.addTypeParameter(TypeData.builder((Class) type).build()); - } - } - /** * @return the class this {@code ClassTypeData} represents */