From 8ec9556619c08702345a22b97f83819297badde7 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 12 Aug 2026 17:30:18 +0800 Subject: [PATCH 1/2] fix(java): fix compatible metadata state regressions Keep non-root native TypeDef layers as field-scope metadata and isolate compatible read TypeInfo state from later local writes and copies.\n\nFixes #3926\nFixes #3929 --- .../fory/builder/BaseObjectCodecBuilder.java | 24 +- .../fory/meta/NativeTypeDefDecoder.java | 4 +- .../java/org/apache/fory/meta/TypeDef.java | 2 + .../serializer/AbstractObjectSerializer.java | 2 +- .../fory/serializer/ArraySerializers.java | 6 +- .../apache/fory/serializer/FieldGroups.java | 10 + .../collection/CollectionLikeSerializer.java | 7 +- .../collection/CollectionSerializers.java | 10 +- .../fory/meta/NativeTypeDefEncoderTest.java | 41 +++ .../serializer/CompatibleReadWriteTest.java | 299 ++++++++++++++++++ 10 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleReadWriteTest.java diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java index 6ac592f963..9b03ca34d9 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java @@ -1192,27 +1192,37 @@ protected Reference addExactTypeInfoField(Class cls) { } protected Reference addTypeInfoHolderField(Class cls) { + return addTypeInfoHolderField(cls, "TypeInfoHolder"); + } + + private Reference addTypeInfoHolderField(Class cls, String fieldSuffix) { // Final type need to write classinfo when meta share enabled. - String key; + String fieldKey; if (ReflectionUtils.isMonomorphic(cls)) { - key = "classInfoHolder:" + cls; + fieldKey = fieldSuffix + ":" + cls; } else { - key = "classInfoHolder:" + cls + walkPath; + fieldKey = fieldSuffix + ":" + cls + walkPath; } - Reference reference = (Reference) sharedFieldMap.get(key); + Reference reference = (Reference) sharedFieldMap.get(fieldKey); if (reference != null) { return reference; } Expression classInfoHolderExpr = inlineInvoke(typeResolverRef, "nilTypeInfoHolder", classInfoHolderTypeRef); - String name = ctx.newName(cls, "TypeInfoHolder"); + String name = ctx.newName(cls, fieldSuffix); ctx.addField(true, ctx.type(TypeInfoHolder.class), name, classInfoHolderExpr); // The class info field read only once, no need to shallow. reference = new Reference(name, classInfoHolderTypeRef); - sharedFieldMap.put(key, reference); + sharedFieldMap.put(fieldKey, reference); return reference; } + protected Reference addWriteTypeInfoHolderField(Class cls) { + // Compatible reads may retain remote schema metadata. Keep generated collection writes on a + // distinct local cache even when both paths have the same class and walk path. + return addTypeInfoHolderField(cls, "WriteTypeInfoHolder"); + } + protected Expression readTypeInfo(Class cls, Expression ignored) { return readTypeInfo(cls, ignored, true); } @@ -1544,7 +1554,7 @@ private Tuple2 writeElementsHeader( return Tuple2.of(bitmap, null); } else { Expression elementTypeExpr = getClassExpr(elementType); - Expression classInfoHolder = addTypeInfoHolderField(elementType); + Expression classInfoHolder = addWriteTypeInfoHolderField(elementType); Expression bitmap; if (trackingRef) { if (elementType == Object.class) { diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java b/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java index a690895863..e48ace5961 100644 --- a/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java +++ b/java/fory-core/src/main/java/org/apache/fory/meta/NativeTypeDefDecoder.java @@ -179,7 +179,9 @@ static TypeDef decodeTypeDef( int typeId = i == numClasses - 1 ? rootTypeId : resolver.getTypeIdForTypeDef(cls); classSpec = new ClassSpec(cls, typeId, resolver.getUserTypeIdForTypeDef(cls)); currentClass = cls; - } else { + } else if (i == numClasses - 1) { + // Only the root layer represents a dynamic object type. Non-root layers only label field + // ownership, so compatible matching uses their wire names without loading those classes. // `loadClassForMeta` keeps name-level checks before Class.forName; do not replace this // metadata path with direct class loading from the remote TypeDef name. Class cls = diff --git a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java index 0584fc74c6..6e8995a073 100644 --- a/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java +++ b/java/fory-core/src/main/java/org/apache/fory/meta/TypeDef.java @@ -445,6 +445,8 @@ private List buildDescriptors( } else { String fieldName = fieldInfo.getFieldName(); String definedClass = fieldInfo.getDefinedClass(); + // Match the wire declaring-class name before any compatibility fallback attempts to + // resolve that name as a class. descriptor = descriptorsMap.get(definedClass + "." + fieldName); } boolean remoteOnly = false; diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java index a71679456e..5f0d7d6be6 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java @@ -542,7 +542,7 @@ private static Object readContainerFieldValueNoRef( if (readContext.getConfig().isXlang()) { return readContext.readNonRef(fieldInfo.containerTypeInfo); } - return readContext.readNonRef(fieldInfo.classInfoHolder); + return readContext.readNonRef(fieldInfo.classInfoReadHolder); } private static Object readContainerFieldValueRef( diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/ArraySerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/ArraySerializers.java index 5f94bc0998..5b6714d497 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/ArraySerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/ArraySerializers.java @@ -109,7 +109,10 @@ public static Serializer newObjectArraySerializer(TypeResolver typeResolver, @SuppressWarnings({"unchecked", "rawtypes"}) public static final class ObjectArraySerializer extends Serializer { private final TypeResolver typeResolver; + // Compatible reads may cache remote schema metadata; local writes and copies retain the + // original local holder and must not reuse read state. private final TypeInfoHolder elementTypeInfoHolder; + private final TypeInfoHolder elementTypeInfoReadHolder; public ObjectArraySerializer(TypeResolver typeResolver, Class cls) { super(typeResolver.getConfig(), (Class) cls); @@ -119,6 +122,7 @@ public ObjectArraySerializer(TypeResolver typeResolver, Class cls) { } Preconditions.checkArgument(cls.isArray() && !cls.getComponentType().isPrimitive()); elementTypeInfoHolder = typeResolver.nilTypeInfoHolder(); + elementTypeInfoReadHolder = typeResolver.nilTypeInfoHolder(); } @Override @@ -155,7 +159,7 @@ public Object[] read(ReadContext readContext) { readArrayElements( readContext, typeResolver, - elementTypeInfoHolder, + elementTypeInfoReadHolder, type.getComponentType(), value, numElements); diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java index 06e7637071..8d479233f8 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java @@ -183,7 +183,10 @@ public static final class SerializationFieldInfo { public final TypeInfo typeInfo; public final Serializer serializer; public final GenericType genericType; + // Native compatible container reads may cache remote schema metadata; writes retain the + // original local holder and must not reuse read state. public final TypeInfoHolder classInfoHolder; + public final TypeInfoHolder classInfoReadHolder; public final TypeInfo containerTypeInfo; public final Serializer containerSerializerOverride; public final FieldCodecCategory codecCategory; @@ -289,6 +292,13 @@ public SerializationFieldInfo(TypeResolver resolver, Descriptor d) { } else { containerSerializerOverride = null; } + classInfoReadHolder = + codecCategory == FieldCodecCategory.CONTAINER + && refMode != RefMode.TRACKING + && !resolver.isCrossLanguage() + && containerSerializerOverride == null + ? resolver.nilTypeInfoHolder() + : null; if (!resolver.isCrossLanguage()) { containerTypeInfo = null; } else { diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java index 668256374e..be5affc2be 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java @@ -55,7 +55,10 @@ public abstract class CollectionLikeSerializer extends Serializer { private final int collectionOwnerBytes; protected final Config config; protected final boolean supportCodegenHook; + // Compatible reads may cache remote schema metadata; local writes and copies retain the + // original local holder and must not reuse read state. protected final TypeInfoHolder elementTypeInfoHolder; + protected final TypeInfoHolder elementTypeInfoReadHolder; protected final TypeResolver typeResolver; // For subclass whose element type are instantiated already, such as @@ -97,6 +100,7 @@ protected CollectionLikeSerializer( this.collectionOwnerBytes = collectionOwnerBytes; this.supportCodegenHook = supportCodegenHook; elementTypeInfoHolder = typeResolver.nilTypeInfoHolder(); + elementTypeInfoReadHolder = typeResolver.nilTypeInfoHolder(); this.typeResolver = typeResolver; } @@ -657,7 +661,8 @@ private void generalJavaRead( Serializer serializer; TypeResolver typeResolver = this.typeResolver; if ((flags & CollectionFlags.IS_DECL_ELEMENT_TYPE) != CollectionFlags.IS_DECL_ELEMENT_TYPE) { - serializer = typeResolver.readTypeInfo(readContext, elementTypeInfoHolder).getSerializer(); + serializer = + typeResolver.readTypeInfo(readContext, elementTypeInfoReadHolder).getSerializer(); } else { serializer = elemGenericType.getSerializer(typeResolver); } diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java index 0d2fce274b..cade00a4d6 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java @@ -717,14 +717,20 @@ public Collection onCollectionWrite(WriteContext writeContext, Set value) { public static final class ConcurrentHashMapKeySetViewSerializer extends CollectionSerializer { + // Compatible reads may cache remote schema metadata; local writes retain the original local + // holders and must not reuse read state. private final TypeInfoHolder mapTypeInfoHolder; + private final TypeInfoHolder mapTypeInfoReadHolder; private final TypeInfoHolder valueTypeInfoHolder; + private final TypeInfoHolder valueTypeInfoReadHolder; public ConcurrentHashMapKeySetViewSerializer( TypeResolver typeResolver, Class type) { super(typeResolver, type, false); mapTypeInfoHolder = typeResolver.nilTypeInfoHolder(); + mapTypeInfoReadHolder = typeResolver.nilTypeInfoHolder(); valueTypeInfoHolder = typeResolver.nilTypeInfoHolder(); + valueTypeInfoReadHolder = typeResolver.nilTypeInfoHolder(); } @Override @@ -735,8 +741,8 @@ public void write(WriteContext writeContext, ConcurrentHashMap.KeySetView value) @Override public ConcurrentHashMap.KeySetView read(ReadContext readContext) { - ConcurrentHashMap map = (ConcurrentHashMap) readContext.readRef(mapTypeInfoHolder); - Object value = readContext.readRef(valueTypeInfoHolder); + ConcurrentHashMap map = (ConcurrentHashMap) readContext.readRef(mapTypeInfoReadHolder); + Object value = readContext.readRef(valueTypeInfoReadHolder); readContext.reserveGraphMemory(KEY_SET_VIEW_OWNER_BYTES); return map.keySet(value); } diff --git a/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java b/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java index 6cec7d5083..f90eb9fc92 100644 --- a/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java +++ b/java/fory-core/src/test/java/org/apache/fory/meta/NativeTypeDefEncoderTest.java @@ -681,6 +681,47 @@ public void testAbstractParentClass() { } } + @Test + public void testUnregisteredParentLayer() { + Fory writer = registeredChildFory(); + Fory reader = registeredChildFory(); + writer.register(ChildClass.class, 100); + reader.register(ChildClass.class, 100); + ClassResolver readerResolver = (ClassResolver) reader.getTypeResolver(); + Assert.assertFalse(readerResolver.isRegisteredByName(BaseAbstractClass.class.getName())); + + ChildClass child = new ChildClass(); + child.setId("123"); + child.setName("test"); + ChildClass decoded = (ChildClass) reader.deserialize(writer.serialize(child)); + Assert.assertEquals(decoded.getId(), "123"); + Assert.assertEquals(decoded.getName(), "test"); + + TypeDef typeDef = TypeDef.buildTypeDef(writer.getTypeResolver(), ChildClass.class); + TypeDef decodedTypeDef = + TypeDef.readTypeDef(readerResolver, MemoryBuffer.fromByteArray(typeDef.getEncoded())); + FieldInfo parentField = + decodedTypeDef.getFieldsInfo().stream() + .filter(field -> field.getFieldName().equals("id")) + .findFirst() + .orElseThrow(AssertionError::new); + Assert.assertEquals(parentField.getDefinedClass(), BaseAbstractClass.class.getName()); + Assert.assertEquals(decodedTypeDef.getClassSpec().type, ChildClass.class); + } + + private static Fory registeredChildFory() { + return Fory.builder() + .withXlang(false) + .withRefTracking(false) + .withMetaShare(true) + .withScopedMetaShare(true) + .withCompatible(true) + .withDeserializeUnknownClass(false) + .withAsyncCompilation(false) + .requireClassRegistration(true) + .build(); + } + @Data public abstract static class BaseAbstractClass { private String id; diff --git a/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleReadWriteTest.java b/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleReadWriteTest.java new file mode 100644 index 0000000000..90433f2d08 --- /dev/null +++ b/java/fory-core/src/test/java/org/apache/fory/serializer/CompatibleReadWriteTest.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.fory.serializer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.apache.fory.Fory; +import org.apache.fory.ForyTestBase; +import org.apache.fory.builder.Generated; +import org.apache.fory.context.ReadContext; +import org.apache.fory.context.WriteContext; +import org.apache.fory.memory.MemoryBuffer; +import org.apache.fory.resolver.TypeInfoHolder; +import org.apache.fory.serializer.collection.CollectionSerializers; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class CompatibleReadWriteTest extends ForyTestBase { + private static final int ITEM_ID = 100; + private static final int CONTAINER_ID = 101; + private static final int BOX_ID = 102; + + public static class ItemV1 { + public String name; + } + + public static class ItemV2 { + public String name; + public String tag; + } + + public static class ArrayBoxV1 { + public ItemV1[] items; + } + + public static class ArrayBoxV2 { + public ItemV2[] items; + } + + public static class ListBox { + public List items; + } + + public static class CustomListV1 extends ArrayList { + public String name; + } + + public static class CustomListV2 extends ArrayList { + public String name; + public String tag; + } + + @Test(dataProvider = "twoBoolOptions") + public void testArrayReadThenWrite(boolean referenceTracking, boolean codegen) { + Fory oldWriter = compatibleFory(referenceTracking, codegen); + oldWriter.register(ItemV1.class, ITEM_ID); + oldWriter.register(ItemV1[].class, CONTAINER_ID); + oldWriter.register(ArrayBoxV1.class, BOX_ID); + ArrayBoxV1 oldBox = new ArrayBoxV1(); + oldBox.items = new ItemV1[] {oldItem()}; + + Fory fory = compatibleFory(referenceTracking, codegen); + Fory fresh = compatibleFory(referenceTracking, codegen); + for (Fory current : new Fory[] {fory, fresh}) { + current.register(ItemV2.class, ITEM_ID); + current.register(ItemV2[].class, CONTAINER_ID); + current.register(ArrayBoxV2.class, BOX_ID); + } + ArrayBoxV2 newBox = new ArrayBoxV2(); + newBox.items = new ItemV2[] {newItem()}; + + byte[] expected = fresh.serialize(newBox); + fory.deserialize(oldWriter.serialize(oldBox)); + ItemV2[] copy = fory.copy(newBox.items); + Assert.assertNotSame(copy, newBox.items); + Assert.assertNotSame(copy[0], newBox.items[0]); + Assert.assertEquals(copy[0].tag, "item-v2"); + + byte[] actual = fory.serialize(newBox); + Assert.assertEquals(actual, expected); + Assert.assertEquals(((ArrayBoxV2) fresh.deserialize(actual)).items[0].tag, "item-v2"); + } + + @Test(dataProvider = "twoBoolOptions") + public void testCollectionReadThenWrite(boolean referenceTracking, boolean codegen) { + Fory oldWriter = compatibleFory(referenceTracking, codegen); + oldWriter.register(ItemV1.class, ITEM_ID); + ArrayList oldItems = new ArrayList<>(); + oldItems.add(oldItem()); + + Fory fory = compatibleFory(referenceTracking, codegen); + Fory fresh = compatibleFory(referenceTracking, codegen); + for (Fory current : new Fory[] {fory, fresh}) { + current.register(ItemV2.class, ITEM_ID); + } + ArrayList newItems = new ArrayList<>(); + newItems.add(newItem()); + + byte[] expected = fresh.serialize(newItems); + fory.deserialize(oldWriter.serialize(oldItems)); + ArrayList copy = fory.copy(newItems); + Assert.assertNotSame(copy, newItems); + Assert.assertNotSame(copy.get(0), newItems.get(0)); + Assert.assertEquals(copy.get(0).tag, "item-v2"); + + byte[] actual = fory.serialize(newItems); + Assert.assertEquals(actual, expected); + ArrayList decoded = (ArrayList) fresh.deserialize(actual); + Assert.assertEquals(decoded.get(0).tag, "item-v2"); + } + + @Test(dataProvider = "oneBoolOption") + public void testGeneratedCollectionReadThenWrite(boolean referenceTracking) { + Fory oldWriter = compatibleFory(referenceTracking, true); + oldWriter.register(ItemV1.class, ITEM_ID); + oldWriter.register(ListBox.class, BOX_ID); + ListBox oldBox = new ListBox(); + oldBox.items = new ArrayList<>(); + oldBox.items.add(oldItem()); + + Fory fory = compatibleFory(referenceTracking, true); + Fory fresh = compatibleFory(referenceTracking, true); + for (Fory current : new Fory[] {fory, fresh}) { + current.register(ItemV2.class, ITEM_ID); + current.register(ListBox.class, BOX_ID); + } + Assert.assertTrue( + Generated.GeneratedSerializer.class.isAssignableFrom( + fory.getTypeResolver().getSerializerClass(ListBox.class))); + List holderNames = + Arrays.stream(fory.getSerializer(ListBox.class).getClass().getDeclaredFields()) + .filter(field -> field.getType() == TypeInfoHolder.class) + .map(field -> field.getName()) + .collect(Collectors.toList()); + Assert.assertTrue( + holderNames.stream().anyMatch(name -> name.contains("WriteTypeInfoHolder")), + holderNames.toString()); + ListBox newBox = new ListBox(); + newBox.items = new ArrayList<>(); + newBox.items.add(newItem()); + + byte[] expected = fresh.serialize(newBox); + Assert.assertEquals(fory.serialize(newBox), expected); + fory.deserialize(oldWriter.serialize(oldBox)); + byte[] actual = fory.serialize(newBox); + Assert.assertEquals(actual, expected); + ListBox decoded = (ListBox) fresh.deserialize(actual); + Assert.assertEquals(((ItemV2) decoded.items.get(0)).tag, "item-v2"); + } + + @Test + public void testContainerFieldReadThenWrite() { + Fory oldWriter = compatibleFory(false, false); + oldWriter.register(ItemV1.class, ITEM_ID); + oldWriter.register(CustomListV1.class, CONTAINER_ID); + CustomListV1 oldItems = new CustomListV1(); + oldItems.name = "old-list"; + oldItems.add(oldItem()); + + Fory fory = compatibleFory(false, false); + Fory fresh = compatibleFory(false, false); + for (Fory current : new Fory[] {fory, fresh}) { + current.register(ItemV2.class, ITEM_ID); + current.register(CustomListV2.class, CONTAINER_ID); + } + CustomListV2 newItems = new CustomListV2(); + newItems.name = "new-list"; + newItems.tag = "list-v2"; + newItems.add(newItem()); + + FieldGroups.SerializationFieldInfo oldFieldInfo = containerFieldInfo(oldWriter); + FieldGroups.SerializationFieldInfo fieldInfo = containerFieldInfo(fory); + FieldGroups.SerializationFieldInfo freshFieldInfo = containerFieldInfo(fresh); + byte[] expected = writeContainerField(fresh, freshFieldInfo, newItems); + readContainerField(fory, fieldInfo, writeContainerField(oldWriter, oldFieldInfo, oldItems)); + Assert.assertEquals(fieldInfo.classInfoReadHolder.typeInfo.getType(), CustomListV2.class); + Assert.assertNotEquals(fieldInfo.classInfoHolder.typeInfo.getType(), CustomListV2.class); + byte[] actual = writeContainerField(fory, fieldInfo, newItems); + Assert.assertEquals(actual, expected); + CustomListV2 decoded = (CustomListV2) readContainerField(fresh, freshFieldInfo, actual); + Assert.assertEquals(decoded.tag, "list-v2"); + Assert.assertEquals(((ItemV2) decoded.get(0)).tag, "item-v2"); + } + + @Test(dataProvider = "oneBoolOption") + public void testKeySetViewReadThenWrite(boolean referenceTracking) { + Fory oldWriter = compatibleFory(referenceTracking, false); + oldWriter.register(ItemV1.class, ITEM_ID); + ConcurrentHashMap.KeySetView oldView = + new ConcurrentHashMap().keySet(oldItem()); + + Fory fory = compatibleFory(referenceTracking, false); + Fory fresh = compatibleFory(referenceTracking, false); + for (Fory current : new Fory[] {fory, fresh}) { + current.register(ItemV2.class, ITEM_ID); + } + Assert.assertEquals( + fory.getTypeResolver().getSerializerClass(oldView.getClass()), + CollectionSerializers.ConcurrentHashMapKeySetViewSerializer.class); + ConcurrentHashMap.KeySetView newView = + new ConcurrentHashMap().keySet(newItem()); + + byte[] expected = fresh.serialize(newView); + fory.deserialize(oldWriter.serialize(oldView)); + byte[] actual = fory.serialize(newView); + Assert.assertEquals(actual, expected); + ConcurrentHashMap.KeySetView decoded = + (ConcurrentHashMap.KeySetView) fresh.deserialize(actual); + Assert.assertEquals(decoded.getMappedValue().tag, "item-v2"); + } + + private static Fory compatibleFory(boolean referenceTracking, boolean codegen) { + return Fory.builder() + .withXlang(false) + .withRefTracking(referenceTracking) + .withCompatible(true) + .withCodegen(codegen) + .withAsyncCompilation(false) + .requireClassRegistration(true) + .build(); + } + + private static FieldGroups.SerializationFieldInfo containerFieldInfo(Fory fory) { + return FieldGroups.buildFieldsInfo( + fory.getTypeResolver(), Arrays.asList(ListBox.class.getDeclaredFields())) + .containerFields[0]; + } + + private static byte[] writeContainerField( + Fory fory, FieldGroups.SerializationFieldInfo fieldInfo, Object value) { + MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(256); + WriteContext writeContext = fory.getWriteContext(); + writeContext.prepare(buffer, null); + try { + AbstractObjectSerializer.writeContainerFieldValue( + writeContext, + fory.getTypeResolver(), + writeContext.getRefWriter(), + writeContext.getGenerics(), + fieldInfo, + buffer, + value); + return buffer.getBytes(0, buffer.writerIndex()); + } finally { + writeContext.reset(); + } + } + + private static Object readContainerField( + Fory fory, FieldGroups.SerializationFieldInfo fieldInfo, byte[] bytes) { + MemoryBuffer buffer = MemoryBuffer.fromByteArray(bytes); + ReadContext readContext = fory.getReadContext(); + readContext.prepare(buffer, null, false); + try { + return AbstractObjectSerializer.readContainerFieldValue( + readContext, + fory.getTypeResolver(), + readContext.getRefReader(), + readContext.getGenerics(), + fieldInfo, + buffer); + } finally { + readContext.reset(); + } + } + + private static ItemV1 oldItem() { + ItemV1 item = new ItemV1(); + item.name = "item"; + return item; + } + + private static ItemV2 newItem() { + ItemV2 item = new ItemV2(); + item.name = "item"; + item.tag = "item-v2"; + return item; + } +} From d0c9cb3dfb2e5dc48dfd1b8450ccb1f0ad4aa783 Mon Sep 17 00:00:00 2001 From: chaokunyang Date: Wed, 12 Aug 2026 19:18:21 +0800 Subject: [PATCH 2/2] fix(java): preserve compatible remote field categories --- .../apache/fory/builder/CompatibleCodecBuilder.java | 12 +++++------- .../java/org/apache/fory/serializer/FieldGroups.java | 8 ++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java index 7be4a6b044..ebb1ee5647 100644 --- a/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java +++ b/java/fory-core/src/main/java/org/apache/fory/builder/CompatibleCodecBuilder.java @@ -52,6 +52,7 @@ import org.apache.fory.resolver.TypeResolver; import org.apache.fory.serializer.CodegenSerializer; import org.apache.fory.serializer.CompatibleSerializer; +import org.apache.fory.serializer.FieldGroups; import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo; import org.apache.fory.serializer.ObjectSerializer; import org.apache.fory.serializer.Serializer; @@ -561,13 +562,10 @@ private void ensureCompatibleFieldInfos() { public static SerializationFieldInfo[] buildRemoteFieldInfos( TypeResolver typeResolver, Class cls, TypeDef typeDef) { - List descriptors = - typeResolver.createDescriptorGrouper(typeDef, cls).getSortedDescriptors(); - SerializationFieldInfo[] fieldInfos = new SerializationFieldInfo[descriptors.size()]; - for (int i = 0; i < descriptors.size(); i++) { - fieldInfos[i] = new SerializationFieldInfo(typeResolver, descriptors.get(i)); - } - return fieldInfos; + DescriptorGrouper grouper = typeResolver.createDescriptorGrouper(typeDef, cls); + // Generated skip helpers dispatch from SerializationFieldInfo.codecCategory. Preserve the + // grouper's category instead of rebuilding every remote field as OTHER. + return FieldGroups.buildFieldInfos(typeResolver, grouper).allFields; } public static SerializationFieldInfo[] buildLocalFieldInfosByRemoteOrder( diff --git a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java index 8d479233f8..c94600c5dc 100644 --- a/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java +++ b/java/fory-core/src/main/java/org/apache/fory/serializer/FieldGroups.java @@ -292,13 +292,9 @@ public SerializationFieldInfo(TypeResolver resolver, Descriptor d) { } else { containerSerializerOverride = null; } + // Container fields retain distinct remote-read and local-write type metadata caches. classInfoReadHolder = - codecCategory == FieldCodecCategory.CONTAINER - && refMode != RefMode.TRACKING - && !resolver.isCrossLanguage() - && containerSerializerOverride == null - ? resolver.nilTypeInfoHolder() - : null; + codecCategory == FieldCodecCategory.CONTAINER ? resolver.nilTypeInfoHolder() : null; if (!resolver.isCrossLanguage()) { containerTypeInfo = null; } else {