Skip to content
Merged
210 changes: 210 additions & 0 deletions java/fory-core/src/test/java/org/apache/fory/CrossLanguageTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,22 @@ private void structRoundBack(Fory fory, Object obj, String testName) throws IOEx
Assert.assertEquals(fory.deserialize(Files.readAllBytes(dataFile)), obj);
}

private void structBackwardCompatibility(Fory fory, Object obj, String testName)
throws IOException {
byte[] serialized = fory.serialize(obj);
Assert.assertEquals(fory.deserialize(serialized), obj);
Path dataFile = Paths.get(testName);
System.out.println(dataFile.toAbsolutePath());
Files.deleteIfExists(dataFile);
Files.write(dataFile, serialized);
dataFile.toFile().deleteOnExit();
ImmutableList<String> command =
ImmutableList.of(
PYTHON_EXECUTABLE, "-m", PYTHON_MODULE, testName, dataFile.toAbsolutePath().toString());
// Just test that Python can read the data - don't check round-trip
Assert.assertTrue(executeCommand(command, 30));
}

private static class ComplexObject1Serializer extends Serializer<ComplexObject1> {

public ComplexObject1Serializer(Fory fory, Class<ComplexObject1> cls) {
Expand Down Expand Up @@ -842,4 +858,198 @@ public void testEnumField(boolean compatible) throws java.io.IOException {
Assert.assertEquals(xserDe(fory, a), a);
structRoundBack(fory, a, "test_enum_field" + (compatible ? "_compatible" : ""));
}

@Test
public void testCrossLanguageMetaShare() throws Exception {
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();
fory.register(ComplexObject2.class, "test.ComplexObject2");

ComplexObject2 obj = new ComplexObject2();
obj.f1 = true;
obj.f2 = new HashMap<>(ImmutableMap.of((byte) -1, 2));

// Test with meta share enabled
byte[] serialized = fory.serialize(obj);
Assert.assertEquals(fory.deserialize(serialized), obj);

structRoundBack(fory, obj, "test_cross_language_meta_share");
}

@Test
public void testCrossLanguageMetaShareComplex() throws Exception {
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();
fory.register(ComplexObject1.class, "test.ComplexObject1");
fory.register(ComplexObject2.class, "test.ComplexObject2");

ComplexObject2 obj2 = new ComplexObject2();
obj2.f1 = true;
obj2.f2 = ImmutableMap.of((byte) -1, 2);

ComplexObject1 obj = new ComplexObject1();
obj.f1 = obj2;
obj.f2 = "meta_share_test";
obj.f3 = Arrays.asList("compatible", "mode");
obj.f4 = ImmutableMap.of((byte) 1, 2);
obj.f5 = Byte.MAX_VALUE;
obj.f6 = Short.MAX_VALUE;
obj.f7 = Integer.MAX_VALUE;
obj.f8 = Long.MAX_VALUE;
obj.f9 = 1.0f / 2;
obj.f10 = 1 / 3.0;
obj.f11 = new short[] {(short) 1, (short) 2};
obj.f12 = ImmutableList.of((short) -1, (short) 4);

// Test with meta share enabled
byte[] serialized = fory.serialize(obj);
Assert.assertEquals(fory.deserialize(serialized), obj);

structRoundBack(fory, obj, "test_cross_language_meta_share_complex");
}

// Compatibility test classes - Version 1 (original)
@Data
public static class CompatTestV1 {
String name;
Integer age;
}

// Compatibility test classes - Version 2 (with additional field)
@Data
public static class CompatTestV2 {
String name;
Integer age;
String email; // New field added
}

// Compatibility test classes - Version 3 (with reordered fields)
@Data
public static class CompatTestV3 {
Integer age; // Reordered
String name; // Reordered
String email;
Boolean active; // Another new field
}

@Test
public void testSchemaEvolution() throws Exception {
// Test simple schema evolution compatibility
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();

fory.register(CompatTestV1.class, "test.CompatTest");

CompatTestV1 objV1 = new CompatTestV1();
objV1.name = "Schema Evolution Test";
objV1.age = 42;

// Serialize with V1 schema
Assert.assertEquals(fory.deserialize(fory.serialize(objV1)), objV1);

structRoundBack(fory, objV1, "test_schema_evolution");
}

@Test
public void testBackwardCompatibility() throws Exception {
// Test that old version can read new data (ignoring unknown fields)
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();

fory.register(CompatTestV2.class, "test.CompatTest");

CompatTestV2 objV2 = new CompatTestV2();
objV2.name = "Bob";
objV2.age = 30;
objV2.email = "bob@example.com";

// Serialize with V2 schema
Assert.assertEquals(fory.deserialize(fory.serialize(objV2)), objV2);

// Test: old version (V1) reads new version (V2) data
// Expected: V1 should successfully read name and age, ignoring email
structBackwardCompatibility(fory, objV2, "test_backward_compatibility");
}

@Test
public void testFieldReorderingCompatibility() throws Exception {
// Test that field reordering doesn't break compatibility
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();

fory.register(CompatTestV3.class, "test.CompatTest");

CompatTestV3 objV3 = new CompatTestV3();
objV3.name = "Charlie";
objV3.age = 35;
objV3.email = "charlie@example.com";
objV3.active = true;

// Serialize with V3 schema (reordered fields)
Assert.assertEquals(fory.deserialize(fory.serialize(objV3)), objV3);

structRoundBack(fory, objV3, "test_field_reordering_compatibility");
}

@Data
public static class CompatContainer {
CompatTestV1 oldObject;
CompatTestV2 newObject;
}

@Test
public void testCrossVersionCompatibility() throws Exception {
// Test mixed version compatibility in one test
Fory fory =
Fory.builder()
.withLanguage(Language.XLANG)
.withRefTracking(true)
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.requireClassRegistration(false)
.build();

fory.register(CompatContainer.class, "test.CompatContainer");
fory.register(CompatTestV1.class, "test.CompatTestV1");
fory.register(CompatTestV2.class, "test.CompatTestV2");

CompatTestV1 v1 = new CompatTestV1();
v1.name = "Old Format";
v1.age = 20;

CompatTestV2 v2 = new CompatTestV2();
v2.name = "New Format";
v2.age = 25;
v2.email = "new@example.com";

CompatContainer container = new CompatContainer();
container.oldObject = v1;
container.newObject = v2;

structRoundBack(fory, container, "test_cross_version_compatibility");
}
}
20 changes: 17 additions & 3 deletions python/pyfory/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ def _register_xtype(
type_id = TypeId.NAMED_STRUCT if type_id is None else ((type_id << 8) + TypeId.STRUCT)
elif not internal:
type_id = TypeId.NAMED_EXT if type_id is None else ((type_id << 8) + TypeId.EXT)

return self.__register_type(
cls,
type_id=type_id,
Expand Down Expand Up @@ -420,8 +421,17 @@ def __register_type(
internal: bool = False,
):
dynamic_type = type_id is not None and type_id < 0
if not internal and serializer is None:
# In metashare mode, for struct types, we want to keep serializer=None
# so that _set_typeinfo will be called to create the TypeDef-based serializer
should_create_serializer = (
not internal
and serializer is None
and not (self.meta_share and typename is not None and type_id is not None and is_struct_type(type_id & 0xFF))
)

if should_create_serializer:
serializer = self._create_serializer(cls)

if typename is None:
typeinfo = TypeInfo(cls, type_id, serializer, None, None, dynamic_type)
else:
Expand Down Expand Up @@ -514,8 +524,12 @@ def _set_typeinfo(self, typeinfo):
if is_struct_type(type_id):
if self.meta_share:
type_def = encode_typedef(self, typeinfo.cls)
typeinfo.serializer = type_def.create_serializer(self)
typeinfo.type_def = type_def
if type_def is not None:
typeinfo.serializer = type_def.create_serializer(self)
typeinfo.type_def = type_def
else:
# Fallback to regular serializer
typeinfo.serializer = DataClassSerializer(self.fory, typeinfo.cls, xlang=not self.fory.is_py)
else:
typeinfo.serializer = DataClassSerializer(self.fory, typeinfo.cls, xlang=not self.fory.is_py)
else:
Expand Down
4 changes: 3 additions & 1 deletion python/pyfory/meta/typedef_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ def encode_typedef(type_resolver, cls):
if len(splits) == 1:
splits.insert(0, "")
namespace, typename = splits
return TypeDef(namespace, typename, cls, type_id, field_infos, binary, is_compressed)

result = TypeDef(namespace, typename, cls, type_id, field_infos, binary, is_compressed)
return result


def prepend_header(buffer: bytes, is_compressed: bool, has_fields_meta: bool):
Expand Down
Loading
Loading