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 2877be2b5 feat(c++): support variant-based union type serialization 
for c++ (#3032)
2877be2b5 is described below

commit 2877be2b5a93be3227aa6e02932cc770610597fd
Author: Shawn Yang <[email protected]>
AuthorDate: Tue Dec 9 21:35:20 2025 +0800

    feat(c++): support variant-based union type serialization for c++ (#3032)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    Closes #3025
    #3027
    
    ## 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
---
 cpp/fory/serialization/BUILD                      |  11 +
 cpp/fory/serialization/CMakeLists.txt             |   7 +
 cpp/fory/serialization/fory.h                     |   1 +
 cpp/fory/serialization/serializer_traits.h        |  15 ++
 cpp/fory/serialization/skip.cc                    |  32 +++
 cpp/fory/serialization/skip.h                     |   3 +
 cpp/fory/serialization/variant_serializer.h       | 256 ++++++++++++++++++++++
 cpp/fory/serialization/variant_serializer_test.cc | 199 +++++++++++++++++
 cpp/fory/type/type.h                              |   4 +
 docs/specification/xlang_serialization_spec.md    |   8 +-
 docs/specification/xlang_type_mapping.md          |  80 +++----
 11 files changed, 574 insertions(+), 42 deletions(-)

diff --git a/cpp/fory/serialization/BUILD b/cpp/fory/serialization/BUILD
index 534613da0..fe51077c3 100644
--- a/cpp/fory/serialization/BUILD
+++ b/cpp/fory/serialization/BUILD
@@ -27,6 +27,7 @@ cc_library(
         "type_info.h",
         "type_resolver.h",
         "unsigned_serializer.h",
+        "variant_serializer.h",
     ],
     strip_include_prefix = "/cpp",
     deps = [
@@ -99,6 +100,16 @@ cc_test(
     ],
 )
 
+cc_test(
+    name = "variant_serializer_test",
+    srcs = ["variant_serializer_test.cc"],
+    deps = [
+        ":fory_serialization",
+        "@googletest//:gtest",
+        "@googletest//:gtest_main",
+    ],
+)
+
 cc_test(
     name = "tuple_serializer_test",
     srcs = ["tuple_serializer_test.cc"],
diff --git a/cpp/fory/serialization/CMakeLists.txt 
b/cpp/fory/serialization/CMakeLists.txt
index 0a752cc17..509b2f6ee 100644
--- a/cpp/fory/serialization/CMakeLists.txt
+++ b/cpp/fory/serialization/CMakeLists.txt
@@ -37,8 +37,11 @@ set(FORY_SERIALIZATION_HEADERS
     smart_ptr_serializers.h
     struct_serializer.h
     temporal_serializers.h
+    tuple_serializer.h
     type_info.h
     type_resolver.h
+    unsigned_serializer.h
+    variant_serializer.h
 )
 
 add_library(fory_serialization ${FORY_SERIALIZATION_SOURCES})
@@ -85,6 +88,10 @@ if(FORY_BUILD_TESTS)
     add_executable(fory_serialization_map_test map_serializer_test.cc)
     target_link_libraries(fory_serialization_map_test fory_serialization 
GTest::gtest GTest::gtest_main)
     gtest_discover_tests(fory_serialization_map_test)
+
+    add_executable(fory_serialization_variant_test variant_serializer_test.cc)
+    target_link_libraries(fory_serialization_variant_test fory_serialization 
GTest::gtest GTest::gtest_main)
+    gtest_discover_tests(fory_serialization_variant_test)
 endif()
 
 # xlang test binary
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index ba492b014..7b331a9cf 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -30,6 +30,7 @@
 #include "fory/serialization/temporal_serializers.h"
 #include "fory/serialization/tuple_serializer.h"
 #include "fory/serialization/type_resolver.h"
+#include "fory/serialization/variant_serializer.h"
 #include "fory/util/buffer.h"
 #include "fory/util/error.h"
 #include "fory/util/pool.h"
diff --git a/cpp/fory/serialization/serializer_traits.h 
b/cpp/fory/serialization/serializer_traits.h
index ccee4e1e1..b1846b9fe 100644
--- a/cpp/fory/serialization/serializer_traits.h
+++ b/cpp/fory/serialization/serializer_traits.h
@@ -34,6 +34,7 @@
 #include <type_traits>
 #include <unordered_map>
 #include <unordered_set>
+#include <variant>
 #include <vector>
 
 namespace fory {
@@ -93,6 +94,14 @@ struct is_tuple<std::tuple<Ts...>> : std::true_type {};
 
 template <typename T> inline constexpr bool is_tuple_v = is_tuple<T>::value;
 
+/// Detect std::variant
+template <typename T> struct is_variant : std::false_type {};
+
+template <typename... Ts>
+struct is_variant<std::variant<Ts...>> : std::true_type {};
+
+template <typename T> inline constexpr bool is_variant_v = 
is_variant<T>::value;
+
 /// Detect std::weak_ptr
 template <typename T> struct is_weak_ptr : std::false_type {};
 
@@ -507,6 +516,12 @@ template <typename... Ts> struct 
TypeIndex<std::tuple<Ts...>> {
       fnv1a_64_combine(fnv1a_64("std::tuple"), (type_index<Ts>() ^ ...));
 };
 
+// variant<Ts...>
+template <typename... Ts> struct TypeIndex<std::variant<Ts...>> {
+  static constexpr uint64_t value =
+      fnv1a_64_combine(fnv1a_64("std::variant"), (type_index<Ts>() ^ ...));
+};
+
 // ============================================================================
 // type_index<T>() function - main entry point
 // ============================================================================
diff --git a/cpp/fory/serialization/skip.cc b/cpp/fory/serialization/skip.cc
index b10f1441c..41c3e1d98 100644
--- a/cpp/fory/serialization/skip.cc
+++ b/cpp/fory/serialization/skip.cc
@@ -393,6 +393,30 @@ void skip_unknown(ReadContext &ctx) {
   }
 }
 
+void skip_union(ReadContext &ctx) {
+  // Read the variant index
+  (void)ctx.read_varuint32(ctx.error());
+  if (FORY_PREDICT_FALSE(ctx.has_error())) {
+    return;
+  }
+  // Read and skip the alternative's type info
+  const TypeInfo *type_info = ctx.read_any_typeinfo(ctx.error());
+  if (FORY_PREDICT_FALSE(ctx.has_error())) {
+    return;
+  }
+  if (!type_info) {
+    ctx.set_error(
+        Error::type_error("TypeInfo not found for UNION alternative skip"));
+    return;
+  }
+
+  // Skip the alternative's value
+  FieldType alt_field_type;
+  alt_field_type.type_id = type_info->type_id;
+  alt_field_type.nullable = false;
+  skip_field_value(ctx, alt_field_type, false);
+}
+
 void skip_field_value(ReadContext &ctx, const FieldType &field_type,
                       bool read_ref_flag) {
   // Read ref flag if needed
@@ -568,6 +592,14 @@ void skip_field_value(ReadContext &ctx, const FieldType 
&field_type,
     skip_unknown(ctx);
     return;
 
+  case TypeId::UNION:
+    skip_union(ctx);
+    return;
+
+  case TypeId::NONE:
+    // NONE (not-applicable/monostate) has no data to skip
+    return;
+
   default:
     ctx.set_error(
         Error::type_error("Unknown field type to skip: " +
diff --git a/cpp/fory/serialization/skip.h b/cpp/fory/serialization/skip.h
index f5d9c1bb8..64e730688 100644
--- a/cpp/fory/serialization/skip.h
+++ b/cpp/fory/serialization/skip.h
@@ -52,6 +52,9 @@ void skip_set(ReadContext &ctx, const FieldType &field_type);
 /// Skip a map value
 void skip_map(ReadContext &ctx, const FieldType &field_type);
 
+/// Skip a union (variant) value
+void skip_union(ReadContext &ctx);
+
 /// Skip a struct value
 void skip_struct(ReadContext &ctx, const FieldType &field_type);
 
diff --git a/cpp/fory/serialization/variant_serializer.h 
b/cpp/fory/serialization/variant_serializer.h
new file mode 100644
index 000000000..b7c551f86
--- /dev/null
+++ b/cpp/fory/serialization/variant_serializer.h
@@ -0,0 +1,256 @@
+/*
+ * 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/type/type.h"
+#include "fory/util/error.h"
+#include <cstdint>
+#include <variant>
+
+namespace fory {
+namespace serialization {
+
+// ============================================================================
+// std::monostate Serializer
+// ============================================================================
+
+/// Serializer for std::monostate (empty variant alternative)
+///
+/// std::monostate represents an empty state in variants. It has no data
+/// to serialize, so serialization is a no-op.
+template <> struct Serializer<std::monostate> {
+  static constexpr TypeId type_id =
+      TypeId::NONE; // Use NONE for empty/not-applicable type
+
+  static inline void write_type_info(WriteContext &ctx) {
+    ctx.write_varuint32(static_cast<uint32_t>(type_id));
+  }
+
+  static inline void read_type_info(ReadContext &ctx) {
+    // Read and validate type_id for monostate
+    ctx.read_varuint32(ctx.error());
+    // Accept any type since monostate is just a marker with no data
+  }
+
+  static inline void write(const std::monostate &, WriteContext &ctx,
+                           bool write_ref, bool write_type,
+                           bool has_generics = false) {
+    (void)has_generics;
+    write_not_null_ref_flag(ctx, write_ref);
+    if (write_type) {
+      write_type_info(ctx);
+    }
+    // No data to write for monostate
+  }
+
+  static inline void write_data(const std::monostate &, WriteContext &ctx) {
+    // No data to write for monostate
+  }
+
+  static inline void write_data_generic(const std::monostate &,
+                                        WriteContext &ctx, bool has_generics) {
+    // No data to write for monostate
+  }
+
+  static inline std::monostate read(ReadContext &ctx, bool read_ref,
+                                    bool read_type) {
+    bool has_value = consume_ref_flag(ctx, read_ref);
+    if (!has_value) {
+      return std::monostate{};
+    }
+    if (read_type) {
+      read_type_info(ctx);
+    }
+    return std::monostate{};
+  }
+
+  static inline std::monostate read_data(ReadContext &ctx) {
+    // No data to read for monostate
+    return std::monostate{};
+  }
+
+  static inline std::monostate read_with_type_info(ReadContext &ctx,
+                                                   bool read_ref,
+                                                   const TypeInfo &type_info) {
+    return read(ctx, read_ref, false);
+  }
+};
+
+// ============================================================================
+// std::variant Serialization Helpers
+// ============================================================================
+
+/// Helper to write variant data by dispatching to the correct alternative's
+/// serializer based on the active index.
+template <typename Variant, size_t Index = 0>
+inline void write_variant_by_index(const Variant &variant, WriteContext &ctx,
+                                   size_t active_index, bool write_type) {
+  if constexpr (Index < std::variant_size_v<Variant>) {
+    if (Index == active_index) {
+      using AlternativeType = std::variant_alternative_t<Index, Variant>;
+      const auto &value = std::get<Index>(variant);
+      Serializer<AlternativeType>::write(value, ctx, false, write_type);
+    } else {
+      write_variant_by_index<Variant, Index + 1>(variant, ctx, active_index,
+                                                 write_type);
+    }
+  } else {
+    // Should never reach here if active_index is valid
+    ctx.set_error(Error::invalid_data("Invalid variant index: " +
+                                      std::to_string(active_index)));
+  }
+}
+
+/// Helper to read variant data by dispatching to the correct alternative's
+/// serializer based on the stored index.
+template <typename Variant, size_t Index = 0>
+inline Variant read_variant_by_index(ReadContext &ctx, size_t stored_index,
+                                     bool read_type) {
+  if constexpr (Index < std::variant_size_v<Variant>) {
+    if (Index == stored_index) {
+      using AlternativeType = std::variant_alternative_t<Index, Variant>;
+      AlternativeType value =
+          Serializer<AlternativeType>::read(ctx, false, read_type);
+      if (FORY_PREDICT_FALSE(ctx.has_error())) {
+        // Return a default-constructed variant with the first alternative
+        return Variant{};
+      }
+      return Variant{std::in_place_index<Index>, std::move(value)};
+    } else {
+      return read_variant_by_index<Variant, Index + 1>(ctx, stored_index,
+                                                       read_type);
+    }
+  } else {
+    // Invalid index - set error and return default
+    ctx.set_error(Error::invalid_data("Invalid variant index during read: " +
+                                      std::to_string(stored_index)));
+    return Variant{};
+  }
+}
+
+// ============================================================================
+// std::variant Serializer
+// ============================================================================
+
+/// Serializer for std::variant<Ts...>
+///
+/// Serializes variant by storing:
+/// 1. The active alternative index (as varuint32)
+/// 2. The value of the active alternative
+///
+/// This allows the deserializer to determine which alternative to construct
+/// and forward to the appropriate serializer.
+///
+/// Example:
+/// ```cpp
+/// std::variant<int, std::string, double> v = "hello";
+/// // Serializes: index=1, then string data
+/// ```
+template <typename... Ts> struct Serializer<std::variant<Ts...>> {
+  // Use UNION type_id since variant is a sum type (union) that can hold
+  // one of several alternative types. The actual alternative type is 
determined
+  // by the stored index.
+  static constexpr TypeId type_id = TypeId::UNION;
+
+  using VariantType = std::variant<Ts...>;
+
+  static inline void write_type_info(WriteContext &ctx) {
+    // Write UNION type_id to indicate variant/sum type
+    ctx.write_varuint32(static_cast<uint32_t>(type_id));
+  }
+
+  static inline void read_type_info(ReadContext &ctx) {
+    // Read and validate UNION type_id
+    uint32_t type_id_read = ctx.read_varuint32(ctx.error());
+    if (FORY_PREDICT_FALSE(ctx.has_error())) {
+      return;
+    }
+    if (!type_id_matches(type_id_read, static_cast<uint32_t>(type_id))) {
+      ctx.set_error(
+          Error::type_mismatch(type_id_read, static_cast<uint32_t>(type_id)));
+    }
+  }
+
+  static inline void write(const VariantType &variant, WriteContext &ctx,
+                           bool write_ref, bool write_type,
+                           bool has_generics = false) {
+    (void)has_generics;
+    write_not_null_ref_flag(ctx, write_ref);
+    if (write_type) {
+      write_type_info(ctx);
+    }
+    write_data(variant, ctx);
+  }
+
+  static inline void write_data(const VariantType &variant, WriteContext &ctx) 
{
+    // Write the active variant index
+    size_t active_index = variant.index();
+    ctx.write_varuint32(static_cast<uint32_t>(active_index));
+
+    // Dispatch to the appropriate alternative's serializer
+    // In xlang/compatible mode, write type info for the alternative
+    bool write_alt_type = ctx.is_xlang() || ctx.is_compatible();
+    write_variant_by_index(variant, ctx, active_index, write_alt_type);
+  }
+
+  static inline void write_data_generic(const VariantType &variant,
+                                        WriteContext &ctx, bool has_generics) {
+    write_data(variant, ctx);
+  }
+
+  static inline VariantType read(ReadContext &ctx, bool read_ref,
+                                 bool read_type) {
+    bool has_value = consume_ref_flag(ctx, read_ref);
+    if (!has_value) {
+      return VariantType{};
+    }
+    if (read_type) {
+      read_type_info(ctx);
+    }
+    return read_data(ctx);
+  }
+
+  static inline VariantType read_data(ReadContext &ctx) {
+    // Read the stored variant index
+    uint32_t stored_index = ctx.read_varuint32(ctx.error());
+    // Validate index is within bounds
+    if (FORY_PREDICT_FALSE(stored_index >= std::variant_size_v<VariantType>)) {
+      ctx.set_error(Error::invalid_data(
+          "Variant index out of bounds: " + std::to_string(stored_index) +
+          " (max: " + std::to_string(std::variant_size_v<VariantType> - 1) +
+          ")"));
+      return VariantType{};
+    }
+
+    // Dispatch to the appropriate alternative's serializer
+    // In xlang/compatible mode, read type info for the alternative
+    bool read_alt_type = ctx.is_xlang() || ctx.is_compatible();
+    return read_variant_by_index<VariantType>(ctx, stored_index, 
read_alt_type);
+  }
+
+  static inline VariantType read_with_type_info(ReadContext &ctx, bool 
read_ref,
+                                                const TypeInfo &type_info) {
+    return read(ctx, read_ref, false);
+  }
+};
+
+} // namespace serialization
+} // namespace fory
diff --git a/cpp/fory/serialization/variant_serializer_test.cc 
b/cpp/fory/serialization/variant_serializer_test.cc
new file mode 100644
index 000000000..1d0a408a6
--- /dev/null
+++ b/cpp/fory/serialization/variant_serializer_test.cc
@@ -0,0 +1,199 @@
+/*
+ * 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/fory.h"
+#include "gtest/gtest.h"
+#include <string>
+#include <variant>
+#include <vector>
+
+using namespace fory::serialization;
+
+// Structs for schema evolution testing
+namespace {
+using VariantType = std::variant<int, std::string>;
+
+// Old schema: struct with two variant fields
+struct OldStruct {
+  int id;
+  VariantType field1;
+  VariantType field2;
+};
+FORY_STRUCT(OldStruct, id, field1, field2);
+
+// New schema: struct with only one variant field
+struct NewStruct {
+  int id;
+  VariantType field1;
+};
+FORY_STRUCT(NewStruct, id, field1);
+} // namespace
+
+// Helper to create a Fory instance
+Fory create_fory() {
+  return Fory::builder().xlang(false).track_ref(true).build();
+}
+
+Fory create_xlang_fory() {
+  return Fory::builder().xlang(true).track_ref(true).build();
+}
+
+// Test basic variant serialization with primitive types
+TEST(VariantSerializerTest, BasicVariant) {
+  Fory fory = create_fory();
+
+  // Test with int active
+  {
+    std::variant<int, std::string, double> v1 = 42;
+    auto result1 = fory.serialize(v1);
+    ASSERT_TRUE(result1.has_value());
+
+    auto read_result1 =
+        fory.deserialize<std::variant<int, std::string, double>>(
+            result1.value());
+    ASSERT_TRUE(read_result1.has_value()) << read_result1.error().message();
+    ASSERT_EQ(read_result1.value().index(), 0);
+    ASSERT_EQ(std::get<0>(read_result1.value()), 42);
+  }
+
+  // Test with string active
+  {
+    std::variant<int, std::string, double> v2 = std::string("hello");
+    auto result2 = fory.serialize(v2);
+    ASSERT_TRUE(result2.has_value());
+
+    auto read_result2 =
+        fory.deserialize<std::variant<int, std::string, double>>(
+            result2.value());
+    ASSERT_TRUE(read_result2.has_value());
+    ASSERT_EQ(read_result2.value().index(), 1);
+    ASSERT_EQ(std::get<1>(read_result2.value()), "hello");
+  }
+
+  // Test with double active
+  {
+    std::variant<int, std::string, double> v3 = 3.14;
+    auto result3 = fory.serialize(v3);
+    ASSERT_TRUE(result3.has_value());
+
+    auto read_result3 =
+        fory.deserialize<std::variant<int, std::string, double>>(
+            result3.value());
+    ASSERT_TRUE(read_result3.has_value());
+    ASSERT_EQ(read_result3.value().index(), 2);
+    ASSERT_DOUBLE_EQ(std::get<2>(read_result3.value()), 3.14);
+  }
+}
+
+// Test variant with complex types
+TEST(VariantSerializerTest, ComplexVariant) {
+  Fory fory = create_fory();
+
+  std::variant<std::vector<int>, std::string> v1 =
+      std::vector<int>{1, 2, 3, 4, 5};
+  auto result = fory.serialize(v1);
+  ASSERT_TRUE(result.has_value());
+
+  auto read_result =
+      fory.deserialize<std::variant<std::vector<int>, std::string>>(
+          result.value());
+  ASSERT_TRUE(read_result.has_value());
+  ASSERT_EQ(read_result.value().index(), 0);
+
+  auto vec = std::get<0>(read_result.value());
+  ASSERT_EQ(vec.size(), 5);
+  ASSERT_EQ(vec[0], 1);
+  ASSERT_EQ(vec[4], 5);
+}
+
+// Test variant in xlang mode
+TEST(VariantSerializerTest, XlangMode) {
+  Fory fory = create_xlang_fory();
+
+  std::variant<int, std::string, bool> v = std::string("xlang");
+  auto result = fory.serialize(v);
+  ASSERT_TRUE(result.has_value());
+
+  auto read_result =
+      fory.deserialize<std::variant<int, std::string, bool>>(result.value());
+  ASSERT_TRUE(read_result.has_value());
+  ASSERT_EQ(read_result.value().index(), 1);
+  ASSERT_EQ(std::get<1>(read_result.value()), "xlang");
+}
+
+// Test variant with monostate (empty variant)
+TEST(VariantSerializerTest, MonostateVariant) {
+  Fory fory = create_fory();
+
+  std::variant<std::monostate, int, std::string> v1;
+  auto result = fory.serialize(v1);
+  ASSERT_TRUE(result.has_value());
+
+  auto read_result =
+      fory.deserialize<std::variant<std::monostate, int, std::string>>(
+          result.value());
+  ASSERT_TRUE(read_result.has_value());
+  ASSERT_EQ(read_result.value().index(), 0);
+  ASSERT_TRUE(std::holds_alternative<std::monostate>(read_result.value()));
+}
+
+// Test nested variants
+TEST(VariantSerializerTest, NestedVariant) {
+  Fory fory = create_fory();
+
+  using InnerVariant = std::variant<int, std::string>;
+  using OuterVariant = std::variant<InnerVariant, double>;
+
+  OuterVariant v = InnerVariant{std::string("nested")};
+  auto result = fory.serialize(v);
+  ASSERT_TRUE(result.has_value());
+
+  auto read_result = fory.deserialize<OuterVariant>(result.value());
+  ASSERT_TRUE(read_result.has_value());
+  ASSERT_EQ(read_result.value().index(), 0);
+
+  auto inner = std::get<0>(read_result.value());
+  ASSERT_EQ(inner.index(), 1);
+  ASSERT_EQ(std::get<1>(inner), "nested");
+}
+
+// Test that variant can be skipped in compatible mode for schema evolution
+TEST(VariantSerializerTest, SkipInCompatibleMode) {
+  constexpr uint32_t type_id = 1;
+
+  // Create first Fory instance and register OldStruct
+  auto fory1 = Fory::builder().compatible(true).xlang(true).build();
+  fory1.register_struct<OldStruct>(type_id);
+
+  // Serialize with old schema (two variants)
+  OldStruct old_data{42, std::string("hello"), 123};
+  auto result = fory1.serialize(old_data);
+  ASSERT_TRUE(result.has_value()) << result.error().message();
+
+  // Create second Fory instance and register NewStruct with same type_id
+  auto fory2 = Fory::builder().compatible(true).xlang(true).build();
+  fory2.register_struct<NewStruct>(type_id);
+
+  // Deserialize with new schema (one variant) - should skip field2
+  auto read_result = fory2.deserialize<NewStruct>(result.value());
+  ASSERT_TRUE(read_result.has_value()) << read_result.error().message();
+  ASSERT_EQ(read_result.value().id, 42);
+  ASSERT_EQ(read_result.value().field1.index(), 1);
+  ASSERT_EQ(std::get<1>(read_result.value().field1), "hello");
+}
diff --git a/cpp/fory/type/type.h b/cpp/fory/type/type.h
index d6221fbbb..9501e4fc4 100644
--- a/cpp/fory/type/type.h
+++ b/cpp/fory/type/type.h
@@ -103,6 +103,10 @@ enum class TypeId : int32_t {
   FLOAT32_ARRAY = 36,
   // one-dimensional float64 array.
   FLOAT64_ARRAY = 37,
+  // an union type that can hold different types of values.
+  UNION = 38,
+  // a null value with no data.
+  NONE = 39,
   // Unsigned integer types (native mode only, not supported in xlang mode)
   // an 8-bit unsigned integer.
   U8 = 64,
diff --git a/docs/specification/xlang_serialization_spec.md 
b/docs/specification/xlang_serialization_spec.md
index 3f25b7197..0715c9c4f 100644
--- a/docs/specification/xlang_serialization_spec.md
+++ b/docs/specification/xlang_serialization_spec.md
@@ -60,7 +60,7 @@ This specification defines the Fory xlang binary format. The 
format is dynamic r
 - named_ext: an `ext` type whose type mapping will be encoded as a name.
 - list: a sequence of objects.
 - set: an unordered set of unique elements.
-- map: a map of key-value pairs. Mutable types such as 
`list/map/set/array/tensor` are not allowed as key of map.
+- map: a map of key-value pairs. Mutable types such as `list/map/set/array` 
are not allowed as key of map.
 - duration: an absolute length of time, independent of any calendar/timezone, 
as a count of nanoseconds.
 - timestamp: a point in time, independent of any calendar/timezone, as a count 
of nanoseconds. The count is relative
   to an epoch at UTC midnight on January 1, 1970.
@@ -77,7 +77,8 @@ This specification defines the Fory xlang binary format. The 
format is dynamic r
   - float16_array: one dimensional half_float_16 array.
   - float32_array: one dimensional float32 array.
   - float64_array: one dimensional float64 array.
-- tensor: multidimensional array which every sub-array have same size and type.
+- union: a tagged union type that can hold one of several alternative types. 
The active alternative is identified by an index.
+- none: represents an empty/unit value with no data (e.g., for empty union 
alternatives).
 
 Note:
 
@@ -186,7 +187,8 @@ custom types (struct/ext/enum). User type IDs are in a 
separate namespace and co
 | 35      | FLOAT16_ARRAY           | 1D float16 array                         
           |
 | 36      | FLOAT32_ARRAY           | 1D float32 array                         
           |
 | 37      | FLOAT64_ARRAY           | 1D float64 array                         
           |
-| 38      | TENSOR                  | Multi-dimensional array                  
           |
+| 38      | UNION                   | Tagged union type (one of several 
alternatives)     |
+| 39      | NONE                    | Empty/unit type (no data)                
           |
 
 #### Type ID Encoding for User Types
 
diff --git a/docs/specification/xlang_type_mapping.md 
b/docs/specification/xlang_type_mapping.md
index a3b8a70ab..1661d0b77 100644
--- a/docs/specification/xlang_type_mapping.md
+++ b/docs/specification/xlang_type_mapping.md
@@ -27,45 +27,47 @@ Note:
 
 ## Type Mapping
 
-| Fory Type               | Fory Type ID | Java            | Python            
                | Javascript      | C++                            | Golang     
      | Rust             |
-| ----------------------- | ------------ | --------------- | 
--------------------------------- | --------------- | 
------------------------------ | ---------------- | ---------------- |
-| bool                    | 1            | bool/Boolean    | bool              
                | Boolean         | bool                           | bool       
      | bool             |
-| int8                    | 2            | byte/Byte       | int/pyfory.Int8   
                | Type.int8()     | int8_t                         | int8       
      | i8               |
-| int16                   | 3            | short/Short     | int/pyfory.Int16  
                | Type.int16()    | int16_t                        | int16      
      | i6               |
-| int32                   | 4            | int/Integer     | int/pyfory.Int32  
                | Type.int32()    | int32_t                        | int32      
      | i32              |
-| var_int32               | 5            | int/Integer     | 
int/pyfory.VarInt32               | Type.varint32() | fory::varint32_t          
     | fory.varint32    | fory::varint32   |
-| int64                   | 6            | long/Long       | int/pyfory.Int64  
                | Type.int64()    | int64_t                        | int64      
      | i64              |
-| var_int64               | 7            | long/Long       | 
int/pyfory.VarInt64               | Type.varint64() | fory::varint64_t          
     | fory.varint64    | fory::varint64   |
-| sli_int64               | 8            | long/Long       | 
int/pyfory.SliInt64               | Type.sliint64() | fory::sliint64_t          
     | fory.sliint64    | fory::sliint64   |
-| float16                 | 9            | float/Float     | 
float/pyfory.Float16              | Type.float16()  | fory::float16_t           
     | fory.float16     | fory::f16        |
-| float32                 | 10           | float/Float     | 
float/pyfory.Float32              | Type.float32()  | float                     
     | float32          | f32              |
-| float64                 | 11           | double/Double   | 
float/pyfory.Float64              | Type.float64()  | double                    
     | float64          | f64              |
-| string                  | 12           | String          | str               
                | String          | string                         | string     
      | String/str       |
-| enum                    | 13           | Enum subclasses | enum subclasses   
                | /               | enum                           | /          
      | enum             |
-| named_enum              | 14           | Enum subclasses | enum subclasses   
                | /               | enum                           | /          
      | enum             |
-| struct                  | 15           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| compatible_struct       | 16           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| named_struct            | 17           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| named_compatible_struct | 18           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| ext                     | 19           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| named_ext               | 20           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct           |
-| list                    | 21           | List/Collection | list/tuple        
                | array           | vector                         | slice      
      | Vec              |
-| set                     | 22           | Set             | set               
                | /               | set                            | fory.Set   
      | Set              |
-| map                     | 23           | Map             | dict              
                | Map             | unordered_map                  | map        
      | HashMap          |
-| duration                | 24           | Duration        | timedelta         
                | Number          | duration                       | Duration   
      | Duration         |
-| timestamp               | 25           | Instant         | datetime          
                | Number          | std::chrono::nanoseconds       | Time       
      | DateTime         |
-| local_date              | 26           | Date            | datetime          
                | Number          | std::chrono::nanoseconds       | Time       
      | DateTime         |
-| decimal                 | 27           | BigDecimal      | Decimal           
                | bigint          | /                              | /          
      | /                |
-| binary                  | 28           | byte[]          | bytes             
                | /               | `uint8_t[n]/vector<T>`         | 
`[n]uint8/[]T`   | `Vec<uint8_t>`   |
-| array                   | 29           | array           | np.ndarray        
                | /               | /                              | 
array/slice      | Vec              |
-| bool_array              | 30           | bool[]          | 
ndarray(np.bool\_)                | /               | `bool[n]`                 
     | `[n]bool/[]T`    | `Vec<bool>`      |
-| int8_array              | 31           | byte[]          | ndarray(int8)     
                | /               | `int8_t[n]/vector<T>`          | 
`[n]int8/[]T`    | `Vec<i18>`       |
-| int16_array             | 32           | short[]         | ndarray(int16)    
                | /               | `int16_t[n]/vector<T>`         | 
`[n]int16/[]T`   | `Vec<i16>`       |
-| int32_array             | 33           | int[]           | ndarray(int32)    
                | /               | `int32_t[n]/vector<T>`         | 
`[n]int32/[]T`   | `Vec<i32>`       |
-| int64_array             | 34           | long[]          | ndarray(int64)    
                | /               | `int64_t[n]/vector<T>`         | 
`[n]int64/[]T`   | `Vec<i64>`       |
-| float16_array           | 35           | float[]         | ndarray(float16)  
                | /               | `fory::float16_t[n]/vector<T>` | 
`[n]float16/[]T` | `Vec<fory::f16>` |
-| float32_array           | 36           | float[]         | ndarray(float32)  
                | /               | `float[n]/vector<T>`           | 
`[n]float32/[]T` | `Vec<f32>`       |
-| float64_array           | 37           | double[]        | ndarray(float64)  
                | /               | `double[n]/vector<T>`          | 
`[n]float64/[]T` | `Vec<f64>`       |
+| Fory Type               | Fory Type ID | Java            | Python            
                | Javascript      | C++                            | Golang     
      | Rust              |
+| ----------------------- | ------------ | --------------- | 
--------------------------------- | --------------- | 
------------------------------ | ---------------- | ----------------- |
+| bool                    | 1            | bool/Boolean    | bool              
                | Boolean         | bool                           | bool       
      | bool              |
+| int8                    | 2            | byte/Byte       | int/pyfory.Int8   
                | Type.int8()     | int8_t                         | int8       
      | i8                |
+| int16                   | 3            | short/Short     | int/pyfory.Int16  
                | Type.int16()    | int16_t                        | int16      
      | i6                |
+| int32                   | 4            | int/Integer     | int/pyfory.Int32  
                | Type.int32()    | int32_t                        | int32      
      | i32               |
+| var_int32               | 5            | int/Integer     | 
int/pyfory.VarInt32               | Type.varint32() | fory::varint32_t          
     | fory.varint32    | fory::varint32    |
+| int64                   | 6            | long/Long       | int/pyfory.Int64  
                | Type.int64()    | int64_t                        | int64      
      | i64               |
+| var_int64               | 7            | long/Long       | 
int/pyfory.VarInt64               | Type.varint64() | fory::varint64_t          
     | fory.varint64    | fory::varint64    |
+| sli_int64               | 8            | long/Long       | 
int/pyfory.SliInt64               | Type.sliint64() | fory::sliint64_t          
     | fory.sliint64    | fory::sliint64    |
+| float16                 | 9            | float/Float     | 
float/pyfory.Float16              | Type.float16()  | fory::float16_t           
     | fory.float16     | fory::f16         |
+| float32                 | 10           | float/Float     | 
float/pyfory.Float32              | Type.float32()  | float                     
     | float32          | f32               |
+| float64                 | 11           | double/Double   | 
float/pyfory.Float64              | Type.float64()  | double                    
     | float64          | f64               |
+| string                  | 12           | String          | str               
                | String          | string                         | string     
      | String/str        |
+| enum                    | 13           | Enum subclasses | enum subclasses   
                | /               | enum                           | /          
      | enum              |
+| named_enum              | 14           | Enum subclasses | enum subclasses   
                | /               | enum                           | /          
      | enum              |
+| struct                  | 15           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| compatible_struct       | 16           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| named_struct            | 17           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| named_compatible_struct | 18           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| ext                     | 19           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| named_ext               | 20           | pojo/record     | data class / type 
with type hints | object          | struct/class                   | struct     
      | struct            |
+| list                    | 21           | List/Collection | list/tuple        
                | array           | vector                         | slice      
      | Vec               |
+| set                     | 22           | Set             | set               
                | /               | set                            | fory.Set   
      | Set               |
+| map                     | 23           | Map             | dict              
                | Map             | unordered_map                  | map        
      | HashMap           |
+| duration                | 24           | Duration        | timedelta         
                | Number          | duration                       | Duration   
      | Duration          |
+| timestamp               | 25           | Instant         | datetime          
                | Number          | std::chrono::nanoseconds       | Time       
      | DateTime          |
+| local_date              | 26           | Date            | datetime          
                | Number          | std::chrono::nanoseconds       | Time       
      | DateTime          |
+| decimal                 | 27           | BigDecimal      | Decimal           
                | bigint          | /                              | /          
      | /                 |
+| binary                  | 28           | byte[]          | bytes             
                | /               | `uint8_t[n]/vector<T>`         | 
`[n]uint8/[]T`   | `Vec<uint8_t>`    |
+| array                   | 29           | array           | np.ndarray        
                | /               | /                              | 
array/slice      | Vec               |
+| bool_array              | 30           | bool[]          | 
ndarray(np.bool\_)                | /               | `bool[n]`                 
     | `[n]bool/[]T`    | `Vec<bool>`       |
+| int8_array              | 31           | byte[]          | ndarray(int8)     
                | /               | `int8_t[n]/vector<T>`          | 
`[n]int8/[]T`    | `Vec<i18>`        |
+| int16_array             | 32           | short[]         | ndarray(int16)    
                | /               | `int16_t[n]/vector<T>`         | 
`[n]int16/[]T`   | `Vec<i16>`        |
+| int32_array             | 33           | int[]           | ndarray(int32)    
                | /               | `int32_t[n]/vector<T>`         | 
`[n]int32/[]T`   | `Vec<i32>`        |
+| int64_array             | 34           | long[]          | ndarray(int64)    
                | /               | `int64_t[n]/vector<T>`         | 
`[n]int64/[]T`   | `Vec<i64>`        |
+| float16_array           | 35           | float[]         | ndarray(float16)  
                | /               | `fory::float16_t[n]/vector<T>` | 
`[n]float16/[]T` | `Vec<fory::f16>`  |
+| float32_array           | 36           | float[]         | ndarray(float32)  
                | /               | `float[n]/vector<T>`           | 
`[n]float32/[]T` | `Vec<f32>`        |
+| float64_array           | 37           | double[]        | ndarray(float64)  
                | /               | `double[n]/vector<T>`          | 
`[n]float64/[]T` | `Vec<f64>`        |
+| union                   | 38           | /               | /                 
                | /               | `std::variant<Ts...>`          | /          
      | tagged union enum |
+| none                    | 39           | null            | None              
                | null            | `std::monostate`               | nil        
      | `()`              |
 
 ## Type info(not implemented currently)
 


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


Reply via email to