This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new d6f646139 fix(rust): enable Union type cross-language serialization 
between Rust and Java (#3094)
d6f646139 is described below

commit d6f646139521c8e822534a75bc40c803c9aca5cd
Author: Damon Zhao <[email protected]>
AuthorDate: Wed Dec 31 13:52:42 2025 +0800

    fix(rust): enable Union type cross-language serialization between Rust and 
Java (#3094)
    
    ## Why?
    
    ### Struct fingerprint mismatch for enum fields
    
    When computing struct version hash for cross-language compatibility,
    Java and C++ treat enum fields as nullable=true by default. However,
    Rust's proc-macro cannot determine at compile time whether a field type
    is an enum, causing fingerprint mismatch.
    
    ### Cross-language Union serialization fails
    
    Java's AbstractObjectSerializer expects to read type_id for non-final
    fields, but Rust/C++ skip type_id for Union fields per xlang spec. This
    caused Type id 104 not registered errors when Java tried to deserialize
    Rust-serialized Union data.
    
    ## What does this PR do?
    
    ### Rust changes:
    - Union-compatible enum handling for xlang mode
    - Fix `field_need_write_type_info()` to handle UNION TypeId
    
    ### Java changes:
    - `AbstractObjectSerializer`: Skip `type_id` read for Union types
    - `UnionSerializer`: Add `read()`/`write()` delegating to
    `xread()`/`xwrite()`
    
    
    ### Tests:
    - Add testUnionXlang for Rust enum <-> Java Union2 interoperability
    
    ## Related issues
    
    ## Does this PR introduce any user-facing change?
    
    [ ] Does this PR introduce any public API change?
    [x] Does this PR introduce any binary protocol compatibility change?
    Note: Struct version hash for structs containing enum fields will now
    match Java/C++.
    
    ## Benchmark
    
    
    ## Others
    It's really hard to determine this bug :sob:...
---
 .../org/apache/fory/resolver/ClassResolver.java    |   6 +
 .../apache/fory/serializer/UnionSerializer.java    |  10 +
 .../test/java/org/apache/fory/CPPXlangTest.java    |   7 +
 .../src/test/java/org/apache/fory/GoXlangTest.java |   7 +
 .../test/java/org/apache/fory/PythonXlangTest.java |   7 +
 .../test/java/org/apache/fory/RustXlangTest.java   |   5 +
 .../test/java/org/apache/fory/XlangTestBase.java   |  49 ++++
 rust/fory-core/src/resolver/type_resolver.rs       |   8 +-
 rust/fory-core/src/serializer/core.rs              |   9 +-
 rust/fory-core/src/serializer/util.rs              |   6 +-
 rust/fory-core/src/types.rs                        |  13 ++
 rust/fory-derive/src/object/derive_enum.rs         | 257 +++++++++++++++++----
 rust/fory-derive/src/object/read.rs                |  10 +-
 rust/fory-derive/src/object/serializer.rs          |  10 +-
 rust/fory-derive/src/object/util.rs                | 160 +++++++------
 rust/fory-derive/src/object/write.rs               |  10 +-
 rust/tests/tests/test_cross_language.rs            |  55 +++++
 rust/tests/tests/test_enum.rs                      | 138 +++++++++++
 18 files changed, 627 insertions(+), 140 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index df773101f..95f90637f 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -146,6 +146,7 @@ import org.apache.fory.type.Descriptor;
 import org.apache.fory.type.DescriptorGrouper;
 import org.apache.fory.type.GenericType;
 import org.apache.fory.type.TypeUtils;
+import org.apache.fory.type.union.Union;
 import org.apache.fory.util.GraalvmSupport;
 import org.apache.fory.util.Preconditions;
 import org.apache.fory.util.StringUtils;
@@ -706,6 +707,11 @@ public class ClassResolver extends TypeResolver {
         Class<?> component = TypeUtils.getArrayComponent(clz);
         return isMonomorphic(component);
       }
+      // Union types (Union2~6) are final classes, treat them as monomorphic
+      // so they don't need to read/write type info
+      if (Union.class.isAssignableFrom(clz)) {
+        return true;
+      }
       return (isInnerClass(clz) || clz.isEnum());
     }
     return ReflectionUtils.isMonomorphic(clz);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
index 2d95fec7e..a02453545 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/serializer/UnionSerializer.java
@@ -84,6 +84,11 @@ public class UnionSerializer extends Serializer<Union> {
     }
   }
 
+  @Override
+  public void write(MemoryBuffer buffer, Union union) {
+    xwrite(buffer, union);
+  }
+
   @Override
   public void xwrite(MemoryBuffer buffer, Union union) {
     int index = union.getIndex();
@@ -97,6 +102,11 @@ public class UnionSerializer extends Serializer<Union> {
     }
   }
 
