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]