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 3fddc9c68 feat(compiler): generate getter/setter/has/clear methods for 
c++ (#3199)
3fddc9c68 is described below

commit 3fddc9c687050a61db3da0dab535a75cba8f28dd
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jan 25 22:19:19 2026 +0800

    feat(compiler): generate getter/setter/has/clear methods for c++ (#3199)
    
    ## Why?
    
    Message-typed fields in FDL are effectively optional and need safe
    accessors in generated C++ APIs. Aligning defaults and generated APIs
    avoids direct member access and keeps cross-language IDL tests
    consistent.
    
    ## What does this PR do?
    
    - Generate C++ getters/setters/has/clear/mutable for fields, store
    message fields as `std::unique_ptr`, and update equality + field config
    to use member names.
    - Apply FDL defaults so message fields are always optional and assign
    tag IDs from field numbers; add validator tests.
    - Update IDL integration tests (C++/Go/Rust) for optional message fields
    and accessors; treat `unique_ptr` as nullable in C++ type resolver.
    - Make Rust derive field ordering deterministic by tag ID/name
    tie-breakers.
    
    ## Related issues
    
    Closes #3198
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 compiler/fory_compiler/generators/cpp.py           | 198 ++++++++++++++++++---
 compiler/fory_compiler/ir/validator.py             |  66 ++++++-
 .../fory_compiler/tests/test_message_defaults.py   |  70 ++++++++
 cpp/fory/serialization/type_resolver.h             |   5 +-
 integration_tests/idl_tests/cpp/main.cc            | 150 ++++++++--------
 .../idl_tests/go/idl_roundtrip_test.go             |   4 +-
 .../idl_tests/rust/tests/idl_roundtrip.rs          |   4 +-
 rust/fory-derive/src/object/util.rs                |  72 ++++----
 8 files changed, 435 insertions(+), 134 deletions(-)

diff --git a/compiler/fory_compiler/generators/cpp.py 
b/compiler/fory_compiler/generators/cpp.py
index 49298cb86..a966d2d81 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -18,6 +18,7 @@
 """C++ code generator."""
 
 from typing import Dict, List, Optional, Set, Tuple
+import typing
 
 from fory_compiler.generators.base import BaseGenerator, GeneratedFile
 from fory_compiler.ir.ast import (
@@ -120,7 +121,9 @@ class CppGenerator(BaseGenerator):
 
         # Collect includes (including from nested types)
         includes.add("<cstdint>")
+        includes.add("<memory>")
         includes.add("<string>")
+        includes.add("<utility>")
         includes.add('"fory/serialization/fory.h"')
         if self.schema_has_unions():
             includes.add("<utility>")
@@ -384,6 +387,152 @@ class CppGenerator(BaseGenerator):
         qualified_name = self.get_namespaced_type_name(enum.name, parent_stack)
         return f"FORY_ENUM({qualified_name}, {value_names});"
 
+    def resolve_named_type(
+        self, name: str, parent_stack: Optional[List[Message]]
+    ) -> Optional[typing.Union[Message, Enum, Union]]:
+        """Resolve a named type to its schema definition."""
+        parts = name.split(".")
+        if len(parts) > 1:
+            current = self.find_top_level_type(parts[0])
+            for part in parts[1:]:
+                if isinstance(current, Message):
+                    current = current.get_nested_type(part)
+                else:
+                    return None
+            return current
+        if parent_stack:
+            for msg in reversed(parent_stack):
+                nested = msg.get_nested_type(name)
+                if nested is not None:
+                    return nested
+        return self.find_top_level_type(name)
+
+    def find_top_level_type(
+        self, name: str
+    ) -> Optional[typing.Union[Message, Enum, Union]]:
+        """Find a top-level type definition by name."""
+        for enum in self.schema.enums:
+            if enum.name == name:
+                return enum
+        for union in self.schema.unions:
+            if union.name == name:
+                return union
+        for message in self.schema.messages:
+            if message.name == name:
+                return message
+        return None
+
+    def is_message_type(
+        self, field_type: FieldType, parent_stack: Optional[List[Message]]
+    ) -> bool:
+        if not isinstance(field_type, NamedType):
+            return False
+        resolved = self.resolve_named_type(field_type.name, parent_stack)
+        return isinstance(resolved, Message)
+
+    def get_field_member_name(self, field: Field) -> str:
+        return f"{self.to_snake_case(field.name)}_"
+
+    def get_field_storage_type(
+        self, field: Field, parent_stack: Optional[List[Message]]
+    ) -> str:
+        if self.is_message_type(field.field_type, parent_stack):
+            type_name = self.resolve_nested_type_name(
+                field.field_type.name, parent_stack
+            )
+            return f"std::unique_ptr<{type_name}>"
+        return self.generate_type(
+            field.field_type,
+            field.optional,
+            field.ref,
+            field.element_optional,
+            field.element_ref,
+            parent_stack,
+        )
+
+    def get_field_value_type(
+        self, field: Field, parent_stack: Optional[List[Message]]
+    ) -> str:
+        if self.is_message_type(field.field_type, parent_stack):
+            return self.resolve_nested_type_name(field.field_type.name, 
parent_stack)
+        return self.generate_type(
+            field.field_type,
+            False,
+            field.ref,
+            field.element_optional,
+            field.element_ref,
+            parent_stack,
+        )
+
+    def get_field_eq_expression(
+        self, field: Field, parent_stack: Optional[List[Message]]
+    ) -> str:
+        member_name = self.get_field_member_name(field)
+        other_member = f"other.{member_name}"
+        if self.is_message_type(field.field_type, parent_stack):
+            return (
+                f"(({member_name} && {other_member}) ? "
+                f"(*{member_name} == *{other_member}) : "
+                f"({member_name} == {other_member}))"
+            )
+        return f"{member_name} == {other_member}"
+
+    def generate_field_accessors(
+        self, field: Field, parent_stack: Optional[List[Message]], indent: str
+    ) -> List[str]:
+        lines: List[str] = []
+        field_name = self.to_snake_case(field.name)
+        member_name = self.get_field_member_name(field)
+        value_type = self.get_field_value_type(field, parent_stack)
+
+        if self.is_message_type(field.field_type, parent_stack):
+            lines.append(f"{indent}bool has_{field_name}() const {{")
+            lines.append(f"{indent}  return {member_name} != nullptr;")
+            lines.append(f"{indent}}}")
+            lines.append("")
+            lines.append(f"{indent}const {value_type}& {field_name}() const 
{{")
+            lines.append(f"{indent}  return *{member_name};")
+            lines.append(f"{indent}}}")
+            lines.append("")
+            lines.append(f"{indent}{value_type}* mutable_{field_name}() {{")
+            lines.append(f"{indent}  if (!{member_name}) {{")
+            lines.append(
+                f"{indent}    {member_name} = 
std::make_unique<{value_type}>();"
+            )
+            lines.append(f"{indent}  }}")
+            lines.append(f"{indent}  return {member_name}.get();")
+            lines.append(f"{indent}}}")
+            lines.append("")
+            lines.append(f"{indent}void clear_{field_name}() {{")
+            lines.append(f"{indent}  {member_name}.reset();")
+            lines.append(f"{indent}}}")
+            return lines
+
+        if field.optional:
+            lines.append(f"{indent}bool has_{field_name}() const {{")
+            lines.append(f"{indent}  return {member_name}.has_value();")
+            lines.append(f"{indent}}}")
+            lines.append("")
+
+        lines.append(f"{indent}const {value_type}& {field_name}() const {{")
+        if field.optional:
+            lines.append(f"{indent}  return *{member_name};")
+        else:
+            lines.append(f"{indent}  return {member_name};")
+        lines.append(f"{indent}}}")
+        lines.append("")
+        lines.append(f"{indent}void set_{field_name}({value_type} value) {{")
+        lines.append(f"{indent}  {member_name} = std::move(value);")
+        lines.append(f"{indent}}}")
+
+        if field.optional:
+            lines.append("")
+            lines.append(f"{indent}void clear_{field_name}() {{")
+            lines.append(f"{indent}  {member_name}.reset();")
+            lines.append(f"{indent}}}")
+
+        return lines
+
     def generate_message_definition(
         self,
         message: Message,
@@ -394,7 +543,7 @@ class CppGenerator(BaseGenerator):
         indent: str,
     ) -> List[str]:
         """Generate a C++ class definition with nested types."""
-        lines = []
+        lines: List[str] = []
         class_name = message.name
         lineage = parent_stack + [message]
         body_indent = f"{indent}  "
@@ -402,6 +551,11 @@ class CppGenerator(BaseGenerator):
 
         lines.append(f"{indent}class {class_name} final {{")
         lines.append(f"{body_indent}public:")
+        if message.fields:
+            lines.append(
+                f"{body_indent}  friend struct 
::fory::detail::ForyFieldConfigImpl<{class_name}>;"
+            )
+            lines.append("")
 
         for nested_enum in message.nested_enums:
             lines.extend(self.generate_enum_definition(nested_enum, 
body_indent))
@@ -432,28 +586,20 @@ class CppGenerator(BaseGenerator):
             union_macros.extend(self.generate_union_macros(nested_union, 
lineage))
             lines.append("")
 
-        for field in message.fields:
-            cpp_type = self.generate_type(
-                field.field_type,
-                field.optional,
-                field.ref,
-                field.element_optional,
-                field.element_ref,
-                lineage,
-            )
-            field_name = self.to_snake_case(field.name)
-            lines.append(f"{field_indent}{cpp_type} {field_name};")
-
-        lines.append("")
+        if message.fields:
+            for index, field in enumerate(message.fields):
+                lines.extend(self.generate_field_accessors(field, lineage, 
body_indent))
+                if index + 1 < len(message.fields):
+                    lines.append("")
+            lines.append("")
 
         lines.append(
             f"{body_indent}bool operator==(const {class_name}& other) const {{"
         )
         if message.fields:
-            conditions = []
-            for field in message.fields:
-                field_name = self.to_snake_case(field.name)
-                conditions.append(f"{field_name} == other.{field_name}")
+            conditions = [
+                self.get_field_eq_expression(field, lineage) for field in 
message.fields
+            ]
             lines.append(f"{body_indent}  return {' && '.join(conditions)};")
         else:
             lines.append(f"{body_indent}  return true;")
@@ -461,7 +607,17 @@ class CppGenerator(BaseGenerator):
 
         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)
+            lines.append("")
+            lines.append(f"{body_indent}private:")
+            for field in message.fields:
+                field_type = self.get_field_storage_type(field, lineage)
+                member_name = self.get_field_member_name(field)
+                lines.append(f"{field_indent}{field_type} {member_name};")
+            lines.append("")
+            lines.append(f"{body_indent}public:")
+            field_members = ", ".join(
+                self.get_field_member_name(f) for f in message.fields
+            )
             field_config_type_name = self.get_field_config_type_and_alias(
                 message.name, parent_stack
             )
@@ -471,7 +627,7 @@ class CppGenerator(BaseGenerator):
                 )
             )
             lines.append(
-                f"{body_indent}FORY_STRUCT({struct_type_name}, {field_names});"
+                f"{body_indent}FORY_STRUCT({struct_type_name}, 
{field_members});"
             )
         else:
             lines.append(f"{body_indent}FORY_STRUCT({struct_type_name});")
@@ -1002,7 +1158,7 @@ class CppGenerator(BaseGenerator):
         """Generate FORY_FIELD_CONFIG macro for a message."""
         entries = []
         for field in message.fields:
-            field_name = self.to_snake_case(field.name)
+            field_name = self.get_field_member_name(field)
             meta = self.get_field_meta(field)
             entries.append(f"({field_name}, {meta})")
         joined = ", ".join(entries)
diff --git a/compiler/fory_compiler/ir/validator.py 
b/compiler/fory_compiler/ir/validator.py
index d1aaffae3..3bdec8e06 100644
--- a/compiler/fory_compiler/ir/validator.py
+++ b/compiler/fory_compiler/ir/validator.py
@@ -18,7 +18,7 @@
 """Schema validation for Fory IDL."""
 
 from dataclasses import dataclass
-from typing import List, Optional
+from typing import List, Optional, Union as TypingUnion
 
 from fory_compiler.ir.ast import (
     Schema,
@@ -57,6 +57,7 @@ class SchemaValidator:
         self.warnings: List[ValidationIssue] = []
 
     def validate(self) -> bool:
+        self._apply_field_defaults()
         self._check_duplicate_type_names()
         self._check_duplicate_type_ids()
         self._check_messages()
@@ -198,6 +199,69 @@ class SchemaValidator:
         for message in self.schema.messages:
             validate_message(message)
 
+    def _apply_field_defaults(self) -> None:
+        def apply_message_fields(
+            message: Message,
+            enclosing_messages: Optional[List[Message]] = None,
+        ) -> None:
+            lineage = (enclosing_messages or []) + [message]
+            for field in message.fields:
+                if self.schema.source_format == "fdl" and field.tag_id is None:
+                    field.tag_id = field.number
+                if self._is_message_type(field.field_type, lineage):
+                    explicit_optional = field.optional
+                    if self.schema.source_format == "fdl" and 
explicit_optional:
+                        self._error(
+                            "Message fields are always optional; remove the 
optional modifier",
+                            field.location,
+                        )
+                    field.optional = True
+            for nested_msg in message.nested_messages:
+                apply_message_fields(nested_msg, lineage)
+
+        for message in self.schema.messages:
+            apply_message_fields(message)
+
+    def _is_message_type(
+        self, field_type: FieldType, parent_stack: List[Message]
+    ) -> bool:
+        if not isinstance(field_type, NamedType):
+            return False
+        resolved = self._resolve_named_type(field_type.name, parent_stack)
+        return isinstance(resolved, Message)
+
+    def _resolve_named_type(
+        self, name: str, parent_stack: List[Message]
+    ) -> Optional[TypingUnion[Message, Enum, Union]]:
+        parts = name.split(".")
+        if len(parts) > 1:
+            current = self._find_top_level_type(parts[0])
+            for part in parts[1:]:
+                if isinstance(current, Message):
+                    current = current.get_nested_type(part)
+                else:
+                    return None
+            return current
+        for msg in reversed(parent_stack):
+            nested = msg.get_nested_type(name)
+            if nested is not None:
+                return nested
+        return self._find_top_level_type(name)
+
+    def _find_top_level_type(
+        self, name: str
+    ) -> Optional[TypingUnion[Message, Enum, Union]]:
+        for enum in self.schema.enums:
+            if enum.name == name:
+                return enum
+        for union in self.schema.unions:
+            if union.name == name:
+                return union
+        for message in self.schema.messages:
+            if message.name == name:
+                return message
+        return None
+
     def _check_type_references(self) -> None:
         def check_type_ref(
             field_type: FieldType,
diff --git a/compiler/fory_compiler/tests/test_message_defaults.py 
b/compiler/fory_compiler/tests/test_message_defaults.py
new file mode 100644
index 000000000..e71d8435f
--- /dev/null
+++ b/compiler/fory_compiler/tests/test_message_defaults.py
@@ -0,0 +1,70 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for message field defaults in FDL."""
+
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.ir.validator import SchemaValidator
+
+
+def test_message_fields_are_optional_by_default():
+    source = """
+    message Foo {
+        Bar bar = 1;
+    }
+
+    message Bar {
+        int32 id = 1;
+    }
+    """
+    schema = Parser(Lexer(source).tokenize()).parse()
+    validator = SchemaValidator(schema)
+    assert validator.validate()
+    field = schema.messages[0].fields[0]
+    assert field.optional is True
+
+
+def test_message_fields_disallow_optional_modifier_in_fdl():
+    source = """
+    message Foo {
+        optional Bar bar = 1;
+    }
+
+    message Bar {
+        int32 id = 1;
+    }
+    """
+    schema = Parser(Lexer(source).tokenize()).parse()
+    validator = SchemaValidator(schema)
+    assert not validator.validate()
+    assert any(
+        "Message fields are always optional" in str(err) for err in 
validator.errors
+    )
+
+
+def test_fdl_field_numbers_set_tag_ids():
+    source = """
+    message Foo {
+        int32 id = 7;
+    }
+    """
+    schema = Parser(Lexer(source).tokenize()).parse()
+    validator = SchemaValidator(schema)
+    assert validator.validate()
+    field = schema.messages[0].fields[0]
+    assert field.tag_id == field.number
diff --git a/cpp/fory/serialization/type_resolver.h 
b/cpp/fory/serialization/type_resolver.h
index 827243d1f..670665607 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -497,9 +497,10 @@ constexpr bool compute_is_nullable() {
   } else if constexpr (::fory::detail::has_field_tags_v<T>) {
     return ::fory::detail::GetFieldTagEntry<T, Index>::is_nullable;
   } else {
-    // Default: nullable if std::optional or std::shared_ptr
+    // Default: nullable if std::optional or smart pointers.
     return is_optional_v<UnwrappedFieldType> ||
-           is_shared_ptr_v<UnwrappedFieldType>;
+           is_shared_ptr_v<UnwrappedFieldType> ||
+           is_unique_ptr_v<UnwrappedFieldType>;
   }
 }
 
diff --git a/integration_tests/idl_tests/cpp/main.cc 
b/integration_tests/idl_tests/cpp/main.cc
index 026e2f384..9b8740944 100644
--- a/integration_tests/idl_tests/cpp/main.cc
+++ b/integration_tests/idl_tests/cpp/main.cc
@@ -70,33 +70,33 @@ fory::Result<void, fory::Error> RunRoundTrip() {
   complex_fbs::RegisterTypes(fory);
 
   addressbook::Person::PhoneNumber mobile;
-  mobile.number = "555-0100";
-  mobile.phone_type = addressbook::Person::PhoneType::MOBILE;
+  mobile.set_number("555-0100");
+  mobile.set_phone_type(addressbook::Person::PhoneType::MOBILE);
 
   addressbook::Person::PhoneNumber work;
-  work.number = "555-0111";
-  work.phone_type = addressbook::Person::PhoneType::WORK;
+  work.set_number("555-0111");
+  work.set_phone_type(addressbook::Person::PhoneType::WORK);
 
   addressbook::Person person;
-  person.name = "Alice";
-  person.id = 123;
-  person.email = "[email protected]";
-  person.tags = {"friend", "colleague"};
-  person.scores = {{"math", 100}, {"science", 98}};
-  person.salary = 120000.5;
-  person.phones = {mobile, work};
+  person.set_name("Alice");
+  person.set_id(123);
+  person.set_email("[email protected]");
+  person.set_tags({"friend", "colleague"});
+  person.set_scores({{"math", 100}, {"science", 98}});
+  person.set_salary(120000.5);
+  person.set_phones({mobile, work});
   addressbook::Dog dog;
-  dog.name = "Rex";
-  dog.bark_volume = 5;
-  person.pet = addressbook::Animal::dog(dog);
+  dog.set_name("Rex");
+  dog.set_bark_volume(5);
+  person.set_pet(addressbook::Animal::dog(dog));
   addressbook::Cat cat;
-  cat.name = "Mimi";
-  cat.lives = 9;
-  person.pet = addressbook::Animal::cat(cat);
+  cat.set_name("Mimi");
+  cat.set_lives(9);
+  person.set_pet(addressbook::Animal::cat(cat));
 
   addressbook::AddressBook book;
-  book.people = {person};
-  book.people_by_name = {{person.name, person}};
+  book.set_people({person});
+  book.set_people_by_name({{person.name(), person}});
 
   FORY_TRY(bytes, fory.serialize(book));
   FORY_TRY(roundtrip, fory.deserialize<addressbook::AddressBook>(bytes.data(),
@@ -108,27 +108,27 @@ fory::Result<void, fory::Error> RunRoundTrip() {
   }
 
   addressbook::PrimitiveTypes types;
-  types.bool_value = true;
-  types.int8_value = 12;
-  types.int16_value = 1234;
-  types.int32_value = -123456;
-  types.varint32_value = -12345;
-  types.int64_value = -123456789;
-  types.varint64_value = -987654321;
-  types.tagged_int64_value = 123456789;
-  types.uint8_value = 200;
-  types.uint16_value = 60000;
-  types.uint32_value = 1234567890;
-  types.var_uint32_value = 1234567890;
-  types.uint64_value = 9876543210ULL;
-  types.var_uint64_value = 12345678901ULL;
-  types.tagged_uint64_value = 2222222222ULL;
-  types.float16_value = 1.5F;
-  types.float32_value = 2.5F;
-  types.float64_value = 3.5;
-  types.contact = addressbook::PrimitiveTypes::Contact::email(
-      std::string("[email protected]"));
-  types.contact = addressbook::PrimitiveTypes::Contact::phone(12345);
+  types.set_bool_value(true);
+  types.set_int8_value(12);
+  types.set_int16_value(1234);
+  types.set_int32_value(-123456);
+  types.set_varint32_value(-12345);
+  types.set_int64_value(-123456789);
+  types.set_varint64_value(-987654321);
+  types.set_tagged_int64_value(123456789);
+  types.set_uint8_value(200);
+  types.set_uint16_value(60000);
+  types.set_uint32_value(1234567890);
+  types.set_var_uint32_value(1234567890);
+  types.set_uint64_value(9876543210ULL);
+  types.set_var_uint64_value(12345678901ULL);
+  types.set_tagged_uint64_value(2222222222ULL);
+  types.set_float16_value(1.5F);
+  types.set_float32_value(2.5F);
+  types.set_float64_value(3.5);
+  types.set_contact(addressbook::PrimitiveTypes::Contact::email(
+      std::string("[email protected]")));
+  types.set_contact(addressbook::PrimitiveTypes::Contact::phone(12345));
 
   FORY_TRY(primitive_bytes, fory.serialize(types));
   FORY_TRY(primitive_roundtrip,
@@ -141,19 +141,19 @@ fory::Result<void, fory::Error> RunRoundTrip() {
   }
 
   monster::Vec3 pos;
-  pos.x = 1.0F;
-  pos.y = 2.0F;
-  pos.z = 3.0F;
+  pos.set_x(1.0F);
+  pos.set_y(2.0F);
+  pos.set_z(3.0F);
 
   monster::Monster monster_value;
-  monster_value.pos = pos;
-  monster_value.mana = 200;
-  monster_value.hp = 80;
-  monster_value.name = "Orc";
-  monster_value.friendly = true;
-  monster_value.inventory = {static_cast<uint8_t>(1), static_cast<uint8_t>(2),
-                             static_cast<uint8_t>(3)};
-  monster_value.color = monster::Color::Blue;
+  *monster_value.mutable_pos() = pos;
+  monster_value.set_mana(200);
+  monster_value.set_hp(80);
+  monster_value.set_name("Orc");
+  monster_value.set_friendly(true);
+  monster_value.set_inventory({static_cast<uint8_t>(1), 
static_cast<uint8_t>(2),
+                               static_cast<uint8_t>(3)});
+  monster_value.set_color(monster::Color::Blue);
 
   FORY_TRY(monster_bytes, fory.serialize(monster_value));
   FORY_TRY(monster_roundtrip, fory.deserialize<monster::Monster>(
@@ -164,34 +164,32 @@ fory::Result<void, fory::Error> RunRoundTrip() {
         fory::Error::invalid("flatbuffers monster roundtrip mismatch"));
   }
 
-  complex_fbs::ScalarPack scalars;
-  scalars.b = -8;
-  scalars.ub = 200;
-  scalars.s = -1234;
-  scalars.us = 40000;
-  scalars.i = -123456;
-  scalars.ui = 123456;
-  scalars.l = -123456789;
-  scalars.ul = 987654321;
-  scalars.f = 1.5F;
-  scalars.d = 2.5;
-  scalars.ok = true;
-
   complex_fbs::Container container;
-  container.id = 9876543210ULL;
-  container.status = complex_fbs::Status::STARTED;
-  container.bytes = {static_cast<int8_t>(1), static_cast<int8_t>(2),
-                     static_cast<int8_t>(3)};
-  container.numbers = {10, 20, 30};
-  container.scalars = scalars;
-  container.names = {"alpha", "beta"};
-  container.flags = {true, false};
+  container.set_id(9876543210ULL);
+  container.set_status(complex_fbs::Status::STARTED);
+  container.set_bytes(
+      {static_cast<int8_t>(1), static_cast<int8_t>(2), 
static_cast<int8_t>(3)});
+  container.set_numbers({10, 20, 30});
+  auto *scalars = container.mutable_scalars();
+  scalars->set_b(-8);
+  scalars->set_ub(200);
+  scalars->set_s(-1234);
+  scalars->set_us(40000);
+  scalars->set_i(-123456);
+  scalars->set_ui(123456);
+  scalars->set_l(-123456789);
+  scalars->set_ul(987654321);
+  scalars->set_f(1.5F);
+  scalars->set_d(2.5);
+  scalars->set_ok(true);
+  container.set_names({"alpha", "beta"});
+  container.set_flags({true, false});
   complex_fbs::Note note;
-  note.text = "alpha";
-  container.payload = complex_fbs::Payload::note(note);
+  note.set_text("alpha");
+  container.set_payload(complex_fbs::Payload::note(note));
   complex_fbs::Metric metric;
-  metric.value = 42.0;
-  container.payload = complex_fbs::Payload::metric(metric);
+  metric.set_value(42.0);
+  container.set_payload(complex_fbs::Payload::metric(metric));
 
   FORY_TRY(container_bytes, fory.serialize(container));
   FORY_TRY(container_roundtrip,
diff --git a/integration_tests/idl_tests/go/idl_roundtrip_test.go 
b/integration_tests/idl_tests/go/idl_roundtrip_test.go
index c588af27e..3e4cd4e7c 100644
--- a/integration_tests/idl_tests/go/idl_roundtrip_test.go
+++ b/integration_tests/idl_tests/go/idl_roundtrip_test.go
@@ -205,7 +205,7 @@ func runFilePrimitiveRoundTrip(t *testing.T, f *fory.Fory, 
types PrimitiveTypes)
 }
 
 func buildMonster() monster.Monster {
-       pos := monster.Vec3{
+       pos := &monster.Vec3{
                X: 1.0,
                Y: 2.0,
                Z: 3.0,
@@ -265,7 +265,7 @@ func runFileMonsterRoundTrip(t *testing.T, f *fory.Fory, 
monsterValue monster.Mo
 }
 
 func buildContainer() complexfbs.Container {
-       scalars := complexfbs.ScalarPack{
+       scalars := &complexfbs.ScalarPack{
                B:  -8,
                Ub: 200,
                S:  -1234,
diff --git a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs 
b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
index f0617b080..dfe3572bf 100644
--- a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
+++ b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
@@ -98,7 +98,7 @@ fn build_monster() -> Monster {
         z: 3.0,
     };
     Monster {
-        pos,
+        pos: Some(pos),
         mana: 200,
         hp: 80,
         name: "Orc".to_string(),
@@ -132,7 +132,7 @@ fn build_container() -> Container {
         status: Status::Started,
         bytes: vec![1, 2, 3],
         numbers: vec![10, 20, 30],
-        scalars,
+        scalars: Some(scalars),
         names: vec!["alpha".to_string(), "beta".to_string()],
         flags: vec![true, false],
         payload,
diff --git a/rust/fory-derive/src/object/util.rs 
b/rust/fory-derive/src/object/util.rs
index 866b6cf42..d8182828f 100644
--- a/rust/fory-derive/src/object/util.rs
+++ b/rust/fory-derive/src/object/util.rs
@@ -1123,7 +1123,8 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups 
{
         if is_forward_field(&field.ty) {
             let raw_ident = get_field_name(field, idx);
             let ident = to_snake_case(&raw_ident);
-            other_fields.push((ident, "Forward".to_string(), TypeId::UNKNOWN 
as u32));
+            // Forward fields don't have explicit IDs; sort by name.
+            other_fields.push((ident.clone(), ident, TypeId::UNKNOWN as u32));
         }
     }
 
@@ -1136,8 +1137,13 @@ fn group_fields_by_type(fields: &[&Field]) -> 
FieldGroups {
             continue;
         }
 
-        // Parse field metadata to get encoding attributes
+        // Parse field metadata to get encoding attributes and field ID
         let meta = parse_field_meta(field).unwrap_or_default();
+        let sort_key = if meta.uses_tag_id() {
+            meta.effective_id().to_string()
+        } else {
+            ident.clone()
+        };
 
         let ty: String = field
             .ty
@@ -1148,27 +1154,28 @@ fn group_fields_by_type(fields: &[&Field]) -> 
FieldGroups {
             .collect::<String>();
 
         // Closure to group non-option fields, considering encoding attributes
-        let mut group_field = |ident: String, ty_str: &str, is_primitive: 
bool| {
-            let base_type_id = get_type_id_by_name(ty_str);
-            // Adjust type ID based on encoding attributes for u32/u64 fields
-            let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
-
-            // Categorize based on type_id
-            if is_primitive {
-                primitive_fields.push((ident, ty_str.to_string(), type_id));
-            } else if is_internal_type_id(type_id) {
-                internal_type_fields.push((ident, ty_str.to_string(), 
type_id));
-            } else if type_id == TypeId::LIST as u32 {
-                list_fields.push((ident, ty_str.to_string(), type_id));
-            } else if type_id == TypeId::SET as u32 {
-                set_fields.push((ident, ty_str.to_string(), type_id));
-            } else if type_id == TypeId::MAP as u32 {
-                map_fields.push((ident, ty_str.to_string(), type_id));
-            } else {
-                // User-defined type
-                other_fields.push((ident, ty_str.to_string(), type_id));
-            }
-        };
+        let mut group_field =
+            |ident: String, sort_key: String, ty_str: &str, is_primitive: 
bool| {
+                let base_type_id = get_type_id_by_name(ty_str);
+                // Adjust type ID based on encoding attributes for u32/u64 
fields
+                let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
+
+                // Categorize based on type_id
+                if is_primitive {
+                    primitive_fields.push((ident, sort_key, type_id));
+                } else if is_internal_type_id(type_id) {
+                    internal_type_fields.push((ident, sort_key, type_id));
+                } else if type_id == TypeId::LIST as u32 {
+                    list_fields.push((ident, sort_key, type_id));
+                } else if type_id == TypeId::SET as u32 {
+                    set_fields.push((ident, sort_key, type_id));
+                } else if type_id == TypeId::MAP as u32 {
+                    map_fields.push((ident, sort_key, type_id));
+                } else {
+                    // User-defined type
+                    other_fields.push((ident, sort_key, type_id));
+                }
+            };
 
         // handle Option<Primitive> specially
         if let Some(inner) = extract_option_inner(&ty) {
@@ -1176,14 +1183,14 @@ fn group_fields_by_type(fields: &[&Field]) -> 
FieldGroups {
                 // Get base type ID and adjust for encoding attributes
                 let base_type_id = get_primitive_type_id(inner);
                 let type_id = adjust_type_id_for_encoding(base_type_id, &meta);
-                nullable_primitive_fields.push((ident, ty.to_string(), 
type_id));
+                nullable_primitive_fields.push((ident, sort_key, type_id));
             } else {
-                group_field(ident, inner, false);
+                group_field(ident, sort_key, inner, false);
             }
         } else if PRIMITIVE_TYPE_NAMES.contains(&ty.as_str()) {
-            group_field(ident, &ty, true);
+            group_field(ident, sort_key, &ty, true);
         } else {
-            group_field(ident, &ty, false);
+            group_field(ident, sort_key, &ty, false);
         }
     }
 
@@ -1197,6 +1204,9 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups 
{
             .then_with(|| size_b.cmp(&size_a))
             // Use descending type_id order to match Java's 
COMPARATOR_BY_PRIMITIVE_TYPE_ID
             .then_with(|| b.2.cmp(&a.2))
+            // Field identifier (tag ID or name) as tie-breaker
+            .then_with(|| a.1.cmp(&b.1))
+            // Deterministic fallback for duplicate identifiers
             .then_with(|| a.0.cmp(&b.0))
     }
 
@@ -1204,11 +1214,13 @@ fn group_fields_by_type(fields: &[&Field]) -> 
FieldGroups {
         a: &(String, String, u32),
         b: &(String, String, u32),
     ) -> std::cmp::Ordering {
-        a.2.cmp(&b.2).then_with(|| a.0.cmp(&b.0))
+        a.2.cmp(&b.2)
+            .then_with(|| a.1.cmp(&b.1))
+            .then_with(|| a.0.cmp(&b.0))
     }
 
     fn name_sorter(a: &(String, String, u32), b: &(String, String, u32)) -> 
std::cmp::Ordering {
-        a.0.cmp(&b.0)
+        a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))
     }
 
     primitive_fields.sort_by(numeric_sorter);
@@ -1217,7 +1229,7 @@ fn group_fields_by_type(fields: &[&Field]) -> FieldGroups 
{
     list_fields.sort_by(name_sorter);
     set_fields.sort_by(name_sorter);
     map_fields.sort_by(name_sorter);
-    other_fields.sort_by(type_id_then_name_sorter);
+    other_fields.sort_by(name_sorter);
 
     (
         primitive_fields,


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


Reply via email to