+  @Override
+  public Union read(MemoryBuffer buffer) {
+    return xread(buffer);
+  }
+
   @Override
   public Union xread(MemoryBuffer buffer) {
     int index = buffer.readVarUint32();
diff --git a/java/fory-core/src/test/java/org/apache/fory/CPPXlangTest.java 
b/java/fory-core/src/test/java/org/apache/fory/CPPXlangTest.java
index c35700002..01bcd0199 100644
--- a/java/fory-core/src/test/java/org/apache/fory/CPPXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/CPPXlangTest.java
@@ -268,4 +268,11 @@ public class CPPXlangTest extends XlangTestBase {
   public void testEnumSchemaEvolutionCompatible() throws java.io.IOException {
     super.testEnumSchemaEvolutionCompatible();
   }
+
+  @Test
+  @Override
+  public void testUnionXlang() throws java.io.IOException {
+    // Skip: C++ doesn't have Union xlang support yet
+    throw new SkipException("Skipping testUnionXlang: C++ Union xlang support 
not implemented");
+  }
 }
diff --git a/java/fory-core/src/test/java/org/apache/fory/GoXlangTest.java 
b/java/fory-core/src/test/java/org/apache/fory/GoXlangTest.java
index bc47e4400..ee0cc5821 100644
--- a/java/fory-core/src/test/java/org/apache/fory/GoXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/GoXlangTest.java
@@ -298,4 +298,11 @@ public class GoXlangTest extends XlangTestBase {
     // Go writes null for nil pointers (nullable=true by default for pointer 
types)
     Assert.assertNull(result2.f2);
   }
+
+  @Test
+  @Override
+  public void testUnionXlang() throws java.io.IOException {
+    // Skip: Go doesn't have Union xlang support yet
+    throw new SkipException("Skipping testUnionXlang: Go Union xlang support 
not implemented");
+  }
 }
diff --git a/java/fory-core/src/test/java/org/apache/fory/PythonXlangTest.java 
b/java/fory-core/src/test/java/org/apache/fory/PythonXlangTest.java
index 748ecf3e2..cdc3f09a0 100644
--- a/java/fory-core/src/test/java/org/apache/fory/PythonXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/PythonXlangTest.java
@@ -191,4 +191,11 @@ public class PythonXlangTest extends XlangTestBase {
   public void testPolymorphicMap() throws IOException {
     super.testPolymorphicMap();
   }
+
+  @Override
+  @Test
+  public void testUnionXlang() throws IOException {
+    // Skip: Python doesn't have Union xlang support yet
+    throw new SkipException("Skipping testUnionXlang: Python Union xlang 
support not implemented");
+  }
 }
diff --git a/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java 
b/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java
index 26a33a0a8..339a1d24e 100644
--- a/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/RustXlangTest.java
@@ -230,4 +230,9 @@ public class RustXlangTest extends XlangTestBase {
   public void testEnumSchemaEvolutionCompatible() throws java.io.IOException {
     super.testEnumSchemaEvolutionCompatible();
   }
+
+  @Test
+  public void testUnionXlang() throws java.io.IOException {
+    super.testUnionXlang();
+  }
 }
diff --git a/java/fory-core/src/test/java/org/apache/fory/XlangTestBase.java 
b/java/fory-core/src/test/java/org/apache/fory/XlangTestBase.java
index 11f96514d..de42816ce 100644
--- a/java/fory-core/src/test/java/org/apache/fory/XlangTestBase.java
+++ b/java/fory-core/src/test/java/org/apache/fory/XlangTestBase.java
@@ -762,6 +762,55 @@ public abstract class XlangTestBase extends ForyTestBase {
     Assert.assertEquals(fory.deserialize(buffer2), Color.White);
   }
 
+  // Union xlang test - Java Union2 <-> Rust enum with single-field variants
+  //
+  // Union fields in xlang mode follow a special format:
+  // - Rust writes: ref_flag + union_data (no type_id, since Union fields skip 
type info)
+  // - Java reads: null_flag + union_data (directly calls 
UnionSerializer.read())
+  //
+  // AbstractObjectSerializer.readFinalObjectFieldValue and readOtherFieldValue
+  // have special handling for Union types to skip reading type_id.
+  @Data
+  static class StructWithUnion2 {
+    org.apache.fory.type.union.Union2<String, Long> union;
+  }
+
+  @Test
+  public void testUnionXlang() throws java.io.IOException {
+    String caseName = "test_union_xlang";
+    Fory fory =
+        Fory.builder()
+            .withLanguage(Language.XLANG)
+            .withCompatibleMode(CompatibleMode.COMPATIBLE)
+            .withCodegen(false)
+            .build();
+    fory.register(StructWithUnion2.class, 301);
+
+    // Create Union with String value (index 0)
+    StructWithUnion2 struct1 = new StructWithUnion2();
+    struct1.union = org.apache.fory.type.union.Union2.ofT1("hello");
+
+    // Create Union with Long value (index 1)
+    StructWithUnion2 struct2 = new StructWithUnion2();
+    struct2.union = org.apache.fory.type.union.Union2.ofT2(42L);
+
+    MemoryBuffer buffer = MemoryUtils.buffer(64);
+    fory.serialize(buffer, struct1);
+    fory.serialize(buffer, struct2);
+
+    ExecutionContext ctx = prepareExecution(caseName, buffer.getBytes(0, 
buffer.writerIndex()));
+    runPeer(ctx);
+
+    MemoryBuffer buffer2 = readBuffer(ctx.dataFile());
+    StructWithUnion2 readStruct1 = (StructWithUnion2) 
fory.deserialize(buffer2);
+    Assert.assertEquals(readStruct1.union.getValue(), "hello");
+    Assert.assertEquals(readStruct1.union.getIndex(), 0);
+
+    StructWithUnion2 readStruct2 = (StructWithUnion2) 
fory.deserialize(buffer2);
+    Assert.assertEquals(readStruct2.union.getValue(), 42L);
+    Assert.assertEquals(readStruct2.union.getIndex(), 1);
+  }
+
   @Data
   static class StructWithList {
     List<String> items;
diff --git a/rust/fory-core/src/resolver/type_resolver.rs 
b/rust/fory-core/src/resolver/type_resolver.rs
index 3d94cc90a..8e32bc051 100644
--- a/rust/fory-core/src/resolver/type_resolver.rs
+++ b/rust/fory-core/src/resolver/type_resolver.rs
@@ -22,7 +22,7 @@ use crate::meta::{
     TYPE_NAME_ENCODINGS,
 };
 use crate::serializer::{ForyDefault, Serializer, StructSerializer};
-use crate::types::RefMode;
+use crate::types::{is_enum_type_id, RefMode};
 use crate::util::get_ext_actual_type_id;
 use crate::TypeId;
 use chrono::{NaiveDate, NaiveDateTime};
@@ -363,7 +363,8 @@ fn build_struct_type_infos<T: StructSerializer>(
     let mut result = vec![(std::any::TypeId::of::<T>(), main_type_info)];
 
     // Handle enum variants in compatible mode
-    if type_resolver.compatible && T::fory_static_type_id() == TypeId::ENUM {
+    // Check for ENUM, NAMED_ENUM, and UNION (Union-compatible Rust enums 
return UNION TypeId)
+    if type_resolver.compatible && is_enum_type_id(T::fory_static_type_id()) {
         // Fields are already sorted with IDs assigned by the macro
         let variants_info = T::fory_variants_fields_info(type_resolver)?;
         for (idx, (variant_name, variant_type_id, fields_info)) in
@@ -662,7 +663,8 @@ impl TypeResolver {
                 "Either id must be non-zero for ID registration, or type_name 
must be non-empty for name registration",
             ));
         }
-        let actual_type_id = T::fory_actual_type_id(id, register_by_name, 
self.compatible);
+        let actual_type_id =
+            T::fory_actual_type_id(id, register_by_name, self.compatible, 
self.xlang);
 
         fn write<T2: 'static + Serializer>(
             this: &dyn Any,
diff --git a/rust/fory-core/src/serializer/core.rs 
b/rust/fory-core/src/serializer/core.rs
index 248ee2f3b..98e06b5c1 100644
--- a/rust/fory-core/src/serializer/core.rs
+++ b/rust/fory-core/src/serializer/core.rs
@@ -1346,6 +1346,7 @@ pub trait StructSerializer: Serializer + 'static {
     /// * `type_id` - The base type ID
     /// * `register_by_name` - Whether type was registered by name (vs by hash)
     /// * `compatible` - Whether compatibility mode is enabled
+    /// * `xlang` - Whether cross-language mode is enabled
     ///
     /// # Returns
     ///
@@ -1357,7 +1358,13 @@ pub trait StructSerializer: Serializer + 'static {
     /// - Handles type ID transformations for compatibility
     /// - **Do not override** for user types with custom serialization (EXT 
types)
     #[inline(always)]
