From 3ed8b71e2505b6f7b08e0f0524b8cabf1c8e047e Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Sun, 12 Jul 2026 21:48:08 -0700 Subject: [PATCH 01/10] JAVA-6065: Resolve forwarded type arguments across POJO hierarchy edges Adds substitution-aware TypeData.newInstance and wires PojoBuilderHelper to use it at both superclass and interface edges. Closes JAVA-5110 and JAVA-6138. --- .../bson/codecs/pojo/PojoBuilderHelper.java | 20 ++- .../main/org/bson/codecs/pojo/TypeData.java | 47 ++++++++ .../org/bson/codecs/pojo/ClassModelTest.java | 45 +++++++ .../bson/codecs/pojo/PojoRoundTripTest.java | 24 ++++ .../org/bson/codecs/pojo/TypeDataTest.java | 114 ++++++++++++++++++ .../codecs/pojo/entities/ForwardingDeep1.java | 27 +++++ .../codecs/pojo/entities/ForwardingDeep2.java | 27 +++++ .../codecs/pojo/entities/ForwardingDeep3.java | 61 ++++++++++ .../pojo/entities/ForwardingDeepModel.java | 27 +++++ .../pojo/entities/ForwardingInterface1.java | 20 +++ .../pojo/entities/ForwardingInterface2.java | 23 ++++ .../entities/ForwardingInterfaceModel.java | 63 ++++++++++ .../pojo/entities/ForwardingLevel2.java | 27 +++++ .../pojo/entities/ForwardingLevel3.java | 61 ++++++++++ .../codecs/pojo/entities/ForwardingModel.java | 27 +++++ .../pojo/entities/ForwardingNestedBase.java | 61 ++++++++++ .../pojo/entities/ForwardingNestedMid.java | 29 +++++ .../pojo/entities/ForwardingNestedModel.java | 29 +++++ .../codecs/pojo/entities/HolderAbstract.java | 63 ++++++++++ .../pojo/entities/HolderConcreteMapModel.java | 29 +++++ .../codecs/pojo/entities/HolderInterface.java | 23 ++++ 21 files changed, 843 insertions(+), 4 deletions(-) create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel2.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel3.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingModel.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java diff --git a/bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java b/bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java index 9a0f5e69d0e..514f4558945 100644 --- a/bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java +++ b/bson/src/main/org/bson/codecs/pojo/PojoBuilderHelper.java @@ -264,11 +264,23 @@ private static Set> getClassHierarchy(fin TypeData parentClassTypeData = classTypeData; while (currentClass != null && !currentClass.isEnum() && !currentClass.equals(Object.class)) { classesToScan.add(new ClassWithParentTypeData<>(currentClass, parentClassTypeData)); - parentClassTypeData = TypeData.newInstance(currentClass.getGenericSuperclass(), currentClass); - for (Class interfaceClass : currentClass.getInterfaces()) { - classesToScan.addAll(getClassHierarchy((Class) interfaceClass, parentClassTypeData)); + + List> currentTypeParams = asList(currentClass.getTypeParameters()); + Type[] genericInterfaces = currentClass.getGenericInterfaces(); + Class[] interfaces = currentClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + TypeData ifaceResolved = TypeData.newInstance( + genericInterfaces[i], interfaces[i], currentTypeParams, parentClassTypeData); + classesToScan.addAll(getClassHierarchy((Class) interfaces[i], ifaceResolved)); + } + + Class superClass = currentClass.getSuperclass(); + if (superClass != null) { + parentClassTypeData = TypeData.newInstance( + currentClass.getGenericSuperclass(), superClass, + currentTypeParams, parentClassTypeData); } - currentClass = currentClass.getSuperclass(); + currentClass = superClass; } return classesToScan; } diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index aebdba4c08f..77511b11d58 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -71,6 +71,53 @@ public static TypeData newInstance(final Type genericType, final Class return builder.build(); } + static TypeData newInstance(final Type genericParentType, final Class parentClass, + final List> currentClassTypeParameters, + final TypeData currentClassTypeData) { + TypeData.Builder builder = TypeData.builder(parentClass); + if (genericParentType instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) genericParentType; + for (Type argType : pType.getActualTypeArguments()) { + builder.addTypeParameter(resolveTypeArgument(argType, currentClassTypeParameters, currentClassTypeData)); + } + } + return builder.build(); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static TypeData resolveTypeArgument(final Type type, + final List> currentClassTypeParameters, + final TypeData currentClassTypeData) { + if (type instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) type; + TypeData.Builder paramBuilder = TypeData.builder((Class) pType.getRawType()); + for (Type argType : pType.getActualTypeArguments()) { + paramBuilder.addTypeParameter(resolveTypeArgument(argType, currentClassTypeParameters, currentClassTypeData)); + } + return paramBuilder.build(); + } else if (type instanceof TypeVariable) { + // JLS §4.4: a type variable is declared once and referenced by the same instance + // everywhere it appears in scope, so reference equality is sufficient. + TypeVariable tv = (TypeVariable) type; + for (int i = 0; i < currentClassTypeParameters.size(); i++) { + if (currentClassTypeParameters.get(i) == tv) { + if (currentClassTypeData != null && i < currentClassTypeData.getTypeParameters().size()) { + return currentClassTypeData.getTypeParameters().get(i); + } + break; + } + } + return TypeData.builder(Object.class).build(); + } else if (type instanceof Class) { + return TypeData.builder((Class) type).build(); + } 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. + return TypeData.builder(Object.class).build(); + } + } + @SuppressWarnings({"unchecked", "rawtypes"}) private static void getNestedTypeData(final TypeData.Builder builder, final Type type) { if (type instanceof ParameterizedType) { diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index d0ee3cb1cc7..cce2a0770b0 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -20,7 +20,12 @@ import org.bson.codecs.pojo.entities.CollectionNestedPojoModel; import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel; +import org.bson.codecs.pojo.entities.ForwardingDeepModel; +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.GenericHolderModel; +import org.bson.codecs.pojo.entities.HolderConcreteMapModel; import org.bson.codecs.pojo.entities.InterfaceBasedModel; import org.bson.codecs.pojo.entities.ListGenericExtendedModel; import org.bson.codecs.pojo.entities.ListListGenericExtendedModel; @@ -266,6 +271,46 @@ public void testSimpleWithStaticModel() { } + @Test + public void testForwardingClassChain() { + ClassModel classModel = ClassModel.builder(ForwardingModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(String.class), classModel.getPropertyModel("value").getTypeData()); + } + + @Test + public void testForwardingDeepChain() { + ClassModel classModel = ClassModel.builder(ForwardingDeepModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(Long.class), classModel.getPropertyModel("value").getTypeData()); + } + + @Test + public void testForwardingInterfaceChain() { + ClassModel classModel = ClassModel.builder(ForwardingInterfaceModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("value").getTypeData()); + } + + @Test + public void testForwardingNested() { + ClassModel classModel = ClassModel.builder(ForwardingNestedModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(List.class, String.class), classModel.getPropertyModel("value").getTypeData()); + } + + @Test + public void testHolderConcreteMap() { + ClassModel classModel = ClassModel.builder(HolderConcreteMapModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(Map.class, String.class, Object.class), classModel.getPropertyModel("value").getTypeData()); + } + TypeData.Builder createBuilder(final Class clazz, final Class... types) { TypeData.Builder builder = TypeData.builder(clazz); List> subTypes = new ArrayList<>(); 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 53f5d363535..aa702b44c69 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java @@ -30,6 +30,10 @@ import org.bson.codecs.pojo.entities.ConventionModel; import org.bson.codecs.pojo.entities.DuplicateAnnotationAllowedModel; import org.bson.codecs.pojo.entities.FieldAndPropertyTypeMismatchModel; +import org.bson.codecs.pojo.entities.ForwardingDeepModel; +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.GenericHolderModel; import org.bson.codecs.pojo.entities.GenericTreeModel; import org.bson.codecs.pojo.entities.InterfaceBasedModel; @@ -526,6 +530,26 @@ private static List testCases() { getPojoCodecProviderBuilder(BsonExtraElementsMapModel.class), "{'integerField': 42, 'stringField': 'myString', 'a': 'a', 'b': 'b'}")); + data.add(new TestData("Forwarding class chain resolves to String", + new ForwardingModel("hello"), + getPojoCodecProviderBuilder(ForwardingModel.class), + "{'value': 'hello'}")); + + data.add(new TestData("Forwarding deep class chain resolves to Long", + new ForwardingDeepModel(42L), + getPojoCodecProviderBuilder(ForwardingDeepModel.class), + "{'value': {'$numberLong': '42'}}")); + + data.add(new TestData("Forwarding interface chain resolves to Integer", + new ForwardingInterfaceModel(7), + getPojoCodecProviderBuilder(ForwardingInterfaceModel.class), + "{'value': 7}")); + + data.add(new TestData("Forwarding nested generic chain resolves to List", + new ForwardingNestedModel(asList("a", "b", "c")), + getPojoCodecProviderBuilder(ForwardingNestedModel.class), + "{'value': ['a', 'b', 'c']}")); + 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 ee52e7e7bcf..78100a84643 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -19,10 +19,15 @@ import org.bson.codecs.pojo.entities.GenericHolderModel; import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Arrays; import java.util.List; import java.util.Map; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -77,4 +82,113 @@ public void testRecursiveTypeData() { typeData.toString(); } + + @Test + public void newInstanceResolvesTypeVariableAgainstContext() throws NoSuchFieldException { + Field listField = Holder.class.getDeclaredField("list"); + Type listGenericType = listField.getGenericType(); + List> typeParams = asList(Holder.class.getTypeParameters()); + + TypeData resolvedString = TypeData.builder(String.class).build(); + TypeData currentResolved = TypeData.builder(Holder.class).addTypeParameter(resolvedString).build(); + + TypeData expected = TypeData.builder(List.class).addTypeParameter(resolvedString).build(); + TypeData actual = TypeData.newInstance(listGenericType, List.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + + @Test + public void newInstanceResolvesTypeVariableMiss() throws NoSuchFieldException { + Field listField = Holder.class.getDeclaredField("list"); + Type listGenericType = listField.getGenericType(); + + List> emptyTypeParams = emptyList(); + TypeData resolvedString = TypeData.builder(String.class).build(); + TypeData currentResolved = TypeData.builder(Holder.class).addTypeParameter(resolvedString).build(); + + TypeData expected = TypeData.builder(List.class).addTypeParameter(TypeData.builder(Object.class).build()).build(); + TypeData actual = TypeData.newInstance(listGenericType, List.class, emptyTypeParams, currentResolved); + assertEquals(expected, actual); + } + + @Test + public void newInstanceTypeVariableHitWithNullCurrentResolved() throws NoSuchFieldException { + Field listField = Holder.class.getDeclaredField("list"); + Type listGenericType = listField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + + TypeData expected = TypeData.builder(List.class).addTypeParameter(TypeData.builder(Object.class).build()).build(); + TypeData actual = TypeData.newInstance(listGenericType, List.class, typeParams, null); + assertEquals(expected, actual); + } + + @Test + public void newInstanceTypeVariableHitIndexOutOfRange() throws NoSuchFieldException { + Field listField = Holder.class.getDeclaredField("list"); + Type listGenericType = listField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + TypeData currentResolved = TypeData.builder(Holder.class).build(); + + TypeData expected = TypeData.builder(List.class).addTypeParameter(TypeData.builder(Object.class).build()).build(); + TypeData actual = TypeData.newInstance(listGenericType, List.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + + @Test + public void newInstanceResolvesNestedParameterizedTypeContainingTypeVariable() throws NoSuchFieldException { + Field nestedField = Holder.class.getDeclaredField("nestedMap"); + Type nestedGenericType = nestedField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + TypeData resolvedInt = TypeData.builder(Integer.class).build(); + TypeData currentResolved = TypeData.builder(Holder.class).addTypeParameter(resolvedInt).build(); + + TypeData stringType = TypeData.builder(String.class).build(); + TypeData expected = TypeData.builder(Map.class).addTypeParameter(stringType).addTypeParameter(resolvedInt).build(); + TypeData actual = TypeData.newInstance(nestedGenericType, Map.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + + @Test + public void newInstanceResolvesConcreteClassArgument() throws NoSuchFieldException { + Field concreteField = Holder.class.getDeclaredField("concrete"); + Type concreteGenericType = concreteField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + TypeData currentResolved = TypeData.builder(Holder.class) + .addTypeParameter(TypeData.builder(Integer.class).build()).build(); + + TypeData expected = TypeData.builder(List.class).addTypeParameter(TypeData.builder(String.class).build()).build(); + TypeData actual = TypeData.newInstance(concreteGenericType, List.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + + @Test + public void newInstanceWithEmptyTypeParamsReturnsRawTypeData() throws NoSuchFieldException { + Field stringField = NonGeneric.class.getDeclaredField("s"); + Type stringGenericType = stringField.getGenericType(); + + TypeData actual = TypeData.newInstance( + stringGenericType, + String.class, + emptyList(), + null + ); + + TypeData expected = TypeData.builder(String.class).build(); + assertEquals(expected, actual); + } + + private static class Holder { + T scalar; + List list; + List concrete; + Map nestedMap; + } + + private static class NonGeneric { + String s; + } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java new file mode 100644 index 00000000000..95fb5d68436 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingDeep1 extends ForwardingDeep2 { + + public ForwardingDeep1() { + } + + public ForwardingDeep1(final C value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java new file mode 100644 index 00000000000..a1de15b4b35 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingDeep2 extends ForwardingDeep3 { + + public ForwardingDeep2() { + } + + public ForwardingDeep2(final B value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java new file mode 100644 index 00000000000..8b10b60ef52 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java @@ -0,0 +1,61 @@ +/* + * 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.Objects; + +public class ForwardingDeep3 { + private A value; + + public ForwardingDeep3() { + } + + public ForwardingDeep3(final A value) { + this.value = value; + } + + public A getValue() { + return value; + } + + public void setValue(final A value) { + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ForwardingDeep3 that = (ForwardingDeep3) o; + + if (!Objects.equals(value, that.value)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java new file mode 100644 index 00000000000..abc337edc06 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingDeepModel extends ForwardingDeep1 { + + public ForwardingDeepModel() { + } + + public ForwardingDeepModel(final Long value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java new file mode 100644 index 00000000000..bd54552cb07 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface ForwardingInterface1 extends ForwardingInterface2 { +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java new file mode 100644 index 00000000000..dafac4e981b --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java @@ -0,0 +1,23 @@ +/* + * 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; + +public interface ForwardingInterface2 { + K getValue(); + + void setValue(K value); +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java new file mode 100644 index 00000000000..c736c57a41e --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java @@ -0,0 +1,63 @@ +/* + * 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.Objects; + +public class ForwardingInterfaceModel implements ForwardingInterface1 { + private Integer value; + + public ForwardingInterfaceModel() { + } + + public ForwardingInterfaceModel(final Integer value) { + this.value = value; + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public void setValue(final Integer value) { + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ForwardingInterfaceModel that = (ForwardingInterfaceModel) o; + + if (!Objects.equals(value, that.value)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel2.java new file mode 100644 index 00000000000..8c37daf4554 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel2.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingLevel2 extends ForwardingLevel3 { + + public ForwardingLevel2() { + } + + public ForwardingLevel2(final T value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel3.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel3.java new file mode 100644 index 00000000000..b58c45e7b6a --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingLevel3.java @@ -0,0 +1,61 @@ +/* + * 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.Objects; + +public class ForwardingLevel3 { + private K value; + + public ForwardingLevel3() { + } + + public ForwardingLevel3(final K value) { + this.value = value; + } + + public K getValue() { + return value; + } + + public void setValue(final K value) { + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ForwardingLevel3 that = (ForwardingLevel3) o; + + if (!Objects.equals(value, that.value)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingModel.java new file mode 100644 index 00000000000..c3ccafcee2c --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingModel.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingModel extends ForwardingLevel2 { + + public ForwardingModel() { + } + + public ForwardingModel(final String value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java new file mode 100644 index 00000000000..92d56a0a387 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java @@ -0,0 +1,61 @@ +/* + * 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.Objects; + +public class ForwardingNestedBase { + private E value; + + public ForwardingNestedBase() { + } + + public ForwardingNestedBase(final E value) { + this.value = value; + } + + public E getValue() { + return value; + } + + public void setValue(final E value) { + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ForwardingNestedBase that = (ForwardingNestedBase) o; + + if (!Objects.equals(value, that.value)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java new file mode 100644 index 00000000000..7198a8e18b4 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.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 ForwardingNestedMid extends ForwardingNestedBase> { + + public ForwardingNestedMid() { + } + + public ForwardingNestedMid(final List value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java new file mode 100644 index 00000000000..283239c94d8 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.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 ForwardingNestedModel extends ForwardingNestedMid { + + public ForwardingNestedModel() { + } + + public ForwardingNestedModel(final List value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java new file mode 100644 index 00000000000..9416c51844e --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java @@ -0,0 +1,63 @@ +/* + * 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.Objects; + +public abstract class HolderAbstract implements HolderInterface { + private V value; + + public HolderAbstract() { + } + + public HolderAbstract(final V value) { + this.value = value; + } + + @Override + public V getValue() { + return value; + } + + @Override + public void setValue(final V v) { + this.value = v; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + HolderAbstract that = (HolderAbstract) o; + + if (!Objects.equals(value, that.value)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java new file mode 100644 index 00000000000..584cb21691f --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.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.Map; + +public class HolderConcreteMapModel extends HolderAbstract> { + + public HolderConcreteMapModel() { + } + + public HolderConcreteMapModel(final Map value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java new file mode 100644 index 00000000000..57aadf40989 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java @@ -0,0 +1,23 @@ +/* + * 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; + +public interface HolderInterface { + V getValue(); + + void setValue(V v); +} From e71a4968b55eec18801c1fcf982f06914271ca6a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:43:31 +0100 Subject: [PATCH 02/10] 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 78100a84643..67bbcf0e5d7 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 d143d53a7790afa0795bd52a67305c135af040df Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:33:23 +0100 Subject: [PATCH 03/10] 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 77511b11d58..e8cfd6aca79 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 aa702b44c69..798f0781f19 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 67bbcf0e5d7..e3455ea5e85 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 00000000000..1a68e08f86f --- /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 dcd9f98a8cacde1236acd477827f716a2fde45e0 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Mon, 13 Jul 2026 14:38:05 +0100 Subject: [PATCH 04/10] 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 +++---------------- ...ngDeep1.java => ForwardingDeepLevel1.java} | 6 ++-- ...ngDeep2.java => ForwardingDeepLevel2.java} | 6 ++-- ...ngDeep3.java => ForwardingDeepLevel3.java} | 8 ++--- .../pojo/entities/ForwardingDeepModel.java | 2 +- ...e1.java => ForwardingInterfaceLevel1.java} | 2 +- ...e2.java => ForwardingInterfaceLevel2.java} | 2 +- .../entities/ForwardingInterfaceModel.java | 2 +- 8 files changed, 19 insertions(+), 43 deletions(-) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeep1.java => ForwardingDeepLevel1.java} (81%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeep2.java => ForwardingDeepLevel2.java} (81%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeep3.java => ForwardingDeepLevel3.java} (87%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingInterface1.java => ForwardingInterfaceLevel1.java} (88%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingInterface2.java => ForwardingInterfaceLevel2.java} (93%) diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index e8cfd6aca79..c669a8e6551 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 */ diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java similarity index 81% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java index 95fb5d68436..b2c13c89028 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep1.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java @@ -16,12 +16,12 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeep1 extends ForwardingDeep2 { +public class ForwardingDeepLevel1 extends ForwardingDeepLevel2 { - public ForwardingDeep1() { + public ForwardingDeepLevel1() { } - public ForwardingDeep1(final C value) { + public ForwardingDeepLevel1(final C value) { super(value); } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java similarity index 81% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java index a1de15b4b35..e1f2ec02620 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep2.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java @@ -16,12 +16,12 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeep2 extends ForwardingDeep3 { +public class ForwardingDeepLevel2 extends ForwardingDeepLevel3 { - public ForwardingDeep2() { + public ForwardingDeepLevel2() { } - public ForwardingDeep2(final B value) { + public ForwardingDeepLevel2(final B value) { super(value); } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java similarity index 87% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java index 8b10b60ef52..1b310c4f634 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeep3.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java @@ -18,13 +18,13 @@ import java.util.Objects; -public class ForwardingDeep3 { +public class ForwardingDeepLevel3 { private A value; - public ForwardingDeep3() { + public ForwardingDeepLevel3() { } - public ForwardingDeep3(final A value) { + public ForwardingDeepLevel3(final A value) { this.value = value; } @@ -45,7 +45,7 @@ public boolean equals(final Object o) { return false; } - ForwardingDeep3 that = (ForwardingDeep3) o; + ForwardingDeepLevel3 that = (ForwardingDeepLevel3) o; if (!Objects.equals(value, that.value)) { return false; diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java index abc337edc06..979f2ec3fac 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java @@ -16,7 +16,7 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeepModel extends ForwardingDeep1 { +public class ForwardingDeepModel extends ForwardingDeepLevel1 { public ForwardingDeepModel() { } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel1.java similarity index 88% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel1.java index bd54552cb07..cf60b9f9d4a 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface1.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel1.java @@ -16,5 +16,5 @@ package org.bson.codecs.pojo.entities; -public interface ForwardingInterface1 extends ForwardingInterface2 { +public interface ForwardingInterfaceLevel1 extends ForwardingInterfaceLevel2 { } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel2.java similarity index 93% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel2.java index dafac4e981b..68513250f63 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterface2.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceLevel2.java @@ -16,7 +16,7 @@ package org.bson.codecs.pojo.entities; -public interface ForwardingInterface2 { +public interface ForwardingInterfaceLevel2 { K getValue(); void setValue(K value); diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java index c736c57a41e..26054c04750 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingInterfaceModel.java @@ -18,7 +18,7 @@ import java.util.Objects; -public class ForwardingInterfaceModel implements ForwardingInterface1 { +public class ForwardingInterfaceModel implements ForwardingInterfaceLevel1 { private Integer value; public ForwardingInterfaceModel() { From 620c869c6b0bd74164874acce0817b335857cf87 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Mon, 13 Jul 2026 23:37:06 -0700 Subject: [PATCH 05/10] Add tests, --- .../main/org/bson/codecs/pojo/TypeData.java | 35 ++++++++++-------- .../org/bson/codecs/pojo/ClassModelTest.java | 12 +++++++ .../org/bson/codecs/pojo/TypeDataTest.java | 15 ++++++++ .../pojo/entities/ForwardingArrayLevel1.java | 27 ++++++++++++++ .../pojo/entities/ForwardingArrayLevel2.java | 36 +++++++++++++++++++ .../pojo/entities/ForwardingArrayModel.java | 27 ++++++++++++++ ...edMid.java => ForwardingNestedLevel1.java} | 6 ++-- ...dBase.java => ForwardingNestedLevel2.java} | 8 ++--- .../pojo/entities/ForwardingNestedModel.java | 2 +- .../entities/ForwardingWildcardModel.java | 2 +- 10 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayModel.java rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingNestedMid.java => ForwardingNestedLevel1.java} (80%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingNestedBase.java => ForwardingNestedLevel2.java} (86%) diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index c669a8e6551..d3cf35b8dad 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -16,6 +16,7 @@ package org.bson.codecs.pojo; +import javax.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -67,7 +68,7 @@ public static TypeData newInstance(final Type genericType, final Class static TypeData newInstance(final Type genericParentType, final Class parentClass, final List> currentClassTypeParameters, - final TypeData currentClassTypeData) { + @Nullable final TypeData currentClassTypeData) { TypeData.Builder builder = TypeData.builder(parentClass); if (genericParentType instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) genericParentType; @@ -81,7 +82,7 @@ static TypeData newInstance(final Type genericParentType, final Class @SuppressWarnings({"unchecked", "rawtypes"}) private static TypeData resolveTypeArgument(final Type type, final List> currentClassTypeParameters, - final TypeData currentClassTypeData) { + @Nullable final TypeData currentClassTypeData) { if (type instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) type; TypeData.Builder paramBuilder = TypeData.builder((Class) pType.getRawType()); @@ -90,18 +91,7 @@ private static TypeData resolveTypeArgument(final Type type, } return paramBuilder.build(); } else if (type instanceof TypeVariable) { - // JLS §4.4: a type variable is declared once and referenced by the same instance - // everywhere it appears in scope, so reference equality is sufficient. - TypeVariable tv = (TypeVariable) type; - for (int i = 0; i < currentClassTypeParameters.size(); i++) { - if (currentClassTypeParameters.get(i) == tv) { - if (currentClassTypeData != null && i < currentClassTypeData.getTypeParameters().size()) { - return currentClassTypeData.getTypeParameters().get(i); - } - break; - } - } - return TypeData.builder(Object.class).build(); + return resolveTypeVariable((TypeVariable) type, currentClassTypeParameters, currentClassTypeData); } else if (type instanceof Class) { return TypeData.builder((Class) type).build(); } else if (type instanceof WildcardType) { @@ -117,6 +107,23 @@ private static TypeData resolveTypeArgument(final Type type, } } + private static TypeData resolveTypeVariable(final TypeVariable type, final List> currentClassTypeParameters, + @Nullable final TypeData currentClassTypeData) { + if (currentClassTypeData != null) { + for (int i = 0; i < currentClassTypeParameters.size(); i++) { + // JLS §4.4: a type variable is declared once and referenced by the same instance + // everywhere it appears in scope, so reference equality is sufficient. + if (currentClassTypeParameters.get(i) == type) { + if (i < currentClassTypeData.getTypeParameters().size()) { + return currentClassTypeData.getTypeParameters().get(i); + } + break; + } + } + } + return TypeData.builder(Object.class).build(); + } + /** * @return the class this {@code ClassTypeData} represents */ diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index cce2a0770b0..b6f55c29a4a 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -23,6 +23,7 @@ import org.bson.codecs.pojo.entities.ForwardingDeepModel; import org.bson.codecs.pojo.entities.ForwardingInterfaceModel; import org.bson.codecs.pojo.entities.ForwardingModel; +import org.bson.codecs.pojo.entities.ForwardingArrayModel; import org.bson.codecs.pojo.entities.ForwardingNestedModel; import org.bson.codecs.pojo.entities.GenericHolderModel; import org.bson.codecs.pojo.entities.HolderConcreteMapModel; @@ -303,6 +304,17 @@ public void testForwardingNested() { assertEquals(createTypeData(List.class, String.class), classModel.getPropertyModel("value").getTypeData()); } + @Test + public void testForwardingArrayTypeVariableErasedToObject() { + // T[] fields in a generic base class are GenericArrayType; getTypeParameterMap does not + // handle GenericArrayType component types, so the T[] erases to Object[] regardless of + // the concrete binding at the leaf subclass. + ClassModel classModel = ClassModel.builder(ForwardingArrayModel.class).build(); + + assertEquals(1, classModel.getPropertyModels().size()); + assertEquals(createTypeData(Object[].class), classModel.getPropertyModel("value").getTypeData()); + } + @Test public void testHolderConcreteMap() { ClassModel classModel = ClassModel.builder(HolderConcreteMapModel.class).build(); 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 e3455ea5e85..1560770e537 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -181,6 +181,20 @@ public void newInstanceWithEmptyTypeParamsReturnsRawTypeData() throws NoSuchFiel assertEquals(expected, actual); } + @Test + public void newInstanceErasesGenericArrayTypeToObject() throws NoSuchFieldException { + Field arrayField = Holder.class.getDeclaredField("genericArray"); + Type arrayGenericType = arrayField.getGenericType(); + + List> typeParams = asList(Holder.class.getTypeParameters()); + TypeData currentResolved = TypeData.builder(Holder.class) + .addTypeParameter(TypeData.builder(String.class).build()).build(); + + TypeData actual = TypeData.newInstance(arrayGenericType, Object[].class, typeParams, currentResolved); + TypeData expected = TypeData.builder(Object[].class).build(); + assertEquals(expected, actual); + } + @Test public void newInstanceResolvesBoundedWildcardToUpperBound() throws NoSuchFieldException { Field boundedWildcardField = Holder.class.getDeclaredField("boundedWildcard"); @@ -201,6 +215,7 @@ private static class Holder { private List concrete; private Map nestedMap; private List boundedWildcard; + private T[] genericArray; } private static class NonGeneric { diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java new file mode 100644 index 00000000000..ee86cbf8bfd --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingArrayLevel1 extends ForwardingArrayLevel2 { + + public ForwardingArrayLevel1() { + } + + public ForwardingArrayLevel1(final U[] value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java new file mode 100644 index 00000000000..05c99c2c485 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java @@ -0,0 +1,36 @@ +/* + * 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; + +public class ForwardingArrayLevel2 { + private T[] value; + + public ForwardingArrayLevel2() { + } + + public ForwardingArrayLevel2(final T[] value) { + this.value = value; + } + + public T[] getValue() { + return value; + } + + public void setValue(final T[] value) { + this.value = value; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayModel.java new file mode 100644 index 00000000000..be9141ea572 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayModel.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingArrayModel extends ForwardingArrayLevel1 { + + public ForwardingArrayModel() { + } + + public ForwardingArrayModel(final String[] value) { + super(value); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel1.java similarity index 80% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel1.java index 7198a8e18b4..36855f248aa 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedMid.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel1.java @@ -18,12 +18,12 @@ import java.util.List; -public class ForwardingNestedMid extends ForwardingNestedBase> { +public class ForwardingNestedLevel1 extends ForwardingNestedLevel2> { - public ForwardingNestedMid() { + public ForwardingNestedLevel1() { } - public ForwardingNestedMid(final List value) { + public ForwardingNestedLevel1(final List value) { super(value); } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel2.java similarity index 86% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel2.java index 92d56a0a387..2308035d504 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedBase.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedLevel2.java @@ -18,13 +18,13 @@ import java.util.Objects; -public class ForwardingNestedBase { +public class ForwardingNestedLevel2 { private E value; - public ForwardingNestedBase() { + public ForwardingNestedLevel2() { } - public ForwardingNestedBase(final E value) { + public ForwardingNestedLevel2(final E value) { this.value = value; } @@ -45,7 +45,7 @@ public boolean equals(final Object o) { return false; } - ForwardingNestedBase that = (ForwardingNestedBase) o; + ForwardingNestedLevel2 that = (ForwardingNestedLevel2) o; if (!Objects.equals(value, that.value)) { return false; diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java index 283239c94d8..af8b029cd2e 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingNestedModel.java @@ -18,7 +18,7 @@ import java.util.List; -public class ForwardingNestedModel extends ForwardingNestedMid { +public class ForwardingNestedModel extends ForwardingNestedLevel1 { public ForwardingNestedModel() { } 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 index 1a68e08f86f..745f1e80de1 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingWildcardModel.java @@ -18,7 +18,7 @@ import java.util.List; -public class ForwardingWildcardModel extends ForwardingNestedBase> { +public class ForwardingWildcardModel extends ForwardingNestedLevel2> { public ForwardingWildcardModel() { } From 173071b439f343b0b5a39eecbe2d945380636b64 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Tue, 14 Jul 2026 21:56:18 -0700 Subject: [PATCH 06/10] Fix tests. --- .../main/org/bson/codecs/pojo/TypeData.java | 7 ++++--- .../org/bson/codecs/pojo/ClassModelTest.java | 2 +- .../org/bson/codecs/pojo/TypeDataTest.java | 18 ++++++++++++++++++ .../pojo/entities/ForwardingArrayLevel1.java | 4 ++-- .../pojo/entities/ForwardingArrayLevel2.java | 10 +++++----- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index d3cf35b8dad..8cdff4e75b5 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -111,9 +111,10 @@ private static TypeData resolveTypeVariable(final TypeVariable type, final @Nullable final TypeData currentClassTypeData) { if (currentClassTypeData != null) { for (int i = 0; i < currentClassTypeParameters.size(); i++) { - // JLS §4.4: a type variable is declared once and referenced by the same instance - // everywhere it appears in scope, so reference equality is sufficient. - if (currentClassTypeParameters.get(i) == type) { + // TypeVariable.equals() is the documented contract: the JDK spec explicitly states + // that multiple instances may represent the same type variable and that == must not + // be relied upon — only equals() is guaranteed to hold between them. + if (currentClassTypeParameters.get(i).equals(type)) { if (i < currentClassTypeData.getTypeParameters().size()) { return currentClassTypeData.getTypeParameters().get(i); } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index b6f55c29a4a..b86db62ccbd 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -312,7 +312,7 @@ public void testForwardingArrayTypeVariableErasedToObject() { ClassModel classModel = ClassModel.builder(ForwardingArrayModel.class).build(); assertEquals(1, classModel.getPropertyModels().size()); - assertEquals(createTypeData(Object[].class), classModel.getPropertyModel("value").getTypeData()); + assertEquals(createTypeData(Object.class), classModel.getPropertyModel("value").getTypeData()); } @Test 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 1560770e537..3269ab647b5 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -195,6 +195,23 @@ public void newInstanceErasesGenericArrayTypeToObject() throws NoSuchFieldExcept assertEquals(expected, actual); } + @Test + public void newInstanceErasesGenericArrayTypeArgumentToObject() throws NoSuchFieldException { + // List — the List is a ParameterizedType whose type argument T[] is a GenericArrayType. + // resolveTypeArgument is called on T[] and must hit the else branch, erasing it to Object. + Field field = Holder.class.getDeclaredField("listOfGenericArrays"); + Type genericType = field.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(Object.class).build()).build(); + TypeData actual = TypeData.newInstance(genericType, List.class, typeParams, currentResolved); + assertEquals(expected, actual); + } + @Test public void newInstanceResolvesBoundedWildcardToUpperBound() throws NoSuchFieldException { Field boundedWildcardField = Holder.class.getDeclaredField("boundedWildcard"); @@ -216,6 +233,7 @@ private static class Holder { private Map nestedMap; private List boundedWildcard; private T[] genericArray; + private List listOfGenericArrays; } private static class NonGeneric { diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java index ee86cbf8bfd..5c718582ca1 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel1.java @@ -16,12 +16,12 @@ package org.bson.codecs.pojo.entities; -public class ForwardingArrayLevel1 extends ForwardingArrayLevel2 { +public class ForwardingArrayLevel1 extends ForwardingArrayLevel2 { public ForwardingArrayLevel1() { } - public ForwardingArrayLevel1(final U[] value) { + public ForwardingArrayLevel1(final T[] value) { super(value); } } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java index 05c99c2c485..f9a0c4bdfa9 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingArrayLevel2.java @@ -16,21 +16,21 @@ package org.bson.codecs.pojo.entities; -public class ForwardingArrayLevel2 { - private T[] value; +public class ForwardingArrayLevel2 { + private E value; public ForwardingArrayLevel2() { } - public ForwardingArrayLevel2(final T[] value) { + public ForwardingArrayLevel2(final E value) { this.value = value; } - public T[] getValue() { + public E getValue() { return value; } - public void setValue(final T[] value) { + public void setValue(final E value) { this.value = value; } } From 25af3509f583d3e6d08464e293984f4b79b3ca04 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Tue, 14 Jul 2026 23:44:44 -0700 Subject: [PATCH 07/10] Change Javadoc. --- bson/src/main/org/bson/codecs/pojo/TypeData.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bson/src/main/org/bson/codecs/pojo/TypeData.java b/bson/src/main/org/bson/codecs/pojo/TypeData.java index 8cdff4e75b5..f50f53569ec 100644 --- a/bson/src/main/org/bson/codecs/pojo/TypeData.java +++ b/bson/src/main/org/bson/codecs/pojo/TypeData.java @@ -111,9 +111,12 @@ private static TypeData resolveTypeVariable(final TypeVariable type, final @Nullable final TypeData currentClassTypeData) { if (currentClassTypeData != null) { for (int i = 0; i < currentClassTypeParameters.size(); i++) { - // TypeVariable.equals() is the documented contract: the JDK spec explicitly states - // that multiple instances may represent the same type variable and that == must not - // be relied upon — only equals() is guaranteed to hold between them. + // Given 'class B extends A {}': + // - JLS §6.3: the scope of B's type parameter T includes the superclass clause. + // - JLS §6.5.5.1: T in "extends A" therefore denotes B's T, not A's. + // Both reflection paths represent the same declaration, and the TypeVariable + // contract states "all instances representing a type variable must be equal() to + // each other". if (currentClassTypeParameters.get(i).equals(type)) { if (i < currentClassTypeData.getTypeParameters().size()) { return currentClassTypeData.getTypeParameters().get(i); From 0980eacf66e1544584a6abb35a3ac10b8c752ee7 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Wed, 15 Jul 2026 10:42:03 -0700 Subject: [PATCH 08/10] Add mixed interface/class tests. --- .../org/bson/codecs/pojo/ClassModelTest.java | 29 ++++++--- .../bson/codecs/pojo/PojoRoundTripTest.java | 6 -- .../pojo/entities/ForwardingDeepLevel3.java | 61 ------------------- .../ForwardingDualInterfaceLevel1.java | 46 ++++++++++++++ .../ForwardingDualInterfaceModel.java | 27 ++++++++ ... ForwardingDualSecondInterfaceLevel1.java} | 11 +--- ...va => ForwardingMixedInterfaceLevel1.java} | 11 +--- .../pojo/entities/ForwardingMixedLevel1.java | 46 ++++++++++++++ ...epModel.java => ForwardingMixedModel.java} | 8 +-- 9 files changed, 149 insertions(+), 96 deletions(-) delete mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceLevel1.java create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceModel.java rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeepLevel1.java => ForwardingDualSecondInterfaceLevel1.java} (77%) rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeepLevel2.java => ForwardingMixedInterfaceLevel1.java} (77%) create mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedLevel1.java rename bson/src/test/unit/org/bson/codecs/pojo/entities/{ForwardingDeepModel.java => ForwardingMixedModel.java} (74%) diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index b86db62ccbd..bcdb76e6fe2 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -20,8 +20,9 @@ import org.bson.codecs.pojo.entities.CollectionNestedPojoModel; import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel; -import org.bson.codecs.pojo.entities.ForwardingDeepModel; import org.bson.codecs.pojo.entities.ForwardingInterfaceModel; +import org.bson.codecs.pojo.entities.ForwardingDualInterfaceModel; +import org.bson.codecs.pojo.entities.ForwardingMixedModel; import org.bson.codecs.pojo.entities.ForwardingModel; import org.bson.codecs.pojo.entities.ForwardingArrayModel; import org.bson.codecs.pojo.entities.ForwardingNestedModel; @@ -280,14 +281,6 @@ public void testForwardingClassChain() { assertEquals(createTypeData(String.class), classModel.getPropertyModel("value").getTypeData()); } - @Test - public void testForwardingDeepChain() { - ClassModel classModel = ClassModel.builder(ForwardingDeepModel.class).build(); - - assertEquals(1, classModel.getPropertyModels().size()); - assertEquals(createTypeData(Long.class), classModel.getPropertyModel("value").getTypeData()); - } - @Test public void testForwardingInterfaceChain() { ClassModel classModel = ClassModel.builder(ForwardingInterfaceModel.class).build(); @@ -315,6 +308,24 @@ public void testForwardingArrayTypeVariableErasedToObject() { assertEquals(createTypeData(Object.class), classModel.getPropertyModel("value").getTypeData()); } + @Test + public void testForwardingMixedClassAndInterface() { + ClassModel classModel = ClassModel.builder(ForwardingMixedModel.class).build(); + + assertEquals(2, classModel.getPropertyModels().size()); + assertEquals(createTypeData(String.class), classModel.getPropertyModel("field1").getTypeData()); + assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("field2").getTypeData()); + } + + @Test + public void testForwardingDualInterface() { + ClassModel classModel = ClassModel.builder(ForwardingDualInterfaceModel.class).build(); + + assertEquals(2, classModel.getPropertyModels().size()); + assertEquals(createTypeData(String.class), classModel.getPropertyModel("field2").getTypeData()); + assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("field1").getTypeData()); + } + @Test public void testHolderConcreteMap() { ClassModel classModel = ClassModel.builder(HolderConcreteMapModel.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 798f0781f19..f997efa57d3 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/PojoRoundTripTest.java @@ -30,7 +30,6 @@ import org.bson.codecs.pojo.entities.ConventionModel; import org.bson.codecs.pojo.entities.DuplicateAnnotationAllowedModel; import org.bson.codecs.pojo.entities.FieldAndPropertyTypeMismatchModel; -import org.bson.codecs.pojo.entities.ForwardingDeepModel; import org.bson.codecs.pojo.entities.ForwardingInterfaceModel; import org.bson.codecs.pojo.entities.ForwardingModel; import org.bson.codecs.pojo.entities.ForwardingNestedModel; @@ -536,11 +535,6 @@ private static List testCases() { getPojoCodecProviderBuilder(ForwardingModel.class), "{'value': 'hello'}")); - data.add(new TestData("Forwarding deep class chain resolves to Long", - new ForwardingDeepModel(42L), - getPojoCodecProviderBuilder(ForwardingDeepModel.class), - "{'value': {'$numberLong': '42'}}")); - data.add(new TestData("Forwarding interface chain resolves to Integer", new ForwardingInterfaceModel(7), getPojoCodecProviderBuilder(ForwardingInterfaceModel.class), diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java deleted file mode 100644 index 1b310c4f634..00000000000 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel3.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.Objects; - -public class ForwardingDeepLevel3 { - private A value; - - public ForwardingDeepLevel3() { - } - - public ForwardingDeepLevel3(final A value) { - this.value = value; - } - - public A getValue() { - return value; - } - - public void setValue(final A value) { - this.value = value; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - ForwardingDeepLevel3 that = (ForwardingDeepLevel3) o; - - if (!Objects.equals(value, that.value)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } -} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceLevel1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceLevel1.java new file mode 100644 index 00000000000..57031610232 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceLevel1.java @@ -0,0 +1,46 @@ +/* + * 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; + +public abstract class ForwardingDualInterfaceLevel1 implements ForwardingMixedInterfaceLevel1, ForwardingDualSecondInterfaceLevel1 { + private A field2; + private B field1; + + public ForwardingDualInterfaceLevel1() { + } + + public ForwardingDualInterfaceLevel1(final A field2, final B field1) { + this.field2 = field2; + this.field1 = field1; + } + + public A getField2() { + return field2; + } + + public void setField2(final A field2) { + this.field2 = field2; + } + + public B getField1() { + return field1; + } + + public void setField1(final B field1) { + this.field1 = field1; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceModel.java new file mode 100644 index 00000000000..1bedd92794e --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualInterfaceModel.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class ForwardingDualInterfaceModel extends ForwardingDualInterfaceLevel1 { + + public ForwardingDualInterfaceModel() { + } + + public ForwardingDualInterfaceModel(final String field2, final Integer field1) { + super(field2, field1); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualSecondInterfaceLevel1.java similarity index 77% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualSecondInterfaceLevel1.java index b2c13c89028..dd0105d68ec 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel1.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDualSecondInterfaceLevel1.java @@ -16,12 +16,7 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeepLevel1 extends ForwardingDeepLevel2 { - - public ForwardingDeepLevel1() { - } - - public ForwardingDeepLevel1(final C value) { - super(value); - } +public interface ForwardingDualSecondInterfaceLevel1 { + J getField1(); + void setField1(J field1); } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedInterfaceLevel1.java similarity index 77% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedInterfaceLevel1.java index e1f2ec02620..ade2ef48b7c 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepLevel2.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedInterfaceLevel1.java @@ -16,12 +16,7 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeepLevel2 extends ForwardingDeepLevel3 { - - public ForwardingDeepLevel2() { - } - - public ForwardingDeepLevel2(final B value) { - super(value); - } +public interface ForwardingMixedInterfaceLevel1 { + I getField2(); + void setField2(I field2); } diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedLevel1.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedLevel1.java new file mode 100644 index 00000000000..6ef5a919fac --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedLevel1.java @@ -0,0 +1,46 @@ +/* + * 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; + +public abstract class ForwardingMixedLevel1 implements ForwardingMixedInterfaceLevel1 { + private T field1; + private I field2; + + public ForwardingMixedLevel1() { + } + + public ForwardingMixedLevel1(final T field1, final I field2) { + this.field1 = field1; + this.field2 = field2; + } + + public T getField1() { + return field1; + } + + public void setField1(final T field1) { + this.field1 = field1; + } + + public I getField2() { + return field2; + } + + public void setField2(final I field2) { + this.field2 = field2; + } +} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedModel.java similarity index 74% rename from bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java rename to bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedModel.java index 979f2ec3fac..a7864b6e3bd 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingDeepModel.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/entities/ForwardingMixedModel.java @@ -16,12 +16,12 @@ package org.bson.codecs.pojo.entities; -public class ForwardingDeepModel extends ForwardingDeepLevel1 { +public class ForwardingMixedModel extends ForwardingMixedLevel1 { - public ForwardingDeepModel() { + public ForwardingMixedModel() { } - public ForwardingDeepModel(final Long value) { - super(value); + public ForwardingMixedModel(final String field1, final Integer field2) { + super(field1, field2); } } From 9ef14c2f514b05c6c632569e9dbe576c995f9176 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Wed, 15 Jul 2026 23:21:40 -0700 Subject: [PATCH 09/10] Fix comment and name. --- bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java | 6 +++--- bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index bcdb76e6fe2..bbffecde133 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -299,9 +299,9 @@ public void testForwardingNested() { @Test public void testForwardingArrayTypeVariableErasedToObject() { - // T[] fields in a generic base class are GenericArrayType; getTypeParameterMap does not - // handle GenericArrayType component types, so the T[] erases to Object[] regardless of - // the concrete binding at the leaf subclass. + // The type argument `T[]` in `extends ForwardingArrayLevel2` is a GenericArrayType; + // getTypeParameterMap does not handle GenericArrayType, so the `value` property erases to + // Object regardless of the concrete binding at the leaf subclass. ClassModel classModel = ClassModel.builder(ForwardingArrayModel.class).build(); assertEquals(1, classModel.getPropertyModels().size()); 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 3269ab647b5..6d6c866ca79 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/TypeDataTest.java @@ -182,7 +182,7 @@ public void newInstanceWithEmptyTypeParamsReturnsRawTypeData() throws NoSuchFiel } @Test - public void newInstanceErasesGenericArrayTypeToObject() throws NoSuchFieldException { + public void newInstanceErasesGenericArrayTypeToObjectArray() throws NoSuchFieldException { Field arrayField = Holder.class.getDeclaredField("genericArray"); Type arrayGenericType = arrayField.getGenericType(); From c3a77f46ed058ed37fe5bf4ed3845aba48e0c3a5 Mon Sep 17 00:00:00 2001 From: Slav Babanin Date: Thu, 16 Jul 2026 08:08:37 -0700 Subject: [PATCH 10/10] Remove redundant holder tests. --- .../org/bson/codecs/pojo/ClassModelTest.java | 11 +--- .../codecs/pojo/entities/HolderAbstract.java | 63 ------------------- .../pojo/entities/HolderConcreteMapModel.java | 29 --------- .../codecs/pojo/entities/HolderInterface.java | 23 ------- 4 files changed, 1 insertion(+), 125 deletions(-) delete mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java delete mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java delete mode 100644 bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java diff --git a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java index bbffecde133..ddee9ae75dc 100644 --- a/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java +++ b/bson/src/test/unit/org/bson/codecs/pojo/ClassModelTest.java @@ -27,7 +27,6 @@ import org.bson.codecs.pojo.entities.ForwardingArrayModel; import org.bson.codecs.pojo.entities.ForwardingNestedModel; import org.bson.codecs.pojo.entities.GenericHolderModel; -import org.bson.codecs.pojo.entities.HolderConcreteMapModel; import org.bson.codecs.pojo.entities.InterfaceBasedModel; import org.bson.codecs.pojo.entities.ListGenericExtendedModel; import org.bson.codecs.pojo.entities.ListListGenericExtendedModel; @@ -326,15 +325,7 @@ public void testForwardingDualInterface() { assertEquals(createTypeData(Integer.class), classModel.getPropertyModel("field1").getTypeData()); } - @Test - public void testHolderConcreteMap() { - ClassModel classModel = ClassModel.builder(HolderConcreteMapModel.class).build(); - - assertEquals(1, classModel.getPropertyModels().size()); - assertEquals(createTypeData(Map.class, String.class, Object.class), classModel.getPropertyModel("value").getTypeData()); - } - - TypeData.Builder createBuilder(final Class clazz, final Class... types) { + TypeData.Builder createBuilder(final Class clazz, final Class... types) { TypeData.Builder builder = TypeData.builder(clazz); List> subTypes = new ArrayList<>(); for (final Class type : types) { diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java deleted file mode 100644 index 9416c51844e..00000000000 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderAbstract.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.Objects; - -public abstract class HolderAbstract implements HolderInterface { - private V value; - - public HolderAbstract() { - } - - public HolderAbstract(final V value) { - this.value = value; - } - - @Override - public V getValue() { - return value; - } - - @Override - public void setValue(final V v) { - this.value = v; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - HolderAbstract that = (HolderAbstract) o; - - if (!Objects.equals(value, that.value)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } -} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java deleted file mode 100644 index 584cb21691f..00000000000 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderConcreteMapModel.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.Map; - -public class HolderConcreteMapModel extends HolderAbstract> { - - public HolderConcreteMapModel() { - } - - public HolderConcreteMapModel(final Map value) { - super(value); - } -} diff --git a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java b/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java deleted file mode 100644 index 57aadf40989..00000000000 --- a/bson/src/test/unit/org/bson/codecs/pojo/entities/HolderInterface.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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; - -public interface HolderInterface { - V getValue(); - - void setValue(V v); -}