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 b1725938d feat(c++): support private fields of c++ class (#3193)
b1725938d is described below

commit b1725938d8bfc1ef2dd84b53c54018a8e853cdd6
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jan 25 19:05:02 2026 +0800

    feat(c++): support private fields of c++ class (#3193)
    
    ## Why?
    
    The previous `FORY_FIELD_INFO` macro required defining field metadata
    outside the class/struct definition, which prevented it from accessing
    private fields. This limitation made it impossible to serialize classes
    with private members unless they were made public or friends were added.
    
    ## What does this PR do?
    
    This PR introduces a new `FORY_STRUCT` macro that must be used inside
    the class/struct definition as a replacement for `FORY_FIELD_INFO`. The
    key changes include:
    
    1. **New `FORY_STRUCT` macro**: Replaces `FORY_FIELD_INFO` and must be
    placed inside the struct/class body, enabling access to private fields
    via hidden friend functions
    2. **Hidden friend function approach**: Uses ADL (Argument-Dependent
    Lookup) to define `ForyFieldInfo()` as a friend function within the
    class, granting access to private members
    3. **Backward compatibility**: Keeps `FORY_STRUCT_FIELDS` for internal
    use and provides empty struct support via `FORY_STRUCT_EMPTY`
    4. **Comprehensive migration**: Updates all usages across:
       - Test files (encoder, serialization, field tests)
       - Documentation (C++ row format guide, xlang spec)
       - Examples (hello_row example)
       - README files
    
    **Migration pattern:**
    ```cpp
    // Before:
    class MyClass {
      int private_field;
    };
    FORY_FIELD_INFO(MyClass, private_field);  // Outside class, no access to 
private
    
    // After:
    class MyClass {
      int private_field;
      FORY_STRUCT(MyClass, private_field);  // Inside class, can access private
    };
    ```
    
    For external struct:
    
    ```cpp
    namespace thirdparty {
    struct Foo {
      int32_t id;
      std::string name;
    };
    
    FORY_STRUCT(Foo, id, name);
    } // namespace thirdparty
    
    auto fory = Fory::builder().build();
    fory->register_struct<thirdparty::Foo>(1);
    ```
    
    To include base-class fields in a derived type, list `FORY_BASE(Base)`
    inside
    `FORY_STRUCT`. The base must define its own `FORY_STRUCT` so its fields
    can be
    referenced.
    
    ```cpp
    struct Base {
      int32_t id;
      FORY_STRUCT(Base, id);
    };
    
    struct Derived : Base {
      std::string name;
      FORY_STRUCT(Derived, FORY_BASE(Base), name);
    };
    ```
    
    
    ## Related issues
    
    #2906
    
    ## Does this PR introduce any user-facing change?
    
    Yes, this introduces a breaking API change requiring users to migrate
    from `FORY_FIELD_INFO` to `FORY_STRUCT`.
    
    - [x] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
    
    No performance impact expected - this is a compile-time macro change
    that does not affect runtime serialization logic.
---
 .github/workflows/ci.yml                           |   9 +
 .gitignore                                         |   1 +
 benchmarks/cpp_benchmark/README.md                 |  16 +-
 compiler/README.md                                 |   4 +-
 compiler/fory_compiler/generators/cpp.py           |  24 +-
 cpp/README.md                                      |  49 ++-
 cpp/fory/encoder/row_encode_trait.h                |   6 +-
 cpp/fory/encoder/row_encode_trait_test.cc          |  21 +-
 cpp/fory/encoder/row_encoder_test.cc               |  85 +++++-
 cpp/fory/meta/field_info.h                         | 332 ++++++++++++++++++---
 cpp/fory/meta/field_info_test.cc                   |  18 +-
 cpp/fory/meta/field_test.cc                        |  16 +-
 cpp/fory/meta/preprocessor.h                       |  18 ++
 cpp/fory/row/schema.h                              |   2 +-
 .../serialization/collection_serializer_test.cc    |  28 +-
 cpp/fory/serialization/field_serializer_test.cc    |  38 +--
 cpp/fory/serialization/fory.h                      |   6 +-
 cpp/fory/serialization/map_serializer_test.cc      |  15 +-
 cpp/fory/serialization/serialization_test.cc       |  11 +-
 cpp/fory/serialization/serializer_traits.h         |   6 +-
 .../serialization/smart_ptr_serializer_test.cc     |  26 +-
 cpp/fory/serialization/struct_compatible_test.cc   |  26 +-
 cpp/fory/serialization/struct_serializer.h         | 101 ++-----
 cpp/fory/serialization/struct_test.cc              | 139 +++++++--
 cpp/fory/serialization/tuple_serializer_test.cc    |  14 +-
 cpp/fory/serialization/type_resolver.h             |   4 +-
 cpp/fory/serialization/unsigned_serializer_test.cc |   6 +-
 cpp/fory/serialization/variant_serializer_test.cc  |   4 +-
 cpp/fory/serialization/weak_ptr_serializer.h       |   2 +-
 cpp/fory/serialization/weak_ptr_serializer_test.cc |  10 +-
 cpp/fory/serialization/xlang_test_main.cc          |  92 +++---
 cpp/fory/util/pool_test.cc                         |   4 +-
 docs/benchmarks/cpp/README.md                      |  32 +-
 docs/benchmarks/cpp/throughput.png                 | Bin 48028 -> 44522 bytes
 docs/guide/cpp/basic-serialization.md              |  85 +++++-
 docs/guide/cpp/index.md                            |  18 ++
 docs/guide/cpp/row-format.md                       |  44 ++-
 docs/guide/java/row-format.md                      |   6 +-
 docs/guide/python/row-format.md                    |   6 +-
 docs/specification/xlang_serialization_spec.md     |   2 +-
 examples/cpp/hello_row/README.md                   |   7 +-
 examples/cpp/hello_row/main.cc                     |   7 +-
 examples/cpp/hello_world/README.md                 |   3 +-
 examples/cpp/hello_world/main.cc                   |  13 +-
 java/fory-format/pom.xml                           |   7 +
 python/README.md                                   |   6 +-
 46 files changed, 931 insertions(+), 438 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0f8e5d1e8..a9028cb28 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -380,6 +380,15 @@ jobs:
           key: bazel-build-cpp-${{ runner.os }}-${{ runner.arch }}-${{ 
hashFiles('cpp/**', 'BUILD', 'WORKSPACE') }}
       - name: Run C++ CI with Bazel
         run: python ./ci/run_ci.py cpp
+      - name: Upload Bazel Test Logs
+        uses: actions/upload-artifact@v4
+        if: ${{ !cancelled() }}
+        with:
+          name: bazel-test-logs-${{ matrix.os }}
+          path: |
+            bazel-out/*/testlogs/**/*.log
+            bazel-out/*/testlogs/**/*.xml
+          if-no-files-found: ignore
 
   cpp_xlang:
     name: C++ Xlang Test
diff --git a/.gitignore b/.gitignore
index 63d109ab7..35b82d6f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -115,3 +115,4 @@ examples/cpp/cmake_example/build
 **/benchmark_*.png
 **/results/
 benchmarks/**/report/
+ignored/**
\ No newline at end of file
diff --git a/benchmarks/cpp_benchmark/README.md 
b/benchmarks/cpp_benchmark/README.md
index b66a3b04f..42c1e4cf0 100644
--- a/benchmarks/cpp_benchmark/README.md
+++ b/benchmarks/cpp_benchmark/README.md
@@ -29,14 +29,14 @@ Note: Protobuf is fetched automatically via CMake 
FetchContent, so no manual ins
 <img src="../../docs/benchmarks/cpp/throughput.png" width="90%">
 </p>
 
-| Datatype     | Operation   | Fory TPS   | Protobuf TPS | Faster      |
-| ------------ | ----------- | ---------- | ------------ | ----------- |
-| Mediacontent | Serialize   | 11,319,876 | 1,181,595    | Fory (9.6x) |
-| Mediacontent | Deserialize | 2,729,388  | 835,956      | Fory (3.3x) |
-| Sample       | Serialize   | 16,899,403 | 10,575,760   | Fory (1.6x) |
-| Sample       | Deserialize | 3,079,241  | 1,450,789    | Fory (2.1x) |
-| Struct       | Serialize   | 43,184,198 | 29,359,454   | Fory (1.5x) |
-| Struct       | Deserialize | 54,599,691 | 38,796,674   | Fory (1.4x) |
+| Datatype     | Operation   | Fory TPS  | Protobuf TPS | Faster      |
+| ------------ | ----------- | --------- | ------------ | ----------- |
+| Mediacontent | Serialize   | 2,254,915 | 504,410      | Fory (4.5x) |
+| Mediacontent | Deserialize | 741,303   | 396,013      | Fory (1.9x) |
+| Sample       | Serialize   | 4,248,973 | 3,229,102    | Fory (1.3x) |
+| Sample       | Deserialize | 935,709   | 715,837      | Fory (1.3x) |
+| Struct       | Serialize   | 9,143,618 | 5,881,005    | Fory (1.6x) |
+| Struct       | Deserialize | 7,746,787 | 6,202,164    | Fory (1.2x) |
 
 ## Quick Start
 
diff --git a/compiler/README.md b/compiler/README.md
index b8fbb77c7..452bce913 100644
--- a/compiler/README.md
+++ b/compiler/README.md
@@ -384,8 +384,10 @@ struct Cat {
     std::shared_ptr<Dog> friend;
     std::optional<std::string> name;
     std::vector<std::string> tags;
+    int32_t scores;
+    int32_t lives;
+    FORY_STRUCT(Cat, friend, name, tags, scores, lives);
 };
-FORY_STRUCT(Cat, friend, name, tags, scores, lives);
 ```
 
 ## CLI Reference
diff --git a/compiler/fory_compiler/generators/cpp.py 
b/compiler/fory_compiler/generators/cpp.py
index 2580c5abb..49298cb86 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -114,7 +114,6 @@ class CppGenerator(BaseGenerator):
         lines = []
         includes: Set[str] = set()
         enum_macros: List[str] = []
-        struct_macros: List[str] = []
         union_macros: List[str] = []
         field_config_macros: List[str] = []
         definition_items = self.get_definition_order()
@@ -173,9 +172,7 @@ class CppGenerator(BaseGenerator):
         # Generate top-level unions/messages in dependency order
         for kind, item in definition_items:
             if kind == "union":
-                lines.extend(
-                    self.generate_union_definition(item, [], struct_macros, "")
-                )
+                lines.extend(self.generate_union_definition(item, [], ""))
                 union_macros.extend(self.generate_union_macros(item, []))
                 lines.append("")
                 continue
@@ -183,7 +180,6 @@ class CppGenerator(BaseGenerator):
                 self.generate_message_definition(
                     item,
                     [],
-                    struct_macros,
                     enum_macros,
                     union_macros,
                     field_config_macros,
@@ -192,10 +188,6 @@ class CppGenerator(BaseGenerator):
             )
             lines.append("")
 
-        if struct_macros:
-            lines.extend(struct_macros)
-            lines.append("")
-
         if namespace:
             lines.append(f"}} // namespace {namespace}")
             lines.append("")
@@ -396,7 +388,6 @@ class CppGenerator(BaseGenerator):
         self,
         message: Message,
         parent_stack: List[Message],
-        struct_macros: List[str],
         enum_macros: List[str],
         union_macros: List[str],
         field_config_macros: List[str],
@@ -422,7 +413,6 @@ class CppGenerator(BaseGenerator):
                 self.generate_message_definition(
                     nested_msg,
                     lineage,
-                    struct_macros,
                     enum_macros,
                     union_macros,
                     field_config_macros,
@@ -436,7 +426,6 @@ class CppGenerator(BaseGenerator):
                 self.generate_union_definition(
                     nested_union,
                     lineage,
-                    struct_macros,
                     body_indent,
                 )
             )
@@ -470,12 +459,9 @@ class CppGenerator(BaseGenerator):
             lines.append(f"{body_indent}  return true;")
         lines.append(f"{body_indent}}}")
 
-        lines.append(f"{indent}}};")
-
         struct_type_name = self.get_qualified_type_name(message.name, 
parent_stack)
         if message.fields:
             field_names = ", ".join(self.to_snake_case(f.name) for f in 
message.fields)
-            struct_macros.append(f"FORY_STRUCT({struct_type_name}, 
{field_names});")
             field_config_type_name = self.get_field_config_type_and_alias(
                 message.name, parent_stack
             )
@@ -484,8 +470,13 @@ class CppGenerator(BaseGenerator):
                     message, field_config_type_name[0], 
field_config_type_name[1]
                 )
             )
+            lines.append(
+                f"{body_indent}FORY_STRUCT({struct_type_name}, {field_names});"
+            )
         else:
-            struct_macros.append(f"FORY_STRUCT({struct_type_name});")
+            lines.append(f"{body_indent}FORY_STRUCT({struct_type_name});")
+
+        lines.append(f"{indent}}};")
 
         return lines
 
@@ -493,7 +484,6 @@ class CppGenerator(BaseGenerator):
         self,
         union: Union,
         parent_stack: List[Message],
-        struct_macros: List[str],
         indent: str,
     ) -> List[str]:
         """Generate a C++ union class definition."""
diff --git a/cpp/README.md b/cpp/README.md
index dc74e7919..0680cc3c9 100644
--- a/cpp/README.md
+++ b/cpp/README.md
@@ -23,7 +23,9 @@ The C++ implementation provides high-performance 
serialization with compile-time
 ```cpp
 #include "fory/serialization/fory.h"
 
-// Define your struct with FORY_STRUCT macro
+// Define your class with FORY_STRUCT macro (struct works the same way).
+// Place it after all fields.
+// When used inside a class, it must be placed in a public: section.
 struct Person {
   std::string name;
   int32_t age;
@@ -84,14 +86,16 @@ Apache Fory™ provides automatic serialization of complex 
object graphs, preser
 ```cpp
 #include "fory/serialization/fory.h"
 
-struct Address {
+class Address {
+public:
   std::string street;
   std::string city;
   std::string country;
   FORY_STRUCT(Address, street, city, country);
 };
 
-struct Person {
+class Person {
+public:
   std::string name;
   int32_t age;
   Address address;
@@ -116,6 +120,43 @@ auto bytes = fory->serialize(person).value();
 auto decoded = fory->deserialize<Person>(bytes).value();
 ```
 
+### 1.1 External/Third-Party Types
+
+For third-party types where you cannot modify the class definition, use
+`FORY_STRUCT` at namespace scope. This works for public fields only.
+
+```cpp
+namespace thirdparty {
+struct Foo {
+  int32_t id;
+  std::string name;
+};
+
+FORY_STRUCT(Foo, id, name);
+} // namespace thirdparty
+
+auto fory = apache::fory::ForyBuilder().build();
+fory->register_struct<thirdparty::Foo>(1);
+```
+
+### 1.2 Inherited Fields
+
+To include base-class fields in a derived type, use `FORY_BASE(Base)` inside
+`FORY_STRUCT`. The base must define its own `FORY_STRUCT` so its fields can be
+referenced.
+
+```cpp
+struct Base {
+  int32_t id;
+  FORY_STRUCT(Base, id);
+};
+
+struct Derived : Base {
+  std::string name;
+  FORY_STRUCT(Derived, FORY_BASE(Base), name);
+};
+```
+
 ### 2. Shared References
 
 Apache Fory™ automatically tracks and preserves reference identity for shared 
objects using `std::shared_ptr<T>`. When the same object is referenced multiple 
times, Fory serializes it only once and uses reference IDs for subsequent 
occurrences.
@@ -230,7 +271,7 @@ struct UserProfile {
   std::string email;
   std::vector<int32_t> scores;
   bool is_active;
-  FORY_FIELD_INFO(UserProfile, id, username, email, scores, is_active);
+  FORY_STRUCT(UserProfile, id, username, email, scores, is_active);
 };
 
 apache::fory::RowEncoder<UserProfile> encoder;
diff --git a/cpp/fory/encoder/row_encode_trait.h 
b/cpp/fory/encoder/row_encode_trait.h
index 134cf9643..b1facd3c3 100644
--- a/cpp/fory/encoder/row_encode_trait.h
+++ b/cpp/fory/encoder/row_encode_trait.h
@@ -192,7 +192,7 @@ private:
 
   template <size_t I> static FieldPtr GetField() {
     using FieldType = meta::RemoveMemberPointerCVRefT<
-        std::tuple_element_t<I, decltype(FieldInfo::Ptrs)>>;
+        std::tuple_element_t<I, decltype(FieldInfo::Ptrs())>>;
     return field(details::StringViewToString(FieldInfo::Names[I]),
                  RowEncodeTrait<FieldType>::Type());
   }
@@ -205,9 +205,9 @@ private:
   template <size_t I, typename V>
   static void WriteField(V &&visitor, const T &value, RowWriter &writer) {
     using FieldType = meta::RemoveMemberPointerCVRefT<
-        std::tuple_element_t<I, decltype(FieldInfo::Ptrs)>>;
+        std::tuple_element_t<I, decltype(FieldInfo::Ptrs())>>;
     RowEncodeTrait<FieldType>::Write(std::forward<V>(visitor),
-                                     value.*std::get<I>(FieldInfo::Ptrs),
+                                     value.*std::get<I>(FieldInfo::PtrsRef()),
                                      writer, I);
   }
 
diff --git a/cpp/fory/encoder/row_encode_trait_test.cc 
b/cpp/fory/encoder/row_encode_trait_test.cc
index 3d153d2e7..4fd381908 100644
--- a/cpp/fory/encoder/row_encode_trait_test.cc
+++ b/cpp/fory/encoder/row_encode_trait_test.cc
@@ -34,10 +34,9 @@ struct A {
   int x;
   float y;
   bool z;
+  FORY_STRUCT(A, x, y, z);
 };
 
-FORY_FIELD_INFO(A, x, y, z);
-
 TEST(RowEncodeTrait, Basic) {
   auto field_vector = encoder::RowEncodeTrait<A>::FieldVector();
 
@@ -67,10 +66,9 @@ TEST(RowEncodeTrait, Basic) {
 struct B {
   int num;
   std::string str;
+  FORY_STRUCT(B, num, str);
 };
 
-FORY_FIELD_INFO(B, num, str);
-
 TEST(RowEncodeTrait, String) {
   RowWriter writer(encoder::RowEncodeTrait<B>::Schema());
   writer.Reset();
@@ -89,10 +87,9 @@ struct C {
   const int a;
   volatile float b;
   bool c;
+  FORY_STRUCT(C, a, b, c);
 };
 
-FORY_FIELD_INFO(C, a, b, c);
-
 TEST(RowEncodeTrait, Const) {
   RowWriter writer(encoder::RowEncodeTrait<C>::Schema());
   writer.Reset();
@@ -110,10 +107,9 @@ struct D {
   int x;
   A y;
   B z;
+  FORY_STRUCT(D, x, y, z);
 };
 
-FORY_FIELD_INFO(D, x, y, z);
-
 TEST(RowEncodeTrait, NestedStruct) {
   RowWriter writer(encoder::RowEncodeTrait<D>::Schema());
   std::vector<std::unique_ptr<RowWriter>> children;
@@ -202,10 +198,9 @@ TEST(RowEncodeTrait, StructInArray) {
 struct E {
   int a;
   std::vector<int> b;
+  FORY_STRUCT(E, a, b);
 };
 
-FORY_FIELD_INFO(E, a, b);
-
 TEST(RowEncodeTrait, ArrayInStruct) {
   E e{233, {10, 20, 30}};
 
@@ -256,10 +251,9 @@ struct F {
   bool a;
   std::optional<int> b;
   int c;
+  FORY_STRUCT(F, a, b, c);
 };
 
-FORY_FIELD_INFO(F, a, b, c);
-
 TEST(RowEncodeTrait, Optional) {
   F x{false, 233, 111}, y{true, std::nullopt, 222};
 
@@ -301,10 +295,9 @@ TEST(RowEncodeTrait, Optional) {
 struct G {
   std::map<int, std::map<int, int>> a;
   std::map<std::string, A> b;
+  FORY_STRUCT(G, a, b);
 };
 
-FORY_FIELD_INFO(G, a, b);
-
 TEST(RowEncodeTrait, Map) {
   G v{{{1, {{3, 4}, {5, 6}}}, {2, {{7, 8}, {9, 10}, {11, 12}}}},
       {{"a", A{1, 1.1, true}}, {"b", A{2, 3.3, false}}}};
diff --git a/cpp/fory/encoder/row_encoder_test.cc 
b/cpp/fory/encoder/row_encoder_test.cc
index 93355d2ec..a410ce33a 100644
--- a/cpp/fory/encoder/row_encoder_test.cc
+++ b/cpp/fory/encoder/row_encoder_test.cc
@@ -32,16 +32,48 @@ namespace test2 {
 struct A {
   float a;
   std::string b;
+  FORY_STRUCT(A, a, b);
 };
 
-FORY_FIELD_INFO(A, a, b);
-
 struct B {
   int x;
   A y;
+  FORY_STRUCT(B, x, y);
+};
+
+namespace external_row {
+
+struct ExternalRow {
+  int32_t id;
+  std::string name;
+};
+
+FORY_STRUCT(ExternalRow, id, name);
+
+struct ExternalRowEmpty {};
+
+FORY_STRUCT(ExternalRowEmpty);
+
+} // namespace external_row
+
+namespace nested_row {
+namespace inner {
+
+struct InClassRow {
+  int32_t id;
+  std::string name;
+  FORY_STRUCT(InClassRow, id, name);
 };
 
-FORY_FIELD_INFO(B, x, y);
+struct OutClassRow {
+  int32_t id;
+  std::string name;
+};
+
+FORY_STRUCT(OutClassRow, id, name);
+
+} // namespace inner
+} // namespace nested_row
 
 TEST(RowEncoder, Simple) {
   B v{233, {1.23, "hello"}};
@@ -66,13 +98,56 @@ TEST(RowEncoder, Simple) {
   ASSERT_FLOAT_EQ(y_row->GetFloat(0), 1.23);
 }
 
+TEST(RowEncoder, ExternalStruct) {
+  external_row::ExternalRow v{7, "external"};
+  encoder::RowEncoder<external_row::ExternalRow> enc;
+
+  auto &schema = enc.GetSchema();
+  ASSERT_EQ(schema.field_names(), (std::vector<std::string>{"id", "name"}));
+
+  enc.Encode(v);
+  auto row = enc.GetWriter().ToRow();
+  ASSERT_EQ(row->GetInt32(0), 7);
+  ASSERT_EQ(row->GetString(1), "external");
+}
+
+TEST(RowEncoder, ExternalEmptyStruct) {
+  external_row::ExternalRowEmpty v{};
+  encoder::RowEncoder<external_row::ExternalRowEmpty> enc;
+
+  auto &schema = enc.GetSchema();
+  ASSERT_TRUE(schema.field_names().empty());
+
+  enc.Encode(v);
+  auto row = enc.GetWriter().ToRow();
+  ASSERT_EQ(row->num_fields(), 0);
+}
+
+TEST(RowEncoder, NestedNamespaceStructs) {
+  nested_row::inner::InClassRow in{11, "in"};
+  nested_row::inner::OutClassRow out{22, "out"};
+
+  encoder::RowEncoder<nested_row::inner::InClassRow> in_enc;
+  encoder::RowEncoder<nested_row::inner::OutClassRow> out_enc;
+
+  in_enc.Encode(in);
+  out_enc.Encode(out);
+
+  auto in_row = in_enc.GetWriter().ToRow();
+  auto out_row = out_enc.GetWriter().ToRow();
+
+  ASSERT_EQ(in_row->GetInt32(0), 11);
+  ASSERT_EQ(in_row->GetString(1), "in");
+  ASSERT_EQ(out_row->GetInt32(0), 22);
+  ASSERT_EQ(out_row->GetString(1), "out");
+}
+
 struct C {
   std::vector<A> x;
   bool y;
+  FORY_STRUCT(C, x, y);
 };
 
-FORY_FIELD_INFO(C, x, y);
-
 TEST(RowEncoder, SimpleArray) {
   std::vector<C> v{C{{{1, "a"}, {2, "b"}}, false},
                    C{{{1.1, "x"}, {2.2, "y"}, {3.3, "z"}}, true}};
diff --git a/cpp/fory/meta/field_info.h b/cpp/fory/meta/field_info.h
index ec7dcd3a9..de2fa3b09 100644
--- a/cpp/fory/meta/field_info.h
+++ b/cpp/fory/meta/field_info.h
@@ -31,22 +31,54 @@ namespace fory {
 
 namespace meta {
 
-// decltype(ForyFieldInfo<T>(v)) records field meta information for type T
-// it includes:
-// - number of fields: typed size_t
-// - field names: typed `std::string_view`
-// - field member points: typed `decltype(a) T::*` for any member `T::a`
-template <typename T> constexpr auto ForyFieldInfo(const T &) noexcept {
-  static_assert(AlwaysFalse<T>,
-                "FORY_FIELD_INFO for type T is expected but not defined");
-}
+template <typename T> struct Identity {
+  using Type = T;
+};
 
 namespace details {
 
+template <typename T>
+using MemberStructInfo =
+    decltype(T::ForyStructInfo(std::declval<Identity<T>>()));
+
+template <typename T, typename = void>
+struct HasMemberStructInfo : std::false_type {};
+
+template <typename T>
+struct HasMemberStructInfo<T, std::void_t<MemberStructInfo<T>>>
+    : std::true_type {};
+
+template <typename T>
+using AdlStructInfo = decltype(ForyStructInfo(std::declval<Identity<T>>()));
+
+template <typename T, typename = void>
+struct HasAdlStructInfo : std::false_type {};
+
+template <typename T>
+struct HasAdlStructInfo<T, std::void_t<AdlStructInfo<T>>> : std::true_type {};
+
+template <typename T> struct TupleWrapper {
+  T value;
+};
+
+template <typename T> constexpr TupleWrapper<T> WrapTuple(const T &value) {
+  return {value};
+}
+
+template <typename T> constexpr const T &UnwrapTuple(const T &value) {
+  return value;
+}
+
+template <typename T>
+constexpr const T &UnwrapTuple(const TupleWrapper<T> &value) {
+  return value.value;
+}
+
 // it must be able to be executed in compile-time
 template <typename FieldInfo, size_t... I>
 constexpr bool IsValidFieldInfoImpl(std::index_sequence<I...>) {
-  return IsUnique<std::get<I>(FieldInfo::Ptrs)...>::value;
+  constexpr auto ptrs = FieldInfo::Ptrs();
+  return IsUnique<std::get<I>(ptrs)...>::value;
 }
 
 } // namespace details
@@ -56,41 +88,267 @@ template <typename FieldInfo> constexpr bool 
IsValidFieldInfo() {
       std::make_index_sequence<FieldInfo::Size>{});
 }
 
+template <typename T>
+struct HasForyStructInfo
+    : std::bool_constant<details::HasMemberStructInfo<T>::value ||
+                         details::HasAdlStructInfo<T>::value> {};
+
+// decltype(ForyFieldInfo<T>(v)) records field meta information for type T
+// it includes:
+// - number of fields: typed size_t
+// - field names: typed `std::string_view`
+// - field member points: typed `decltype(a) T::*` for any member `T::a`
+template <typename T>
+constexpr auto ForyFieldInfo([[maybe_unused]] const T &value) noexcept {
+  if constexpr (details::HasMemberStructInfo<T>::value) {
+    using FieldInfo = decltype(T::ForyStructInfo(Identity<T>{}));
+    static_assert(IsValidFieldInfo<FieldInfo>(),
+                  "duplicated fields in FORY_STRUCT arguments are detected");
+    return T::ForyStructInfo(Identity<T>{});
+  } else if constexpr (details::HasAdlStructInfo<T>::value) {
+    using FieldInfo = decltype(ForyStructInfo(Identity<T>{}));
+    static_assert(IsValidFieldInfo<FieldInfo>(),
+                  "duplicated fields in FORY_STRUCT arguments are detected");
+    return ForyStructInfo(Identity<T>{});
+  } else {
+    static_assert(AlwaysFalse<T>,
+                  "FORY_STRUCT for type T is expected but not defined");
+  }
+}
+
+constexpr std::array<std::string_view, 0> ConcatArrays() { return {}; }
+
+template <size_t N>
+constexpr std::array<std::string_view, N>
+ConcatArrays(const std::array<std::string_view, N> &value) {
+  return value;
+}
+
+template <size_t N, size_t M>
+constexpr std::array<std::string_view, N + M>
+ConcatArrays(const std::array<std::string_view, N> &left,
+             const std::array<std::string_view, M> &right) {
+  std::array<std::string_view, N + M> out{};
+  for (size_t i = 0; i < N; ++i) {
+    out[i] = left[i];
+  }
+  for (size_t i = 0; i < M; ++i) {
+    out[N + i] = right[i];
+  }
+  return out;
+}
+
+template <size_t N, size_t M, typename... Rest>
+constexpr auto ConcatArrays(const std::array<std::string_view, N> &left,
+                            const std::array<std::string_view, M> &right,
+                            const Rest &...rest) {
+  return ConcatArrays(ConcatArrays(left, right), rest...);
+}
+
+constexpr std::tuple<> ConcatTuples() { return {}; }
+
+template <typename Tuple> constexpr Tuple ConcatTuples(const Tuple &tuple) {
+  return tuple;
+}
+
+template <typename Tuple1, typename Tuple2, size_t... I, size_t... J>
+constexpr auto ConcatTwoTuplesImpl(const Tuple1 &left, const Tuple2 &right,
+                                   std::index_sequence<I...>,
+                                   std::index_sequence<J...>) {
+  return std::tuple{std::get<I>(left)..., std::get<J>(right)...};
+}
+
+template <typename Tuple1, typename Tuple2>
+constexpr auto ConcatTuples(const Tuple1 &left, const Tuple2 &right) {
+  return ConcatTwoTuplesImpl(
+      left, right, std::make_index_sequence<std::tuple_size_v<Tuple1>>{},
+      std::make_index_sequence<std::tuple_size_v<Tuple2>>{});
+}
+
+template <typename Tuple1, typename Tuple2, typename... Rest>
+constexpr auto ConcatTuples(const Tuple1 &left, const Tuple2 &right,
+                            const Rest &...rest) {
+  return ConcatTuples(ConcatTuples(left, right), rest...);
+}
+
+template <typename Tuple, size_t... I>
+constexpr auto ConcatArraysFromTupleImpl(const Tuple &tuple,
+                                         std::index_sequence<I...>) {
+  return ConcatArrays(std::get<I>(tuple)...);
+}
+
+template <typename Tuple>
+constexpr auto ConcatArraysFromTuple(const Tuple &tuple) {
+  return ConcatArraysFromTupleImpl(
+      tuple, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
+}
+
+template <typename Tuple, size_t... I>
+constexpr auto ConcatTuplesFromTupleImpl(const Tuple &tuple,
+                                         std::index_sequence<I...>) {
+  return ConcatTuples(details::UnwrapTuple(std::get<I>(tuple))...);
+}
+
+template <typename Tuple>
+constexpr auto ConcatTuplesFromTuple(const Tuple &tuple) {
+  return ConcatTuplesFromTupleImpl(
+      tuple, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
+}
+
 } // namespace meta
 
 } // namespace fory
 
-#define FORY_FIELD_INFO_NAMES_FUNC(field) #field,
-#define FORY_FIELD_INFO_PTRS_FUNC(type, field) &type::field,
+#define FORY_BASE(type) (FORY_BASE_TAG, type)
+
+#define FORY_PP_IS_BASE_TAG(x)                                                 
\
+  FORY_PP_CHECK(FORY_PP_CONCAT(FORY_PP_IS_BASE_TAG_PROBE_, x))
+#define FORY_PP_IS_BASE_TAG_PROBE_FORY_BASE_TAG FORY_PP_PROBE()
+
+#define FORY_PP_IS_BASE(arg) FORY_PP_IS_BASE_IMPL(FORY_PP_IS_PAREN(arg), arg)
+#define FORY_PP_IS_BASE_IMPL(is_paren, arg)                                    
\
+  FORY_PP_CONCAT(FORY_PP_IS_BASE_IMPL_, is_paren)(arg)
+#define FORY_PP_IS_BASE_IMPL_0(arg) 0
+#define FORY_PP_IS_BASE_IMPL_1(arg)                                            
\
+  FORY_PP_IS_BASE_TAG(FORY_PP_TUPLE_FIRST(arg))
 
-// here we define function overloads in the current namespace rather than
-// template specialization of classes since specialization of template in
-// different namespace is hard
-// NOTE: for performing ADL (argument-dependent lookup),
-// `FORY_FIELD_INFO(T, ...)` must be defined in the same namespace as `T`
-#define FORY_FIELD_INFO(type, ...)                                             
\
+#define FORY_BASE_TYPE(arg) FORY_PP_TUPLE_SECOND(arg)
+
+#define FORY_FIELD_INFO_NAMES_FIELD(field) #field,
+#define FORY_FIELD_INFO_NAMES_FUNC(arg)                                        
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))                                             
\
+  (FORY_PP_EMPTY(), FORY_FIELD_INFO_NAMES_FIELD(arg))
+
+#define FORY_FIELD_INFO_PTRS_FIELD(type, field) &type::field,
+#define FORY_FIELD_INFO_PTRS_FUNC(type, arg)                                   
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))                                             
\
+  (FORY_PP_EMPTY(), FORY_FIELD_INFO_PTRS_FIELD(type, arg))
+
+#define FORY_BASE_NAMES_ARG(arg)                                               
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))                                             
\
+  (FORY_BASE_NAMES_ARG_IMPL(arg), FORY_PP_EMPTY())
+#define FORY_BASE_NAMES_ARG_IMPL(arg)                                          
\
+  decltype(::fory::meta::ForyFieldInfo(                                        
\
+      std::declval<FORY_BASE_TYPE(arg)>()))::Names,
+
+#define FORY_BASE_PTRS_ARG(arg)                                                
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))                                             
\
+  (FORY_BASE_PTRS_ARG_IMPL(arg), FORY_PP_EMPTY())
+#define FORY_BASE_PTRS_ARG_IMPL(arg)                                           
\
+  fory::meta::details::WrapTuple(decltype(::fory::meta::ForyFieldInfo(         
\
+      std::declval<FORY_BASE_TYPE(arg)>()))::Ptrs()),
+
+#define FORY_BASE_SIZE_ADD(arg)                                                
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))                                             
\
+  (+decltype(::fory::meta::ForyFieldInfo(                                      
\
+       std::declval<FORY_BASE_TYPE(arg)>()))::Size,                            
\
+   FORY_PP_EMPTY())
+
+#define FORY_FIELD_SIZE_ADD(arg)                                               
\
+  FORY_PP_IF(FORY_PP_IS_BASE(arg))(FORY_PP_EMPTY(), +1)
+
+// NOTE: FORY_STRUCT can be used inside the class/struct definition or at
+// namespace scope. The macro defines constexpr functions which are detected
+// via member lookup (in-class) or ADL (namespace scope).
+// MSVC (VS 2022 17.11, 19.41) fixes in-class pointer-to-member constexpr
+// issues; keep evaluation inside `Ptrs` function instead of field for older
+// toolsets.
+#define FORY_STRUCT_FIELDS(type, unique_id, ...)                               
\
   static_assert(std::is_class_v<type>, "it must be a class type");             
\
-  template <typename> struct ForyFieldInfoImpl;                                
\
-  template <> struct ForyFieldInfoImpl<type> {                                 
\
-    static inline constexpr size_t Size = FORY_PP_NARG(__VA_ARGS__);           
\
+  struct FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id) {                 
\
+    static inline constexpr size_t BaseSize =                                  
\
+        0 FORY_PP_FOREACH(FORY_BASE_SIZE_ADD, __VA_ARGS__);                    
\
+    static inline constexpr size_t FieldSize =                                 
\
+        0 FORY_PP_FOREACH(FORY_FIELD_SIZE_ADD, __VA_ARGS__);                   
\
+    static inline constexpr size_t Size = BaseSize + FieldSize;                
\
     static inline constexpr std::string_view Name = #type;                     
\
-    static inline constexpr std::array<std::string_view, Size> Names = {       
\
-        FORY_PP_FOREACH(FORY_FIELD_INFO_NAMES_FUNC, __VA_ARGS__)};             
\
-    static inline constexpr auto Ptrs = std::tuple{                            
\
-        FORY_PP_FOREACH_1(FORY_FIELD_INFO_PTRS_FUNC, type, __VA_ARGS__)};      
\
+    static inline constexpr auto BaseNames =                                   
\
+        fory::meta::ConcatArraysFromTuple(                                     
\
+            std::tuple{FORY_PP_FOREACH(FORY_BASE_NAMES_ARG, __VA_ARGS__)});    
\
+    static inline constexpr std::array<std::string_view, FieldSize>            
\
+        FieldNames = {                                                         
\
+            FORY_PP_FOREACH(FORY_FIELD_INFO_NAMES_FUNC, __VA_ARGS__)};         
\
+    static inline constexpr auto Names =                                       
\
+        fory::meta::ConcatArrays(BaseNames, FieldNames);                       
\
+    using BasePtrsType = decltype(fory::meta::ConcatTuplesFromTuple(           
\
+        std::tuple{FORY_PP_FOREACH(FORY_BASE_PTRS_ARG, __VA_ARGS__)}));        
\
+    static constexpr BasePtrsType BasePtrs() {                                 
\
+      return fory::meta::ConcatTuplesFromTuple(                                
\
+          std::tuple{FORY_PP_FOREACH(FORY_BASE_PTRS_ARG, __VA_ARGS__)});       
\
+    }                                                                          
\
+    using FieldPtrsType = decltype(std::tuple{                                 
\
+        FORY_PP_FOREACH_1(FORY_FIELD_INFO_PTRS_FUNC, type, __VA_ARGS__)});     
\
+    static constexpr FieldPtrsType FieldPtrs() {                               
\
+      return std::tuple{                                                       
\
+          FORY_PP_FOREACH_1(FORY_FIELD_INFO_PTRS_FUNC, type, __VA_ARGS__)};    
\
+    }                                                                          
\
+    using PtrsType = decltype(fory::meta::ConcatTuples(                        
\
+        std::declval<BasePtrsType>(), std::declval<FieldPtrsType>()));         
\
+    static constexpr PtrsType Ptrs() {                                         
\
+      return fory::meta::ConcatTuples(BasePtrs(), FieldPtrs());                
\
+    }                                                                          
\
+    static const PtrsType &PtrsRef() {                                         
\
+      static const PtrsType value = Ptrs();                                    
\
+      return value;                                                            
\
+    }                                                                          
\
   };                                                                           
\
+  static_assert(FORY_PP_CONCAT(ForyFieldInfoDescriptor_,                       
\
+                               unique_id)::Name.data() != nullptr,             
\
+                "ForyFieldInfoDescriptor name must be available");             
\
   static_assert(                                                               
\
-      fory::meta::IsValidFieldInfo<ForyFieldInfoImpl<type>>(),                 
\
-      "duplicated fields in FORY_FIELD_INFO arguments are detected");          
\
-  static_assert(ForyFieldInfoImpl<type>::Name.data() != nullptr,               
\
-                "ForyFieldInfoImpl name must be available");                   
\
-  static_assert(ForyFieldInfoImpl<type>::Names.size() ==                       
\
-                    ForyFieldInfoImpl<type>::Size,                             
\
-                "ForyFieldInfoImpl names size mismatch");                      
\
-  inline constexpr auto ForyFieldInfo(const type &) noexcept {                 
\
-    return ForyFieldInfoImpl<type>{};                                          
\
+      FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id)::Names.size() ==     
\
+          FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id)::Size,           
\
+      "ForyFieldInfoDescriptor names size mismatch");                          
\
+  [[maybe_unused]] inline static constexpr auto ForyStructInfo(                
\
+      const ::fory::meta::Identity<type> &) noexcept {                         
\
+    return FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id){};              
\
   }                                                                            
\
+  [[maybe_unused]] inline static constexpr std::true_type ForyStructMarker(    
\
+      const ::fory::meta::Identity<type> &) noexcept {                         
\
+    return {};                                                                 
\
+  }
+
+#define FORY_STRUCT_DETAIL_EMPTY(type, unique_id)                              
\
+  static_assert(std::is_class_v<type>, "it must be a class type");             
\
+  struct FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id) {                 
\
+    static inline constexpr size_t Size = 0;                                   
\
+    static inline constexpr std::string_view Name = #type;                     
\
+    static inline constexpr std::array<std::string_view, Size> Names = {};     
\
+    using PtrsType = decltype(std::tuple{});                                   
\
+    static constexpr PtrsType Ptrs() { return {}; }                            
\
+    static const PtrsType &PtrsRef() {                                         
\
+      static const PtrsType value = Ptrs();                                    
\
+      return value;                                                            
\
+    }                                                                          
\
+  };                                                                           
\
+  static_assert(FORY_PP_CONCAT(ForyFieldInfoDescriptor_,                       
\
+                               unique_id)::Name.data() != nullptr,             
\
+                "ForyFieldInfoDescriptor name must be available");             
\
   static_assert(                                                               
\
-      static_cast<ForyFieldInfoImpl<type> (*)(const type &) noexcept>(         
\
-          &ForyFieldInfo) != nullptr,                                          
\
-      "ForyFieldInfo must be declared");
+      FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id)::Names.size() ==     
\
+          FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id)::Size,           
\
+      "ForyFieldInfoDescriptor names size mismatch");                          
\
+  [[maybe_unused]] inline static constexpr auto ForyStructInfo(                
\
+      const ::fory::meta::Identity<type> &) noexcept {                         
\
+    return FORY_PP_CONCAT(ForyFieldInfoDescriptor_, unique_id){};              
\
+  }                                                                            
\
+  [[maybe_unused]] inline static constexpr std::true_type ForyStructMarker(    
\
+      const ::fory::meta::Identity<type> &) noexcept {                         
\
+    return {};                                                                 
\
+  }
+
+#define FORY_STRUCT_WITH_FIELDS(type, unique_id, ...)                          
\
+  FORY_STRUCT_FIELDS(type, unique_id, __VA_ARGS__)
+
+#define FORY_STRUCT_EMPTY(type, unique_id)                                     
\
+  FORY_STRUCT_DETAIL_EMPTY(type, unique_id)
+
+#define FORY_STRUCT_1(type, unique_id, ...) FORY_STRUCT_EMPTY(type, unique_id)
+#define FORY_STRUCT_0(type, unique_id, ...)                                    
\
+  FORY_STRUCT_WITH_FIELDS(type, unique_id, __VA_ARGS__)
+
+#define FORY_STRUCT_IMPL(type, unique_id, ...)                                 
\
+  FORY_PP_CONCAT(FORY_STRUCT_, FORY_PP_IS_EMPTY(__VA_ARGS__))                  
\
+  (type, unique_id, __VA_ARGS__)
+
+#define FORY_STRUCT(type, ...) FORY_STRUCT_IMPL(type, __LINE__, __VA_ARGS__)
diff --git a/cpp/fory/meta/field_info_test.cc b/cpp/fory/meta/field_info_test.cc
index 6cb79a9b2..f07bf0067 100644
--- a/cpp/fory/meta/field_info_test.cc
+++ b/cpp/fory/meta/field_info_test.cc
@@ -29,13 +29,12 @@ struct A {
   int x;
   float y;
   bool z;
+  FORY_STRUCT(A, x, y, z);
 };
 
-FORY_FIELD_INFO(A, x, y, z);
-
 TEST(FieldInfo, Simple) {
   A a;
-  constexpr auto info = ForyFieldInfo(a);
+  constexpr auto info = meta::ForyFieldInfo(a);
 
   static_assert(info.Size == 3);
 
@@ -45,21 +44,20 @@ TEST(FieldInfo, Simple) {
   static_assert(info.Names[1] == "y");
   static_assert(info.Names[2] == "z");
 
-  static_assert(std::get<0>(info.Ptrs) == &A::x);
-  static_assert(std::get<1>(info.Ptrs) == &A::y);
-  static_assert(std::get<2>(info.Ptrs) == &A::z);
+  static_assert(std::get<0>(decltype(info)::Ptrs()) == &A::x);
+  static_assert(std::get<1>(decltype(info)::Ptrs()) == &A::y);
+  static_assert(std::get<2>(decltype(info)::Ptrs()) == &A::z);
 }
 
 struct B {
   A a;
   int hidden;
+  FORY_STRUCT(B, a);
 };
 
-FORY_FIELD_INFO(B, a);
-
 TEST(FieldInfo, Hidden) {
   B b;
-  constexpr auto info = ForyFieldInfo(b);
+  constexpr auto info = meta::ForyFieldInfo(b);
 
   static_assert(info.Size == 1);
 
@@ -67,7 +65,7 @@ TEST(FieldInfo, Hidden) {
 
   static_assert(info.Names[0] == "a");
 
-  static_assert(std::get<0>(info.Ptrs) == &B::a);
+  static_assert(std::get<0>(decltype(info)::Ptrs()) == &B::a);
 }
 
 } // namespace test
diff --git a/cpp/fory/meta/field_test.cc b/cpp/fory/meta/field_test.cc
index 11379cfb2..f3976fd05 100644
--- a/cpp/fory/meta/field_test.cc
+++ b/cpp/fory/meta/field_test.cc
@@ -240,10 +240,9 @@ struct Person {
   field<std::optional<std::string>, 2> nickname;
   field<std::shared_ptr<Person>, 3, ref> parent;
   field<std::shared_ptr<Person>, 4, nullable> guardian;
+  FORY_STRUCT(Person, name, age, nickname, parent, guardian);
 };
 
-FORY_FIELD_INFO(Person, name, age, nickname, parent, guardian);
-
 TEST(FieldStruct, BasicUsage) {
   Person p;
   p.name = "Alice";
@@ -262,7 +261,7 @@ TEST(FieldStruct, BasicUsage) {
 
 TEST(FieldStruct, FieldInfo) {
   Person p;
-  constexpr auto info = ForyFieldInfo(p);
+  constexpr auto info = meta::ForyFieldInfo(p);
 
   static_assert(info.Size == 5);
   static_assert(info.Name == "Person");
@@ -293,6 +292,8 @@ struct Document {
   std::shared_ptr<Document> reviewer;
   std::shared_ptr<Document> parent;
   std::unique_ptr<std::string> metadata;
+  FORY_STRUCT(Document, title, version, description, author, reviewer, parent,
+              metadata);
 };
 
 // Test struct with nullable + ref combined
@@ -300,18 +301,18 @@ struct Node {
   std::string name;
   std::shared_ptr<Node> left;
   std::shared_ptr<Node> right;
+  FORY_STRUCT(Node, name, left, right);
 };
 
 // Test with single field
 struct SingleField {
   int32_t value;
+  FORY_STRUCT(SingleField, value);
 };
 
 } // namespace field_tags_test
 
-// FORY_FIELD_INFO and FORY_FIELD_TAGS must be in global namespace
-FORY_FIELD_INFO(field_tags_test::Document, title, version, description, author,
-                reviewer, parent, metadata);
+// FORY_FIELD_TAGS must be in global namespace
 
 // Define field tags separately (non-invasive)
 FORY_FIELD_TAGS(field_tags_test::Document, (title, 0), // string: non-nullable
@@ -322,12 +323,9 @@ FORY_FIELD_TAGS(field_tags_test::Document, (title, 0), // 
string: non-nullable
                 (parent, 5, ref),         // shared_ptr: non-nullable, with ref
                 (metadata, 6, nullable)); // unique_ptr: nullable
 
-FORY_FIELD_INFO(field_tags_test::Node, name, left, right);
-
 FORY_FIELD_TAGS(field_tags_test::Node, (name, 0), (left, 1, nullable, ref),
                 (right, 2, nullable, ref));
 
-FORY_FIELD_INFO(field_tags_test::SingleField, value);
 FORY_FIELD_TAGS(field_tags_test::SingleField, (value, 0));
 
 namespace fory {
diff --git a/cpp/fory/meta/preprocessor.h b/cpp/fory/meta/preprocessor.h
index 771365f11..79f02eb14 100644
--- a/cpp/fory/meta/preprocessor.h
+++ b/cpp/fory/meta/preprocessor.h
@@ -63,6 +63,15 @@
 
 #define FORY_PP_TRIGGER_PARENTHESIS_(...) ,
 
+#define FORY_PP_EMPTY()
+
+#define FORY_PP_PROBE() ~, 1
+#define FORY_PP_CHECK_N(x, n, ...) n
+#define FORY_PP_CHECK(...) FORY_PP_CHECK_N(__VA_ARGS__, 0)
+
+#define FORY_PP_IS_PAREN(x) FORY_PP_CHECK(FORY_PP_IS_PAREN_PROBE x)
+#define FORY_PP_IS_PAREN_PROBE(...) FORY_PP_PROBE()
+
 #define FORY_PP_IS_EMPTY(...)                                                  
\
   FORY_PP_IS_EMPTY_I(                                                          
\
       FORY_PP_HAS_COMMA(__VA_ARGS__),                                          
\
@@ -80,6 +89,15 @@
 #define FORY_PP_NOT(x) FORY_PP_CONCAT(FORY_PP_NOT_, x)
 #define FORY_PP_HAS_ARGS(...) FORY_PP_NOT(FORY_PP_IS_EMPTY(__VA_ARGS__))
 
+#define FORY_PP_IF(c) FORY_PP_CONCAT(FORY_PP_IF_, c)
+#define FORY_PP_IF_1(t, f) t
+#define FORY_PP_IF_0(t, f) f
+
+#define FORY_PP_TUPLE_FIRST(tuple) FORY_PP_TUPLE_FIRST_IMPL tuple
+#define FORY_PP_TUPLE_FIRST_IMPL(a, ...) a
+#define FORY_PP_TUPLE_SECOND(tuple) FORY_PP_TUPLE_SECOND_IMPL tuple
+#define FORY_PP_TUPLE_SECOND_IMPL(a, b, ...) b
+
 #define FORY_PP_INVOKE(X, ...) X(__VA_ARGS__)
 
 // FORY_PP_FOREACH(X, a, b, c) -> X(a) X(b) X(c)
diff --git a/cpp/fory/row/schema.h b/cpp/fory/row/schema.h
index f6ac98646..05e72a8d0 100644
--- a/cpp/fory/row/schema.h
+++ b/cpp/fory/row/schema.h
@@ -89,7 +89,7 @@ public:
 
   virtual int num_fields() const { return 0; }
 
-  virtual FieldPtr field(int i) const { return nullptr; }
+  virtual FieldPtr field(int /*i*/) const { return nullptr; }
 
   virtual std::vector<FieldPtr> fields() const { return {}; }
 
diff --git a/cpp/fory/serialization/collection_serializer_test.cc 
b/cpp/fory/serialization/collection_serializer_test.cc
index e8dddf5f4..d66451079 100644
--- a/cpp/fory/serialization/collection_serializer_test.cc
+++ b/cpp/fory/serialization/collection_serializer_test.cc
@@ -39,31 +39,31 @@ struct Animal {
   virtual ~Animal() = default;
   virtual std::string speak() const = 0;
   int32_t age = 0;
+  FORY_STRUCT(Animal, age);
 };
-FORY_STRUCT(Animal, age);
 
 struct Dog : Animal {
   std::string speak() const override { return "Woof"; }
   std::string name;
+  FORY_STRUCT(Dog, FORY_BASE(Animal), name);
 };
-FORY_STRUCT(Dog, age, name);
 
 struct Cat : Animal {
   std::string speak() const override { return "Meow"; }
   int32_t lives = 9;
+  FORY_STRUCT(Cat, FORY_BASE(Animal), lives);
 };
-FORY_STRUCT(Cat, age, lives);
 
 // Holder structs for testing collections as struct fields
 struct VectorPolymorphicHolder {
   std::vector<std::shared_ptr<Animal>> animals;
+  FORY_STRUCT(VectorPolymorphicHolder, animals);
 };
-FORY_STRUCT(VectorPolymorphicHolder, animals);
 
 struct VectorHomogeneousHolder {
   std::vector<std::shared_ptr<Dog>> dogs;
+  FORY_STRUCT(VectorHomogeneousHolder, dogs);
 };
-FORY_STRUCT(VectorHomogeneousHolder, dogs);
 
 namespace {
 
@@ -285,13 +285,13 @@ TEST(CollectionSerializerTest, VectorEmpty) {
 
 struct VectorStringHolder {
   std::vector<std::string> strings;
+  FORY_STRUCT(VectorStringHolder, strings);
 };
-FORY_STRUCT(VectorStringHolder, strings);
 
 struct VectorIntHolder {
   std::vector<int32_t> numbers;
+  FORY_STRUCT(VectorIntHolder, numbers);
 };
-FORY_STRUCT(VectorIntHolder, numbers);
 
 TEST(CollectionSerializerTest, VectorStringRoundTrip) {
   auto fory = Fory::builder().xlang(true).build();
@@ -342,8 +342,8 @@ TEST(CollectionSerializerTest, VectorIntRoundTrip) {
 
 struct VectorOptionalHolder {
   std::vector<std::optional<std::string>> values;
+  FORY_STRUCT(VectorOptionalHolder, values);
 };
-FORY_STRUCT(VectorOptionalHolder, values);
 
 TEST(CollectionSerializerTest, VectorOptionalWithNulls) {
   auto fory = Fory::builder().xlang(true).build();
@@ -377,13 +377,13 @@ TEST(CollectionSerializerTest, VectorOptionalWithNulls) {
 
 struct ListStringHolder {
   std::list<std::string> strings;
+  FORY_STRUCT(ListStringHolder, strings);
 };
-FORY_STRUCT(ListStringHolder, strings);
 
 struct ListIntHolder {
   std::list<int32_t> numbers;
+  FORY_STRUCT(ListIntHolder, numbers);
 };
-FORY_STRUCT(ListIntHolder, numbers);
 
 TEST(CollectionSerializerTest, ListStringRoundTrip) {
   auto fory = Fory::builder().xlang(true).build();
@@ -459,13 +459,13 @@ TEST(CollectionSerializerTest, ListEmptyRoundTrip) {
 
 struct DequeStringHolder {
   std::deque<std::string> strings;
+  FORY_STRUCT(DequeStringHolder, strings);
 };
-FORY_STRUCT(DequeStringHolder, strings);
 
 struct DequeIntHolder {
   std::deque<int32_t> numbers;
+  FORY_STRUCT(DequeIntHolder, numbers);
 };
-FORY_STRUCT(DequeIntHolder, numbers);
 
 TEST(CollectionSerializerTest, DequeStringRoundTrip) {
   auto fory = Fory::builder().xlang(true).build();
@@ -539,13 +539,13 @@ TEST(CollectionSerializerTest, DequeEmptyRoundTrip) {
 
 struct ForwardListStringHolder {
   std::forward_list<std::string> strings;
+  FORY_STRUCT(ForwardListStringHolder, strings);
 };
-FORY_STRUCT(ForwardListStringHolder, strings);
 
 struct ForwardListIntHolder {
   std::forward_list<int32_t> numbers;
+  FORY_STRUCT(ForwardListIntHolder, numbers);
 };
-FORY_STRUCT(ForwardListIntHolder, numbers);
 
 TEST(CollectionSerializerTest, ForwardListStringRoundTrip) {
   auto fory = Fory::builder().xlang(true).build();
diff --git a/cpp/fory/serialization/field_serializer_test.cc 
b/cpp/fory/serialization/field_serializer_test.cc
index c40f4cf8f..142381e2f 100644
--- a/cpp/fory/serialization/field_serializer_test.cc
+++ b/cpp/fory/serialization/field_serializer_test.cc
@@ -54,8 +54,8 @@ struct FieldPerson {
            score.value == other.score.value &&
            active.value == other.active.value;
   }
+  FORY_STRUCT(FieldPerson, name, age, score, active);
 };
-FORY_STRUCT(FieldPerson, name, age, score, active);
 
 // Struct with optional fields
 struct FieldOptionalData {
@@ -68,8 +68,8 @@ struct FieldOptionalData {
            optional_age.value == other.optional_age.value &&
            optional_email.value == other.optional_email.value;
   }
+  FORY_STRUCT(FieldOptionalData, required_name, optional_age, optional_email);
 };
-FORY_STRUCT(FieldOptionalData, required_name, optional_age, optional_email);
 
 // Struct with shared_ptr fields (non-nullable by default)
 struct FieldSharedPtrHolder {
@@ -87,8 +87,8 @@ struct FieldSharedPtrHolder {
       return false;
     return true;
   }
+  FORY_STRUCT(FieldSharedPtrHolder, value, text);
 };
-FORY_STRUCT(FieldSharedPtrHolder, value, text);
 
 // Struct with nullable shared_ptr fields
 struct FieldNullableSharedPtr {
@@ -110,15 +110,15 @@ struct FieldNullableSharedPtr {
       return false;
     return true;
   }
+  FORY_STRUCT(FieldNullableSharedPtr, nullable_value, nullable_text);
 };
-FORY_STRUCT(FieldNullableSharedPtr, nullable_value, nullable_text);
 
 // Struct with unique_ptr fields
 struct FieldUniquePtrHolder {
   fory::field<std::unique_ptr<int32_t>, 0> value;
   fory::field<std::unique_ptr<int32_t>, 1, fory::nullable> nullable_value;
+  FORY_STRUCT(FieldUniquePtrHolder, value, nullable_value);
 };
-FORY_STRUCT(FieldUniquePtrHolder, value, nullable_value);
 
 // Nested struct for reference tracking tests
 struct FieldNode {
@@ -128,27 +128,27 @@ struct FieldNode {
   bool operator==(const FieldNode &other) const {
     return id.value == other.id.value && name.value == other.name.value;
   }
+  FORY_STRUCT(FieldNode, id, name);
 };
-FORY_STRUCT(FieldNode, id, name);
 
 // Struct with ref tracking for shared_ptr
 struct FieldRefTrackingHolder {
   fory::field<std::shared_ptr<FieldNode>, 0, fory::ref> first;
   fory::field<std::shared_ptr<FieldNode>, 1, fory::ref> second;
+  FORY_STRUCT(FieldRefTrackingHolder, first, second);
 };
-FORY_STRUCT(FieldRefTrackingHolder, first, second);
 
 // Struct with nullable + ref
 struct FieldNullableRefHolder {
   fory::field<std::shared_ptr<FieldNode>, 0, fory::nullable, fory::ref> node;
+  FORY_STRUCT(FieldNullableRefHolder, node);
 };
-FORY_STRUCT(FieldNullableRefHolder, node);
 
 // Struct with not_null + ref
 struct FieldNotNullRefHolder {
   fory::field<std::shared_ptr<FieldNode>, 0, fory::not_null, fory::ref> node;
+  FORY_STRUCT(FieldNotNullRefHolder, node);
 };
-FORY_STRUCT(FieldNotNullRefHolder, node);
 
 // Struct with vector of field-wrapped structs
 struct FieldVectorHolder {
@@ -157,8 +157,8 @@ struct FieldVectorHolder {
   bool operator==(const FieldVectorHolder &other) const {
     return nodes.value == other.nodes.value;
   }
+  FORY_STRUCT(FieldVectorHolder, nodes);
 };
-FORY_STRUCT(FieldVectorHolder, nodes);
 
 // Mixed struct: some fields with fory::field, some without
 struct MixedFieldStruct {
@@ -171,8 +171,8 @@ struct MixedFieldStruct {
            plain_age == other.plain_age &&
            field_score.value == other.field_score.value;
   }
+  FORY_STRUCT(MixedFieldStruct, field_name, plain_age, field_score);
 };
-FORY_STRUCT(MixedFieldStruct, field_name, plain_age, field_score);
 
 // ============================================================================
 // Test Implementation
@@ -669,7 +669,7 @@ TEST(FieldSerializerTest, FieldMetadataCompileTime) {
 
 // ============================================================================
 // FORY_FIELD_TAGS Serialization Tests
-// Structs defined in global namespace to allow template specialization
+// FORY_FIELD_TAGS remains namespace-scope, FORY_STRUCT is declared in-class
 // ============================================================================
 
 // Simple helper struct for testing FORY_FIELD_TAGS
@@ -680,9 +680,9 @@ struct TagsTestData {
   bool operator==(const TagsTestData &other) const {
     return content == other.content && value == other.value;
   }
+  FORY_STRUCT(TagsTestData, content, value);
 };
 
-FORY_STRUCT(TagsTestData, content, value);
 FORY_FIELD_TAGS(TagsTestData, (content, 0), (value, 1));
 
 // Pure C++ struct with FORY_FIELD_TAGS metadata (non-invasive)
@@ -706,10 +706,10 @@ struct TagsTestDocument {
     return title == other.title && version == other.version &&
            description == other.description && data_eq && opt_data_eq;
   }
+  FORY_STRUCT(TagsTestDocument, title, version, description, data,
+              optional_data);
 };
 
-FORY_STRUCT(TagsTestDocument, title, version, description, data, 
optional_data);
-
 FORY_FIELD_TAGS(TagsTestDocument, (title, 0), // string: non-nullable
                 (version, 1),                 // int: non-nullable
                 (description, 2),             // optional: inherently nullable
@@ -724,27 +724,27 @@ struct TagsRefNode {
   bool operator==(const TagsRefNode &other) const {
     return name == other.name && id == other.id;
   }
+  FORY_STRUCT(TagsRefNode, name, id);
 };
 
-FORY_STRUCT(TagsRefNode, name, id);
 FORY_FIELD_TAGS(TagsRefNode, (name, 0), (id, 1));
 
 // Struct with ref tracking via FORY_FIELD_TAGS
 struct TagsRefHolder {
   std::shared_ptr<TagsRefNode> first;
   std::shared_ptr<TagsRefNode> second;
+  FORY_STRUCT(TagsRefHolder, first, second);
 };
 
-FORY_STRUCT(TagsRefHolder, first, second);
 FORY_FIELD_TAGS(TagsRefHolder, (first, 0, ref), (second, 1, ref));
 
 // Struct with nullable + ref via FORY_FIELD_TAGS
 struct TagsNullableRefHolder {
   std::shared_ptr<TagsRefNode> required_node;
   std::shared_ptr<TagsRefNode> optional_node;
+  FORY_STRUCT(TagsNullableRefHolder, required_node, optional_node);
 };
 
-FORY_STRUCT(TagsNullableRefHolder, required_node, optional_node);
 FORY_FIELD_TAGS(TagsNullableRefHolder, (required_node, 0, ref),
                 (optional_node, 1, nullable, ref));
 
@@ -753,9 +753,9 @@ struct TagsTreeNode {
   std::string value;
   std::shared_ptr<TagsTreeNode> left;
   std::shared_ptr<TagsTreeNode> right;
+  FORY_STRUCT(TagsTreeNode, value, left, right);
 };
 
-FORY_STRUCT(TagsTreeNode, value, left, right);
 FORY_FIELD_TAGS(TagsTreeNode, (value, 0), (left, 1, nullable, ref),
                 (right, 2, nullable, ref));
 
diff --git a/cpp/fory/serialization/fory.h b/cpp/fory/serialization/fory.h
index 501df1f07..496a8e62e 100644
--- a/cpp/fory/serialization/fory.h
+++ b/cpp/fory/serialization/fory.h
@@ -221,8 +221,10 @@ public:
   ///
   /// Example:
   /// ```cpp
-  /// struct MyStruct { int32_t value; };
-  /// FORY_STRUCT(MyStruct, value);
+  /// struct MyStruct {
+  ///   int32_t value;
+  ///   FORY_STRUCT(MyStruct, value);
+  /// };
   ///
   /// fory.register_struct<MyStruct>(1);
   /// ```
diff --git a/cpp/fory/serialization/map_serializer_test.cc 
b/cpp/fory/serialization/map_serializer_test.cc
index 40caac43a..bf91e939f 100644
--- a/cpp/fory/serialization/map_serializer_test.cc
+++ b/cpp/fory/serialization/map_serializer_test.cc
@@ -342,6 +342,8 @@ struct BaseKey {
   bool operator==(const BaseKey &other) const {
     return id == other.id && type_name() == other.type_name();
   }
+
+  FORY_STRUCT(BaseKey, id);
 };
 
 struct DerivedKeyA : public BaseKey {
@@ -352,6 +354,7 @@ struct DerivedKeyA : public BaseKey {
       : BaseKey(id), data(std::move(data)) {}
 
   std::string type_name() const override { return "DerivedKeyA"; }
+  FORY_STRUCT(DerivedKeyA, FORY_BASE(BaseKey), data);
 };
 
 struct DerivedKeyB : public BaseKey {
@@ -361,6 +364,7 @@ struct DerivedKeyB : public BaseKey {
   DerivedKeyB(int32_t id, double value) : BaseKey(id), value(value) {}
 
   std::string type_name() const override { return "DerivedKeyB"; }
+  FORY_STRUCT(DerivedKeyB, FORY_BASE(BaseKey), value);
 };
 
 // Base class for polymorphic values
@@ -378,6 +382,8 @@ struct BaseValue {
     return name == other.name && get_priority() == other.get_priority() &&
            type_name() == other.type_name();
   }
+
+  FORY_STRUCT(BaseValue, name);
 };
 
 struct DerivedValueX : public BaseValue {
@@ -389,6 +395,7 @@ struct DerivedValueX : public BaseValue {
 
   int32_t get_priority() const override { return priority; }
   std::string type_name() const override { return "DerivedValueX"; }
+  FORY_STRUCT(DerivedValueX, FORY_BASE(BaseValue), priority);
 };
 
 struct DerivedValueY : public BaseValue {
@@ -402,15 +409,9 @@ struct DerivedValueY : public BaseValue {
 
   int32_t get_priority() const override { return priority; }
   std::string type_name() const override { return "DerivedValueY"; }
+  FORY_STRUCT(DerivedValueY, FORY_BASE(BaseValue), priority, tags);
 };
 
-// FORY_STRUCT macro invocations for polymorphic DERIVED types only
-// Must be inside the namespace where the types are defined
-FORY_STRUCT(DerivedKeyA, id, data);
-FORY_STRUCT(DerivedKeyB, id, value);
-FORY_STRUCT(DerivedValueX, name, priority);
-FORY_STRUCT(DerivedValueY, name, priority, tags);
-
 } // namespace polymorphic_test
 
 // Type IDs for polymorphic types
diff --git a/cpp/fory/serialization/serialization_test.cc 
b/cpp/fory/serialization/serialization_test.cc
index bf584bdbd..9b7ee8e98 100644
--- a/cpp/fory/serialization/serialization_test.cc
+++ b/cpp/fory/serialization/serialization_test.cc
@@ -31,7 +31,7 @@
 #include "fory/type/type.h"
 
 // ============================================================================
-// Test Struct Definitions (must be at global scope for FORY_STRUCT)
+// Test Struct Definitions (FORY_STRUCT is declared inside each struct)
 // ============================================================================
 
 struct SimpleStruct {
@@ -41,10 +41,9 @@ struct SimpleStruct {
   bool operator==(const SimpleStruct &other) const {
     return x == other.x && y == other.y;
   }
+  FORY_STRUCT(SimpleStruct, x, y);
 };
 
-FORY_STRUCT(SimpleStruct, x, y);
-
 struct ComplexStruct {
   std::string name;
   int32_t age;
@@ -53,10 +52,9 @@ struct ComplexStruct {
   bool operator==(const ComplexStruct &other) const {
     return name == other.name && age == other.age && hobbies == other.hobbies;
   }
+  FORY_STRUCT(ComplexStruct, name, age, hobbies);
 };
 
-FORY_STRUCT(ComplexStruct, name, age, hobbies);
-
 struct NestedStruct {
   SimpleStruct point;
   std::string label;
@@ -64,10 +62,9 @@ struct NestedStruct {
   bool operator==(const NestedStruct &other) const {
     return point == other.point && label == other.label;
   }
+  FORY_STRUCT(NestedStruct, point, label);
 };
 
-FORY_STRUCT(NestedStruct, point, label);
-
 enum class Color { RED, GREEN, BLUE };
 enum class LegacyStatus : int32_t { NEG = -3, ZERO = 0, LARGE = 42 };
 FORY_ENUM(LegacyStatus, NEG, ZERO, LARGE);
diff --git a/cpp/fory/serialization/serializer_traits.h 
b/cpp/fory/serialization/serializer_traits.h
index 786777690..cd3ea0480 100644
--- a/cpp/fory/serialization/serializer_traits.h
+++ b/cpp/fory/serialization/serializer_traits.h
@@ -216,9 +216,9 @@ inline const nullable_element_t<T> &deref_nullable(const T 
&value) {
 // Fory Struct Detection
 // ============================================================================
 
-/// Check if type has FORY_FIELD_INFO defined via ADL
+/// Check if type has FORY_STRUCT defined via member lookup or ADL.
 /// This trait only evaluates to true if ForyFieldInfo is available AND doesn't
-/// trigger static_assert
+/// trigger static_assert.
 template <typename T, typename = void>
 struct has_fory_field_info : std::false_type {};
 
@@ -230,7 +230,7 @@ struct has_fory_field_info<
 template <typename T>
 inline constexpr bool has_fory_field_info_v = has_fory_field_info<T>::value;
 
-/// Check if type is serializable (has both FORY_FIELD_INFO and
+/// Check if type is serializable (has both FORY_STRUCT and
 /// SerializationMeta)
 template <typename T, typename = void>
 struct is_fory_serializable : std::false_type {};
diff --git a/cpp/fory/serialization/smart_ptr_serializer_test.cc 
b/cpp/fory/serialization/smart_ptr_serializer_test.cc
index d69eaff97..8c4b03c29 100644
--- a/cpp/fory/serialization/smart_ptr_serializer_test.cc
+++ b/cpp/fory/serialization/smart_ptr_serializer_test.cc
@@ -29,24 +29,24 @@ namespace serialization {
 
 struct OptionalIntHolder {
   std::optional<int32_t> value;
+  FORY_STRUCT(OptionalIntHolder, value);
 };
-FORY_STRUCT(OptionalIntHolder, value);
 
 struct OptionalSharedHolder {
   std::optional<std::shared_ptr<int32_t>> value;
+  FORY_STRUCT(OptionalSharedHolder, value);
 };
-FORY_STRUCT(OptionalSharedHolder, value);
 
 struct SharedPair {
   std::shared_ptr<int32_t> first;
   std::shared_ptr<int32_t> second;
+  FORY_STRUCT(SharedPair, first, second);
 };
-FORY_STRUCT(SharedPair, first, second);
 
 struct UniqueHolder {
   std::unique_ptr<int32_t> value;
+  FORY_STRUCT(UniqueHolder, value);
 };
-FORY_STRUCT(UniqueHolder, value);
 
 namespace {
 
@@ -188,30 +188,30 @@ struct Base {
   virtual ~Base() = default;
   virtual std::string get_type() const = 0;
   int32_t base_value = 0;
+  FORY_STRUCT(Base, base_value);
 };
-FORY_STRUCT(Base, base_value);
 
 struct Derived1 : Base {
   std::string get_type() const override { return "Derived1"; }
   std::string derived1_data;
+  FORY_STRUCT(Derived1, FORY_BASE(Base), derived1_data);
 };
-FORY_STRUCT(Derived1, base_value, derived1_data);
 
 struct Derived2 : Base {
   std::string get_type() const override { return "Derived2"; }
   int32_t derived2_data = 0;
+  FORY_STRUCT(Derived2, FORY_BASE(Base), derived2_data);
 };
-FORY_STRUCT(Derived2, base_value, derived2_data);
 
 struct PolymorphicSharedHolder {
   std::shared_ptr<Base> ptr;
+  FORY_STRUCT(PolymorphicSharedHolder, ptr);
 };
-FORY_STRUCT(PolymorphicSharedHolder, ptr);
 
 struct PolymorphicUniqueHolder {
   std::unique_ptr<Base> ptr;
+  FORY_STRUCT(PolymorphicUniqueHolder, ptr);
 };
-FORY_STRUCT(PolymorphicUniqueHolder, ptr);
 
 TEST(SmartPtrSerializerTest, PolymorphicSharedPtrDerived1) {
   auto fory = create_serializer(true);
@@ -334,14 +334,14 @@ struct NestedContainer {
   virtual ~NestedContainer() = default;
   int32_t value = 0;
   std::shared_ptr<NestedContainer> nested;
+  FORY_STRUCT(NestedContainer, value, nested);
 };
-FORY_STRUCT(NestedContainer, value, nested);
 
 // Holder struct to wrap nested container in a polymorphic shared_ptr
 struct NestedContainerHolder {
   std::shared_ptr<NestedContainer> ptr;
+  FORY_STRUCT(NestedContainerHolder, ptr);
 };
-FORY_STRUCT(NestedContainerHolder, ptr);
 
 TEST(SmartPtrSerializerTest, MaxDynDepthExceeded) {
   // Create Fory with max_dyn_depth=2
@@ -443,8 +443,8 @@ struct PolymorphicBaseForMono {
   virtual std::string name() const { return "PolymorphicBaseForMono"; }
   int32_t value = 0;
   std::string data;
+  FORY_STRUCT(PolymorphicBaseForMono, value, data);
 };
-FORY_STRUCT(PolymorphicBaseForMono, value, data);
 
 // Holder with non-dynamic field using fory::field<>
 struct NonDynamicFieldHolder {
@@ -453,8 +453,8 @@ struct NonDynamicFieldHolder {
   fory::field<std::shared_ptr<PolymorphicBaseForMono>, 0, fory::nullable,
               fory::dynamic<false>>
       ptr;
+  FORY_STRUCT(NonDynamicFieldHolder, ptr);
 };
-FORY_STRUCT(NonDynamicFieldHolder, ptr);
 
 TEST(SmartPtrSerializerTest, NonDynamicFieldWithForyField) {
   NonDynamicFieldHolder original;
diff --git a/cpp/fory/serialization/struct_compatible_test.cc 
b/cpp/fory/serialization/struct_compatible_test.cc
index 0cabf9476..5777447cf 100644
--- a/cpp/fory/serialization/struct_compatible_test.cc
+++ b/cpp/fory/serialization/struct_compatible_test.cc
@@ -49,8 +49,8 @@ struct PersonV1 {
   bool operator==(const PersonV1 &other) const {
     return name == other.name && age == other.age;
   }
+  FORY_STRUCT(PersonV1, name, age);
 };
-FORY_STRUCT(PersonV1, name, age);
 
 // V2: Added email field
 struct PersonV2 {
@@ -61,8 +61,8 @@ struct PersonV2 {
   bool operator==(const PersonV2 &other) const {
     return name == other.name && age == other.age && email == other.email;
   }
+  FORY_STRUCT(PersonV2, name, age, email);
 };
-FORY_STRUCT(PersonV2, name, age, email);
 
 // V3: Added multiple fields
 struct PersonV3 {
@@ -76,8 +76,8 @@ struct PersonV3 {
     return name == other.name && age == other.age && email == other.email &&
            phone == other.phone && address == other.address;
   }
+  FORY_STRUCT(PersonV3, name, age, email, phone, address);
 };
-FORY_STRUCT(PersonV3, name, age, email, phone, address);
 
 // ============================================================================
 // Test Case 2: Removing Fields (Forward Compatibility)
@@ -96,8 +96,8 @@ struct UserFull {
            email == other.email && password_hash == other.password_hash &&
            login_count == other.login_count;
   }
+  FORY_STRUCT(UserFull, id, username, email, password_hash, login_count);
 };
-FORY_STRUCT(UserFull, id, username, email, password_hash, login_count);
 
 // Minimal schema (removed 3 fields)
 struct UserMinimal {
@@ -107,8 +107,8 @@ struct UserMinimal {
   bool operator==(const UserMinimal &other) const {
     return id == other.id && username == other.username;
   }
+  FORY_STRUCT(UserMinimal, id, username);
 };
-FORY_STRUCT(UserMinimal, id, username);
 
 // ============================================================================
 // Test Case 3: Field Reordering
@@ -124,8 +124,8 @@ struct ConfigOriginal {
     return host == other.host && port == other.port &&
            enable_ssl == other.enable_ssl && protocol == other.protocol;
   }
+  FORY_STRUCT(ConfigOriginal, host, port, enable_ssl, protocol);
 };
-FORY_STRUCT(ConfigOriginal, host, port, enable_ssl, protocol);
 
 // Reordered fields (different order)
 struct ConfigReordered {
@@ -138,8 +138,8 @@ struct ConfigReordered {
     return host == other.host && port == other.port &&
            enable_ssl == other.enable_ssl && protocol == other.protocol;
   }
+  FORY_STRUCT(ConfigReordered, enable_ssl, protocol, host, port);
 };
-FORY_STRUCT(ConfigReordered, enable_ssl, protocol, host, port);
 
 // ============================================================================
 // Test Case 4: Nested Struct Evolution
@@ -152,8 +152,8 @@ struct AddressV1 {
   bool operator==(const AddressV1 &other) const {
     return street == other.street && city == other.city;
   }
+  FORY_STRUCT(AddressV1, street, city);
 };
-FORY_STRUCT(AddressV1, street, city);
 
 struct AddressV2 {
   std::string street;
@@ -165,8 +165,8 @@ struct AddressV2 {
     return street == other.street && city == other.city &&
            country == other.country && zipcode == other.zipcode;
   }
+  FORY_STRUCT(AddressV2, street, city, country, zipcode);
 };
-FORY_STRUCT(AddressV2, street, city, country, zipcode);
 
 struct EmployeeV1 {
   std::string name;
@@ -175,8 +175,8 @@ struct EmployeeV1 {
   bool operator==(const EmployeeV1 &other) const {
     return name == other.name && home_address == other.home_address;
   }
+  FORY_STRUCT(EmployeeV1, name, home_address);
 };
-FORY_STRUCT(EmployeeV1, name, home_address);
 
 struct EmployeeV2 {
   std::string name;
@@ -187,8 +187,8 @@ struct EmployeeV2 {
     return name == other.name && home_address == other.home_address &&
            employee_id == other.employee_id;
   }
+  FORY_STRUCT(EmployeeV2, name, home_address, employee_id);
 };
-FORY_STRUCT(EmployeeV2, name, home_address, employee_id);
 
 // ============================================================================
 // Test Case 5: Collection Field Evolution
@@ -201,8 +201,8 @@ struct ProductV1 {
   bool operator==(const ProductV1 &other) const {
     return name == other.name && price == other.price;
   }
+  FORY_STRUCT(ProductV1, name, price);
 };
-FORY_STRUCT(ProductV1, name, price);
 
 struct ProductV2 {
   std::string name;
@@ -214,8 +214,8 @@ struct ProductV2 {
     return name == other.name && price == other.price && tags == other.tags &&
            attributes == other.attributes;
   }
+  FORY_STRUCT(ProductV2, name, price, tags, attributes);
 };
-FORY_STRUCT(ProductV2, name, price, tags, attributes);
 
 // ============================================================================
 // TESTS
diff --git a/cpp/fory/serialization/struct_serializer.h 
b/cpp/fory/serialization/struct_serializer.h
index 11bc7a9e0..efa396e62 100644
--- a/cpp/fory/serialization/struct_serializer.h
+++ b/cpp/fory/serialization/struct_serializer.h
@@ -43,6 +43,8 @@
 namespace fory {
 namespace serialization {
 
+using meta::ForyFieldInfo;
+
 /// Field type markers for collection fields in compatible/evolution mode.
 /// These match Java's FieldResolver.FieldTypes values.
 constexpr int8_t FIELD_TYPE_OBJECT = 0;
@@ -54,87 +56,21 @@ constexpr int8_t FIELD_TYPE_MAP_KV_FINAL = 4;
 /// Serialization metadata for a type.
 ///
 /// This template is populated automatically when `FORY_STRUCT` is used to
-/// register a type. The registration macro defines an ADL-visible marker
-/// function which this trait detects in order to enable serialization. The
-/// field count is derived from the generated `ForyFieldInfo` metadata.
+/// register a type. The registration macro defines a constexpr metadata
+/// function that is discovered via member lookup or ADL. The field count is
+/// derived from the generated `ForyFieldInfo` metadata.
 template <typename T, typename Enable> struct SerializationMeta {
   static constexpr bool is_serializable = false;
   static constexpr size_t field_count = 0;
 };
 template <typename T>
-struct SerializationMeta<
-    T, std::void_t<decltype(ForyStructMarker(std::declval<const T &>()))>> {
+struct SerializationMeta<T,
+                         std::enable_if_t<meta::HasForyStructInfo<T>::value>> {
   static constexpr bool is_serializable = true;
   static constexpr size_t field_count =
       decltype(ForyFieldInfo(std::declval<const T &>()))::Size;
 };
 
-/// Main serialization registration macro.
-///
-/// This macro must be placed in the same namespace as the type for ADL
-/// (Argument-Dependent Lookup).
-///
-/// It builds upon FORY_FIELD_INFO to add serialization-specific metadata:
-/// - Marks the type as serializable
-/// - Provides compile-time metadata access
-///
-/// Example:
-/// ```cpp
-/// namespace myapp {
-///   struct Person {
-///     std::string name;
-///     int32_t age;
-///   };
-///   FORY_STRUCT(Person, name, age);
-/// }
-/// ```
-///
-/// After expansion, the type can be serialized using Fory:
-/// ```cpp
-/// fory::serialization::Fory fory;
-/// myapp::Person person{"Alice", 30};
-/// auto bytes = fory.serialize(person);
-/// ```
-/// Main struct registration macro.
-/// TypeIndex uses the fallback (type_fallback_hash based on PRETTY_FUNCTION)
-/// which provides unique type identification without namespace issues.
-#define FORY_STRUCT_TYPE_ONLY(Type)                                            
\
-  static_assert(std::is_class_v<Type>, "it must be a class type");             
\
-  template <typename> struct ForyFieldInfoImpl;                                
\
-  template <> struct ForyFieldInfoImpl<Type> {                                 
\
-    static inline constexpr size_t Size = 0;                                   
\
-    static inline constexpr std::string_view Name = #Type;                     
\
-    static inline constexpr std::array<std::string_view, Size> Names = {};     
\
-    static inline constexpr auto Ptrs = std::tuple{};                          
\
-  };                                                                           
\
-  static_assert(                                                               
\
-      fory::meta::IsValidFieldInfo<ForyFieldInfoImpl<Type>>(),                 
\
-      "duplicated fields in FORY_FIELD_INFO arguments are detected");          
\
-  inline constexpr auto ForyFieldInfo(const Type &) noexcept {                 
\
-    return ForyFieldInfoImpl<Type>{};                                          
\
-  }                                                                            
\
-  inline constexpr std::true_type ForyStructMarker(const Type &) noexcept {    
\
-    return {};                                                                 
\
-  }                                                                            
\
-  static_assert(static_cast<std::true_type (*)(const Type &) noexcept>(        
\
-                    &ForyStructMarker) != nullptr,                             
\
-                "ForyStructMarker must be declared");
-
-#define FORY_STRUCT_WITH_FIELDS(Type, ...)                                     
\
-  FORY_FIELD_INFO(Type, __VA_ARGS__)                                           
\
-  inline constexpr std::true_type ForyStructMarker(const Type &) noexcept {    
\
-    return {};                                                                 
\
-  }                                                                            
\
-  static_assert(static_cast<std::true_type (*)(const Type &) noexcept>(        
\
-                    &ForyStructMarker) != nullptr,                             
\
-                "ForyStructMarker must be declared");
-
-#define FORY_STRUCT_1(Type, ...) FORY_STRUCT_TYPE_ONLY(Type)
-#define FORY_STRUCT_0(Type, ...) FORY_STRUCT_WITH_FIELDS(Type, __VA_ARGS__)
-
-#define FORY_STRUCT(Type, ...)                                                 
\
-  FORY_PP_CONCAT(FORY_STRUCT_, FORY_PP_IS_EMPTY(__VA_ARGS__))(Type, 
__VA_ARGS__)
-
 namespace detail {
 
 /// Helper to check if a TypeId represents a primitive type.
@@ -558,7 +494,7 @@ template <typename T> struct CompileTimeFieldHelpers {
   using FieldDescriptor = decltype(ForyFieldInfo(std::declval<const T &>()));
   static constexpr size_t FieldCount = FieldDescriptor::Size;
   static inline constexpr auto Names = FieldDescriptor::Names;
-  static inline constexpr auto Ptrs = FieldDescriptor::Ptrs;
+  static inline constexpr auto Ptrs = FieldDescriptor::Ptrs();
   using FieldPtrs = decltype(Ptrs);
 
   template <size_t Index> static constexpr uint32_t field_type_id() {
@@ -1590,7 +1526,8 @@ FORY_ALWAYS_INLINE void write_single_fixed_field(const T 
&obj, Buffer &buffer,
   constexpr size_t field_offset =
       compute_fixed_field_write_offset<T, SortedIdx>();
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptr = std::get<original_index>(decltype(field_info)::Ptrs);
+  const auto field_ptr =
+      std::get<original_index>(decltype(field_info)::PtrsRef());
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
   using FieldType = unwrap_field_t<RawFieldType>;
@@ -1631,7 +1568,8 @@ FORY_ALWAYS_INLINE void write_single_varint_field(const T 
&obj, Buffer &buffer,
   using Helpers = CompileTimeFieldHelpers<T>;
   constexpr size_t original_index = Helpers::sorted_indices[SortedPos];
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptr = std::get<original_index>(decltype(field_info)::Ptrs);
+  const auto field_ptr =
+      std::get<original_index>(decltype(field_info)::PtrsRef());
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
   using FieldType = unwrap_field_t<RawFieldType>;
@@ -1673,7 +1611,8 @@ write_single_remaining_field(const T &obj, Buffer 
&buffer, uint32_t &offset) {
   using Helpers = CompileTimeFieldHelpers<T>;
   constexpr size_t original_index = Helpers::sorted_indices[SortedPos];
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptr = std::get<original_index>(decltype(field_info)::Ptrs);
+  const auto field_ptr =
+      std::get<original_index>(decltype(field_info)::PtrsRef());
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
   using FieldType = unwrap_field_t<RawFieldType>;
@@ -1963,7 +1902,7 @@ void write_field_at_sorted_position(const T &obj, 
WriteContext &ctx,
   using Helpers = CompileTimeFieldHelpers<T>;
   constexpr size_t original_index = Helpers::sorted_indices[SortedPosition];
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptrs = decltype(field_info)::Ptrs;
+  const auto &field_ptrs = decltype(field_info)::PtrsRef();
   write_single_field<T, original_index>(obj, ctx, field_ptrs, has_generics);
 }
 
@@ -2154,7 +2093,7 @@ template <size_t Index, typename T>
 void read_single_field_by_index(T &obj, ReadContext &ctx) {
   using Helpers = CompileTimeFieldHelpers<T>;
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptrs = decltype(field_info)::Ptrs;
+  const auto &field_ptrs = decltype(field_info)::PtrsRef();
   const auto field_ptr = std::get<Index>(field_ptrs);
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
@@ -2339,7 +2278,7 @@ void read_single_field_by_index_compatible(T &obj, 
ReadContext &ctx,
                                            uint32_t remote_type_id) {
   using Helpers = CompileTimeFieldHelpers<T>;
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptrs = decltype(field_info)::Ptrs;
+  const auto &field_ptrs = decltype(field_info)::PtrsRef();
   const auto field_ptr = std::get<Index>(field_ptrs);
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
@@ -2598,7 +2537,8 @@ FORY_ALWAYS_INLINE void read_single_fixed_field(T &obj, 
Buffer &buffer,
   constexpr size_t original_index = Helpers::sorted_indices[SortedIdx];
   constexpr size_t field_offset = compute_fixed_field_offset<T, SortedIdx>();
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptr = std::get<original_index>(decltype(field_info)::Ptrs);
+  const auto field_ptr =
+      std::get<original_index>(decltype(field_info)::PtrsRef());
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
   using FieldType = unwrap_field_t<RawFieldType>;
@@ -2676,7 +2616,8 @@ FORY_ALWAYS_INLINE void read_single_varint_field(T &obj, 
Buffer &buffer,
   using Helpers = CompileTimeFieldHelpers<T>;
   constexpr size_t original_index = Helpers::sorted_indices[SortedPos];
   const auto field_info = ForyFieldInfo(obj);
-  const auto field_ptr = std::get<original_index>(decltype(field_info)::Ptrs);
+  const auto field_ptr =
+      std::get<original_index>(decltype(field_info)::PtrsRef());
   using RawFieldType =
       typename meta::RemoveMemberPointerCVRefT<decltype(field_ptr)>;
   using FieldType = unwrap_field_t<RawFieldType>;
diff --git a/cpp/fory/serialization/struct_test.cc 
b/cpp/fory/serialization/struct_test.cc
index 2a5882d26..4d5d55cb5 100644
--- a/cpp/fory/serialization/struct_test.cc
+++ b/cpp/fory/serialization/struct_test.cc
@@ -39,7 +39,8 @@
 #include <vector>
 
 // ============================================================================
-// ALL STRUCT DEFINITIONS MUST BE AT GLOBAL SCOPE (for FORY_STRUCT macro)
+// FORY_STRUCT can be declared inside the struct/class definition or at
+// namespace scope.
 // ============================================================================
 
 // Edge cases
@@ -48,8 +49,8 @@ struct SingleFieldStruct {
   bool operator==(const SingleFieldStruct &other) const {
     return value == other.value;
   }
+  FORY_STRUCT(SingleFieldStruct, value);
 };
-FORY_STRUCT(SingleFieldStruct, value);
 
 struct TwoFieldStruct {
   int32_t x;
@@ -57,8 +58,8 @@ struct TwoFieldStruct {
   bool operator==(const TwoFieldStruct &other) const {
     return x == other.x && y == other.y;
   }
+  FORY_STRUCT(TwoFieldStruct, x, y);
 };
-FORY_STRUCT(TwoFieldStruct, x, y);
 
 struct ManyFieldsStruct {
   bool b1;
@@ -75,8 +76,27 @@ struct ManyFieldsStruct {
            i32 == other.i32 && i64 == other.i64 && f32 == other.f32 &&
            f64 == other.f64 && str == other.str;
   }
+  FORY_STRUCT(ManyFieldsStruct, b1, i8, i16, i32, i64, f32, f64, str);
+};
+
+class PrivateFieldsStruct {
+public:
+  PrivateFieldsStruct() = default;
+  PrivateFieldsStruct(int32_t id, std::string name, std::vector<int32_t> 
scores)
+      : id_(id), name_(std::move(name)), scores_(std::move(scores)) {}
+
+  bool operator==(const PrivateFieldsStruct &other) const {
+    return id_ == other.id_ && name_ == other.name_ && scores_ == 
other.scores_;
+  }
+
+private:
+  int32_t id_ = 0;
+  std::string name_;
+  std::vector<int32_t> scores_;
+
+public:
+  FORY_STRUCT(PrivateFieldsStruct, id_, name_, scores_);
 };
-FORY_STRUCT(ManyFieldsStruct, b1, i8, i16, i32, i64, f32, f64, str);
 
 // All primitives
 struct AllPrimitivesStruct {
@@ -100,10 +120,10 @@ struct AllPrimitivesStruct {
            uint64_val == other.uint64_val && float_val == other.float_val &&
            double_val == other.double_val;
   }
+  FORY_STRUCT(AllPrimitivesStruct, bool_val, int8_val, int16_val, int32_val,
+              int64_val, uint8_val, uint16_val, uint32_val, uint64_val,
+              float_val, double_val);
 };
-FORY_STRUCT(AllPrimitivesStruct, bool_val, int8_val, int16_val, int32_val,
-            int64_val, uint8_val, uint16_val, uint32_val, uint64_val, 
float_val,
-            double_val);
 
 // String handling
 struct StringTestStruct {
@@ -116,8 +136,8 @@ struct StringTestStruct {
     return empty == other.empty && ascii == other.ascii && utf8 == other.utf8 
&&
            long_text == other.long_text;
   }
+  FORY_STRUCT(StringTestStruct, empty, ascii, utf8, long_text);
 };
-FORY_STRUCT(StringTestStruct, empty, ascii, utf8, long_text);
 
 // Nested structs
 struct Point2D {
@@ -126,8 +146,8 @@ struct Point2D {
   bool operator==(const Point2D &other) const {
     return x == other.x && y == other.y;
   }
+  FORY_STRUCT(Point2D, x, y);
 };
-FORY_STRUCT(Point2D, x, y);
 
 struct Point3D {
   int32_t x;
@@ -136,8 +156,8 @@ struct Point3D {
   bool operator==(const Point3D &other) const {
     return x == other.x && y == other.y && z == other.z;
   }
+  FORY_STRUCT(Point3D, x, y, z);
 };
-FORY_STRUCT(Point3D, x, y, z);
 
 struct Rectangle {
   Point2D top_left;
@@ -145,8 +165,8 @@ struct Rectangle {
   bool operator==(const Rectangle &other) const {
     return top_left == other.top_left && bottom_right == other.bottom_right;
   }
+  FORY_STRUCT(Rectangle, top_left, bottom_right);
 };
-FORY_STRUCT(Rectangle, top_left, bottom_right);
 
 struct BoundingBox {
   Rectangle bounds;
@@ -154,8 +174,8 @@ struct BoundingBox {
   bool operator==(const BoundingBox &other) const {
     return bounds == other.bounds && label == other.label;
   }
+  FORY_STRUCT(BoundingBox, bounds, label);
 };
-FORY_STRUCT(BoundingBox, bounds, label);
 
 struct Scene {
   Point3D camera;
@@ -165,8 +185,8 @@ struct Scene {
     return camera == other.camera && light == other.light &&
            viewport == other.viewport;
   }
+  FORY_STRUCT(Scene, camera, light, viewport);
 };
-FORY_STRUCT(Scene, camera, light, viewport);
 
 // Containers
 struct VectorStruct {
@@ -178,8 +198,8 @@ struct VectorStruct {
     return numbers == other.numbers && strings == other.strings &&
            points == other.points;
   }
+  FORY_STRUCT(VectorStruct, numbers, strings, points);
 };
-FORY_STRUCT(VectorStruct, numbers, strings, points);
 
 struct MapStruct {
   std::map<std::string, int32_t> str_to_int;
@@ -190,8 +210,8 @@ struct MapStruct {
     return str_to_int == other.str_to_int && int_to_str == other.int_to_str &&
            named_points == other.named_points;
   }
+  FORY_STRUCT(MapStruct, str_to_int, int_to_str, named_points);
 };
-FORY_STRUCT(MapStruct, str_to_int, int_to_str, named_points);
 
 struct NestedContainerStruct {
   std::vector<std::vector<int32_t>> matrix;
@@ -200,8 +220,8 @@ struct NestedContainerStruct {
   bool operator==(const NestedContainerStruct &other) const {
     return matrix == other.matrix && grouped_numbers == other.grouped_numbers;
   }
+  FORY_STRUCT(NestedContainerStruct, matrix, grouped_numbers);
 };
-FORY_STRUCT(NestedContainerStruct, matrix, grouped_numbers);
 
 // Optional fields
 struct OptionalFieldsStruct {
@@ -214,8 +234,8 @@ struct OptionalFieldsStruct {
     return name == other.name && age == other.age && email == other.email &&
            location == other.location;
   }
+  FORY_STRUCT(OptionalFieldsStruct, name, age, email, location);
 };
-FORY_STRUCT(OptionalFieldsStruct, name, age, email, location);
 
 // Enums
 enum class Color { RED = 0, GREEN = 1, BLUE = 2 };
@@ -227,8 +247,8 @@ struct EnumStruct {
   bool operator==(const EnumStruct &other) const {
     return color == other.color && status == other.status;
   }
+  FORY_STRUCT(EnumStruct, color, status);
 };
-FORY_STRUCT(EnumStruct, color, status);
 
 // Real-world scenarios
 struct UserProfile {
@@ -248,9 +268,9 @@ struct UserProfile {
            follower_count == other.follower_count &&
            is_verified == other.is_verified;
   }
+  FORY_STRUCT(UserProfile, user_id, username, email, bio, interests, metadata,
+              follower_count, is_verified);
 };
-FORY_STRUCT(UserProfile, user_id, username, email, bio, interests, metadata,
-            follower_count, is_verified);
 
 struct Product {
   int64_t product_id;
@@ -267,9 +287,9 @@ struct Product {
            stock == other.stock && tags == other.tags &&
            attributes == other.attributes;
   }
+  FORY_STRUCT(Product, product_id, name, description, price, stock, tags,
+              attributes);
 };
-FORY_STRUCT(Product, product_id, name, description, price, stock, tags,
-            attributes);
 
 struct OrderItem {
   int64_t product_id;
@@ -280,8 +300,8 @@ struct OrderItem {
     return product_id == other.product_id && quantity == other.quantity &&
            unit_price == other.unit_price;
   }
+  FORY_STRUCT(OrderItem, product_id, quantity, unit_price);
 };
-FORY_STRUCT(OrderItem, product_id, quantity, unit_price);
 
 struct Order {
   int64_t order_id;
@@ -295,8 +315,53 @@ struct Order {
            items == other.items && total_amount == other.total_amount &&
            order_status == other.order_status;
   }
+  FORY_STRUCT(Order, order_id, customer_id, items, total_amount, order_status);
 };
-FORY_STRUCT(Order, order_id, customer_id, items, total_amount, order_status);
+
+namespace nested_test {
+namespace inner {
+
+struct InClassStruct {
+  int32_t id;
+  std::string name;
+  bool operator==(const InClassStruct &other) const {
+    return id == other.id && name == other.name;
+  }
+  FORY_STRUCT(InClassStruct, id, name);
+};
+
+struct OutClassStruct {
+  int32_t id;
+  std::string name;
+  bool operator==(const OutClassStruct &other) const {
+    return id == other.id && name == other.name;
+  }
+};
+
+FORY_STRUCT(OutClassStruct, id, name);
+
+} // namespace inner
+} // namespace nested_test
+
+namespace external_test {
+
+struct ExternalStruct {
+  int32_t id;
+  std::string name;
+  bool operator==(const ExternalStruct &other) const {
+    return id == other.id && name == other.name;
+  }
+};
+
+FORY_STRUCT(ExternalStruct, id, name);
+
+struct ExternalEmpty {
+  bool operator==(const ExternalEmpty & /*other*/) const { return true; }
+};
+
+FORY_STRUCT(ExternalEmpty);
+
+} // namespace external_test
 
 // ============================================================================
 // TEST IMPLEMENTATION (Inside namespace)
@@ -314,6 +379,7 @@ inline void register_all_test_types(Fory &fory) {
   fory.register_struct<SingleFieldStruct>(type_id++);
   fory.register_struct<TwoFieldStruct>(type_id++);
   fory.register_struct<ManyFieldsStruct>(type_id++);
+  fory.register_struct<PrivateFieldsStruct>(type_id++);
   fory.register_struct<AllPrimitivesStruct>(type_id++);
   fory.register_struct<StringTestStruct>(type_id++);
   fory.register_struct<Point2D>(type_id++);
@@ -330,6 +396,10 @@ inline void register_all_test_types(Fory &fory) {
   fory.register_struct<Product>(type_id++);
   fory.register_struct<OrderItem>(type_id++);
   fory.register_struct<Order>(type_id++);
+  fory.register_struct<nested_test::inner::InClassStruct>(type_id++);
+  fory.register_struct<nested_test::inner::OutClassStruct>(type_id++);
+  fory.register_struct<external_test::ExternalStruct>(type_id++);
+  fory.register_struct<external_test::ExternalEmpty>(type_id++);
 }
 
 template <typename T> void test_roundtrip(const T &original) {
@@ -377,6 +447,10 @@ TEST(StructComprehensiveTest, ManyFieldsStruct) {
                                   -9223372036854775807LL - 1, -1.0f, -1.0, 
""});
 }
 
+TEST(StructComprehensiveTest, PrivateFieldsStruct) {
+  test_roundtrip(PrivateFieldsStruct{42, "secret", {1, 2, 3}});
+}
+
 TEST(StructComprehensiveTest, AllPrimitivesZero) {
   test_roundtrip(AllPrimitivesStruct{false, 0, 0, 0, 0, 0, 0, 0, 0, 0.0f, 
0.0});
 }
@@ -525,6 +599,23 @@ TEST(StructComprehensiveTest, LargeVectorOfStructs) {
   EXPECT_EQ(points, deser_result.value());
 }
 
+TEST(StructComprehensiveTest, NestedNamespaceInClassStruct) {
+  test_roundtrip(nested_test::inner::InClassStruct{7, "in"});
+}
+
+TEST(StructComprehensiveTest, NestedNamespaceOutClassStruct) {
+  test_roundtrip(nested_test::inner::OutClassStruct{8, "out"});
+}
+
+TEST(StructComprehensiveTest, ExternalStruct) {
+  test_roundtrip(external_test::ExternalStruct{1, "external"});
+  test_roundtrip(external_test::ExternalStruct{42, ""});
+}
+
+TEST(StructComprehensiveTest, ExternalEmptyStruct) {
+  test_roundtrip(external_test::ExternalEmpty{});
+}
+
 } // namespace test
 } // namespace serialization
 } // namespace fory
diff --git a/cpp/fory/serialization/tuple_serializer_test.cc 
b/cpp/fory/serialization/tuple_serializer_test.cc
index 64c3b19b0..8eed7d7a4 100644
--- a/cpp/fory/serialization/tuple_serializer_test.cc
+++ b/cpp/fory/serialization/tuple_serializer_test.cc
@@ -64,33 +64,33 @@ TEST(TupleSerializerTest, HomogeneityDetection) {
 // First test with vector to verify test setup
 struct VectorHolder {
   std::vector<int32_t> values;
+  FORY_STRUCT(VectorHolder, values);
 };
-FORY_STRUCT(VectorHolder, values);
 
 struct TupleHomogeneousHolder {
   std::tuple<int32_t, int32_t, int32_t> values;
+  FORY_STRUCT(TupleHomogeneousHolder, values);
 };
-FORY_STRUCT(TupleHomogeneousHolder, values);
 
 struct TupleHeterogeneousHolder {
   std::tuple<int32_t, std::string, double> values;
+  FORY_STRUCT(TupleHeterogeneousHolder, values);
 };
-FORY_STRUCT(TupleHeterogeneousHolder, values);
 
 struct TupleSingleHolder {
   std::tuple<std::string> value;
+  FORY_STRUCT(TupleSingleHolder, value);
 };
-FORY_STRUCT(TupleSingleHolder, value);
 
 struct TupleEmptyHolder {
   std::tuple<> value;
+  FORY_STRUCT(TupleEmptyHolder, value);
 };
-FORY_STRUCT(TupleEmptyHolder, value);
 
 struct TupleNestedHolder {
   std::tuple<std::tuple<int32_t, int32_t>, std::string> values;
+  FORY_STRUCT(TupleNestedHolder, values);
 };
-FORY_STRUCT(TupleNestedHolder, values);
 
 Fory create_fory() {
   return Fory::builder().xlang(true).track_ref(true).build();
@@ -232,8 +232,8 @@ struct TupleLargeHolder {
   std::tuple<int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t,
              int32_t, int32_t, int32_t>
       values;
+  FORY_STRUCT(TupleLargeHolder, values);
 };
-FORY_STRUCT(TupleLargeHolder, values);
 
 TEST(TupleSerializerTest, LargeTupleRoundTrip) {
   auto fory = create_fory();
diff --git a/cpp/fory/serialization/type_resolver.h 
b/cpp/fory/serialization/type_resolver.h
index efdd891f4..827243d1f 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -62,6 +62,8 @@
 namespace fory {
 namespace serialization {
 
+using meta::ForyFieldInfo;
+
 // Forward declarations
 class Fory;
 class TypeResolver;
@@ -608,7 +610,7 @@ template <typename T, size_t Index> struct FieldInfoBuilder 
{
   static FieldInfo build() {
     const auto meta = ForyFieldInfo(T{});
     const auto field_names = decltype(meta)::Names;
-    const auto field_ptrs = decltype(meta)::Ptrs;
+    const auto &field_ptrs = decltype(meta)::PtrsRef();
 
     // Convert camelCase field name to snake_case for cross-language
     // compatibility
diff --git a/cpp/fory/serialization/unsigned_serializer_test.cc 
b/cpp/fory/serialization/unsigned_serializer_test.cc
index 0bf7fe8c5..30196c1de 100644
--- a/cpp/fory/serialization/unsigned_serializer_test.cc
+++ b/cpp/fory/serialization/unsigned_serializer_test.cc
@@ -42,10 +42,9 @@ struct UnsignedStruct {
     return u8_val == other.u8_val && u16_val == other.u16_val &&
            u32_val == other.u32_val && u64_val == other.u64_val;
   }
+  FORY_STRUCT(UnsignedStruct, u8_val, u16_val, u32_val, u64_val);
 };
 
-FORY_STRUCT(UnsignedStruct, u8_val, u16_val, u32_val, u64_val);
-
 struct UnsignedArrayStruct {
   std::vector<uint8_t> u8_vec;
   std::vector<uint16_t> u16_vec;
@@ -56,10 +55,9 @@ struct UnsignedArrayStruct {
     return u8_vec == other.u8_vec && u16_vec == other.u16_vec &&
            u32_vec == other.u32_vec && u64_vec == other.u64_vec;
   }
+  FORY_STRUCT(UnsignedArrayStruct, u8_vec, u16_vec, u32_vec, u64_vec);
 };
 
-FORY_STRUCT(UnsignedArrayStruct, u8_vec, u16_vec, u32_vec, u64_vec);
-
 // ============================================================================
 // Test Helper for Native Mode (xlang=false)
 // Unsigned types are only supported in native mode (xlang=false)
diff --git a/cpp/fory/serialization/variant_serializer_test.cc 
b/cpp/fory/serialization/variant_serializer_test.cc
index 1d0a408a6..89688129f 100644
--- a/cpp/fory/serialization/variant_serializer_test.cc
+++ b/cpp/fory/serialization/variant_serializer_test.cc
@@ -34,15 +34,15 @@ struct OldStruct {
   int id;
   VariantType field1;
   VariantType field2;
+  FORY_STRUCT(OldStruct, id, field1, 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);
 };
-FORY_STRUCT(NewStruct, id, field1);
 } // namespace
 
 // Helper to create a Fory instance
diff --git a/cpp/fory/serialization/weak_ptr_serializer.h 
b/cpp/fory/serialization/weak_ptr_serializer.h
index 95eaa801e..f762d6ea8 100644
--- a/cpp/fory/serialization/weak_ptr_serializer.h
+++ b/cpp/fory/serialization/weak_ptr_serializer.h
@@ -56,8 +56,8 @@ namespace serialization {
 ///   int32_t value;
 ///   SharedWeak<Node> parent;              // Non-owning back-reference
 ///   std::vector<std::shared_ptr<Node>> children;  // Owning references
+///   FORY_STRUCT(Node, value, parent, children);
 /// };
-/// FORY_STRUCT(Node, value, parent, children);
 ///
 /// auto parent = std::make_shared<Node>();
 /// parent->value = 1;
diff --git a/cpp/fory/serialization/weak_ptr_serializer_test.cc 
b/cpp/fory/serialization/weak_ptr_serializer_test.cc
index 4da9d5300..bd34c6dd1 100644
--- a/cpp/fory/serialization/weak_ptr_serializer_test.cc
+++ b/cpp/fory/serialization/weak_ptr_serializer_test.cc
@@ -136,36 +136,36 @@ TEST(SharedWeakTest, OwnerEquals) {
 
 struct SimpleStruct {
   int32_t value;
+  FORY_STRUCT(SimpleStruct, value);
 };
-FORY_STRUCT(SimpleStruct, value);
 
 struct StructWithWeak {
   int32_t id;
   SharedWeak<SimpleStruct> weak_ref;
+  FORY_STRUCT(StructWithWeak, id, weak_ref);
 };
-FORY_STRUCT(StructWithWeak, id, weak_ref);
 
 struct StructWithBothRefs {
   int32_t id;
   std::shared_ptr<SimpleStruct> strong_ref;
   SharedWeak<SimpleStruct> weak_ref;
+  FORY_STRUCT(StructWithBothRefs, id, strong_ref, weak_ref);
 };
-FORY_STRUCT(StructWithBothRefs, id, strong_ref, weak_ref);
 
 struct MultipleWeakRefsWithOwner {
   std::shared_ptr<SimpleStruct> owner;
   SharedWeak<SimpleStruct> weak1;
   SharedWeak<SimpleStruct> weak2;
   SharedWeak<SimpleStruct> weak3;
+  FORY_STRUCT(MultipleWeakRefsWithOwner, owner, weak1, weak2, weak3);
 };
-FORY_STRUCT(MultipleWeakRefsWithOwner, owner, weak1, weak2, weak3);
 
 struct NodeWithParent {
   int32_t value;
   SharedWeak<NodeWithParent> parent;
   std::vector<std::shared_ptr<NodeWithParent>> children;
+  FORY_STRUCT(NodeWithParent, value, parent, children);
 };
-FORY_STRUCT(NodeWithParent, value, parent, children);
 
 // ============================================================================
 // Serialization Tests
diff --git a/cpp/fory/serialization/xlang_test_main.cc 
b/cpp/fory/serialization/xlang_test_main.cc
index 299ae8e70..f13823268 100644
--- a/cpp/fory/serialization/xlang_test_main.cc
+++ b/cpp/fory/serialization/xlang_test_main.cc
@@ -114,8 +114,8 @@ FORY_ENUM(Color, Green, Red, Blue, White);
 struct Item {
   std::string name;
   bool operator==(const Item &other) const { return name == other.name; }
+  FORY_STRUCT(Item, name);
 };
-FORY_STRUCT(Item, name);
 
 struct SimpleStruct {
   std::map<int32_t, double> f1;
@@ -132,8 +132,8 @@ struct SimpleStruct {
            f4 == other.f4 && f5 == other.f5 && f6 == other.f6 &&
            f7 == other.f7 && f8 == other.f8 && last == other.last;
   }
+  FORY_STRUCT(SimpleStruct, f1, f2, f3, f4, f5, f6, f7, f8, last);
 };
-FORY_STRUCT(SimpleStruct, f1, f2, f3, f4, f5, f6, f7, f8, last);
 
 // Integer struct used for cross-language boxed integer tests.
 // Java xlang mode: all fields are non-nullable by default.
@@ -148,16 +148,16 @@ struct Item1 {
     return f1 == other.f1 && f2 == other.f2 && f3 == other.f3 &&
            f4 == other.f4 && f5 == other.f5 && f6 == other.f6;
   }
+  FORY_STRUCT(Item1, f1, f2, f3, f4, f5, f6);
 };
-FORY_STRUCT(Item1, f1, f2, f3, f4, f5, f6);
 
 struct MyStruct {
   int32_t id;
   MyStruct() = default;
   explicit MyStruct(int32_t v) : id(v) {}
   bool operator==(const MyStruct &other) const { return id == other.id; }
+  FORY_STRUCT(MyStruct, id);
 };
-FORY_STRUCT(MyStruct, id);
 
 struct MyExt {
   int32_t id;
@@ -178,16 +178,16 @@ struct MyWrapper {
     return color == other.color && my_struct == other.my_struct &&
            my_ext == other.my_ext;
   }
+  FORY_STRUCT(MyWrapper, color, my_struct, my_ext);
 };
-FORY_STRUCT(MyWrapper, color, my_struct, my_ext);
 
 struct EmptyWrapper {
   bool operator==(const EmptyWrapper &other) const {
     (void)other;
     return true;
   }
+  FORY_STRUCT(EmptyWrapper);
 };
-FORY_STRUCT(EmptyWrapper);
 
 struct VersionCheckStruct {
   int32_t f1;
@@ -196,24 +196,24 @@ struct VersionCheckStruct {
   bool operator==(const VersionCheckStruct &other) const {
     return f1 == other.f1 && f2 == other.f2 && f3 == other.f3;
   }
+  FORY_STRUCT(VersionCheckStruct, f1, f2, f3);
 };
-FORY_STRUCT(VersionCheckStruct, f1, f2, f3);
 
 struct StructWithList {
   std::vector<std::string> items;
   bool operator==(const StructWithList &other) const {
     return items == other.items;
   }
+  FORY_STRUCT(StructWithList, items);
 };
-FORY_STRUCT(StructWithList, items);
 
 struct StructWithMap {
   std::map<std::string, std::string> data;
   bool operator==(const StructWithMap &other) const {
     return data == other.data;
   }
+  FORY_STRUCT(StructWithMap, data);
 };
-FORY_STRUCT(StructWithMap, data);
 
 // ============================================================================
 // Polymorphic Container Test Types - Using virtual base class
@@ -223,30 +223,30 @@ struct Animal {
   virtual ~Animal() = default;
   virtual std::string speak() const = 0;
   int32_t age = 0;
+  FORY_STRUCT(Animal, age);
 };
-FORY_STRUCT(Animal, age);
 
 struct Dog : Animal {
   std::string speak() const override { return "Woof"; }
   std::optional<std::string> name;
+  FORY_STRUCT(Dog, FORY_BASE(Animal), name);
 };
-FORY_STRUCT(Dog, age, name);
 
 struct Cat : Animal {
   std::string speak() const override { return "Meow"; }
   int32_t lives = 9;
+  FORY_STRUCT(Cat, FORY_BASE(Animal), lives);
 };
-FORY_STRUCT(Cat, age, lives);
 
 struct AnimalListHolder {
   std::vector<std::shared_ptr<Animal>> animals;
+  FORY_STRUCT(AnimalListHolder, animals);
 };
-FORY_STRUCT(AnimalListHolder, animals);
 
 struct AnimalMapHolder {
   std::map<std::string, std::shared_ptr<Animal>> animal_map;
+  FORY_STRUCT(AnimalMapHolder, animal_map);
 };
-FORY_STRUCT(AnimalMapHolder, animal_map);
 
 // ============================================================================
 // Schema Evolution Test Types
@@ -257,16 +257,16 @@ struct EmptyStructEvolution {
   bool operator==(const EmptyStructEvolution &other) const {
     return placeholder == other.placeholder;
   }
+  FORY_STRUCT(EmptyStructEvolution, placeholder);
 };
-FORY_STRUCT(EmptyStructEvolution, placeholder);
 
 struct OneStringFieldStruct {
   std::optional<std::string> f1;
   bool operator==(const OneStringFieldStruct &other) const {
     return f1 == other.f1;
   }
+  FORY_STRUCT(OneStringFieldStruct, f1);
 };
-FORY_STRUCT(OneStringFieldStruct, f1);
 
 struct TwoStringFieldStruct {
   std::string f1;
@@ -274,8 +274,8 @@ struct TwoStringFieldStruct {
   bool operator==(const TwoStringFieldStruct &other) const {
     return f1 == other.f1 && f2 == other.f2;
   }
+  FORY_STRUCT(TwoStringFieldStruct, f1, f2);
 };
-FORY_STRUCT(TwoStringFieldStruct, f1, f2);
 
 enum class TestEnum : int32_t { VALUE_A = 0, VALUE_B = 1, VALUE_C = 2 };
 FORY_ENUM(TestEnum, VALUE_A, VALUE_B, VALUE_C);
@@ -285,8 +285,8 @@ struct OneEnumFieldStruct {
   bool operator==(const OneEnumFieldStruct &other) const {
     return f1 == other.f1;
   }
+  FORY_STRUCT(OneEnumFieldStruct, f1);
 };
-FORY_STRUCT(OneEnumFieldStruct, f1);
 
 struct TwoEnumFieldStruct {
   TestEnum f1;
@@ -294,8 +294,8 @@ struct TwoEnumFieldStruct {
   bool operator==(const TwoEnumFieldStruct &other) const {
     return f1 == other.f1 && f2 == other.f2;
   }
+  FORY_STRUCT(TwoEnumFieldStruct, f1, f2);
 };
-FORY_STRUCT(TwoEnumFieldStruct, f1, f2);
 
 // ============================================================================
 // Nullable Field Test Types - Comprehensive versions matching Java structs
@@ -352,6 +352,12 @@ struct NullableComprehensiveSchemaConsistent {
            nullable_map == other.nullable_map;
   }
 
+  FORY_STRUCT(NullableComprehensiveSchemaConsistent, byte_field, short_field,
+              int_field, long_field, float_field, double_field, bool_field,
+              string_field, list_field, set_field, map_field, nullable_int,
+              nullable_long, nullable_float, nullable_double, nullable_bool,
+              nullable_string, nullable_list, nullable_set, nullable_map);
+
 private:
   static bool compare_optional_float(const std::optional<float> &a,
                                      const std::optional<float> &b) {
@@ -371,11 +377,6 @@ private:
     return std::abs(*a - *b) < 1e-9;
   }
 };
-FORY_STRUCT(NullableComprehensiveSchemaConsistent, byte_field, short_field,
-            int_field, long_field, float_field, double_field, bool_field,
-            string_field, list_field, set_field, map_field, nullable_int,
-            nullable_long, nullable_float, nullable_double, nullable_bool,
-            nullable_string, nullable_list, nullable_set, nullable_map);
 
 // NullableComprehensiveCompatible (type id 402)
 // Matches Java's NullableComprehensiveCompatible for COMPATIBLE mode
@@ -439,6 +440,13 @@ struct NullableComprehensiveCompatible {
            nullable_map2 == other.nullable_map2;
   }
 
+  FORY_STRUCT(NullableComprehensiveCompatible, byte_field, short_field,
+              int_field, long_field, float_field, double_field, bool_field,
+              boxed_int, boxed_long, boxed_float, boxed_double, boxed_bool,
+              string_field, list_field, set_field, map_field, nullable_int1,
+              nullable_long1, nullable_float1, nullable_double1, 
nullable_bool1,
+              nullable_string2, nullable_list2, nullable_set2, nullable_map2);
+
 private:
   static bool compare_optional_float(const std::optional<float> &a,
                                      const std::optional<float> &b) {
@@ -458,12 +466,6 @@ private:
     return std::abs(*a - *b) < 1e-9;
   }
 };
-FORY_STRUCT(NullableComprehensiveCompatible, byte_field, short_field, 
int_field,
-            long_field, float_field, double_field, bool_field, boxed_int,
-            boxed_long, boxed_float, boxed_double, boxed_bool, string_field,
-            list_field, set_field, map_field, nullable_int1, nullable_long1,
-            nullable_float1, nullable_double1, nullable_bool1, 
nullable_string2,
-            nullable_list2, nullable_set2, nullable_map2);
 
 // ============================================================================
 // Reference Tracking Test Types - Cross-language shared reference tests
@@ -480,8 +482,8 @@ struct RefInnerSchemaConsistent {
   bool operator!=(const RefInnerSchemaConsistent &other) const {
     return !(*this == other);
   }
+  FORY_STRUCT(RefInnerSchemaConsistent, id, name);
 };
-FORY_STRUCT(RefInnerSchemaConsistent, id, name);
 
 // Outer struct for reference tracking test (SCHEMA_CONSISTENT mode)
 // Contains two fields that both point to the same inner object.
@@ -499,8 +501,8 @@ struct RefOuterSchemaConsistent {
                       *inner2 == *other.inner2);
     return inner1_eq && inner2_eq;
   }
+  FORY_STRUCT(RefOuterSchemaConsistent, inner1, inner2);
 };
-FORY_STRUCT(RefOuterSchemaConsistent, inner1, inner2);
 FORY_FIELD_TAGS(RefOuterSchemaConsistent, (inner1, 0, nullable, ref),
                 (inner2, 1, nullable, ref));
 // Verify field tags are correctly parsed
@@ -529,8 +531,8 @@ struct RefInnerCompatible {
   bool operator!=(const RefInnerCompatible &other) const {
     return !(*this == other);
   }
+  FORY_STRUCT(RefInnerCompatible, id, name);
 };
-FORY_STRUCT(RefInnerCompatible, id, name);
 
 // Outer struct for reference tracking test (COMPATIBLE mode)
 // Contains two fields that both point to the same inner object.
@@ -548,8 +550,8 @@ struct RefOuterCompatible {
                       *inner2 == *other.inner2);
     return inner1_eq && inner2_eq;
   }
+  FORY_STRUCT(RefOuterCompatible, inner1, inner2);
 };
-FORY_STRUCT(RefOuterCompatible, inner1, inner2);
 FORY_FIELD_TAGS(RefOuterCompatible, (inner1, 0, nullable, ref),
                 (inner2, 1, nullable, ref));
 
@@ -577,8 +579,8 @@ struct CircularRefStruct {
                    (selfRef != nullptr && other.selfRef != nullptr);
     return self_eq;
   }
+  FORY_STRUCT(CircularRefStruct, name, selfRef);
 };
-FORY_STRUCT(CircularRefStruct, name, selfRef);
 FORY_FIELD_TAGS(CircularRefStruct, (name, 0), (selfRef, 1, nullable, ref));
 
 // ============================================================================
@@ -595,8 +597,8 @@ struct UnsignedSchemaConsistentSimple {
     return u64Tagged == other.u64Tagged &&
            u64TaggedNullable == other.u64TaggedNullable;
   }
+  FORY_STRUCT(UnsignedSchemaConsistentSimple, u64Tagged, u64TaggedNullable);
 };
-FORY_STRUCT(UnsignedSchemaConsistentSimple, u64Tagged, u64TaggedNullable);
 FORY_FIELD_CONFIG(UnsignedSchemaConsistentSimple,
                   UnsignedSchemaConsistentSimple,
                   (u64Tagged, fory::F().tagged()),
@@ -640,12 +642,12 @@ struct UnsignedSchemaConsistent {
            u64FixedNullableField == other.u64FixedNullableField &&
            u64TaggedNullableField == other.u64TaggedNullableField;
   }
+  FORY_STRUCT(UnsignedSchemaConsistent, u8Field, u16Field, u32VarField,
+              u32FixedField, u64VarField, u64FixedField, u64TaggedField,
+              u8NullableField, u16NullableField, u32VarNullableField,
+              u32FixedNullableField, u64VarNullableField, 
u64FixedNullableField,
+              u64TaggedNullableField);
 };
-FORY_STRUCT(UnsignedSchemaConsistent, u8Field, u16Field, u32VarField,
-            u32FixedField, u64VarField, u64FixedField, u64TaggedField,
-            u8NullableField, u16NullableField, u32VarNullableField,
-            u32FixedNullableField, u64VarNullableField, u64FixedNullableField,
-            u64TaggedNullableField);
 // Use new FORY_FIELD_CONFIG with builder pattern for encoding specification
 FORY_FIELD_CONFIG(UnsignedSchemaConsistent, UnsignedSchemaConsistent,
                   (u8Field, fory::F()), (u16Field, fory::F()),
@@ -700,11 +702,11 @@ struct UnsignedSchemaCompatible {
            u64FixedField2 == other.u64FixedField2 &&
            u64TaggedField2 == other.u64TaggedField2;
   }
+  FORY_STRUCT(UnsignedSchemaCompatible, u8Field1, u16Field1, u32VarField1,
+              u32FixedField1, u64VarField1, u64FixedField1, u64TaggedField1,
+              u8Field2, u16Field2, u32VarField2, u32FixedField2, u64VarField2,
+              u64FixedField2, u64TaggedField2);
 };
-FORY_STRUCT(UnsignedSchemaCompatible, u8Field1, u16Field1, u32VarField1,
-            u32FixedField1, u64VarField1, u64FixedField1, u64TaggedField1,
-            u8Field2, u16Field2, u32VarField2, u32FixedField2, u64VarField2,
-            u64FixedField2, u64TaggedField2);
 // Use new FORY_FIELD_CONFIG with builder pattern for encoding specification
 // Group 1: nullable in C++ (std::optional), non-nullable in Java
 // Group 2: non-nullable in C++, nullable in Java
diff --git a/cpp/fory/util/pool_test.cc b/cpp/fory/util/pool_test.cc
index 894dc2701..58c4649c5 100644
--- a/cpp/fory/util/pool_test.cc
+++ b/cpp/fory/util/pool_test.cc
@@ -106,8 +106,8 @@ TEST(PoolTest, ConcurrentBorrow) {
   threads.reserve(kThreads);
 
   for (size_t t = 0; t < kThreads; ++t) {
-    threads.emplace_back([&pool, kIterations]() {
-      for (size_t i = 0; i < kIterations; ++i) {
+    threads.emplace_back([&pool, iterations = kIterations]() {
+      for (size_t i = 0; i < iterations; ++i) {
         auto value = pool.acquire();
         (*value)++;
       }
diff --git a/docs/benchmarks/cpp/README.md b/docs/benchmarks/cpp/README.md
index 00b11eed6..a3684c5df 100644
--- a/docs/benchmarks/cpp/README.md
+++ b/docs/benchmarks/cpp/README.md
@@ -36,27 +36,27 @@ python benchmark_report.py --json-file 
build/benchmark_results.json --output-dir
 
 | Datatype     | Operation   | Fory (ns) | Protobuf (ns) | Faster      |
 | ------------ | ----------- | --------- | ------------- | ----------- |
-| Mediacontent | Serialize   | 89.7      | 870.1         | Fory (9.7x) |
-| Mediacontent | Deserialize | 368.4     | 1276.7        | Fory (3.5x) |
-| Sample       | Serialize   | 59.2      | 95.5          | Fory (1.6x) |
-| Sample       | Deserialize | 355.6     | 655.2         | Fory (1.8x) |
-| Struct       | Serialize   | 23.5      | 40.1          | Fory (1.7x) |
-| Struct       | Deserialize | 18.5      | 25.5          | Fory (1.4x) |
+| Mediacontent | Serialize   | 443.5     | 1982.5        | Fory (4.5x) |
+| Mediacontent | Deserialize | 1349.0    | 2525.2        | Fory (1.9x) |
+| Sample       | Serialize   | 235.4     | 309.7         | Fory (1.3x) |
+| Sample       | Deserialize | 1068.7    | 1397.0        | Fory (1.3x) |
+| Struct       | Serialize   | 109.4     | 170.0         | Fory (1.6x) |
+| Struct       | Deserialize | 129.1     | 161.2         | Fory (1.2x) |
 
 ### Throughput Results (ops/sec)
 
-| Datatype     | Operation   | Fory TPS   | Protobuf TPS | Faster      |
-| ------------ | ----------- | ---------- | ------------ | ----------- |
-| Mediacontent | Serialize   | 11,151,839 | 1,149,335    | Fory (9.7x) |
-| Mediacontent | Deserialize | 2,714,139  | 783,243      | Fory (3.5x) |
-| Sample       | Serialize   | 16,889,474 | 10,474,347   | Fory (1.6x) |
-| Sample       | Deserialize | 2,812,439  | 1,526,317    | Fory (1.8x) |
-| Struct       | Serialize   | 42,570,153 | 24,942,338   | Fory (1.7x) |
-| Struct       | Deserialize | 54,146,253 | 39,213,086   | Fory (1.4x) |
+| Datatype     | Operation   | Fory TPS  | Protobuf TPS | Faster      |
+| ------------ | ----------- | --------- | ------------ | ----------- |
+| Mediacontent | Serialize   | 2,254,915 | 504,410      | Fory (4.5x) |
+| Mediacontent | Deserialize | 741,303   | 396,013      | Fory (1.9x) |
+| Sample       | Serialize   | 4,248,973 | 3,229,102    | Fory (1.3x) |
+| Sample       | Deserialize | 935,709   | 715,837      | Fory (1.3x) |
+| Struct       | Serialize   | 9,143,618 | 5,881,005    | Fory (1.6x) |
+| Struct       | Deserialize | 7,746,787 | 6,202,164    | Fory (1.2x) |
 
 ### Serialized Data Sizes (bytes)
 
 | Datatype | Fory | Protobuf |
 | -------- | ---- | -------- |
-| Struct   | 34   | 61       |
-| Sample   | 394  | 375      |
+| Struct   | 32   | 61       |
+| Sample   | 384  | 375      |
diff --git a/docs/benchmarks/cpp/throughput.png 
b/docs/benchmarks/cpp/throughput.png
index ad7696610..e726ebbc1 100644
Binary files a/docs/benchmarks/cpp/throughput.png and 
b/docs/benchmarks/cpp/throughput.png differ
diff --git a/docs/guide/cpp/basic-serialization.md 
b/docs/guide/cpp/basic-serialization.md
index e45df7e38..3d331450b 100644
--- a/docs/guide/cpp/basic-serialization.md
+++ b/docs/guide/cpp/basic-serialization.md
@@ -177,30 +177,97 @@ Common error types:
 
 ## The FORY_STRUCT Macro
 
-The `FORY_STRUCT` macro registers a struct for serialization:
+The `FORY_STRUCT` macro registers a class for serialization (struct works the
+same way):
 
 ```cpp
-struct MyStruct {
+class MyStruct {
+public:
   int32_t x;
   std::string y;
   std::vector<int32_t> z;
+  FORY_STRUCT(MyStruct, x, y, z);
 };
+```
+
+Private fields are supported when the macro is placed in a `public:` section:
+
+```cpp
+class PrivateUser {
+public:
+  PrivateUser(int32_t id, std::string name) : id_(id), name_(std::move(name)) 
{}
+
+  bool operator==(const PrivateUser &other) const {
+    return id_ == other.id_ && name_ == other.name_;
+  }
+
+private:
+  int32_t id_ = 0;
+  std::string name_;
 
-// Must be in the same namespace as the struct
-FORY_STRUCT(MyStruct, x, y, z);
+public:
+  FORY_STRUCT(PrivateUser, id_, name_);
+};
 ```
 
 The macro:
 
 1. Generates compile-time field metadata
-2. Enables ADL (Argument-Dependent Lookup) for serialization
+2. Enables member or ADL (Argument-Dependent Lookup) discovery for 
serialization
 3. Creates efficient serialization code via template specialization
 
 **Requirements:**
 
-- Must be placed in the same namespace as the struct (for ADL)
+- Must be declared inside the class definition (struct works the same way) or
+  at namespace scope
+- Must be placed after all field declarations (when used inside the class)
+- When used inside a class, the macro must be placed in a `public:` section
 - All listed fields must be serializable types
-- Field order in the macro determines serialization order
+- Field order in the macro is not important
+
+## External / Third-Party Types
+
+When you cannot modify a third-party type, use `FORY_STRUCT` at namespace
+scope. This only works with **public** fields.
+
+```cpp
+namespace thirdparty {
+struct Foo {
+  int32_t id;
+  std::string name;
+};
+
+FORY_STRUCT(Foo, id, name);
+} // namespace thirdparty
+```
+
+**Limitations:**
+
+- Must be declared at namespace scope in the same namespace as the type
+- Only public fields are supported
+
+## Inherited Fields
+
+To include base-class fields in a derived type, use `FORY_BASE(Base)` inside
+`FORY_STRUCT`. The base must define its own `FORY_STRUCT` so its fields can be
+referenced.
+
+```cpp
+struct Base {
+  int32_t a;
+  FORY_STRUCT(Base, a);
+};
+
+struct Derived : Base {
+  int32_t b;
+  FORY_STRUCT(Derived, FORY_BASE(Base), b);
+};
+```
+
+**Notes:**
+
+- Base fields are serialized before derived fields.
+- Only fields visible from the derived type are supported.
 
 ## Nested Structs
 
@@ -209,14 +276,14 @@ Nested structs are fully supported:
 ```cpp
 struct Inner {
   int32_t value;
+  FORY_STRUCT(Inner, value);
 };
-FORY_STRUCT(Inner, value);
 
 struct Outer {
   Inner inner;
   std::string label;
+  FORY_STRUCT(Outer, inner, label);
 };
-FORY_STRUCT(Outer, inner, label);
 
 // Both must be registered
 fory.register_struct<Inner>(1);
diff --git a/docs/guide/cpp/index.md b/docs/guide/cpp/index.md
index fd2cc732a..fefaef00d 100644
--- a/docs/guide/cpp/index.md
+++ b/docs/guide/cpp/index.md
@@ -188,6 +188,24 @@ int main() {
 }
 ```
 
+### Inherited Fields
+
+To include base-class fields in a derived type, list `FORY_BASE(Base)` inside
+`FORY_STRUCT`. The base must define its own `FORY_STRUCT` so its fields can be
+referenced.
+
+```cpp
+struct Base {
+  int32_t id;
+  FORY_STRUCT(Base, id);
+};
+
+struct Derived : Base {
+  std::string name;
+  FORY_STRUCT(Derived, FORY_BASE(Base), name);
+};
+```
+
 ## Thread Safety
 
 Apache Fory™ C++ provides two variants for different threading needs:
diff --git a/docs/guide/cpp/row-format.md b/docs/guide/cpp/row-format.md
index a3638ab7c..8c410b7e3 100644
--- a/docs/guide/cpp/row-format.md
+++ b/docs/guide/cpp/row-format.md
@@ -51,16 +51,13 @@ Apache Fory™ Row Format is a binary format optimized for:
 using namespace fory::row;
 using namespace fory::row::encoder;
 
-// Define a struct
 struct Person {
   int32_t id;
   std::string name;
   float score;
+  FORY_STRUCT(Person, id, name, score);
 };
 
-// Register field metadata (required for row encoding)
-FORY_FIELD_INFO(Person, id, name, score);
-
 int main() {
   // Create encoder
   RowEncoder<Person> encoder;
@@ -94,12 +91,11 @@ The `RowEncoder<T>` template class provides type-safe 
encoding:
 ```cpp
 #include "fory/encoder/row_encoder.h"
 
-// Define struct with FORY_FIELD_INFO
 struct Point {
   double x;
   double y;
+  FORY_STRUCT(Point, x, y);
 };
-FORY_FIELD_INFO(Point, x, y);
 
 // Create encoder
 RowEncoder<Point> encoder;
@@ -122,14 +118,14 @@ auto row = encoder.GetWriter().ToRow();
 struct Address {
   std::string city;
   std::string country;
+  FORY_STRUCT(Address, city, country);
 };
-FORY_FIELD_INFO(Address, city, country);
 
 struct Person {
   std::string name;
   Address address;
+  FORY_STRUCT(Person, name, address);
 };
-FORY_FIELD_INFO(Person, name, address);
 
 // Encode nested struct
 RowEncoder<Person> encoder;
@@ -151,8 +147,8 @@ std::string country = address_row->GetString(1);
 struct Record {
   std::vector<int32_t> values;
   std::string label;
+  FORY_STRUCT(Record, values, label);
 };
-FORY_FIELD_INFO(Record, values, label);
 
 RowEncoder<Record> encoder;
 Record record{{1, 2, 3, 4, 5}, "test"};
@@ -339,7 +335,7 @@ RowEncodeTrait<std::vector<int32_t>>::Type();  // Returns 
list(int32())
 RowEncodeTrait<std::map<std::string, int32_t>>::Type();
 // Returns map(utf8(), int32())
 
-// Type inference for structs (requires FORY_FIELD_INFO)
+// Type inference for structs (requires FORY_STRUCT)
 RowEncodeTrait<Person>::Type();  // Returns struct_({...})
 RowEncodeTrait<Person>::Schema();  // Returns schema({...})
 ```
@@ -494,20 +490,20 @@ int32_t id = row.GetInt32(0);
 
 ## Supported Types Summary
 
-| C++ Type                 | Row Type         | Fixed Size |
-| ------------------------ | ---------------- | ---------- |
-| `bool`                   | `boolean()`      | 1 byte     |
-| `int8_t`                 | `int8()`         | 1 byte     |
-| `int16_t`                | `int16()`        | 2 bytes    |
-| `int32_t`                | `int32()`        | 4 bytes    |
-| `int64_t`                | `int64()`        | 8 bytes    |
-| `float`                  | `float32()`      | 4 bytes    |
-| `double`                 | `float64()`      | 8 bytes    |
-| `std::string`            | `utf8()`         | Variable   |
-| `std::vector<T>`         | `list(T)`        | Variable   |
-| `std::map<K,V>`          | `map(K,V)`       | Variable   |
-| `std::optional<T>`       | Inner type       | Nullable   |
-| Struct (FORY_FIELD_INFO) | `struct_({...})` | Variable   |
+| C++ Type             | Row Type         | Fixed Size |
+| -------------------- | ---------------- | ---------- |
+| `bool`               | `boolean()`      | 1 byte     |
+| `int8_t`             | `int8()`         | 1 byte     |
+| `int16_t`            | `int16()`        | 2 bytes    |
+| `int32_t`            | `int32()`        | 4 bytes    |
+| `int64_t`            | `int64()`        | 8 bytes    |
+| `float`              | `float32()`      | 4 bytes    |
+| `double`             | `float64()`      | 8 bytes    |
+| `std::string`        | `utf8()`         | Variable   |
+| `std::vector<T>`     | `list(T)`        | Variable   |
+| `std::map<K,V>`      | `map(K,V)`       | Variable   |
+| `std::optional<T>`   | Inner type       | Nullable   |
+| Struct (FORY_STRUCT) | `struct_({...})` | Variable   |
 
 ## Related Topics
 
diff --git a/docs/guide/java/row-format.md b/docs/guide/java/row-format.md
index 134b58ee1..eb58646b3 100644
--- a/docs/guide/java/row-format.md
+++ b/docs/guide/java/row-format.md
@@ -142,19 +142,17 @@ print(foo_row.f4[100000].f1)
 struct Bar {
   std::string f1;
   std::vector<int64_t> f2;
+  FORY_STRUCT(Bar, f1, f2);
 };
 
-FORY_FIELD_INFO(Bar, f1, f2);
-
 struct Foo {
   int32_t f1;
   std::vector<int32_t> f2;
   std::map<std::string, int32_t> f3;
   std::vector<Bar> f4;
+  FORY_STRUCT(Foo, f1, f2, f3, f4);
 };
 
-FORY_FIELD_INFO(Foo, f1, f2, f3, f4);
-
 fory::encoder::RowEncoder<Foo> encoder;
 encoder.Encode(foo);
 auto row = encoder.GetWriter().ToRow();
diff --git a/docs/guide/python/row-format.md b/docs/guide/python/row-format.md
index c0cc57beb..3fd86f9f3 100644
--- a/docs/guide/python/row-format.md
+++ b/docs/guide/python/row-format.md
@@ -121,19 +121,17 @@ Bar bar2 = barEncoder.fromRow(f4Array.getStruct(20));     
// Deserialize 21st Ba
 struct Bar {
   std::string f1;
   std::vector<int64_t> f2;
+  FORY_STRUCT(Bar, f1, f2);
 };
 
-FORY_FIELD_INFO(Bar, f1, f2);
-
 struct Foo {
   int32_t f1;
   std::vector<int32_t> f2;
   std::map<std::string, int32_t> f3;
   std::vector<Bar> f4;
+  FORY_STRUCT(Foo, f1, f2, f3, f4);
 };
 
-FORY_FIELD_INFO(Foo, f1, f2, f3, f4);
-
 fory::encoder::RowEncoder<Foo> encoder;
 encoder.Encode(foo);
 auto row = encoder.GetWriter().ToRow();
diff --git a/docs/specification/xlang_serialization_spec.md 
b/docs/specification/xlang_serialization_spec.md
index 530edbac5..e02ee979c 100644
--- a/docs/specification/xlang_serialization_spec.md
+++ b/docs/specification/xlang_serialization_spec.md
@@ -1636,7 +1636,7 @@ Meta strings are required for enum and struct 
serialization (encoding field name
 
 ### C++
 
-- Compile-time reflection via macros (`FORY_STRUCT`, `FORY_FIELD_INFO`)
+- Compile-time reflection via macros (`FORY_STRUCT`)
 - Template meta programming for type dispatch and serializer selection
 - Uses `std::shared_ptr` for reference tracking
 - Compile-time field ordering
diff --git a/examples/cpp/hello_row/README.md b/examples/cpp/hello_row/README.md
index 446cfb4e3..8a6fcb059 100644
--- a/examples/cpp/hello_row/README.md
+++ b/examples/cpp/hello_row/README.md
@@ -107,18 +107,17 @@ This example demonstrates:
 
 ## Key Concepts
 
-### Registering Field Info
+### Registering Struct Metadata
 
-Use the `FORY_FIELD_INFO` macro to enable automatic encoding:
+Use the `FORY_STRUCT` macro to enable automatic encoding:
 
 ```cpp
 struct Employee {
   std::string name;
   int32_t id;
   float salary;
+  FORY_STRUCT(Employee, name, id, salary);
 };
-
-FORY_FIELD_INFO(Employee, name, id, salary);
 ```
 
 ### Manual Row Writing
diff --git a/examples/cpp/hello_row/main.cc b/examples/cpp/hello_row/main.cc
index 4d888590f..d53fc664b 100644
--- a/examples/cpp/hello_row/main.cc
+++ b/examples/cpp/hello_row/main.cc
@@ -45,20 +45,17 @@ struct Employee {
   std::string name;
   int32_t id;
   float salary;
+  FORY_STRUCT(Employee, name, id, salary);
 };
 
-// Register field info for automatic encoding
-FORY_FIELD_INFO(Employee, name, id, salary);
-
 // Define a nested struct
 struct Department {
   std::string dept_name;
   Employee manager;
   std::vector<Employee> employees;
+  FORY_STRUCT(Department, dept_name, manager, employees);
 };
 
-FORY_FIELD_INFO(Department, dept_name, manager, employees);
-
 int main() {
   std::cout << "=== Fory C++ Row Format Example ===" << std::endl << std::endl;
 
diff --git a/examples/cpp/hello_world/README.md 
b/examples/cpp/hello_world/README.md
index 76b494c8a..ce0bf7052 100644
--- a/examples/cpp/hello_world/README.md
+++ b/examples/cpp/hello_world/README.md
@@ -106,9 +106,8 @@ Use the `FORY_STRUCT` macro to register struct fields for 
serialization:
 struct Point {
   int32_t x;
   int32_t y;
+  FORY_STRUCT(Point, x, y);
 };
-
-FORY_STRUCT(Point, x, y);
 ```
 
 ### Creating a Fory Instance
diff --git a/examples/cpp/hello_world/main.cc b/examples/cpp/hello_world/main.cc
index 875ea82b7..8862ea4c8 100644
--- a/examples/cpp/hello_world/main.cc
+++ b/examples/cpp/hello_world/main.cc
@@ -40,10 +40,9 @@ struct Point {
   bool operator==(const Point &other) const {
     return x == other.x && y == other.y;
   }
-};
 
-// Register struct fields with Fory using FORY_STRUCT macro
-FORY_STRUCT(Point, x, y);
+  FORY_STRUCT(Point, x, y);
+};
 
 // Define a more complex struct with various field types
 struct Person {
@@ -54,9 +53,9 @@ struct Person {
   bool operator==(const Person &other) const {
     return name == other.name && age == other.age && hobbies == other.hobbies;
   }
-};
 
-FORY_STRUCT(Person, name, age, hobbies);
+  FORY_STRUCT(Person, name, age, hobbies);
+};
 
 // Define a nested struct
 struct Team {
@@ -68,9 +67,9 @@ struct Team {
     return team_name == other.team_name && members == other.members &&
            headquarters == other.headquarters;
   }
-};
 
-FORY_STRUCT(Team, team_name, members, headquarters);
+  FORY_STRUCT(Team, team_name, members, headquarters);
+};
 
 // Define an enum
 enum class Status { PENDING, ACTIVE, COMPLETED };
diff --git a/java/fory-format/pom.xml b/java/fory-format/pom.xml
index 7667a651c..19e685b3e 100644
--- a/java/fory-format/pom.xml
+++ b/java/fory-format/pom.xml
@@ -105,6 +105,13 @@
           </archive>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <argLine>--add-opens=java.base/java.nio=ALL-UNNAMED</argLine>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/python/README.md b/python/README.md
index 4f91c06ff..b74b3fd47 100644
--- a/python/README.md
+++ b/python/README.md
@@ -603,19 +603,17 @@ And in C++ with compile-time type information:
 struct Bar {
   std::string f1;
   std::vector<int64_t> f2;
+  FORY_STRUCT(Bar, f1, f2);
 };
 
-FORY_FIELD_INFO(Bar, f1, f2);
-
 struct Foo {
   int32_t f1;
   std::vector<int32_t> f2;
   std::map<std::string, int32_t> f3;
   std::vector<Bar> f4;
+  FORY_STRUCT(Foo, f1, f2, f3, f4);
 };
 
-FORY_FIELD_INFO(Foo, f1, f2, f3, f4);
-
 // Create large dataset
 Foo foo;
 foo.f1 = 10;


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

Reply via email to