-    fn fory_actual_type_id(type_id: u32, register_by_name: bool, compatible: 
bool) -> u32 {
+    fn fory_actual_type_id(
+        type_id: u32,
+        register_by_name: bool,
+        compatible: bool,
+        xlang: bool,
+    ) -> u32 {
+        let _ = xlang; // Default implementation ignores xlang parameter
         struct_::actual_type_id(type_id, register_by_name, compatible)
     }
 
diff --git a/rust/fory-core/src/serializer/util.rs 
b/rust/fory-core/src/serializer/util.rs
index 99a5d9c07..c6aab1517 100644
--- a/rust/fory-core/src/serializer/util.rs
+++ b/rust/fory-core/src/serializer/util.rs
@@ -20,7 +20,7 @@ use crate::error::Error;
 use crate::resolver::context::{ReadContext, WriteContext};
 use crate::serializer::Serializer;
 use crate::types::TypeId;
-use crate::types::{is_user_type, ENUM, NAMED_ENUM};
+use crate::types::{is_user_type, ENUM, NAMED_ENUM, UNION};
 
 #[inline(always)]
 pub(crate) fn read_basic_type_info<T: Serializer>(context: &mut ReadContext) 
-> Result<(), Error> {
@@ -43,7 +43,7 @@ pub(crate) fn read_basic_type_info<T: Serializer>(context: 
&mut ReadContext) ->
 #[inline]
 pub const fn field_need_read_type_info(type_id: u32) -> bool {
     let internal_type_id = type_id & 0xff;
-    if internal_type_id == ENUM || internal_type_id == NAMED_ENUM {
+    if internal_type_id == ENUM || internal_type_id == NAMED_ENUM || 
internal_type_id == UNION {
         return false;
     }
     is_user_type(internal_type_id)
@@ -52,7 +52,7 @@ pub const fn field_need_read_type_info(type_id: u32) -> bool {
 /// Keep as const fn for compile time evaluation or constant folding
 pub const fn field_need_write_type_info(static_type_id: TypeId) -> bool {
     let static_type_id = static_type_id as u32;
-    if static_type_id == ENUM || static_type_id == NAMED_ENUM {
+    if static_type_id == ENUM || static_type_id == NAMED_ENUM || 
static_type_id == UNION {
         return false;
     }
     is_user_type(static_type_id)
diff --git a/rust/fory-core/src/types.rs b/rust/fory-core/src/types.rs
index a2ec99905..cdecb8cf8 100644
--- a/rust/fory-core/src/types.rs
+++ b/rust/fory-core/src/types.rs
@@ -229,6 +229,19 @@ pub const ISIZE_ARRAY: u32 = TypeId::ISIZE_ARRAY as u32;
 pub const UNKNOWN: u32 = TypeId::UNKNOWN as u32;
 pub const BOUND: u32 = TypeId::BOUND as u32;
 
+/// Returns true if the given TypeId represents an enum type.
+///
+/// This is used during fingerprint computation to match Java/C++ behavior
+/// where enum fields are always treated as nullable (since Java enums are
+/// reference types that can be null).
+///
+/// **NOTE**: ENUM, NAMED_ENUM, and UNION are all considered enum types since 
Rust enums
+/// can be represented as Union in xlang mode when they have data-carrying 
variants.
+#[inline]
+pub const fn is_enum_type_id(type_id: TypeId) -> bool {
+    matches!(type_id, TypeId::ENUM | TypeId::NAMED_ENUM | TypeId::UNION)
+}
+
 const MAX_UNT32: u64 = (1 << 31) - 1;
 
 // todo: struct hash
diff --git a/rust/fory-derive/src/object/derive_enum.rs 
b/rust/fory-derive/src/object/derive_enum.rs
index 777a6309d..07f8ba5e9 100644
--- a/rust/fory-derive/src/object/derive_enum.rs
+++ b/rust/fory-derive/src/object/derive_enum.rs
@@ -28,9 +28,28 @@ fn temp_var_name(i: usize) -> String {
     format!("f{}", i)
 }
 
-pub fn gen_actual_type_id() -> TokenStream {
-    quote! {
-       fory_core::serializer::enum_::actual_type_id(type_id, register_by_name, 
compatible)
+/// For Union-compatible enums with data variants, return UNION TypeId in 
xlang mode.
+pub fn gen_actual_type_id(data_enum: &DataEnum) -> TokenStream {
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+    let has_data_variants = data_enum
+        .variants
+        .iter()
+        .any(|v| !matches!(v.fields, Fields::Unit));
+
+    if is_union_compatible && has_data_variants {
+        // Union-compatible enum: use UNION TypeId ONLY in xlang mode
+        quote! {
+            if xlang {
+                fory_core::types::TypeId::UNION as u32
+            } else {
+                fory_core::serializer::enum_::actual_type_id(type_id, 
register_by_name, compatible)
+            }
+        }
+    } else {
+        quote! {
+            let _ = xlang;
+            fory_core::serializer::enum_::actual_type_id(type_id, 
register_by_name, compatible)
+        }
     }
 }
 
@@ -162,6 +181,8 @@ pub fn gen_write(_data_enum: &DataEnum) -> TokenStream {
 }
 
 fn xlang_variant_branches(data_enum: &DataEnum, default_variant_value: u32) -> 
Vec<TokenStream> {
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+
     data_enum
         .variants
         .iter()
@@ -175,23 +196,58 @@ fn xlang_variant_branches(data_enum: &DataEnum, 
default_variant_value: u32) -> V
 
             match &v.fields {
                 Fields::Unit => {
-                    quote! {
-                        Self::#ident => {
-                            context.writer.write_varuint32(#tag_value);
+                    if is_union_compatible {
+                        // Union-compatible: write tag + null flag (matches 
Java/C++ Union with null value)
+                        quote! {
+                            Self::#ident => {
+                                context.writer.write_varuint32(#tag_value);
+                                // Write null flag for unit variant (no value)
+                                
context.writer.write_i8(fory_core::types::RefFlag::Null as i8);
+                            }
+                        }
+                    } else {
+                        quote! {
+                            Self::#ident => {
+                                context.writer.write_varuint32(#tag_value);
+                            }
                         }
                     }
                 }
-                Fields::Unnamed(_) => {
-                    quote! {
-                        Self::#ident(..) => {
-                            context.writer.write_varuint32(#tag_value);
+                Fields::Unnamed(fields_unnamed) => {
+                    if is_union_compatible && fields_unnamed.unnamed.len() == 
1 {
+                        // Union-compatible single field: write tag + value 
with type info (like xwriteRef)
+                        quote! {
+                            Self::#ident(ref value) => {
+                                context.writer.write_varuint32(#tag_value);
+                                use fory_core::serializer::Serializer;
+                                value.fory_write(context, 
fory_core::types::RefMode::Tracking, true, false)?;
+                            }
+                        }
+                    } else {
+                        quote! {
+                            Self::#ident(..) => {
+                                context.writer.write_varuint32(#tag_value);
+                            }
                         }
                     }
                 }
-                Fields::Named(_) => {
-                    quote! {
-                        Self::#ident { .. } => {
-                            context.writer.write_varuint32(#tag_value);
+                Fields::Named(fields_named) => {
+                    if is_union_compatible && fields_named.named.len() == 1 {
+                        // Union-compatible single field: write tag + value 
with type info (like xwriteRef)
+                        let field_ident =
+                            
fields_named.named.first().unwrap().ident.as_ref().unwrap();
+                        quote! {
+                            Self::#ident { ref #field_ident } => {
+                                context.writer.write_varuint32(#tag_value);
+                                use fory_core::serializer::Serializer;
+                                #field_ident.fory_write(context, 
fory_core::types::RefMode::Tracking, true, false)?;
+                            }
+                        }
+                    } else {
+                        quote! {
+                            Self::#ident { .. } => {
+                                context.writer.write_varuint32(#tag_value);
+                            }
                         }
                     }
                 }
@@ -387,9 +443,27 @@ pub fn gen_write_data(data_enum: &DataEnum) -> TokenStream 
{
     }
 }
 
-pub fn gen_write_type_info() -> TokenStream {
-    quote! {
-        fory_core::serializer::enum_::write_type_info::<Self>(context)
+pub fn gen_write_type_info(data_enum: &DataEnum) -> TokenStream {
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+    let has_data_variants = data_enum
+        .variants
+        .iter()
+        .any(|v| !matches!(v.fields, Fields::Unit));
+
+    if is_union_compatible && has_data_variants {
+        // Union-compatible with data: use UNION TypeId in xlang mode
+        quote! {
+            if context.is_xlang() {
+                context.writer.write_varuint32(fory_core::types::TypeId::UNION 
as u32);
+                Ok(())
+            } else {
+                fory_core::serializer::enum_::write_type_info::<Self>(context)
+            }
+        }
+    } else {
+        quote! {
+            fory_core::serializer::enum_::write_type_info::<Self>(context)
+        }
     }
 }
 
@@ -405,10 +479,46 @@ pub fn gen_read_with_type_info(_: &DataEnum) -> 
TokenStream {
     }
 }
 
+/// Check if enum is Union-compatible:
+/// - Must have at least one data-carrying variant (single-field)
+/// - All variants must be either unit or single-field
+fn is_union_compatible_enum(data_enum: &DataEnum) -> bool {
+    let has_data_variant = data_enum
+        .variants
+        .iter()
+        .any(|v| !matches!(v.fields, Fields::Unit));
+    let all_variants_compatible = data_enum.variants.iter().all(|v| match 
&v.fields {
+        Fields::Unit => true,
+        Fields::Unnamed(f) => f.unnamed.len() == 1,
+        Fields::Named(f) => f.named.len() == 1,
+    });
+
+    has_data_variant && all_variants_compatible
+}
+
+/// Generate the static TypeId for enum.
+/// For Union-compatible enums with data variants, return UNION TypeId
+/// to ensure correct type info handling in xlang mode struct field read/write.
+pub fn gen_static_type_id(data_enum: &DataEnum) -> TokenStream {
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+    let has_data_variants = data_enum
+        .variants
+        .iter()
+        .any(|v| !matches!(v.fields, Fields::Unit));
+
+    if is_union_compatible && has_data_variants {
+        quote! { fory_core::TypeId::UNION }
+    } else {
+        quote! { fory_core::TypeId::ENUM }
+    }
+}
+
 fn xlang_variant_read_branches(
     data_enum: &DataEnum,
     default_variant_value: u32,
 ) -> Vec<TokenStream> {
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+
     data_enum
         .variants
         .iter()
@@ -422,37 +532,71 @@ fn xlang_variant_read_branches(
 
             match &v.fields {
                 Fields::Unit => {
-                    quote! {
-                        #tag_value => Ok(Self::#ident),
+                    if is_union_compatible {
+                        // Union-compatible: read null flag (matches Java/C++ 
Union with null value)
+                        quote! {
+                            #tag_value => {
+                                let _ = context.reader.read_i8()?;
+                                Ok(Self::#ident)
+                            }
+                        }
+                    } else {
+                        quote! {
+                            #tag_value => Ok(Self::#ident),
+                        }
                     }
                 }
                 Fields::Unnamed(fields_unnamed) => {
-                    let default_fields: Vec<TokenStream> = fields_unnamed
-                        .unnamed
-                        .iter()
-                        .map(|f| {
-                            let ty = &f.ty;
-                            quote! { <#ty as 
fory_core::ForyDefault>::fory_default() }
-                        })
-                        .collect();
-
-                    quote! {
-                        #tag_value => Ok(Self::#ident( #(#default_fields),* )),
+                    if is_union_compatible && fields_unnamed.unnamed.len() == 
1 {
+                        // Union-compatible single field: read value with 
ref_info=Tracking, type_info=true
+                        let field_ty = 
&fields_unnamed.unnamed.first().unwrap().ty;
+                        quote! {
+                            #tag_value => {
+                                use fory_core::serializer::Serializer;
+                                let value = <#field_ty as 
Serializer>::fory_read(context, fory_core::types::RefMode::Tracking, true)?;
+                                Ok(Self::#ident(value))
+                            }
+                        }
+                    } else {
+                        let default_fields: Vec<TokenStream> = fields_unnamed
+                            .unnamed
+                            .iter()
+                            .map(|f| {
+                                let ty = &f.ty;
+                                quote! { <#ty as 
fory_core::ForyDefault>::fory_default() }
+                            })
+                            .collect();
+                        quote! {
+                            #tag_value => Ok(Self::#ident( 
#(#default_fields),* )),
+                        }
                     }
                 }
                 Fields::Named(fields_named) => {
-                    let default_fields: Vec<TokenStream> = fields_named
-                        .named
-                        .iter()
-                        .map(|f| {
-                            let field_ident = f.ident.as_ref().unwrap();
-                            let ty = &f.ty;
-                            quote! { #field_ident: <#ty as 
fory_core::ForyDefault>::fory_default() }
-                        })
-                        .collect();
-
-                    quote! {
-                        #tag_value => Ok(Self::#ident { #(#default_fields),* 
}),
+                    if is_union_compatible && fields_named.named.len() == 1 {
+                        // Union-compatible single field: read value with 
ref_info=Tracking, type_info=true
+                        let field = fields_named.named.first().unwrap();
+                        let field_ident = field.ident.as_ref().unwrap();
+                        let field_ty = &field.ty;
+                        quote! {
+                            #tag_value => {
+                                use fory_core::serializer::Serializer;
+                                let value = <#field_ty as 
Serializer>::fory_read(context, fory_core::types::RefMode::Tracking, true)?;
+                                Ok(Self::#ident { #field_ident: value })
+                            }
+                        }
+                    } else {
+                        let default_fields: Vec<TokenStream> = fields_named
+                            .named
+                            .iter()
+                            .map(|f| {
+                                let field_ident = f.ident.as_ref().unwrap();
+                                let ty = &f.ty;
+                                quote! { #field_ident: <#ty as 
fory_core::ForyDefault>::fory_default() }
+                            })
+                            .collect();
+                        quote! {
+                            #tag_value => Ok(Self::#ident { 
#(#default_fields),* }),
+                        }
                     }
                 }
             }
@@ -773,8 +917,31 @@ pub fn gen_read_data(data_enum: &DataEnum) -> TokenStream {
     }
 }
 
-pub fn gen_read_type_info() -> TokenStream {
-    quote! {
-        fory_core::serializer::enum_::read_type_info::<Self>(context)
+pub fn gen_read_type_info(data_enum: &DataEnum) -> TokenStream {
+    // Only use UNION TypeId for Union-compatible enums (unit or single-field 
variants)
+    let is_union_compatible = is_union_compatible_enum(data_enum);
+    let has_data_variants = data_enum
+        .variants
+        .iter()
+        .any(|v| !matches!(v.fields, Fields::Unit));
+
+    if is_union_compatible && has_data_variants {
+        // Union-compatible with data: read UNION TypeId in xlang mode
+        quote! {
+            if context.is_xlang() {
+                let remote_type_id = context.reader.read_varuint32()?;
+                let expected_type_id = fory_core::types::TypeId::UNION as u32;
+                if remote_type_id != expected_type_id {
+                    return 
Err(fory_core::error::Error::type_mismatch(expected_type_id, remote_type_id));
+                }
+                Ok(())
+            } else {
+                fory_core::serializer::enum_::read_type_info::<Self>(context)
+            }
+        }
+    } else {
+        quote! {
+            fory_core::serializer::enum_::read_type_info::<Self>(context)
+        }
     }
 }
diff --git a/rust/fory-derive/src/object/read.rs 
b/rust/fory-derive/src/object/read.rs
index 5ba6aaf18..1bf09fbc5 100644
--- a/rust/fory-derive/src/object/read.rs
+++ b/rust/fory-derive/src/object/read.rs
@@ -20,8 +20,8 @@ use quote::{format_ident, quote};
 use syn::Field;
 
 use super::util::{
-    classify_trait_object_field, compute_struct_version_hash, 
create_wrapper_types_arc,
-    create_wrapper_types_rc, determine_field_ref_mode, extract_type_name,
+    classify_trait_object_field, create_wrapper_types_arc, 
create_wrapper_types_rc,
+    determine_field_ref_mode, extract_type_name, gen_struct_version_hash_ts,
     get_primitive_reader_method, get_struct_name, is_debug_enabled,
     is_direct_primitive_numeric_type, is_primitive_type, is_skip_field,
     should_skip_type_info_for_field, FieldRefMode, StructField,
@@ -304,7 +304,8 @@ fn get_source_fields_loop_ts(source_fields: 
&[SourceField<'_>]) -> TokenStream {
 
 pub fn gen_read_data(source_fields: &[SourceField<'_>]) -> TokenStream {
     let fields: Vec<&Field> = source_fields.iter().map(|sf| 
sf.field).collect();
-    let version_hash = compute_struct_version_hash(&fields);
+    // Generate runtime version hash computation that detects enum fields
+    let version_hash_ts = gen_struct_version_hash_ts(&fields);
     let read_fields = if source_fields.is_empty() {
         quote! {}
     } else {
@@ -341,7 +342,8 @@ pub fn gen_read_data(source_fields: &[SourceField<'_>]) -> 
TokenStream {
         if context.is_check_struct_version() {
             let read_version = context.reader.read_i32()?;
             let type_name = std::any::type_name::<Self>();
-            fory_core::meta::TypeMeta::check_struct_version(read_version, 
#version_hash, type_name)?;
+            let local_version: i32 = #version_hash_ts;
+            fory_core::meta::TypeMeta::check_struct_version(read_version, 
local_version, type_name)?;
         }
         #read_fields
         #self_construction
diff --git a/rust/fory-derive/src/object/serializer.rs 
b/rust/fory-derive/src/object/serializer.rs
index 1a23926b3..d8cb5ec18 100644
--- a/rust/fory-derive/src/object/serializer.rs
+++ b/rust/fory-derive/src/object/serializer.rs
@@ -89,7 +89,7 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs: 
ForyAttrs) -> TokenStrea
             let variant_meta_types =
                 derive_enum::gen_all_variant_meta_types_with_enum_name(name, 
s);
             (
-                derive_enum::gen_actual_type_id(),
+                derive_enum::gen_actual_type_id(s),
                 quote! { &[] },
                 derive_enum::gen_field_fields_info(s),
                 derive_enum::gen_variants_fields_info(name, s),
@@ -134,13 +134,13 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs: 
ForyAttrs) -> TokenStrea
         syn::Data::Enum(e) => (
             derive_enum::gen_write(e),
             derive_enum::gen_write_data(e),
-            derive_enum::gen_write_type_info(),
+            derive_enum::gen_write_type_info(e),
             derive_enum::gen_read(e),
             derive_enum::gen_read_with_type_info(e),
             derive_enum::gen_read_data(e),
-            derive_enum::gen_read_type_info(),
+            derive_enum::gen_read_type_info(e),
             derive_enum::gen_reserved_space(),
-            quote! { fory_core::TypeId::ENUM },
+            derive_enum::gen_static_type_id(e),
         ),
         syn::Data::Union(_) => {
             panic!("Union is not supported")
@@ -165,7 +165,7 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs: 
ForyAttrs) -> TokenStrea
             }
 
             #[inline(always)]
-            fn fory_actual_type_id(type_id: u32, register_by_name: bool, 
compatible: bool) -> u32 {
+            fn fory_actual_type_id(type_id: u32, register_by_name: bool, 
compatible: bool, xlang: bool) -> u32 {
                 #actual_type_id_ts
             }
 
diff --git a/rust/fory-derive/src/object/util.rs 
b/rust/fory-derive/src/object/util.rs
index 48e00d88d..381222728 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -20,11 +20,9 @@ use crate::util::{
     CollectionTraitInfo,
 };
 use fory_core::types::{TypeId, PRIMITIVE_ARRAY_TYPE_MAP};
-use fory_core::util::ENABLE_FORY_DEBUG_OUTPUT;
 use proc_macro2::{Ident, TokenStream};
 use quote::{format_ident, quote, ToTokens};
 use std::cell::RefCell;
-use std::collections::HashMap;
 use std::fmt;
 use syn::{Field, GenericArgument, Index, PathArguments, Type};
 
@@ -1117,109 +1115,123 @@ fn to_snake_case(name: &str) -> String {
     result
 }
 
-/// Computes the fingerprint string for a struct type used in schema 
versioning.
-///
-/// **Fingerprint Format:**
-///
-/// Each field contributes: `<field_name_or_id>,<type_id>,<ref>,<nullable>;`
-///
-/// Fields are sorted by field name (snake_case) lexicographically.
-///
-/// **Field Components:**
-/// - `field_name_or_id`: snake_case field name, or tag ID if `#[fory(id = 
N)]` is set
-/// - `type_id`: Fory TypeId as decimal string (e.g., "4" for INT32)
-/// - `ref`: "1" if reference tracking enabled, "0" otherwise
-/// - `nullable`: "1" if null flag is written, "0" otherwise
-///
-/// **Example fingerprint:** `"age,4,0,0;name,12,0,1;"`
+/// Field metadata for fingerprint computation.
+struct FieldFingerprintInfo {
+    /// Field name (snake_case) or field ID as string
+    name_or_id: String,
+    /// Whether the field has explicit nullable=true/false set via 
#[fory(nullable)]
+    explicit_nullable: Option<bool>,
+    /// Whether reference tracking is enabled
+    ref_tracking: bool,
+    /// The type ID (UNKNOWN for user-defined types including enums/unions)
+    type_id: u32,
+    /// Whether the field type is Option<T>
+    is_option_type: bool,
+}
+
+/// Computes struct fingerprint string at compile time (during proc-macro 
execution).
 ///
-/// This format is consistent across Go, Java, Rust, and C++ implementations.
-pub(crate) fn compute_struct_fingerprint(fields: &[&Field]) -> String {
+/// **Fingerprint Format:** `<field_name_or_id>,<type_id>,<ref>,<nullable>;`
+/// Fields are sorted by name lexicographically.
+fn compute_struct_fingerprint(fields: &[&Field]) -> String {
     use super::field_meta::{classify_field_type, parse_field_meta};
 
-    // (name, type_id, ref_tracking, nullable, field_id)
-    let mut field_info_map: HashMap<String, (u32, bool, bool, i32)> =
-        HashMap::with_capacity(fields.len());
-    for (idx, field) in fields.iter().enumerate() {
-        let name = get_field_name(field, idx);
-        let type_id = get_type_id_by_type_ast(&field.ty);
+    let mut field_infos: Vec<FieldFingerprintInfo> = 
Vec::with_capacity(fields.len());
 
-        // Parse field metadata for nullable/ref tracking
+    for (idx, field) in fields.iter().enumerate() {
         let meta = parse_field_meta(field).unwrap_or_default();
         if meta.skip {
             continue;
         }
 
+        let name = get_field_name(field, idx);
+        let field_id = meta.effective_id();
+        let name_or_id = if field_id >= 0 {
+            field_id.to_string()
+        } else {
+            to_snake_case(&name)
+        };
+
         let type_class = classify_field_type(&field.ty);
-        let nullable = meta.effective_nullable(type_class);
         let ref_tracking = meta.effective_ref_tracking(type_class);
-        let field_id = meta.effective_id();
+        let explicit_nullable = meta.nullable;
 
-        field_info_map.insert(name, (type_id, ref_tracking, nullable, 
field_id));
+        // Get compile-time TypeId (UNKNOWN for user-defined types including 
enums/unions)
+        let type_id = get_type_id_by_type_ast(&field.ty);
+
+        // Check if field type is Option<T>
+        let ty_str: String = field
+            .ty
+            .to_token_stream()
+            .to_string()
+            .chars()
+            .filter(|c| !c.is_whitespace())
+            .collect();
+        let is_option_type = ty_str.starts_with("Option<");
+
+        field_infos.push(FieldFingerprintInfo {
+            name_or_id,
+            explicit_nullable,
+            ref_tracking,
+            type_id,
+            is_option_type,
+        });
     }
 
-    // Sort field names lexicographically for fingerprint computation
-    // This matches Java/Go behavior where fingerprint fields are sorted by 
name,
-    // not by the type-category-based ordering used for serialization
-    let mut sorted_names: Vec<String> = 
field_info_map.keys().cloned().collect();
-    sorted_names.sort();
+    // Sort field infos by name_or_id lexicographically (matches Java/C++ 
behavior)
+    field_infos.sort_by(|a, b| a.name_or_id.cmp(&b.name_or_id));
 
+    // Build fingerprint string
     let mut fingerprint = String::new();
-    for name in sorted_names.iter() {
-        let (type_id, ref_tracking, nullable, field_id) = field_info_map
-            .get(name)
-            .expect("Field metadata missing during struct hash computation");
-
-        // Format: <field_name_or_id>,<type_id>,<ref>,<nullable>;
-        // If field has a tag ID >= 0, use that; otherwise use snake_case 
field name
-        if *field_id >= 0 {
-            fingerprint.push_str(&field_id.to_string());
-        } else {
-            fingerprint.push_str(&to_snake_case(name));
-        }
-        fingerprint.push(',');
+    for info in &field_infos {
+        let ref_flag = if info.ref_tracking { "1" } else { "0" };
+        let nullable = match info.explicit_nullable {
+            Some(true) => true,
+            Some(false) => false,
+            None => info.is_option_type,
+        };
+        let nullable_flag = if nullable { "1" } else { "0" };
 
-        let effective_type_id = if *type_id == TypeId::UNKNOWN as u32 {
-            TypeId::UNKNOWN as u32
+        // User-defined types (UNKNOWN) use 0 in fingerprint, matching Java 
behavior
+        let effective_type_id = if info.type_id == TypeId::UNKNOWN as u32 {
+            0
         } else {
-            *type_id
+            info.type_id
         };
+
+        fingerprint.push_str(&info.name_or_id);
+        fingerprint.push(',');
         fingerprint.push_str(&effective_type_id.to_string());
         fingerprint.push(',');
-        fingerprint.push_str(if *ref_tracking { "1" } else { "0" });
+        fingerprint.push_str(ref_flag);
         fingerprint.push(',');
-        fingerprint.push_str(if *nullable { "1;" } else { "0;" });
+        fingerprint.push_str(nullable_flag);
+        fingerprint.push(';');
     }
 
     fingerprint
 }
 
-/// Computes the struct version hash from field metadata.
-///
-/// Uses `compute_struct_fingerprint` to build the fingerprint string,
-/// then hashes it with MurmurHash3_x64_128 using seed 47, and takes
-/// the low 32 bits as signed i32.
-///
-/// This provides the cross-language struct version ID used by class
-/// version checking, consistent with Go, Java, and C++ implementations.
-pub(crate) fn compute_struct_version_hash(fields: &[&Field]) -> i32 {
+/// Generates TokenStream for struct version hash (computed at compile time).
+pub(crate) fn gen_struct_version_hash_ts(fields: &[&Field]) -> TokenStream {
     let fingerprint = compute_struct_fingerprint(fields);
+    let (hash, _) = 
fory_core::meta::murmurhash3_x64_128(fingerprint.as_bytes(), 47);
+    let version_hash = (hash & 0xFFFF_FFFF) as i32;
 
-    let seed: u64 = 47;
-    let (hash, _) = 
fory_core::meta::murmurhash3_x64_128(fingerprint.as_bytes(), seed);
-    let version = (hash & 0xFFFF_FFFF) as u32;
-    let version = version as i32;
-
-    if ENABLE_FORY_DEBUG_OUTPUT {
-        if let Some(struct_name) = get_struct_name() {
-            println!(
-                "[fory-debug] struct {struct_name} version 
fingerprint=\"{fingerprint}\" hash={version}"
-            );
-        } else {
-            println!("[fory-debug] struct version 
fingerprint=\"{fingerprint}\" hash={version}");
+    quote! {
+        {
+            const VERSION_HASH: i32 = #version_hash;
+            if fory_core::util::ENABLE_FORY_DEBUG_OUTPUT {
+                println!(
+                    "[fory-debug] struct {} version fingerprint=\"{}\" 
hash={}",
+                    std::any::type_name::<Self>(),
+                    #fingerprint,
+                    VERSION_HASH
+                );
+            }
+            VERSION_HASH
         }
     }
-    version
 }
 
 /// Represents the determined RefMode for a field
diff --git a/rust/fory-derive/src/object/write.rs 
b/rust/fory-derive/src/object/write.rs
index 2b1d03e1b..0ebac74fd 100644
--- a/rust/fory-derive/src/object/write.rs
+++ b/rust/fory-derive/src/object/write.rs
@@ -16,8 +16,8 @@
 // under the License.
 
 use super::util::{
-    classify_trait_object_field, compute_struct_version_hash, 
create_wrapper_types_arc,
-    create_wrapper_types_rc, determine_field_ref_mode, extract_type_name, 
get_field_accessor,
+    classify_trait_object_field, create_wrapper_types_arc, 
create_wrapper_types_rc,
+    determine_field_ref_mode, extract_type_name, gen_struct_version_hash_ts, 
get_field_accessor,
     get_field_name, get_filtered_source_fields_iter, 
get_primitive_writer_method, get_struct_name,
     get_type_id_by_type_ast, is_debug_enabled, 
is_direct_primitive_numeric_type,
     should_skip_type_info_for_field, FieldRefMode, StructField,
@@ -335,11 +335,11 @@ pub fn gen_write_data(source_fields: &[SourceField<'_>]) 
-> TokenStream {
         .map(|sf| gen_write_field_with_index(sf.field, sf.original_index, 
true))
         .collect();
 
-    let version_hash = compute_struct_version_hash(&fields);
+    let version_hash_ts = gen_struct_version_hash_ts(&fields);
     quote! {
-        // Write version hash when class version checking is enabled
         if context.is_check_struct_version() {
-            context.writer.write_i32(#version_hash);
+            let version_hash: i32 = #version_hash_ts;
+            context.writer.write_i32(version_hash);
         }
         #(#write_fields_ts)*
         Ok(())
diff --git a/rust/tests/tests/test_cross_language.rs 
b/rust/tests/tests/test_cross_language.rs
index 6d4597d68..4405569f7 100644
--- a/rust/tests/tests/test_cross_language.rs
+++ b/rust/tests/tests/test_cross_language.rs
@@ -1253,3 +1253,58 @@ fn test_enum_schema_evolution_compatible_reverse() {
     let new_bytes = fory.serialize(&value).unwrap();
     fs::write(&data_file_path, new_bytes).unwrap();
 }
+
+// Union Xlang Tests - Rust enum <-> Java Union2
+
+/// Rust enum that matches Java Union2<String, Long>
+/// Each variant has exactly one field to be Union-compatible
+#[derive(ForyObject, Debug, PartialEq)]
+enum StringOrLong {
+    Str(String),
+    Long(i64),
+}
+
+impl Default for StringOrLong {
+    fn default() -> Self {
+        StringOrLong::Str(String::default())
+    }
+}
+
+/// Struct containing a Union field, matches Java StructWithUnion2
+#[derive(ForyObject, Debug, PartialEq)]
+struct StructWithUnion2 {
+    union: StringOrLong,
+}
+
+/// Test cross-language Union serialization between Rust enum and Java Union2.
+///
+/// Rust enum with single-field variants is Union-compatible and can be 
deserialized
+/// from Java Union2 types. Union fields in xlang mode follow a special format:
+/// - Rust writes: ref_flag + union_data (no type_id, since Union fields skip 
type info)
+/// - Java reads: null_flag + union_data (directly calls 
UnionSerializer.read())
+#[test]
+#[ignore]
+fn test_union_xlang() {
+    let data_file_path = get_data_file();
+    let bytes = fs::read(&data_file_path).unwrap();
+
+    let mut fory = Fory::default().compatible(true).xlang(true);
+    // Register both the enum and the struct that contains it
+    fory.register::<StringOrLong>(300).unwrap();
+    fory.register::<StructWithUnion2>(301).unwrap();
+
+    // Read struct1 with String value (index 0)
+    let mut reader = Reader::new(bytes.as_slice());
+    let struct1: StructWithUnion2 = fory.deserialize_from(&mut 
reader).unwrap();
+    assert_eq!(struct1.union, StringOrLong::Str("hello".to_string()));
+
+    // Read struct2 with Long value (index 1)
+    let struct2: StructWithUnion2 = fory.deserialize_from(&mut 
reader).unwrap();
+    assert_eq!(struct2.union, StringOrLong::Long(42));
+
+    // Serialize back
+    let mut buf = Vec::new();
+    fory.serialize_to(&mut buf, &struct1).unwrap();
+    fory.serialize_to(&mut buf, &struct2).unwrap();
+    fs::write(&data_file_path, buf).unwrap();
+}
diff --git a/rust/tests/tests/test_enum.rs b/rust/tests/tests/test_enum.rs
index 317915860..6e5722bb8 100644
--- a/rust/tests/tests/test_enum.rs
+++ b/rust/tests/tests/test_enum.rs
@@ -99,3 +99,141 @@ fn named_enum() {
     assert_eq!(target1, target2);
     assert_eq!(value1, value2);
 }
+
+/// Test that struct with enum field serializes correctly.
+#[test]
+fn struct_with_enum_field() {
+    use fory_core::serializer::Serializer;
+    use fory_core::types::TypeId;
+
+    // Define a simple enum
+    #[derive(ForyObject, Debug, PartialEq, Clone)]
+    enum Color {
+        Red,
+        Green,
+        Blue,
+    }
+
+    // Define a struct with enum field
+    #[derive(ForyObject, Debug, PartialEq)]
+    struct StructWithEnum {
+        name: String,
+        color: Color,
+        value: i32,
+    }
+
+    // Verify Color is recognized as ENUM TypeId
+    assert!(
+        matches!(
+            Color::fory_static_type_id(),
+            TypeId::ENUM | TypeId::NAMED_ENUM
+        ),
+        "Color should have ENUM TypeId, got {:?}",
+        Color::fory_static_type_id()
+    );
+
+    let mut fory = Fory::default().xlang(true).compatible(false);
+    fory.register::<Color>(100).unwrap();
+    fory.register::<StructWithEnum>(101).unwrap();
+
+    let obj = StructWithEnum {
+        name: "test".to_string(),
+        color: Color::Green,
+        value: 42,
+    };
+
+    let bin = fory.serialize(&obj).unwrap();
+    let result: StructWithEnum = fory.deserialize(&bin).unwrap();
+    assert_eq!(obj, result);
+}
+
+/// Test Union-compatible enum xlang serialization format.
+/// This verifies that Rust enum writes: index + ref_flag + type_id + data
+/// which should be compatible with Java's Union: index + xwriteRef(value)
+#[test]
+fn union_compatible_enum_xlang_format() {
+    use fory_core::serializer::Serializer;
+    use fory_core::types::TypeId;
+
+    // Define a Union-compatible enum (each variant has exactly one field)
+    #[derive(ForyObject, Debug, PartialEq, Clone)]
+    enum StringOrLong {
+        Text(String),
+        Number(i64),
+    }
+
+    // Verify it's recognized as UNION TypeId
+    assert_eq!(
+        StringOrLong::fory_static_type_id(),
+        TypeId::UNION,
+        "Union-compatible enum should have UNION TypeId"
+    );
+
+    // Struct containing the Union-compatible enum
+    #[derive(ForyObject, Debug, PartialEq)]
+    struct StructWithUnion {
+        union_field: StringOrLong,
+    }
+
+    // Test xlang mode serialization
+    let mut fory = Fory::default().xlang(true).compatible(false);
+    fory.register::<StringOrLong>(300).unwrap();
+    fory.register::<StructWithUnion>(301).unwrap();
+
+    // Test with String variant (index 0)
+    let obj1 = StructWithUnion {
+        union_field: StringOrLong::Text("hello".to_string()),
+    };
+    let bin1 = fory.serialize(&obj1).unwrap();
+    let result1: StructWithUnion = fory.deserialize(&bin1).unwrap();
+    assert_eq!(obj1, result1);
+
+    // Test with Long variant (index 1)
+    let obj2 = StructWithUnion {
+        union_field: StringOrLong::Number(42),
+    };
+    let bin2 = fory.serialize(&obj2).unwrap();
+    let result2: StructWithUnion = fory.deserialize(&bin2).unwrap();
+    assert_eq!(obj2, result2);
+}
+
+/// Test explicit #[fory(nullable)] attribute on enum field
+#[test]
+fn struct_with_enum_field_explicit_nullable() {
+    use fory_core::serializer::Serializer;
+    use fory_core::types::TypeId;
+
+    #[derive(ForyObject, Debug, PartialEq, Clone)]
+    enum Status {
+        Active,
+        Inactive,
+    }
+
+    #[derive(ForyObject, Debug, PartialEq)]
+    struct StructWithExplicitNullable {
+        name: String,
+        #[fory(id = 0, nullable = true)]
+        status: Status,
+    }
+
+    assert!(
+        matches!(
+            Status::fory_static_type_id(),
+            TypeId::ENUM | TypeId::NAMED_ENUM
+        ),
+        "Status should have ENUM TypeId"
+    );
+
+    let mut fory = Fory::default().xlang(true).compatible(false);
+    fory.register::<Status>(200).unwrap();
+    fory.register::<StructWithExplicitNullable>(201).unwrap();
+
+    let obj = StructWithExplicitNullable {
+        name: "explicit".to_string(),
+        status: Status::Active,
+    };
+
+    let bin = fory.serialize(&obj).unwrap();
+    let result: StructWithExplicitNullable = fory.deserialize(&bin).unwrap();
+    assert_eq!(obj, result);
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to