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 cf747e89b feat(c++): add polymorphic serialization support for `any`
to compiler (#3232)
cf747e89b is described below
commit cf747e89bd16c1cd74cde4a5ebb361005d6d8742
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jan 28 02:18:41 2026 +0800
feat(c++): add polymorphic serialization support for `any` to compiler
(#3232)
## Why?
## What does this PR do?
- Add `any` as a primitive in the compiler IR and proto frontend
(including `google.protobuf.Any`), and generate language bindings across
C++/Go/Java/Python/Rust with nullable handling.
- Introduce C++ `std::any` serialization support plus validator and
resolver updates needed for `any`/union handling in xlang.
- Add `any` IDL/proto examples, cross-language roundtrip coverage, and
docs updates.
## Related issues
Closes #3172
Closes #3231
#3099
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
compiler/fory_compiler/frontend/proto/parser.py | 5 +-
.../fory_compiler/frontend/proto/translator.py | 1 +
compiler/fory_compiler/generators/cpp.py | 26 ++-
compiler/fory_compiler/generators/go.py | 18 +-
compiler/fory_compiler/generators/java.py | 25 ++-
compiler/fory_compiler/generators/python.py | 17 +-
compiler/fory_compiler/generators/rust.py | 42 ++++-
compiler/fory_compiler/ir/types.py | 2 +
compiler/fory_compiler/ir/validator.py | 34 ++++
cpp/fory/serialization/BUILD | 11 ++
cpp/fory/serialization/CMakeLists.txt | 5 +
cpp/fory/serialization/any_serializer.h | 163 ++++++++++++++++
cpp/fory/serialization/any_serializer_test.cc | 97 ++++++++++
cpp/fory/serialization/fory.h | 1 +
cpp/fory/serialization/type_info.h | 5 +
cpp/fory/serialization/type_resolver.cc | 1 +
cpp/fory/serialization/type_resolver.h | 89 +++++++--
docs/compiler/fdl-syntax.md | 7 +
docs/compiler/protobuf-idl.md | 2 +-
docs/compiler/type-system.md | 23 +++
integration_tests/idl_tests/cpp/main.cc | 52 ++++++
integration_tests/idl_tests/generate_idl.py | 4 +
.../idl_tests/go/idl_roundtrip_test.go | 205 +++++++++++++++++++++
.../{rust/src/lib.rs => idl/any_example.fdl} | 28 ++-
integration_tests/idl_tests/idl/any_example.proto | 50 +++++
.../apache/fory/idl_tests/IdlRoundTripTest.java | 33 ++++
.../idl_tests/python/src/idl_tests/roundtrip.py | 27 +++
integration_tests/idl_tests/rust/src/lib.rs | 1 +
.../idl_tests/rust/tests/idl_roundtrip.rs | 93 ++++++++++
.../org/apache/fory/resolver/TypeResolver.java | 8 +
30 files changed, 1040 insertions(+), 35 deletions(-)
diff --git a/compiler/fory_compiler/frontend/proto/parser.py
b/compiler/fory_compiler/frontend/proto/parser.py
index 4bf74db25..127a5cad0 100644
--- a/compiler/fory_compiler/frontend/proto/parser.py
+++ b/compiler/fory_compiler/frontend/proto/parser.py
@@ -104,7 +104,10 @@ class Parser:
raise self.error("Duplicate package declaration")
package = self.parse_package()
elif self.check(TokenType.IMPORT):
- imports.append(self.parse_import())
+ path = self.parse_import()
+ normalized_path = path.lstrip("/")
+ if not normalized_path.startswith("google/protobuf/"):
+ imports.append(path)
elif self.check(TokenType.OPTION):
name, value = self.parse_option_statement()
options[name] = value
diff --git a/compiler/fory_compiler/frontend/proto/translator.py
b/compiler/fory_compiler/frontend/proto/translator.py
index d1f0e6d5c..46f5eeddd 100644
--- a/compiler/fory_compiler/frontend/proto/translator.py
+++ b/compiler/fory_compiler/frontend/proto/translator.py
@@ -74,6 +74,7 @@ class ProtoTranslator:
WELL_KNOWN_TYPES: Dict[str, PrimitiveKind] = {
"google.protobuf.Timestamp": PrimitiveKind.TIMESTAMP,
"google.protobuf.Duration": PrimitiveKind.DURATION,
+ "google.protobuf.Any": PrimitiveKind.ANY,
}
TYPE_OVERRIDES: Dict[str, PrimitiveKind] = {
diff --git a/compiler/fory_compiler/generators/cpp.py
b/compiler/fory_compiler/generators/cpp.py
index 89ee65b8e..746371515 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -65,6 +65,7 @@ class CppGenerator(BaseGenerator):
PrimitiveKind.BYTES: "std::vector<uint8_t>",
PrimitiveKind.DATE: "fory::serialization::Date",
PrimitiveKind.TIMESTAMP: "fory::serialization::Timestamp",
+ PrimitiveKind.ANY: "std::any",
}
NUMERIC_PRIMITIVES = {
PrimitiveKind.BOOL,
@@ -544,6 +545,13 @@ class CppGenerator(BaseGenerator):
) -> str:
member_name = self.get_field_member_name(field)
other_member = f"other.{member_name}"
+ if isinstance(field.field_type, PrimitiveType) and (
+ field.field_type.kind == PrimitiveKind.ANY
+ ):
+ return (
+ f"((!{member_name}.has_value() && !{other_member}.has_value())
|| "
+ f"({member_name}.type() == {other_member}.type()))"
+ )
if self.is_message_type(
field.field_type, parent_stack
) and self.get_field_weak_ref(field):
@@ -1274,6 +1282,8 @@ class CppGenerator(BaseGenerator):
) -> str:
"""Generate C++ type string with package namespace."""
if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.ANY:
+ return self.PRIMITIVE_MAP[field_type.kind]
base_type = self.PRIMITIVE_MAP[field_type.kind]
if nullable:
return f"std::optional<{base_type}>"
@@ -1357,9 +1367,13 @@ class CppGenerator(BaseGenerator):
def get_field_meta(self, field: Field) -> str:
"""Build FieldMeta expression for a field."""
meta = "fory::F()"
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
if field.tag_id is not None:
meta += f".id({field.tag_id})"
- if field.optional:
+ if field.optional or is_any:
meta += ".nullable()"
if field.ref:
meta += ".ref()"
@@ -1374,7 +1388,11 @@ class CppGenerator(BaseGenerator):
def get_union_field_meta(self, field: Field) -> str:
"""Build FieldMeta expression for a union case."""
meta = f"fory::F({field.number})"
- if field.optional:
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
+ if field.optional or is_any:
meta += ".nullable()"
if field.ref:
meta += ".ref()"
@@ -1438,6 +1456,8 @@ class CppGenerator(BaseGenerator):
) -> str:
"""Generate C++ type string."""
if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.ANY:
+ return self.PRIMITIVE_MAP[field_type.kind]
base_type = self.PRIMITIVE_MAP[field_type.kind]
if nullable:
return f"std::optional<{base_type}>"
@@ -1568,6 +1588,8 @@ class CppGenerator(BaseGenerator):
includes.add("<vector>")
elif field_type.kind in (PrimitiveKind.DATE,
PrimitiveKind.TIMESTAMP):
includes.add('"fory/serialization/temporal_serializers.h"')
+ elif field_type.kind == PrimitiveKind.ANY:
+ includes.add("<any>")
elif isinstance(field_type, ListType):
includes.add("<vector>")
diff --git a/compiler/fory_compiler/generators/go.py
b/compiler/fory_compiler/generators/go.py
index c4dac3e0c..c2d5abb9a 100644
--- a/compiler/fory_compiler/generators/go.py
+++ b/compiler/fory_compiler/generators/go.py
@@ -184,6 +184,7 @@ class GoGenerator(BaseGenerator):
PrimitiveKind.BYTES: "[]byte",
PrimitiveKind.DATE: "fory.Date",
PrimitiveKind.TIMESTAMP: "time.Time",
+ PrimitiveKind.ANY: "any",
}
def generate(self) -> List[GeneratedFile]:
@@ -462,6 +463,7 @@ class GoGenerator(BaseGenerator):
PrimitiveKind.BYTES: "fory.BINARY",
PrimitiveKind.DATE: "fory.DATE",
PrimitiveKind.TIMESTAMP: "fory.TIMESTAMP",
+ PrimitiveKind.ANY: "fory.UNKNOWN",
}
return primitive_type_ids.get(kind, "fory.UNKNOWN")
if isinstance(field.field_type, ListType):
@@ -473,15 +475,15 @@ class GoGenerator(BaseGenerator):
if isinstance(type_def, Enum):
if type_def.type_id is None:
return "fory.NAMED_ENUM"
- return f"({type_def.type_id} << 8) | fory.ENUM"
+ return "fory.ENUM"
if isinstance(type_def, Union):
if type_def.type_id is None:
return "fory.NAMED_UNION"
- return f"({type_def.type_id} << 8) | fory.UNION"
+ return "fory.UNION"
if isinstance(type_def, Message):
if type_def.type_id is None:
return "fory.NAMED_STRUCT"
- return f"({type_def.type_id} << 8) | fory.STRUCT"
+ return "fory.STRUCT"
return "fory.UNKNOWN"
def get_union_case_reflect_type_expr(
@@ -605,11 +607,15 @@ class GoGenerator(BaseGenerator):
is_list = isinstance(field.field_type, ListType)
is_map = isinstance(field.field_type, MapType)
is_collection = is_list or is_map
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
nullable_tag: Optional[bool] = None
ref_tag: Optional[bool] = None
if field.tag_id is not None:
tags.append(f"id={field.tag_id}")
- if field.optional:
+ if field.optional or is_any:
nullable_tag = True
elif is_collection and (
field.ref or (is_list and (field.element_optional or
field.element_ref))
@@ -682,6 +688,8 @@ class GoGenerator(BaseGenerator):
if not field.optional or field.ref:
return False
if isinstance(field.field_type, PrimitiveType):
+ if field.field_type.kind == PrimitiveKind.ANY:
+ return False
base_type = self.PRIMITIVE_MAP[field.field_type.kind]
return base_type not in ("[]byte", "time.Time", "fory.Date")
if isinstance(field.field_type, NamedType):
@@ -701,6 +709,8 @@ class GoGenerator(BaseGenerator):
) -> str:
"""Generate Go type string."""
if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.ANY:
+ return "any"
base_type = self.PRIMITIVE_MAP[field_type.kind]
if nullable and base_type not in ("[]byte",):
if (
diff --git a/compiler/fory_compiler/generators/java.py
b/compiler/fory_compiler/generators/java.py
index b005da4d2..ecbf6584a 100644
--- a/compiler/fory_compiler/generators/java.py
+++ b/compiler/fory_compiler/generators/java.py
@@ -98,6 +98,7 @@ class JavaGenerator(BaseGenerator):
PrimitiveKind.TIMESTAMP: "java.time.Instant",
PrimitiveKind.DURATION: "java.time.Duration",
PrimitiveKind.DECIMAL: "java.math.BigDecimal",
+ PrimitiveKind.ANY: "Object",
}
# Boxed versions for nullable primitives
@@ -120,6 +121,7 @@ class JavaGenerator(BaseGenerator):
PrimitiveKind.FLOAT16: "Float",
PrimitiveKind.FLOAT32: "Float",
PrimitiveKind.FLOAT64: "Double",
+ PrimitiveKind.ANY: "Object",
}
# Primitive array types for repeated numeric fields
@@ -647,6 +649,7 @@ class JavaGenerator(BaseGenerator):
PrimitiveKind.BYTES: "Types.BINARY",
PrimitiveKind.DATE: "Types.DATE",
PrimitiveKind.TIMESTAMP: "Types.TIMESTAMP",
+ PrimitiveKind.ANY: "Types.UNKNOWN",
}
return primitive_type_ids.get(kind, "Types.UNKNOWN")
if isinstance(field.field_type, ListType):
@@ -791,9 +794,14 @@ class JavaGenerator(BaseGenerator):
# Generate @ForyField annotation if needed
annotations = []
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
+ nullable = field.optional or is_any
if field.tag_id is not None:
annotations.append(f"id = {field.tag_id}")
- if field.optional:
+ if nullable:
annotations.append("nullable = true")
if field.ref:
annotations.append("ref = true")
@@ -812,7 +820,7 @@ class JavaGenerator(BaseGenerator):
# Field type
java_type = self.generate_type(
field.field_type,
- field.optional,
+ nullable,
field.element_optional,
field.element_ref,
)
@@ -825,9 +833,14 @@ class JavaGenerator(BaseGenerator):
def generate_getter_setter(self, field: Field) -> List[str]:
"""Generate getter and setter for a field."""
lines = []
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
+ nullable = field.optional or is_any
java_type = self.generate_type(
field.field_type,
- field.optional,
+ nullable,
field.element_optional,
field.element_ref,
)
@@ -916,6 +929,10 @@ class JavaGenerator(BaseGenerator):
def collect_field_imports(self, field: Field, imports: Set[str]):
"""Collect imports for a field, including list modifiers."""
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
self.collect_type_imports(
field.field_type,
imports,
@@ -924,7 +941,7 @@ class JavaGenerator(BaseGenerator):
)
self.collect_integer_imports(field.field_type, imports)
self.collect_array_imports(field, imports)
- if field.optional or field.ref or field.tag_id is not None:
+ if field.optional or field.ref or field.tag_id is not None or is_any:
imports.add("org.apache.fory.annotation.ForyField")
def collect_array_imports(self, field: Field, imports: Set[str]) -> None:
diff --git a/compiler/fory_compiler/generators/python.py
b/compiler/fory_compiler/generators/python.py
index 7f559aa8c..8ed180333 100644
--- a/compiler/fory_compiler/generators/python.py
+++ b/compiler/fory_compiler/generators/python.py
@@ -65,6 +65,7 @@ class PythonGenerator(BaseGenerator):
PrimitiveKind.BYTES: "bytes",
PrimitiveKind.DATE: "datetime.date",
PrimitiveKind.TIMESTAMP: "datetime.datetime",
+ PrimitiveKind.ANY: "Any",
}
# Numpy dtype strings for primitive arrays
@@ -134,6 +135,7 @@ class PythonGenerator(BaseGenerator):
PrimitiveKind.BYTES: 'b""',
PrimitiveKind.DATE: "None",
PrimitiveKind.TIMESTAMP: "None",
+ PrimitiveKind.ANY: "None",
}
def safe_name(self, name: str) -> str:
@@ -461,9 +463,14 @@ class PythonGenerator(BaseGenerator):
"""Generate a dataclass field."""
lines = []
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
+ nullable = field.optional or is_any
python_type = self.generate_type(
field.field_type,
- field.optional,
+ nullable,
field.element_optional,
parent_stack,
)
@@ -477,11 +484,11 @@ class PythonGenerator(BaseGenerator):
trailing_comment = f" # {comment}"
tag_id = field.tag_id
- if tag_id is not None or field.ref:
+ if tag_id is not None or field.ref or nullable:
field_args = []
if tag_id is not None:
field_args.append(f"id={tag_id}")
- if field.optional:
+ if nullable:
field_args.append("nullable=True")
if field.ref:
field_args.append("ref=True")
@@ -530,6 +537,8 @@ class PythonGenerator(BaseGenerator):
) -> str:
"""Generate Python type hint."""
if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.ANY:
+ return "Any"
base_type = self.PRIMITIVE_MAP[field_type.kind]
if nullable:
return f"Optional[{base_type}]"
@@ -685,6 +694,8 @@ class PythonGenerator(BaseGenerator):
if isinstance(field_type, PrimitiveType):
if field_type.kind in (PrimitiveKind.DATE,
PrimitiveKind.TIMESTAMP):
imports.add("import datetime")
+ elif field_type.kind == PrimitiveKind.ANY:
+ imports.add("from typing import Any")
elif isinstance(field_type, ListType):
# Add numpy import for primitive arrays
diff --git a/compiler/fory_compiler/generators/rust.py
b/compiler/fory_compiler/generators/rust.py
index fa0e72635..d4c9f2652 100644
--- a/compiler/fory_compiler/generators/rust.py
+++ b/compiler/fory_compiler/generators/rust.py
@@ -64,6 +64,7 @@ class RustGenerator(BaseGenerator):
PrimitiveKind.BYTES: "Vec<u8>",
PrimitiveKind.DATE: "chrono::NaiveDate",
PrimitiveKind.TIMESTAMP: "chrono::NaiveDateTime",
+ PrimitiveKind.ANY: "Box<dyn Any>",
}
def generate(self) -> List[GeneratedFile]:
@@ -233,7 +234,13 @@ class RustGenerator(BaseGenerator):
"""Generate a Rust tagged union."""
lines: List[str] = []
- lines.append("#[derive(ForyObject, Debug, Clone, PartialEq)]")
+ has_any = any(
+ self.field_type_has_any(field.field_type) for field in union.fields
+ )
+ derives = ["ForyObject", "Debug"]
+ if not has_any:
+ derives.extend(["Clone", "PartialEq"])
+ lines.append(f"#[derive({', '.join(derives)})]")
lines.append(f"pub enum {union.name} {{")
for field in union.fields:
@@ -275,7 +282,10 @@ class RustGenerator(BaseGenerator):
type_name = message.name
# Derive macros
- lines.append("#[derive(ForyObject, Debug, Clone, PartialEq, Default)]")
+ derives = ["ForyObject", "Debug"]
+ if not self.message_has_any(message):
+ derives.extend(["Clone", "PartialEq", "Default"])
+ lines.append(f"#[derive({', '.join(derives)})]")
lines.append(f"pub struct {type_name} {{")
@@ -290,6 +300,24 @@ class RustGenerator(BaseGenerator):
return lines
+ def message_has_any(self, message: Message) -> bool:
+ """Return True if a message contains any type fields."""
+ return any(
+ self.field_type_has_any(field.field_type) for field in
message.fields
+ )
+
+ def field_type_has_any(self, field_type: FieldType) -> bool:
+ """Return True if field type or its children is any."""
+ if isinstance(field_type, PrimitiveType):
+ return field_type.kind == PrimitiveKind.ANY
+ if isinstance(field_type, ListType):
+ return self.field_type_has_any(field_type.element_type)
+ if isinstance(field_type, MapType):
+ return self.field_type_has_any(
+ field_type.key_type
+ ) or self.field_type_has_any(field_type.value_type)
+ return False
+
def generate_nested_module(
self,
message: Message,
@@ -354,7 +382,11 @@ class RustGenerator(BaseGenerator):
attrs = []
if field.tag_id is not None:
attrs.append(f"id = {field.tag_id}")
- if field.optional:
+ is_any = (
+ isinstance(field.field_type, PrimitiveType)
+ and field.field_type.kind == PrimitiveKind.ANY
+ )
+ if field.optional or is_any:
attrs.append("nullable = true")
if field.ref:
attrs.append("ref = true")
@@ -454,6 +486,8 @@ class RustGenerator(BaseGenerator):
) -> str:
"""Generate Rust type string."""
if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.ANY:
+ return "Box<dyn Any>"
base_type = self.PRIMITIVE_MAP[field_type.kind]
if nullable:
return f"Option<{base_type}>"
@@ -538,6 +572,8 @@ class RustGenerator(BaseGenerator):
if isinstance(field_type, PrimitiveType):
if field_type.kind in (PrimitiveKind.DATE,
PrimitiveKind.TIMESTAMP):
uses.add("use chrono")
+ if field_type.kind == PrimitiveKind.ANY:
+ uses.add("use std::any::Any")
elif isinstance(field_type, NamedType):
pass # No additional uses needed
diff --git a/compiler/fory_compiler/ir/types.py
b/compiler/fory_compiler/ir/types.py
index 46d96d4ed..3dfc3d8ed 100644
--- a/compiler/fory_compiler/ir/types.py
+++ b/compiler/fory_compiler/ir/types.py
@@ -47,6 +47,7 @@ class PrimitiveKind(PyEnum):
TIMESTAMP = "timestamp"
DURATION = "duration"
DECIMAL = "decimal"
+ ANY = "any"
PRIMITIVE_TYPES = {
@@ -74,6 +75,7 @@ PRIMITIVE_TYPES = {
"timestamp": PrimitiveKind.TIMESTAMP,
"duration": PrimitiveKind.DURATION,
"decimal": PrimitiveKind.DECIMAL,
+ "any": PrimitiveKind.ANY,
}
diff --git a/compiler/fory_compiler/ir/validator.py
b/compiler/fory_compiler/ir/validator.py
index a6e07998d..0f0683147 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -27,11 +27,13 @@ from fory_compiler.ir.ast import (
Union,
Field,
FieldType,
+ PrimitiveType,
NamedType,
ListType,
MapType,
SourceLocation,
)
+from fory_compiler.ir.types import PrimitiveKind
@dataclass
@@ -313,6 +315,12 @@ class SchemaValidator:
check_type_ref(f.field_type, f, None)
def _check_ref_rules(self) -> None:
+ def is_any_type(field_type: FieldType) -> bool:
+ return (
+ isinstance(field_type, PrimitiveType)
+ and field_type.kind == PrimitiveKind.ANY
+ )
+
def resolve_target(
target: NamedType,
enclosing_messages: Optional[List[Message]],
@@ -344,6 +352,32 @@ class SchemaValidator:
field: Field,
enclosing_messages: Optional[List[Message]] = None,
) -> None:
+ if is_any_type(field.field_type) and field.ref:
+ self._error(
+ "ref is not allowed on any fields",
+ field.location,
+ )
+
+ if (
+ isinstance(field.field_type, ListType)
+ and is_any_type(field.field_type.element_type)
+ and field.element_ref
+ ):
+ self._error(
+ "ref is not allowed on repeated any fields",
+ field.location,
+ )
+
+ if (
+ isinstance(field.field_type, MapType)
+ and is_any_type(field.field_type.value_type)
+ and field.field_type.value_ref
+ ):
+ self._error(
+ "ref is not allowed on map values of any type",
+ field.location,
+ )
+
if field.ref:
if isinstance(field.field_type, (ListType, MapType)):
self._error(
diff --git a/cpp/fory/serialization/BUILD b/cpp/fory/serialization/BUILD
index 34a447018..5f737ae5f 100644
--- a/cpp/fory/serialization/BUILD
+++ b/cpp/fory/serialization/BUILD
@@ -8,6 +8,7 @@ cc_library(
"skip.cc",
],
hdrs = [
+ "any_serializer.h",
"array_serializer.h",
"basic_serializer.h",
"collection_serializer.h",
@@ -134,6 +135,16 @@ cc_test(
],
)
+cc_test(
+ name = "any_serializer_test",
+ srcs = ["any_serializer_test.cc"],
+ deps = [
+ ":fory_serialization",
+ "@googletest//:gtest",
+ "@googletest//:gtest_main",
+ ],
+)
+
cc_test(
name = "field_serializer_test",
srcs = ["field_serializer_test.cc"],
diff --git a/cpp/fory/serialization/CMakeLists.txt
b/cpp/fory/serialization/CMakeLists.txt
index 7f5f01d3a..d4852742e 100644
--- a/cpp/fory/serialization/CMakeLists.txt
+++ b/cpp/fory/serialization/CMakeLists.txt
@@ -22,6 +22,7 @@ set(FORY_SERIALIZATION_SOURCES
)
set(FORY_SERIALIZATION_HEADERS
+ any_serializer.h
array_serializer.h
basic_serializer.h
collection_serializer.h
@@ -108,6 +109,10 @@ if(FORY_BUILD_TESTS)
add_executable(fory_serialization_weak_ptr_test
weak_ptr_serializer_test.cc)
target_link_libraries(fory_serialization_weak_ptr_test fory_serialization
GTest::gtest GTest::gtest_main)
gtest_discover_tests(fory_serialization_weak_ptr_test)
+
+ add_executable(fory_serialization_any_test any_serializer_test.cc)
+ target_link_libraries(fory_serialization_any_test fory_serialization
GTest::gtest GTest::gtest_main)
+ gtest_discover_tests(fory_serialization_any_test)
endif()
# xlang test binary
diff --git a/cpp/fory/serialization/any_serializer.h
b/cpp/fory/serialization/any_serializer.h
new file mode 100644
index 000000000..7a10dd6a1
--- /dev/null
+++ b/cpp/fory/serialization/any_serializer.h
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include "fory/serialization/serializer.h"
+#include "fory/serialization/type_info.h"
+#include "fory/serialization/type_resolver.h"
+#include "fory/type/type.h"
+#include "fory/util/error.h"
+#include "fory/util/result.h"
+
+#include <any>
+#include <typeindex>
+
+namespace fory {
+namespace serialization {
+
+// ============================================================================
+// std::any Serializer
+// ============================================================================
+
+/// Serializer for std::any.
+///
+/// std::any serialization requires explicit registration of allowed value
+/// types via register_any_type<T>(). Only registered types can be serialized
+/// or deserialized.
+template <> struct Serializer<std::any> {
+ static constexpr TypeId type_id = TypeId::UNKNOWN;
+
+ static inline void write_type_info(WriteContext &ctx) {
+ ctx.set_error(Error::invalid("std::any requires runtime type info"));
+ }
+
+ static inline void read_type_info(ReadContext &ctx) {
+ ctx.set_error(Error::invalid("std::any requires runtime type info"));
+ }
+
+ static inline void write(const std::any &value, WriteContext &ctx,
+ RefMode ref_mode, bool write_type,
+ bool has_generics = false) {
+ (void)has_generics;
+ if (ref_mode != RefMode::None) {
+ if (!value.has_value()) {
+ ctx.write_int8(NULL_FLAG);
+ return;
+ }
+ write_not_null_ref_flag(ctx, ref_mode);
+ } else if (FORY_PREDICT_FALSE(!value.has_value())) {
+ ctx.set_error(Error::invalid("std::any requires non-empty value"));
+ return;
+ }
+
+ const std::type_index concrete_type_id(value.type());
+ auto type_info_res = ctx.type_resolver().get_type_info(concrete_type_id);
+ if (FORY_PREDICT_FALSE(!type_info_res.ok())) {
+ ctx.set_error(Error::type_error("std::any type is not registered"));
+ return;
+ }
+ const TypeInfo *type_info = type_info_res.value();
+ if (FORY_PREDICT_FALSE(type_info->harness.any_write_fn == nullptr)) {
+ ctx.set_error(Error::type_error("std::any type is not registered"));
+ return;
+ }
+
+ if (write_type) {
+ uint32_t fory_type_id = type_info->type_id;
+ uint32_t type_id_arg = is_internal_type(fory_type_id)
+ ? fory_type_id
+ : static_cast<uint32_t>(TypeId::UNKNOWN);
+ auto write_res = ctx.write_any_typeinfo(type_id_arg, concrete_type_id);
+ if (FORY_PREDICT_FALSE(!write_res.ok())) {
+ ctx.set_error(std::move(write_res).error());
+ return;
+ }
+ }
+
+ type_info->harness.any_write_fn(value, ctx);
+ }
+
+ static inline void write_data(const std::any &, WriteContext &ctx) {
+ ctx.set_error(Error::invalid("std::any requires type info for writing"));
+ }
+
+ static inline void write_data_generic(const std::any &value,
+ WriteContext &ctx, bool has_generics) {
+ (void)has_generics;
+ write_data(value, ctx);
+ }
+
+ static inline std::any read(ReadContext &ctx, RefMode ref_mode,
+ bool read_type) {
+ bool has_value = read_null_only_flag(ctx, ref_mode);
+ if (ctx.has_error() || !has_value) {
+ return std::any();
+ }
+ if (FORY_PREDICT_FALSE(!read_type)) {
+ ctx.set_error(Error::invalid("std::any requires read_type=true"));
+ return std::any();
+ }
+
+ const TypeInfo *type_info = ctx.read_any_typeinfo(ctx.error());
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::any();
+ }
+
+ if (FORY_PREDICT_FALSE(type_info->harness.any_read_fn == nullptr)) {
+ ctx.set_error(Error::type_error("std::any type is not registered"));
+ return std::any();
+ }
+
+ return type_info->harness.any_read_fn(ctx);
+ }
+
+ static inline std::any read_data(ReadContext &ctx) {
+ ctx.set_error(Error::invalid("std::any requires type info for reading"));
+ return std::any();
+ }
+
+ static inline std::any read_with_type_info(ReadContext &ctx, RefMode
ref_mode,
+ const TypeInfo &type_info) {
+ bool has_value = read_null_only_flag(ctx, ref_mode);
+ if (ctx.has_error() || !has_value) {
+ return std::any();
+ }
+
+ if (FORY_PREDICT_FALSE(type_info.harness.any_read_fn == nullptr)) {
+ ctx.set_error(Error::type_error("std::any type is not registered"));
+ return std::any();
+ }
+
+ return type_info.harness.any_read_fn(ctx);
+ }
+};
+
+/// Register a type so it can be serialized inside std::any.
+///
+/// For internal types (primitives, string, temporal), registration does not
+/// require prior type registration. For user-defined types, the type must be
+/// registered with TypeResolver first (e.g., via Fory::register_struct).
+template <typename T>
+Result<void, Error> register_any_type(TypeResolver &resolver) {
+ return resolver.template register_any_type<T>();
+}
+
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/any_serializer_test.cc
b/cpp/fory/serialization/any_serializer_test.cc
new file mode 100644
index 000000000..0135b2517
--- /dev/null
+++ b/cpp/fory/serialization/any_serializer_test.cc
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "fory/serialization/any_serializer.h"
+#include "fory/serialization/fory.h"
+#include "gtest/gtest.h"
+
+#include <any>
+#include <string>
+
+namespace fory {
+namespace serialization {
+namespace test {
+
+struct AnyInnerStruct {
+ int32_t id;
+ std::string name;
+
+ bool operator==(const AnyInnerStruct &other) const {
+ return id == other.id && name == other.name;
+ }
+
+ FORY_STRUCT(AnyInnerStruct, id, name);
+};
+
+inline bool any_equals(const std::any &left, const std::any &right) {
+ if (left.type() != right.type()) {
+ return false;
+ }
+ if (left.type() == typeid(std::string)) {
+ return std::any_cast<const std::string &>(left) ==
+ std::any_cast<const std::string &>(right);
+ }
+ if (left.type() == typeid(AnyInnerStruct)) {
+ return std::any_cast<const AnyInnerStruct &>(left) ==
+ std::any_cast<const AnyInnerStruct &>(right);
+ }
+ return false;
+}
+
+struct AnyHolderStruct {
+ std::any first;
+ std::any second;
+
+ bool operator==(const AnyHolderStruct &other) const {
+ return any_equals(first, other.first) && any_equals(second, other.second);
+ }
+
+ FORY_STRUCT(AnyHolderStruct, first, second);
+};
+
+TEST(AnySerializerTest, RoundTripStructFields) {
+ auto fory = Fory::builder().xlang(true).track_ref(false).build();
+
+ ASSERT_TRUE(fory.register_struct<AnyInnerStruct>(1).ok());
+ ASSERT_TRUE(fory.register_struct<AnyHolderStruct>(2).ok());
+
+ ASSERT_TRUE(register_any_type<std::string>(fory.type_resolver()).ok());
+ ASSERT_TRUE(register_any_type<AnyInnerStruct>(fory.type_resolver()).ok());
+
+ AnyHolderStruct original;
+ original.first = std::string("hello any");
+ original.second = AnyInnerStruct{42, "nested"};
+
+ auto serialize_result = fory.serialize(original);
+ ASSERT_TRUE(serialize_result.ok())
+ << "Serialization failed: " << serialize_result.error().to_string();
+
+ std::vector<uint8_t> bytes = std::move(serialize_result).value();
+ auto deserialize_result =
+ fory.deserialize<AnyHolderStruct>(bytes.data(), bytes.size());
+ ASSERT_TRUE(deserialize_result.ok())
+ << "Deserialization failed: " << deserialize_result.error().to_string();
+
+ AnyHolderStruct deserialized = std::move(deserialize_result).value();
+ EXPECT_EQ(original, deserialized);
+}
+
+} // namespace test
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index 496a8e62e..29217dde5 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -19,6 +19,7 @@
#pragma once
+#include "fory/serialization/any_serializer.h"
#include "fory/serialization/array_serializer.h"
#include "fory/serialization/collection_serializer.h"
#include "fory/serialization/config.h"
diff --git a/cpp/fory/serialization/type_info.h
b/cpp/fory/serialization/type_info.h
index 12a811029..dc6565df8 100644
--- a/cpp/fory/serialization/type_info.h
+++ b/cpp/fory/serialization/type_info.h
@@ -25,6 +25,7 @@
#include "absl/container/flat_hash_map.h"
+#include <any>
#include <cstdint>
#include <memory>
#include <string>
@@ -65,6 +66,8 @@ struct Harness {
const struct TypeInfo *type_info);
using SortedFieldInfosFn =
Result<std::vector<FieldInfo>, Error> (*)(TypeResolver &);
+ using AnyWriteFn = void (*)(const std::any &value, WriteContext &ctx);
+ using AnyReadFn = std::any (*)(ReadContext &ctx);
Harness() = default;
Harness(WriteFn write, ReadFn read, WriteDataFn write_data,
@@ -86,6 +89,8 @@ struct Harness {
ReadDataFn read_data_fn = nullptr;
SortedFieldInfosFn sorted_field_infos_fn = nullptr;
ReadCompatibleFn read_compatible_fn = nullptr;
+ AnyWriteFn any_write_fn = nullptr;
+ AnyReadFn any_read_fn = nullptr;
};
// ============================================================================
diff --git a/cpp/fory/serialization/type_resolver.cc
b/cpp/fory/serialization/type_resolver.cc
index 2f240feb2..491fb0155 100644
--- a/cpp/fory/serialization/type_resolver.cc
+++ b/cpp/fory/serialization/type_resolver.cc
@@ -1220,6 +1220,7 @@ TypeResolver::build_final_type_resolver() {
for (const auto &[key, old_ptr] : type_info_by_runtime_type_) {
final_resolver->type_info_by_runtime_type_[key] = ptr_map[old_ptr];
}
+
for (const auto &[key, old_ptr] : partial_type_infos_) {
final_resolver->partial_type_infos_.put(key, ptr_map[old_ptr]);
}
diff --git a/cpp/fory/serialization/type_resolver.h
b/cpp/fory/serialization/type_resolver.h
index 6df14e00d..ed0a92be7 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -753,6 +753,8 @@ public:
/// @return const pointer to TypeInfo if found, error otherwise
template <typename T> Result<const TypeInfo *, Error> get_type_info() const;
+ template <typename T> Result<void, Error> register_any_type();
+
/// Builds the final TypeResolver by completing all partial type infos
/// created during registration.
///
@@ -923,6 +925,40 @@ inline void TypeResolver::check_registration_thread() {
<< "TypeResolver has been finalized, cannot register more types";
}
+namespace detail {
+
+template <typename T>
+inline void any_write_adapter(const std::any &value, WriteContext &ctx) {
+ const T *ptr = std::any_cast<T>(&value);
+ if (ptr != nullptr) {
+ Serializer<T>::write_data(*ptr, ctx);
+ return;
+ }
+ const auto *shared_ptr = std::any_cast<std::shared_ptr<T>>(&value);
+ if (shared_ptr != nullptr) {
+ if (FORY_PREDICT_FALSE(!(*shared_ptr))) {
+ ctx.set_error(Error::invalid("std::any stored shared_ptr is null"));
+ return;
+ }
+ Serializer<T>::write_data(**shared_ptr, ctx);
+ return;
+ }
+ ctx.set_error(Error::type_error("std::any stored value type mismatch"));
+}
+
+template <typename T> inline std::any any_read_adapter(ReadContext &ctx) {
+ T value = Serializer<T>::read_data(ctx);
+ if (FORY_PREDICT_FALSE(ctx.has_error())) {
+ return std::any();
+ }
+ if constexpr (std::is_copy_constructible<T>::value) {
+ return std::any(std::move(value));
+ }
+ return std::any(std::make_shared<T>(std::move(value)));
+}
+
+} // namespace detail
+
template <typename T> const TypeMeta &TypeResolver::struct_meta() {
constexpr uint64_t ctid = type_index<T>();
TypeInfo *info = type_info_by_ctid_.get_or_default(ctid, nullptr);
@@ -960,6 +996,31 @@ Result<const TypeInfo *, Error>
TypeResolver::get_type_info() const {
return Unexpected(Error::type_error("Type not registered"));
}
+template <typename T> Result<void, Error> TypeResolver::register_any_type() {
+ check_registration_thread();
+ constexpr uint32_t static_type_id =
+ static_cast<uint32_t>(Serializer<T>::type_id);
+ TypeInfo *type_info = nullptr;
+ if (is_internal_type(static_type_id)) {
+ type_info = type_info_by_id_.get_or_default(static_type_id, nullptr);
+ if (FORY_PREDICT_FALSE(type_info == nullptr)) {
+ return Unexpected(Error::type_error("TypeInfo not found for type_id: " +
+ std::to_string(static_type_id)));
+ }
+ } else {
+ constexpr uint64_t ctid = type_index<T>();
+ type_info = type_info_by_ctid_.get_or_default(ctid, nullptr);
+ if (FORY_PREDICT_FALSE(type_info == nullptr)) {
+ return Unexpected(Error::type_error("Type not registered"));
+ }
+ }
+
+ type_info->harness.any_write_fn = &detail::any_write_adapter<T>;
+ type_info->harness.any_read_fn = &detail::any_read_adapter<T>;
+ register_type_internal_runtime(std::type_index(typeid(T)), type_info);
+ return Result<void, Error>();
+}
+
template <typename T>
Result<void, Error> TypeResolver::register_by_id(uint32_t type_id) {
check_registration_thread();
@@ -1331,20 +1392,26 @@ TypeResolver::build_union_type_info(uint32_t type_id,
std::string ns,
}
template <typename T> Harness TypeResolver::make_struct_harness() {
- return Harness(&TypeResolver::harness_write_adapter<T>,
- &TypeResolver::harness_read_adapter<T>,
- &TypeResolver::harness_write_data_adapter<T>,
- &TypeResolver::harness_read_data_adapter<T>,
- &TypeResolver::harness_struct_sorted_fields<T>,
- &TypeResolver::harness_read_compatible_adapter<T>);
+ Harness harness(&TypeResolver::harness_write_adapter<T>,
+ &TypeResolver::harness_read_adapter<T>,
+ &TypeResolver::harness_write_data_adapter<T>,
+ &TypeResolver::harness_read_data_adapter<T>,
+ &TypeResolver::harness_struct_sorted_fields<T>,
+ &TypeResolver::harness_read_compatible_adapter<T>);
+ harness.any_write_fn = &detail::any_write_adapter<T>;
+ harness.any_read_fn = &detail::any_read_adapter<T>;
+ return harness;
}
template <typename T> Harness TypeResolver::make_serializer_harness() {
- return Harness(&TypeResolver::harness_write_adapter<T>,
- &TypeResolver::harness_read_adapter<T>,
- &TypeResolver::harness_write_data_adapter<T>,
- &TypeResolver::harness_read_data_adapter<T>,
- &TypeResolver::harness_empty_sorted_fields<T>);
+ Harness harness(&TypeResolver::harness_write_adapter<T>,
+ &TypeResolver::harness_read_adapter<T>,
+ &TypeResolver::harness_write_data_adapter<T>,
+ &TypeResolver::harness_read_data_adapter<T>,
+ &TypeResolver::harness_empty_sorted_fields<T>);
+ harness.any_write_fn = &detail::any_write_adapter<T>;
+ harness.any_read_fn = &detail::any_read_adapter<T>;
+ return harness;
}
template <typename T>
diff --git a/docs/compiler/fdl-syntax.md b/docs/compiler/fdl-syntax.md
index c8d5707f9..2a3c1e115 100644
--- a/docs/compiler/fdl-syntax.md
+++ b/docs/compiler/fdl-syntax.md
@@ -858,6 +858,7 @@ Modifiers before `repeated` apply to the field/collection.
Modifiers after
| `timestamp` | Date and time with timezone | Variable |
| `duration` | Duration | Variable |
| `decimal` | Decimal value | Variable |
+| `any` | Dynamic value (runtime type) | Variable |
See [Type System](type-system.md) for complete type mappings.
@@ -867,6 +868,11 @@ See [Type System](type-system.md) for complete type
mappings.
- Use `fixed_*` for fixed-width integer encoding.
- Use `tagged_*` for tagged/hybrid encoding (64-bit only).
+**Any type notes:**
+
+- `any` always writes a null flag (same as `nullable`) because the value may
be empty.
+- `ref` is not allowed on `any` fields. Wrap `any` in a message if you need
reference tracking.
+
### Named Types
Reference other messages or enums by name:
@@ -1241,6 +1247,7 @@ primitive_type := 'bool'
| 'float16' | 'float32' | 'float64'
| 'string' | 'bytes'
| 'date' | 'timestamp' | 'duration' | 'decimal'
+ | 'any'
named_type := qualified_name
qualified_name := IDENTIFIER ('.' IDENTIFIER)* // e.g., Parent.Child
map_type := 'map' '<' field_type ',' field_type '>'
diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md
index 0038ffcd4..589d33c39 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -219,7 +219,7 @@ message TreeNode {
| Map | `map<K, V>`
| `map<K, V>`
|
| Nullable | `optional T` (proto3)
| `optional T`
|
| Oneof | `oneof`
| `union` (case id = field number)
|
-| Any | `google.protobuf.Any`
| Not supported
|
+| Any | `google.protobuf.Any`
| `any`
|
| Extensions | `extend`
| Not supported
|
### Wire Format
diff --git a/docs/compiler/type-system.md b/docs/compiler/type-system.md
index 8cdfbfbcf..484c421c7 100644
--- a/docs/compiler/type-system.md
+++ b/docs/compiler/type-system.md
@@ -215,6 +215,29 @@ timestamp created_at = 1;
| Rust | `chrono::NaiveDateTime` | Requires `chrono` crate |
| C++ | `fory::serialization::Timestamp` | |
+### Any
+
+Dynamic value with runtime type information:
+
+```protobuf
+any payload = 1;
+```
+
+| Language | Type | Notes |
+| -------- | -------------- | -------------------- |
+| Java | `Object` | Runtime type written |
+| Python | `Any` | Runtime type written |
+| Go | `any` | Runtime type written |
+| Rust | `Box<dyn Any>` | Runtime type written |
+| C++ | `std::any` | Runtime type written |
+
+**Notes:**
+
+- `any` always writes a null flag (same as `nullable`) because values may be
empty; codegen treats `any` as nullable even without `optional`.
+- Allowed runtime values are limited to `bool`, `string`, `enum`, `message`,
and `union`. Other primitives (numeric, bytes, date/time) and list/map are not
supported; wrap them in a message or use explicit fields instead.
+- `ref` is not allowed on `any` fields (including repeated/map values). Wrap
`any` in a message if you need reference tracking.
+- The runtime type must be registered in the target language schema/IDL
registration; unknown types fail to deserialize.
+
## Enum Types
Enums define named integer constants:
diff --git a/integration_tests/idl_tests/cpp/main.cc
b/integration_tests/idl_tests/cpp/main.cc
index 631dfa67e..2227440d2 100644
--- a/integration_tests/idl_tests/cpp/main.cc
+++ b/integration_tests/idl_tests/cpp/main.cc
@@ -17,6 +17,7 @@
* under the License.
*/
+#include <any>
#include <chrono>
#include <cstdlib>
#include <fstream>
@@ -27,7 +28,9 @@
#include <vector>
#include "addressbook.h"
+#include "any_example.h"
#include "complex_fbs.h"
+#include "fory/serialization/any_serializer.h"
#include "fory/serialization/fory.h"
#include "graph.h"
#include "monster.h"
@@ -151,6 +154,8 @@ fory::Result<void, fory::Error> ValidateGraph(const
graph::Graph &graph_value) {
return fory::Result<void, fory::Error>();
}
+using StringMap = std::map<std::string, std::string>;
+
fory::Result<void, fory::Error> RunRoundTrip() {
auto fory = fory::serialization::Fory::builder()
.xlang(true)
@@ -162,6 +167,29 @@ fory::Result<void, fory::Error> RunRoundTrip() {
monster::RegisterTypes(fory);
complex_fbs::RegisterTypes(fory);
optional_types::RegisterTypes(fory);
+ any_example::RegisterTypes(fory);
+
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<bool>(fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(fory::serialization::register_any_type<std::string>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<fory::serialization::Date>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<fory::serialization::Timestamp>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<any_example::AnyInner>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<any_example::AnyUnion>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<std::vector<std::string>>(
+ fory.type_resolver()));
+ FORY_RETURN_IF_ERROR(
+ fory::serialization::register_any_type<StringMap>(fory.type_resolver()));
addressbook::Person::PhoneNumber mobile;
mobile.set_number("555-0100");
@@ -342,6 +370,30 @@ fory::Result<void, fory::Error> RunRoundTrip() {
fory::Error::invalid("optional types roundtrip mismatch"));
}
+ any_example::AnyInner any_inner;
+ any_inner.set_name("inner");
+
+ any_example::AnyHolder any_holder;
+ any_holder.set_bool_value(std::any(true));
+ any_holder.set_string_value(std::any(std::string("hello")));
+ any_holder.set_date_value(std::any(fory::serialization::Date(19724)));
+ any_holder.set_timestamp_value(std::any(
+ fory::serialization::Timestamp(std::chrono::seconds(1704164645))));
+ any_holder.set_message_value(std::any(any_inner));
+ any_holder.set_union_value(std::any(any_example::AnyUnion::text("union")));
+ any_holder.set_list_value(
+ std::any(std::vector<std::string>{"alpha", "beta"}));
+ any_holder.set_map_value(std::any(StringMap{{"k1", "v1"}, {"k2", "v2"}}));
+
+ FORY_TRY(any_bytes, fory.serialize(any_holder));
+ FORY_TRY(any_roundtrip, fory.deserialize<any_example::AnyHolder>(
+ any_bytes.data(), any_bytes.size()));
+
+ if (!(any_roundtrip == any_holder)) {
+ return fory::Unexpected(
+ fory::Error::invalid("any holder roundtrip mismatch"));
+ }
+
const char *data_file = std::getenv("DATA_FILE");
if (data_file != nullptr && data_file[0] != '\0') {
FORY_TRY(payload, ReadFile(data_file));
diff --git a/integration_tests/idl_tests/generate_idl.py
b/integration_tests/idl_tests/generate_idl.py
index 52e6f29ae..8efdb6ac0 100755
--- a/integration_tests/idl_tests/generate_idl.py
+++ b/integration_tests/idl_tests/generate_idl.py
@@ -29,6 +29,8 @@ SCHEMAS = [
IDL_DIR / "idl" / "optional_types.fdl",
IDL_DIR / "idl" / "tree.fdl",
IDL_DIR / "idl" / "graph.fdl",
+ IDL_DIR / "idl" / "any_example.fdl",
+ IDL_DIR / "idl" / "any_example.proto",
IDL_DIR / "idl" / "monster.fbs",
IDL_DIR / "idl" / "complex_fbs.fbs",
]
@@ -47,6 +49,8 @@ GO_OUTPUT_OVERRIDES = {
"optional_types.fdl": IDL_DIR / "go" / "optional_types",
"tree.fdl": IDL_DIR / "go" / "tree",
"graph.fdl": IDL_DIR / "go" / "graph",
+ "any_example.fdl": IDL_DIR / "go" / "any_example",
+ "any_example.proto": IDL_DIR / "go" / "any_example_pb",
}
diff --git a/integration_tests/idl_tests/go/idl_roundtrip_test.go
b/integration_tests/idl_tests/go/idl_roundtrip_test.go
index 4d3a25bf4..fe3fb7742 100644
--- a/integration_tests/idl_tests/go/idl_roundtrip_test.go
+++ b/integration_tests/idl_tests/go/idl_roundtrip_test.go
@@ -25,6 +25,7 @@ import (
fory "github.com/apache/fory/go/fory"
"github.com/apache/fory/go/fory/optional"
+ anyexample
"github.com/apache/fory/integration_tests/idl_tests/go/any_example"
complexfbs
"github.com/apache/fory/integration_tests/idl_tests/go/complex_fbs"
graphpkg "github.com/apache/fory/integration_tests/idl_tests/go/graph"
monster "github.com/apache/fory/integration_tests/idl_tests/go/monster"
@@ -82,6 +83,9 @@ func TestAddressBookRoundTrip(t *testing.T) {
if err := optionaltypes.RegisterTypes(f); err != nil {
t.Fatalf("register optional types: %v", err)
}
+ if err := anyexample.RegisterTypes(f); err != nil {
+ t.Fatalf("register any example types: %v", err)
+ }
book := buildAddressBook()
runLocalRoundTrip(t, f, book)
@@ -103,6 +107,9 @@ func TestAddressBookRoundTrip(t *testing.T) {
runLocalOptionalRoundTrip(t, f, holder)
runFileOptionalRoundTrip(t, f, holder)
+ anyHolder := buildAnyHolder()
+ runLocalAnyRoundTrip(t, f, anyHolder)
+
refFory := fory.NewFory(fory.WithXlang(true),
fory.WithRefTracking(true))
if err := treepkg.RegisterTypes(refFory); err != nil {
t.Fatalf("register tree types: %v", err)
@@ -118,6 +125,204 @@ func TestAddressBookRoundTrip(t *testing.T) {
runFileGraphRoundTrip(t, refFory, graphValue)
}
+func buildAnyHolder() anyexample.AnyHolder {
+ inner := anyexample.AnyInner{Name: "inner"}
+ unionValue := anyexample.TextAnyUnion("union")
+ return anyexample.AnyHolder{
+ BoolValue: true,
+ StringValue: "hello",
+ DateValue: fory.Date{Year: 2024, Month: time.January, Day:
2},
+ TimestampValue: time.Unix(1704164645, 0).UTC(),
+ MessageValue: &inner,
+ UnionValue: unionValue,
+ ListValue: []string{"alpha", "beta"},
+ MapValue: map[string]string{"k1": "v1", "k2": "v2"},
+ }
+}
+
+func runLocalAnyRoundTrip(t *testing.T, f *fory.Fory, holder
anyexample.AnyHolder) {
+ data, err := f.Serialize(&holder)
+ if err != nil {
+ t.Fatalf("serialize any: %v", err)
+ }
+
+ var out anyexample.AnyHolder
+ if err := f.Deserialize(data, &out); err != nil {
+ t.Fatalf("deserialize any: %v", err)
+ }
+
+ assertAnyHolderEqual(t, holder, out)
+}
+
+func assertAnyHolderEqual(t *testing.T, expected, actual anyexample.AnyHolder)
{
+ t.Helper()
+
+ if !anyBoolEqual(expected.BoolValue, actual.BoolValue) {
+ t.Fatalf("any bool mismatch: %#v != %#v", expected.BoolValue,
actual.BoolValue)
+ }
+ if !anyStringEqual(expected.StringValue, actual.StringValue) {
+ t.Fatalf("any string mismatch: %#v != %#v",
expected.StringValue, actual.StringValue)
+ }
+ if !anyDateEqual(expected.DateValue, actual.DateValue) {
+ t.Fatalf("any date mismatch: %#v != %#v", expected.DateValue,
actual.DateValue)
+ }
+ if !anyTimeEqual(expected.TimestampValue, actual.TimestampValue) {
+ t.Fatalf("any timestamp mismatch: %#v != %#v",
expected.TimestampValue, actual.TimestampValue)
+ }
+ if !anyInnerEqual(expected.MessageValue, actual.MessageValue) {
+ t.Fatalf("any message mismatch: %#v != %#v",
expected.MessageValue, actual.MessageValue)
+ }
+ if !reflect.DeepEqual(expected.UnionValue, actual.UnionValue) {
+ t.Fatalf("any union mismatch: %#v != %#v", expected.UnionValue,
actual.UnionValue)
+ }
+ if !anyStringSliceEqual(expected.ListValue, actual.ListValue) {
+ t.Fatalf("any list mismatch: %#v != %#v", expected.ListValue,
actual.ListValue)
+ }
+ if !anyStringMapEqual(expected.MapValue, actual.MapValue) {
+ t.Fatalf("any map mismatch: %#v != %#v", expected.MapValue,
actual.MapValue)
+ }
+}
+
+func anyBoolEqual(expected, actual any) bool {
+ expectedValue, ok := expected.(bool)
+ if !ok {
+ return false
+ }
+ actualValue, ok := actual.(bool)
+ if !ok {
+ return false
+ }
+ return expectedValue == actualValue
+}
+
+func anyStringEqual(expected, actual any) bool {
+ expectedValue, ok := expected.(string)
+ if !ok {
+ return false
+ }
+ actualValue, ok := actual.(string)
+ if !ok {
+ return false
+ }
+ return expectedValue == actualValue
+}
+
+func anyDateEqual(expected, actual any) bool {
+ expectedValue, ok := expected.(fory.Date)
+ if !ok {
+ return false
+ }
+ actualValue, ok := actual.(fory.Date)
+ if !ok {
+ return false
+ }
+ return expectedValue == actualValue
+}
+
+func anyTimeEqual(expected, actual any) bool {
+ expectedValue, ok := expected.(time.Time)
+ if !ok {
+ return false
+ }
+ actualValue, ok := actual.(time.Time)
+ if !ok {
+ return false
+ }
+ return expectedValue.Equal(actualValue)
+}
+
+func anyInnerEqual(expected, actual any) bool {
+ expectedValue, ok := normalizeAnyInner(expected)
+ if !ok {
+ return false
+ }
+ actualValue, ok := normalizeAnyInner(actual)
+ if !ok {
+ return false
+ }
+ return expectedValue == actualValue
+}
+
+func normalizeAnyInner(value any) (string, bool) {
+ switch typed := value.(type) {
+ case *anyexample.AnyInner:
+ if typed == nil {
+ return "", true
+ }
+ return typed.Name, true
+ case anyexample.AnyInner:
+ return typed.Name, true
+ default:
+ return "", false
+ }
+}
+
+func anyStringSliceEqual(expected, actual any) bool {
+ expectedValue, ok := normalizeStringSlice(expected)
+ if !ok {
+ return false
+ }
+ actualValue, ok := normalizeStringSlice(actual)
+ if !ok {
+ return false
+ }
+ return reflect.DeepEqual(expectedValue, actualValue)
+}
+
+func normalizeStringSlice(value any) ([]string, bool) {
+ switch typed := value.(type) {
+ case []string:
+ return typed, true
+ case []any:
+ out := make([]string, 0, len(typed))
+ for _, item := range typed {
+ str, ok := item.(string)
+ if !ok {
+ return nil, false
+ }
+ out = append(out, str)
+ }
+ return out, true
+ default:
+ return nil, false
+ }
+}
+
+func anyStringMapEqual(expected, actual any) bool {
+ expectedValue, ok := normalizeStringMap(expected)
+ if !ok {
+ return false
+ }
+ actualValue, ok := normalizeStringMap(actual)
+ if !ok {
+ return false
+ }
+ return reflect.DeepEqual(expectedValue, actualValue)
+}
+
+func normalizeStringMap(value any) (map[string]string, bool) {
+ switch typed := value.(type) {
+ case map[string]string:
+ return typed, true
+ case map[any]any:
+ out := make(map[string]string, len(typed))
+ for k, v := range typed {
+ key, ok := k.(string)
+ if !ok {
+ return nil, false
+ }
+ val, ok := v.(string)
+ if !ok {
+ return nil, false
+ }
+ out[key] = val
+ }
+ return out, true
+ default:
+ return nil, false
+ }
+}
+
func runLocalRoundTrip(t *testing.T, f *fory.Fory, book AddressBook) {
data, err := f.Serialize(&book)
if err != nil {
diff --git a/integration_tests/idl_tests/rust/src/lib.rs
b/integration_tests/idl_tests/idl/any_example.fdl
similarity index 66%
copy from integration_tests/idl_tests/rust/src/lib.rs
copy to integration_tests/idl_tests/idl/any_example.fdl
index 7ad67bff6..efb982b7a 100644
--- a/integration_tests/idl_tests/rust/src/lib.rs
+++ b/integration_tests/idl_tests/idl/any_example.fdl
@@ -15,9 +15,25 @@
// specific language governing permissions and limitations
// under the License.
-pub mod addressbook;
-pub mod complex_fbs;
-pub mod monster;
-pub mod optional_types;
-pub mod graph;
-pub mod tree;
+package any_example;
+
+message AnyInner [id=300] {
+ string name = 1;
+}
+
+union AnyUnion [id=301] {
+ string text = 1;
+ bool flag = 2;
+ AnyInner inner = 3;
+}
+
+message AnyHolder [id=302] {
+ any bool_value = 1;
+ any string_value = 2;
+ any date_value = 3;
+ any timestamp_value = 4;
+ any message_value = 5;
+ any union_value = 6;
+ any list_value = 7;
+ any map_value = 8;
+}
diff --git a/integration_tests/idl_tests/idl/any_example.proto
b/integration_tests/idl_tests/idl/any_example.proto
new file mode 100644
index 000000000..7b8c44d70
--- /dev/null
+++ b/integration_tests/idl_tests/idl/any_example.proto
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+syntax = "proto3";
+
+package any_example_pb;
+
+import "google/protobuf/any.proto";
+
+message AnyInner {
+ option (fory).id = 300;
+ string name = 1;
+}
+
+message AnyUnion {
+ option (fory).id = 301;
+ oneof kind {
+ string text = 1;
+ bool flag = 2;
+ AnyInner inner = 3;
+ }
+}
+
+message AnyHolder {
+ option (fory).id = 302;
+ google.protobuf.Any bool_value = 1;
+ google.protobuf.Any string_value = 2;
+ google.protobuf.Any date_value = 3;
+ google.protobuf.Any timestamp_value = 4;
+ google.protobuf.Any message_value = 5;
+ google.protobuf.Any union_value = 6;
+ google.protobuf.Any list_value = 7;
+ google.protobuf.Any map_value = 8;
+}
diff --git
a/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
b/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
index 62cd1991c..d5cc3e93e 100644
---
a/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
+++
b/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
@@ -28,6 +28,10 @@ import addressbook.Person;
import addressbook.Person.PhoneNumber;
import addressbook.Person.PhoneType;
import addressbook.PrimitiveTypes;
+import any_example.AnyExampleForyRegistration;
+import any_example.AnyHolder;
+import any_example.AnyInner;
+import any_example.AnyUnion;
import complex_fbs.ComplexFbsForyRegistration;
import complex_fbs.Container;
import complex_fbs.Metric;
@@ -158,6 +162,19 @@ public class IdlRoundTripTest {
}
}
+ @Test
+ public void testAnyRoundTrip() {
+ Fory fory = Fory.builder().withLanguage(Language.XLANG).build();
+ AnyExampleForyRegistration.register(fory);
+
+ AnyHolder holder = buildAnyHolder();
+ byte[] bytes = fory.serialize(holder);
+ Object decoded = fory.deserialize(bytes);
+
+ Assert.assertTrue(decoded instanceof AnyHolder);
+ Assert.assertEquals(decoded, holder);
+ }
+
@Test
public void testTreeRoundTrip() throws Exception {
Fory fory =
Fory.builder().withLanguage(Language.XLANG).withRefTracking(true).build();
@@ -487,6 +504,22 @@ public class IdlRoundTripTest {
return holder;
}
+ private AnyHolder buildAnyHolder() {
+ AnyInner inner = new AnyInner();
+ inner.setName("inner");
+
+ AnyHolder holder = new AnyHolder();
+ holder.setBoolValue(Boolean.TRUE);
+ holder.setStringValue("hello");
+ holder.setDateValue(LocalDate.of(2024, 1, 2));
+ holder.setTimestampValue(Instant.ofEpochSecond(1704164645L));
+ holder.setMessageValue(inner);
+ holder.setUnionValue(AnyUnion.ofText("union"));
+ holder.setListValue(Arrays.asList("alpha", "beta"));
+ holder.setMapValue(new HashMap<>(Map.of("k1", "v1", "k2", "v2")));
+ return holder;
+ }
+
private TreeNode buildTree() {
TreeNode childA = new TreeNode();
childA.setId("child-a");
diff --git a/integration_tests/idl_tests/python/src/idl_tests/roundtrip.py
b/integration_tests/idl_tests/python/src/idl_tests/roundtrip.py
index 96e76dfbc..e71d8a7e6 100644
--- a/integration_tests/idl_tests/python/src/idl_tests/roundtrip.py
+++ b/integration_tests/idl_tests/python/src/idl_tests/roundtrip.py
@@ -22,6 +22,7 @@ import os
from pathlib import Path
import addressbook
+import any_example
import complex_fbs
import monster
import optional_types
@@ -139,6 +140,28 @@ def build_optional_holder() ->
"optional_types.OptionalHolder":
return optional_types.OptionalHolder(all_types=all_types,
choice=union_value)
+def build_any_holder() -> "any_example.AnyHolder":
+ inner = any_example.AnyInner(name="inner")
+ union_value = any_example.AnyUnion.text("union")
+ return any_example.AnyHolder(
+ bool_value=True,
+ string_value="hello",
+ date_value=datetime.date(2024, 1, 2),
+ timestamp_value=datetime.datetime.fromtimestamp(1704164645),
+ message_value=inner,
+ union_value=union_value,
+ list_value=["alpha", "beta"],
+ map_value={"k1": "v1", "k2": "v2"},
+ )
+
+
+def local_roundtrip_any(fory: pyfory.Fory, holder: "any_example.AnyHolder") ->
None:
+ data = fory.serialize(holder)
+ decoded = fory.deserialize(data)
+ assert isinstance(decoded, any_example.AnyHolder)
+ assert decoded == holder
+
+
def build_monster() -> "monster.Monster":
pos = monster.Vec3(x=1.0, y=2.0, z=3.0)
return monster.Monster(
@@ -435,6 +458,7 @@ def main() -> int:
monster.register_monster_types(fory)
complex_fbs.register_complex_fbs_types(fory)
optional_types.register_optional_types_types(fory)
+ any_example.register_any_example_types(fory)
book = build_address_book()
local_roundtrip(fory, book)
@@ -456,6 +480,9 @@ def main() -> int:
local_roundtrip_optional_types(fory, holder)
file_roundtrip_optional_types(fory, holder)
+ any_holder = build_any_holder()
+ local_roundtrip_any(fory, any_holder)
+
ref_fory = pyfory.Fory(xlang=True, ref=True)
tree.register_tree_types(ref_fory)
graph.register_graph_types(ref_fory)
diff --git a/integration_tests/idl_tests/rust/src/lib.rs
b/integration_tests/idl_tests/rust/src/lib.rs
index 7ad67bff6..9012b5fbb 100644
--- a/integration_tests/idl_tests/rust/src/lib.rs
+++ b/integration_tests/idl_tests/rust/src/lib.rs
@@ -16,6 +16,7 @@
// under the License.
pub mod addressbook;
+pub mod any_example;
pub mod complex_fbs;
pub mod monster;
pub mod optional_types;
diff --git a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
index bf6b984e1..00a679d02 100644
--- a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
+++ b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
@@ -29,6 +29,7 @@ use idl_tests::addressbook::{
use idl_tests::complex_fbs::{self, Container, Note, Payload, ScalarPack,
Status};
use idl_tests::monster::{self, Color, Monster, Vec3};
use idl_tests::optional_types::{self, AllOptionalTypes, OptionalHolder,
OptionalUnion};
+use idl_tests::any_example::{self, AnyHolder, AnyInner, AnyUnion};
use idl_tests::{graph, tree};
fn build_address_book() -> AddressBook {
@@ -185,6 +186,85 @@ fn build_optional_holder() -> OptionalHolder {
}
}
+fn build_any_holder() -> AnyHolder {
+ AnyHolder {
+ bool_value: Box::new(true),
+ string_value: Box::new("hello".to_string()),
+ date_value: Box::new(NaiveDate::from_ymd_opt(2024, 1, 2).unwrap()),
+ timestamp_value: Box::new(
+ NaiveDate::from_ymd_opt(2024, 1, 2)
+ .unwrap()
+ .and_hms_opt(3, 4, 5)
+ .expect("timestamp"),
+ ),
+ message_value: Box::new(AnyInner {
+ name: "inner".to_string(),
+ }),
+ union_value: Box::new(AnyUnion::Text("union".to_string())),
+ list_value: Box::new("list-placeholder".to_string()),
+ map_value: Box::new("map-placeholder".to_string()),
+ }
+}
+
+fn build_any_holder_with_collections() -> AnyHolder {
+ AnyHolder {
+ bool_value: Box::new(true),
+ string_value: Box::new("hello".to_string()),
+ date_value: Box::new(NaiveDate::from_ymd_opt(2024, 1, 2).unwrap()),
+ timestamp_value: Box::new(
+ NaiveDate::from_ymd_opt(2024, 1, 2)
+ .unwrap()
+ .and_hms_opt(3, 4, 5)
+ .expect("timestamp"),
+ ),
+ message_value: Box::new(AnyInner {
+ name: "inner".to_string(),
+ }),
+ union_value: Box::new(AnyUnion::Text("union".to_string())),
+ list_value: Box::new(vec!["alpha".to_string(), "beta".to_string()]),
+ map_value: Box::new(HashMap::from([
+ ("k1".to_string(), "v1".to_string()),
+ ("k2".to_string(), "v2".to_string()),
+ ])),
+ }
+}
+
+fn assert_any_holder(holder: &AnyHolder) {
+ let bool_value = holder.bool_value.downcast_ref::<bool>().expect("bool
any");
+ assert_eq!(*bool_value, true);
+ let string_value = holder
+ .string_value
+ .downcast_ref::<String>()
+ .expect("string any");
+ assert_eq!(string_value, "hello");
+ let date_value = holder
+ .date_value
+ .downcast_ref::<NaiveDate>()
+ .expect("date any");
+ assert_eq!(*date_value, NaiveDate::from_ymd_opt(2024, 1, 2).unwrap());
+ let timestamp_value = holder
+ .timestamp_value
+ .downcast_ref::<chrono::NaiveDateTime>()
+ .expect("timestamp any");
+ assert_eq!(
+ *timestamp_value,
+ NaiveDate::from_ymd_opt(2024, 1, 2)
+ .unwrap()
+ .and_hms_opt(3, 4, 5)
+ .expect("timestamp")
+ );
+ let message_value = holder
+ .message_value
+ .downcast_ref::<AnyInner>()
+ .expect("message any");
+ assert_eq!(message_value.name, "inner");
+ let union_value = holder
+ .union_value
+ .downcast_ref::<AnyUnion>()
+ .expect("union any");
+ assert_eq!(*union_value, AnyUnion::Text("union".to_string()));
+}
+
fn build_tree() -> tree::TreeNode {
let mut child_a = Arc::new(tree::TreeNode {
id: "child-a".to_string(),
@@ -302,6 +382,7 @@ fn test_address_book_roundtrip() {
monster::register_types(&mut fory).expect("register monster types");
complex_fbs::register_types(&mut fory).expect("register flatbuffers
types");
optional_types::register_types(&mut fory).expect("register optional
types");
+ any_example::register_types(&mut fory).expect("register any example
types");
let book = build_address_book();
let bytes = fory.serialize(&book).expect("serialize");
@@ -389,6 +470,18 @@ fn test_address_book_roundtrip() {
fs::write(data_file, encoded).expect("write data file");
}
+ let any_holder = build_any_holder();
+ let bytes = fory.serialize(&any_holder).expect("serialize any");
+ let roundtrip: AnyHolder = fory.deserialize(&bytes).expect("deserialize
any");
+ assert_any_holder(&roundtrip);
+
+ let any_holder_collections = build_any_holder_with_collections();
+ let bytes = fory
+ .serialize(&any_holder_collections)
+ .expect("serialize any collections");
+ let result: Result<AnyHolder, _> = fory.deserialize(&bytes);
+ assert!(result.is_err());
+
let mut ref_fory = Fory::default().xlang(true).track_ref(true);
tree::register_types(&mut ref_fory).expect("register tree types");
graph::register_types(&mut ref_fory).expect("register graph types");
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 2ae96d5ad..5eee7977d 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -427,6 +427,8 @@ public abstract class TypeResolver {
case Types.ENUM:
case Types.STRUCT:
case Types.EXT:
+ case Types.UNION:
+ case Types.TYPED_UNION:
classInfo = requireUserTypeInfoByTypeId(header);
break;
case Types.LIST:
@@ -475,6 +477,8 @@ public abstract class TypeResolver {
case Types.ENUM:
case Types.STRUCT:
case Types.EXT:
+ case Types.UNION:
+ case Types.TYPED_UNION:
classInfo = requireUserTypeInfoByTypeId(header);
break;
case Types.LIST:
@@ -529,6 +533,8 @@ public abstract class TypeResolver {
case Types.ENUM:
case Types.STRUCT:
case Types.EXT:
+ case Types.UNION:
+ case Types.TYPED_UNION:
classInfo = requireUserTypeInfoByTypeId(header);
break;
case Types.LIST:
@@ -583,6 +589,8 @@ public abstract class TypeResolver {
case Types.ENUM:
case Types.STRUCT:
case Types.EXT:
+ case Types.UNION:
+ case Types.TYPED_UNION:
classInfo = requireUserTypeInfoByTypeId(header);
break;
case Types.LIST:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]