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 bf7bc5180 feat(compiler): add evolution option support (#3262)
bf7bc5180 is described below

commit bf7bc51801db1cc199b2b5caa16667749d92c1be
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Feb 4 22:46:08 2026 +0800

    feat(compiler): add evolution option support (#3262)
    
    ## Why?
    
    - Add a cross-language way to disable schema evolution for stable
    messages/structs to avoid compatible metadata overhead.
    - Let IDL files set a default `evolving` behavior that generators and
    runtimes can honor consistently.
    
    ## What does this PR do?
    
    - Adds `evolving` to FDL/proto file options and message options, plus
    generator logic to compute the effective value.
    - Emits evolving-disabled annotations/macros/decorators in codegen for
    C++, Go, Java, Python, and Rust (new `@ForyObject` and
    `pyfory.dataclass` support).
    - Updates runtime type resolution to choose STRUCT vs COMPATIBLE_STRUCT
    (and named variants) based on the evolving flag; adds per-language tests
    for overrides and new IDL roundtrip cases.
    - Updates schema evolution docs and compiler schema reference; removes
    the old type-system doc stub.
    
    ## Related issues
    
    #3099
    #1017
    #2906
    #2982
    
    ## 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/extension/fory_options.proto              |  5 ++
 compiler/fory_compiler/frontend/fdl/parser.py      |  1 +
 compiler/fory_compiler/generators/base.py          | 11 ++++
 compiler/fory_compiler/generators/cpp.py           | 12 ++++
 compiler/fory_compiler/generators/go.py            |  7 +-
 compiler/fory_compiler/generators/java.py          |  7 ++
 compiler/fory_compiler/generators/python.py        |  7 +-
 compiler/fory_compiler/generators/rust.py          |  2 +
 cpp/fory/meta/field_info.h                         |  4 ++
 cpp/fory/meta/type_traits.h                        |  2 +
 cpp/fory/serialization/struct_test.cc              | 31 +++++++++
 cpp/fory/serialization/type_resolver.h             | 10 +--
 docs/compiler/schema-idl.md                        | 31 +++++----
 docs/compiler/type-system.md                       | 25 -------
 docs/guide/cpp/schema-evolution.md                 | 12 ++++
 docs/guide/go/schema-evolution.md                  | 14 ++++
 docs/guide/java/schema-evolution.md                | 14 ++++
 docs/guide/python/schema-evolution.md              | 21 ++++++
 docs/guide/rust/schema-evolution.md                | 14 ++++
 go/fory/codegen/generator.go                       |  7 +-
 go/fory/evolving.go                                | 50 ++++++++++++++
 go/fory/fory.go                                    |  7 +-
 go/fory/struct_test.go                             | 28 ++++++++
 go/fory/type_resolver.go                           | 37 +++++++----
 integration_tests/idl_tests/cpp/main.cc            | 70 ++++++++++++++++++++
 integration_tests/idl_tests/generate_idl.py        |  4 ++
 .../idl_tests/go/idl_roundtrip_test.go             | 76 ++++++++++++++++++++++
 integration_tests/idl_tests/idl/evolving1.idl      | 33 ++++++++++
 integration_tests/idl_tests/idl/evolving2.idl      | 33 ++++++++++
 .../apache/fory/idl_tests/IdlRoundTripTest.java    | 51 +++++++++++++++
 .../idl_tests/python/idl_tests/roundtrip.py        | 34 ++++++++++
 integration_tests/idl_tests/rust/Cargo.lock        |  8 +--
 integration_tests/idl_tests/rust/src/lib.rs        |  4 ++
 .../idl_tests/rust/tests/idl_roundtrip.rs          | 53 +++++++++++++++
 .../org/apache/fory/annotation/ForyObject.java}    | 33 ++++++----
 .../org/apache/fory/resolver/ClassResolver.java    |  4 +-
 .../org/apache/fory/resolver/TypeResolver.java     | 18 ++++-
 .../org/apache/fory/resolver/XtypeResolver.java    | 15 ++++-
 .../org/apache/fory/resolver/TypeInfoTest.java     | 26 ++++++++
 python/pyfory/__init__.py                          |  3 +-
 python/pyfory/field.py                             | 38 +++++++++++
 python/pyfory/registry.py                          |  7 +-
 python/pyfory/tests/test_struct.py                 | 19 ++++++
 rust/fory-derive/src/lib.rs                        | 22 +++++++
 rust/fory-derive/src/object/misc.rs                | 10 +++
 rust/fory-derive/src/object/serializer.rs          |  7 +-
 rust/tests/tests/test_simple_struct.rs             | 30 +++++++++
 47 files changed, 865 insertions(+), 92 deletions(-)

diff --git a/compiler/extension/fory_options.proto 
b/compiler/extension/fory_options.proto
index e77f51dc7..e4985c822 100644
--- a/compiler/extension/fory_options.proto
+++ b/compiler/extension/fory_options.proto
@@ -74,6 +74,11 @@ message ForyFileOptions {
   // namespace and type name. When false, types without explicit IDs are
   // registered by namespace and name.
   optional bool enable_auto_type_id = 3;
+
+  // Enable schema evolution for all messages in this file by default.
+  // When false, messages default to STRUCT/NAMED_STRUCT unless overridden.
+  // Default: true.
+  optional bool evolving = 4;
 }
 
 extend google.protobuf.FileOptions { optional ForyFileOptions fory = 50001; }
diff --git a/compiler/fory_compiler/frontend/fdl/parser.py 
b/compiler/fory_compiler/frontend/fdl/parser.py
index 834d4a955..0b574b1bb 100644
--- a/compiler/fory_compiler/frontend/fdl/parser.py
+++ b/compiler/fory_compiler/frontend/fdl/parser.py
@@ -49,6 +49,7 @@ KNOWN_FILE_OPTIONS: Set[str] = {
     "polymorphism",
     "enable_auto_type_id",
     "go_nested_type_style",
+    "evolving",
 }
 
 # Known field-level options
diff --git a/compiler/fory_compiler/generators/base.py 
b/compiler/fory_compiler/generators/base.py
index 8e67799dc..2e5b4ed4d 100644
--- a/compiler/fory_compiler/generators/base.py
+++ b/compiler/fory_compiler/generators/base.py
@@ -196,6 +196,17 @@ class BaseGenerator(ABC):
         type_id = getattr(type_def, "type_id", None)
         return type_id is not None
 
+    def get_effective_evolving(self, message) -> bool:
+        """Return effective evolving flag for a message."""
+        if message is None:
+            return True
+        if "evolving" in message.options:
+            return bool(message.options.get("evolving"))
+        file_default = self.schema.get_option("evolving")
+        if file_default is None:
+            return True
+        return bool(file_default)
+
     def get_license_header(self, comment_prefix: str = "//") -> str:
         """Get the Apache license header."""
         lines = [
diff --git a/compiler/fory_compiler/generators/cpp.py 
b/compiler/fory_compiler/generators/cpp.py
index 5c3afb4c6..8a9b44881 100644
--- a/compiler/fory_compiler/generators/cpp.py
+++ b/compiler/fory_compiler/generators/cpp.py
@@ -252,6 +252,7 @@ class CppGenerator(BaseGenerator):
         enum_macros: List[str] = []
         union_macros: List[str] = []
         field_config_macros: List[str] = []
+        evolving_macros: List[str] = []
         definition_items = self.get_definition_order()
 
         # Collect includes (including from nested types)
@@ -341,6 +342,7 @@ class CppGenerator(BaseGenerator):
                     enum_macros,
                     union_macros,
                     field_config_macros,
+                    evolving_macros,
                     "",
                 )
             )
@@ -366,6 +368,10 @@ class CppGenerator(BaseGenerator):
             lines.append(f"}} // namespace {namespace}")
             lines.append("")
 
+        if evolving_macros:
+            lines.extend(evolving_macros)
+            lines.append("")
+
         # End header guard
         lines.append(f"#endif // {guard_name}")
         lines.append("")
@@ -872,6 +878,7 @@ class CppGenerator(BaseGenerator):
         enum_macros: List[str],
         union_macros: List[str],
         field_config_macros: List[str],
+        evolving_macros: List[str],
         indent: str,
     ) -> List[str]:
         """Generate a C++ class definition with nested types."""
@@ -901,6 +908,7 @@ class CppGenerator(BaseGenerator):
                     enum_macros,
                     union_macros,
                     field_config_macros,
+                    evolving_macros,
                     body_indent,
                 )
             )
@@ -964,6 +972,10 @@ class CppGenerator(BaseGenerator):
         else:
             lines.append(f"{body_indent}FORY_STRUCT({struct_type_name});")
 
+        if not self.get_effective_evolving(message):
+            qualified_name = self.get_namespaced_type_name(message.name, 
parent_stack)
+            evolving_macros.append(f"FORY_STRUCT_EVOLVING({qualified_name}, 
false);")
+
         lines.append(f"{indent}}};")
 
         return lines
diff --git a/compiler/fory_compiler/generators/go.py 
b/compiler/fory_compiler/generators/go.py
index 812ddc453..d7784a0df 100644
--- a/compiler/fory_compiler/generators/go.py
+++ b/compiler/fory_compiler/generators/go.py
@@ -683,7 +683,7 @@ class GoGenerator(BaseGenerator):
                     return "fory.NAMED_UNION"
                 return "fory.UNION"
             if isinstance(type_def, Message):
-                evolving = bool(type_def.options.get("evolving"))
+                evolving = self.get_effective_evolving(type_def)
                 if type_def.type_id is None:
                     if evolving:
                         return "fory.NAMED_COMPATIBLE_STRUCT"
@@ -761,6 +761,11 @@ class GoGenerator(BaseGenerator):
 
         lines.append("}")
         lines.append("")
+        if not self.get_effective_evolving(message):
+            lines.append(f"func (*{type_name}) ForyEvolving() bool {{")
+            lines.append("\treturn false")
+            lines.append("}")
+            lines.append("")
         lines.append(f"func (m *{type_name}) ToBytes() ([]byte, error) {{")
         lines.append("\treturn getFory().Serialize(m)")
         lines.append("}")
diff --git a/compiler/fory_compiler/generators/java.py 
b/compiler/fory_compiler/generators/java.py
index a6d85b3e8..8f0e7bdc3 100644
--- a/compiler/fory_compiler/generators/java.py
+++ b/compiler/fory_compiler/generators/java.py
@@ -445,6 +445,8 @@ class JavaGenerator(BaseGenerator):
         comment = self.format_type_id_comment(message, "//")
         if comment:
             lines.append(comment)
+        if not self.get_effective_evolving(message):
+            lines.append("@ForyObject(evolving = false)")
         lines.append(f"public class {message.name} {{")
 
         # Generate nested enums as static inner classes
@@ -587,6 +589,9 @@ class JavaGenerator(BaseGenerator):
         for field in message.fields:
             self.collect_field_imports(field, imports)
 
+        if not self.get_effective_evolving(message):
+            imports.add("org.apache.fory.annotation.ForyObject")
+
         # Add imports for equals/hashCode
         imports.add("java.util.Objects")
         if self.has_array_field_recursive(message):
@@ -961,6 +966,8 @@ class JavaGenerator(BaseGenerator):
         comment = self.format_type_id_comment(message, "    " * indent + "//")
         if comment:
             lines.append(comment)
+        if not self.get_effective_evolving(message):
+            lines.append("@ForyObject(evolving = false)")
         lines.append(f"public static class {message.name} {{")
 
         # Generate nested enums
diff --git a/compiler/fory_compiler/generators/python.py 
b/compiler/fory_compiler/generators/python.py
index 95fe856ca..83bb1e146 100644
--- a/compiler/fory_compiler/generators/python.py
+++ b/compiler/fory_compiler/generators/python.py
@@ -266,7 +266,7 @@ class PythonGenerator(BaseGenerator):
         imports: Set[str] = set()
 
         # Collect all imports
-        imports.add("from dataclasses import dataclass, field")
+        imports.add("from dataclasses import field")
         imports.add("from enum import Enum, IntEnum")
         imports.add("from typing import Dict, List, Optional, cast")
         imports.add("import pyfory")
@@ -370,7 +370,10 @@ class PythonGenerator(BaseGenerator):
         comment = self.format_type_id_comment(message, f"{ind}#")
         if comment:
             lines.append(comment)
-        lines.append(f"{ind}@dataclass")
+        if not self.get_effective_evolving(message):
+            lines.append(f"{ind}@pyfory.dataclass(evolving=False)")
+        else:
+            lines.append(f"{ind}@pyfory.dataclass")
         lines.append(f"{ind}class {message.name}:")
 
         # Generate nested enums first (they need to be defined before fields 
reference them)
diff --git a/compiler/fory_compiler/generators/rust.py 
b/compiler/fory_compiler/generators/rust.py
index 23d6fd5d3..67cb6df1f 100644
--- a/compiler/fory_compiler/generators/rust.py
+++ b/compiler/fory_compiler/generators/rust.py
@@ -423,6 +423,8 @@ class RustGenerator(BaseGenerator):
         if not self.message_has_any(message):
             derives.extend(["Clone", "PartialEq", "Default"])
         lines.append(f"#[derive({', '.join(derives)})]")
+        if not self.get_effective_evolving(message):
+            lines.append("#[fory(evolving = false)]")
 
         lines.append(f"pub struct {type_name} {{")
 
diff --git a/cpp/fory/meta/field_info.h b/cpp/fory/meta/field_info.h
index fe8bf52ba..f49cfddfe 100644
--- a/cpp/fory/meta/field_info.h
+++ b/cpp/fory/meta/field_info.h
@@ -352,3 +352,7 @@ constexpr auto concat_tuples_from_tuple(const Tuple &tuple) 
{
   (type, unique_id, __VA_ARGS__)
 
 #define FORY_STRUCT(type, ...) FORY_STRUCT_IMPL(type, __LINE__, __VA_ARGS__)
+
+#define FORY_STRUCT_EVOLVING(type, value)                                      
\
+  template <>                                                                  
\
+  struct fory::meta::StructEvolving<type> : std::bool_constant<value> {}
diff --git a/cpp/fory/meta/type_traits.h b/cpp/fory/meta/type_traits.h
index c46336c13..6634ba1ae 100644
--- a/cpp/fory/meta/type_traits.h
+++ b/cpp/fory/meta/type_traits.h
@@ -120,6 +120,8 @@ template <typename T>
 constexpr inline bool IsPairIterable =
     decltype(details::is_pair_iterable_impl<T>(0))::value;
 
+template <typename T> struct StructEvolving : std::true_type {};
+
 } // namespace meta
 
 } // namespace fory
diff --git a/cpp/fory/serialization/struct_test.cc 
b/cpp/fory/serialization/struct_test.cc
index 8d7cc21da..bd2713fe6 100644
--- a/cpp/fory/serialization/struct_test.cc
+++ b/cpp/fory/serialization/struct_test.cc
@@ -30,6 +30,7 @@
  */
 
 #include "fory/serialization/fory.h"
+#include "fory/type/type.h"
 #include "gtest/gtest.h"
 #include <cfloat>
 #include <climits>
@@ -188,6 +189,20 @@ struct Scene {
   FORY_STRUCT(Scene, camera, light, viewport);
 };
 
+struct EvolvingStruct {
+  int32_t id;
+
+  FORY_STRUCT(EvolvingStruct, id);
+};
+
+struct FixedStruct {
+  int32_t id;
+
+  FORY_STRUCT(FixedStruct, id);
+};
+
+FORY_STRUCT_EVOLVING(FixedStruct, false);
+
 // Containers
 struct VectorStruct {
   std::vector<int32_t> numbers;
@@ -647,6 +662,22 @@ TEST(StructComprehensiveTest, ExternalEmptyStruct) {
   test_roundtrip(external_test::ExternalEmpty{});
 }
 
+TEST(StructComprehensiveTest, StructEvolvingOverride) {
+  auto fory =
+      Fory::builder().xlang(true).compatible(true).track_ref(false).build();
+  ASSERT_TRUE(fory.register_struct<EvolvingStruct>(1).ok());
+  ASSERT_TRUE(fory.register_struct<FixedStruct>(2).ok());
+
+  auto evolving_info = fory.type_resolver().get_type_info<EvolvingStruct>();
+  ASSERT_TRUE(evolving_info.ok());
+  EXPECT_EQ(evolving_info.value()->type_id,
+            static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT));
+
+  auto fixed_info = fory.type_resolver().get_type_info<FixedStruct>();
+  ASSERT_TRUE(fixed_info.ok());
+  EXPECT_EQ(fixed_info.value()->type_id, 
static_cast<uint32_t>(TypeId::STRUCT));
+}
+
 } // namespace test
 } // namespace serialization
 } // namespace fory
diff --git a/cpp/fory/serialization/type_resolver.h 
b/cpp/fory/serialization/type_resolver.h
index cd237c8f7..6bdf6521f 100644
--- a/cpp/fory/serialization/type_resolver.h
+++ b/cpp/fory/serialization/type_resolver.h
@@ -1344,8 +1344,9 @@ Result<void, Error> TypeResolver::register_by_id(uint32_t 
type_id) {
 
   if constexpr (is_fory_serializable_v<T>) {
     uint32_t actual_type_id =
-        compatible_ ? static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT)
-                    : static_cast<uint32_t>(TypeId::STRUCT);
+        compatible_ && meta::StructEvolving<T>::value
+            ? static_cast<uint32_t>(TypeId::COMPATIBLE_STRUCT)
+            : static_cast<uint32_t>(TypeId::STRUCT);
     uint32_t user_type_id = type_id;
 
     FORY_TRY(info, build_struct_type_info<T>(actual_type_id, user_type_id, "",
@@ -1398,8 +1399,9 @@ TypeResolver::register_by_name(const std::string &ns,
 
   if constexpr (is_fory_serializable_v<T>) {
     uint32_t actual_type_id =
-        compatible_ ? static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT)
-                    : static_cast<uint32_t>(TypeId::NAMED_STRUCT);
+        compatible_ && meta::StructEvolving<T>::value
+            ? static_cast<uint32_t>(TypeId::NAMED_COMPATIBLE_STRUCT)
+            : static_cast<uint32_t>(TypeId::NAMED_STRUCT);
 
     FORY_TRY(info, build_struct_type_info<T>(actual_type_id, 
kInvalidUserTypeId,
                                              ns, type_name, true));
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 96c026716..9eace213f 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -1374,14 +1374,16 @@ FDL supports protobuf-style extension options for 
Fory-specific configuration. T
 option (fory).use_record_for_java_message = true;
 option (fory).polymorphism = true;
 option (fory).enable_auto_type_id = true;
+option (fory).evolving = true;
 ```
 
-| Option                        | Type   | Description                         
                         |
-| ----------------------------- | ------ | 
------------------------------------------------------------ |
-| `use_record_for_java_message` | bool   | Generate Java records instead of 
classes                     |
-| `polymorphism`                | bool   | Enable polymorphism for all types   
                         |
-| `enable_auto_type_id`         | bool   | Auto-generate numeric type IDs when 
omitted (default: true)  |
-| `go_nested_type_style`        | string | Go nested type naming: `underscore` 
(default) or `camelcase` |
+| Option                        | Type   | Description                         
                                                                                
                |
+| ----------------------------- | ------ | 
-----------------------------------------------------------------------------------------------------------------------------------
 |
+| `use_record_for_java_message` | bool   | Generate Java records instead of 
classes                                                                         
                   |
+| `polymorphism`                | bool   | Enable polymorphism for all types   
                                                                                
                |
+| `enable_auto_type_id`         | bool   | Auto-generate numeric type IDs when 
omitted (default: true)                                                         
                |
+| `evolving`                    | bool   | Default schema evolution for 
messages in this file (default: true). Set false to reduce payload size for 
messages that never change |
+| `go_nested_type_style`        | string | Go nested type naming: `underscore` 
(default) or `camelcase`                                                        
                |
 
 ### Message-Level Fory Options
 
@@ -1396,14 +1398,14 @@ message MyMessage {
 }
 ```
 
-| Option                | Type   | Description                                 
                                         |
-| --------------------- | ------ | 
------------------------------------------------------------------------------------
 |
-| `id`                  | int    | Type ID for serialization (auto-generated 
if omitted and enable_auto_type_id = true) |
-| `alias`               | string | Alternate name used as hash source for 
auto-generated IDs                            |
-| `evolving`            | bool   | Schema evolution support (default: true). 
When false, schema is fixed like a struct  |
-| `use_record_for_java` | bool   | Generate Java record for this message       
                                         |
-| `deprecated`          | bool   | Mark this message as deprecated             
                                         |
-| `namespace`           | string | Custom namespace for type registration      
                                         |
+| Option                | Type   | Description                                 
                                                                       |
+| --------------------- | ------ | 
------------------------------------------------------------------------------------------------------------------
 |
+| `id`                  | int    | Type ID for serialization (auto-generated 
if omitted and enable_auto_type_id = true)                               |
+| `alias`               | string | Alternate name used as hash source for 
auto-generated IDs                                                          |
+| `evolving`            | bool   | Schema evolution support (default: true). 
When false, schema is fixed like a struct and avoids compatible metadata |
+| `use_record_for_java` | bool   | Generate Java record for this message       
                                                                       |
+| `deprecated`          | bool   | Mark this message as deprecated             
                                                                       |
+| `namespace`           | string | Custom namespace for type registration      
                                                                       |
 
 **Note:** `option (fory).id = 100` is equivalent to the inline syntax `message 
MyMessage [id=100]`.
 
@@ -1502,6 +1504,7 @@ message ForyFileOptions {
     optional bool use_record_for_java_message = 1;
     optional bool polymorphism = 2;
     optional bool enable_auto_type_id = 3;
+    optional bool evolving = 4;
 }
 
 // Message-level options
diff --git a/docs/compiler/type-system.md b/docs/compiler/type-system.md
deleted file mode 100644
index 88758626a..000000000
--- a/docs/compiler/type-system.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: Type System
-sidebar_position: 4
-id: type_system
-license: |
-  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.
----
-
-This content has moved to [Schema IDL](schema-idl.md#type-system).
-
-The Schema IDL document now contains the full type system reference, language 
mappings,
-and best practices.
diff --git a/docs/guide/cpp/schema-evolution.md 
b/docs/guide/cpp/schema-evolution.md
index 89b57ff70..84cf354b9 100644
--- a/docs/guide/cpp/schema-evolution.md
+++ b/docs/guide/cpp/schema-evolution.md
@@ -76,6 +76,18 @@ int main() {
 }
 ```
 
+### Disable Evolution for Stable Structs
+
+If a struct schema is stable and will not change, you can disable evolution 
for that struct to avoid compatible metadata overhead. Use 
`FORY_STRUCT_EVOLVING` after `FORY_STRUCT`:
+
+```cpp
+struct StableMessage {
+  int32_t id;
+};
+FORY_STRUCT(StableMessage, id);
+FORY_STRUCT_EVOLVING(StableMessage, false);
+```
+
 ## Schema Evolution Features
 
 Compatible mode supports the following schema changes:
diff --git a/docs/guide/go/schema-evolution.md 
b/docs/guide/go/schema-evolution.md
index d84d4848c..eea486e00 100644
--- a/docs/guide/go/schema-evolution.md
+++ b/docs/guide/go/schema-evolution.md
@@ -43,6 +43,20 @@ f := fory.New(fory.WithCompatible(true))
 - Supports adding, removing, and reordering fields
 - Enables forward and backward compatibility
 
+### Disable Evolution for Stable Structs
+
+If a struct schema is stable and will not change, you can disable evolution 
for that struct to avoid compatible metadata overhead. Implement the 
`ForyEvolving` interface and return `false`:
+
+```go
+type StableMessage struct {
+    ID int64
+}
+
+func (StableMessage) ForyEvolving() bool {
+    return false
+}
+```
+
 ## Supported Schema Changes
 
 ### Adding Fields
diff --git a/docs/guide/java/schema-evolution.md 
b/docs/guide/java/schema-evolution.md
index c2be9b169..c7db18965 100644
--- a/docs/guide/java/schema-evolution.md
+++ b/docs/guide/java/schema-evolution.md
@@ -46,6 +46,20 @@ System.out.println(fory.deserialize(bytes));
 
 This compatible mode involves serializing class metadata into the serialized 
output. Despite Fory's use of sophisticated compression techniques to minimize 
overhead, there is still some additional space cost associated with class 
metadata.
 
+### Disable Evolution for Stable Classes
+
+If a class schema is stable and will not change, you can opt out of schema 
evolution on a per-class basis to avoid compatible metadata overhead. Annotate 
the class with `@ForyObject(evolving = false)` to force `STRUCT/NAMED_STRUCT` 
type IDs even when Compatible mode is enabled.
+
+```java
+import org.apache.fory.annotation.ForyObject;
+
+@ForyObject(evolving = false)
+public class StableMessage {
+  public int id;
+  public String name;
+}
+```
+
 ## Meta Sharing
 
 To further reduce metadata costs, Fory introduces a class metadata sharing 
mechanism, which allows the metadata to be sent to the deserialization process 
only once.
diff --git a/docs/guide/python/schema-evolution.md 
b/docs/guide/python/schema-evolution.md
index 6990c392c..bfd0a7482 100644
--- a/docs/guide/python/schema-evolution.md
+++ b/docs/guide/python/schema-evolution.md
@@ -29,6 +29,27 @@ import pyfory
 f = pyfory.Fory(xlang=True, compatible=True)
 ```
 
+## Disable Evolution for Stable Classes
+
+If a dataclass schema is stable and will not change, you can disable evolution 
for that class to avoid compatible metadata overhead. Use `pyfory.dataclass` 
with `evolving=False`:
+
+```python
+import pyfory
+
[email protected](evolving=False)
+class StableMessage:
+    id: int
+    name: str
+```
+
+`pyfory.dataclass` also supports `slots=True`:
+
+```python
[email protected](slots=True)
+class SlotMessage:
+    id: int
+```
+
 ## Schema Evolution Example
 
 ```python
diff --git a/docs/guide/rust/schema-evolution.md 
b/docs/guide/rust/schema-evolution.md
index 54f911461..07d175eff 100644
--- a/docs/guide/rust/schema-evolution.md
+++ b/docs/guide/rust/schema-evolution.md
@@ -69,6 +69,20 @@ assert_eq!(person_v2.age, 30);
 assert_eq!(person_v2.phone, None);
 ```
 
+### Disable Evolution for Stable Structs
+
+If a struct schema is stable and will not change, you can disable evolution 
for that struct to avoid compatible metadata overhead. Use `#[fory(evolving = 
false)]`:
+
+```rust
+use fory::ForyObject;
+
+#[derive(ForyObject)]
+#[fory(evolving = false)]
+struct StableMessage {
+    id: i32,
+}
+```
+
 ## Schema Evolution Features
 
 - Add new fields with default values
diff --git a/go/fory/codegen/generator.go b/go/fory/codegen/generator.go
index 5c0cea4a2..e719c5fc7 100644
--- a/go/fory/codegen/generator.go
+++ b/go/fory/codegen/generator.go
@@ -479,7 +479,12 @@ func generateWriteMethod(buf *bytes.Buffer, s *StructInfo) 
error {
        fmt.Fprintf(buf, "\t\tctx.Buffer().WriteInt8(-1) // NotNullValueFlag\n")
        fmt.Fprintf(buf, "\t}\n")
        fmt.Fprintf(buf, "\tif writeType {\n")
-       fmt.Fprintf(buf, 
"\t\tctx.Buffer().WriteVarUint32(uint32(fory.NAMED_STRUCT))\n")
+       fmt.Fprintf(buf, "\t\ttypeInfo, err := 
ctx.TypeResolver().GetTypeInfo(value, true)\n")
+       fmt.Fprintf(buf, "\t\tif err != nil {\n")
+       fmt.Fprintf(buf, "\t\t\tctx.SetError(fory.FromError(err))\n")
+       fmt.Fprintf(buf, "\t\t\treturn\n")
+       fmt.Fprintf(buf, "\t\t}\n")
+       fmt.Fprintf(buf, "\t\tctx.TypeResolver().WriteTypeInfo(ctx.Buffer(), 
typeInfo, ctx.Err())\n")
        fmt.Fprintf(buf, "\t}\n")
        fmt.Fprintf(buf, "\tg.WriteData(ctx, value)\n")
        fmt.Fprintf(buf, "}\n\n")
diff --git a/go/fory/evolving.go b/go/fory/evolving.go
new file mode 100644
index 000000000..1661900f6
--- /dev/null
+++ b/go/fory/evolving.go
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package fory
+
+import "reflect"
+
+// ForyEvolving allows a struct to override schema evolution behavior.
+// Returning false disables compatible struct type IDs for this struct.
+type ForyEvolving interface {
+       ForyEvolving() bool
+}
+
+var foryEvolvingType = reflect.TypeOf((*ForyEvolving)(nil)).Elem()
+
+func structEvolvingOverride(type_ reflect.Type) (bool, bool) {
+       if type_ == nil {
+               return false, false
+       }
+       if type_.Kind() == reflect.Ptr {
+               type_ = type_.Elem()
+       }
+       if type_.Kind() != reflect.Struct {
+               return false, false
+       }
+       if type_.Implements(foryEvolvingType) {
+               value := reflect.Zero(type_).Interface().(ForyEvolving)
+               return value.ForyEvolving(), true
+       }
+       ptrType := reflect.PtrTo(type_)
+       if ptrType.Implements(foryEvolvingType) {
+               value := reflect.New(type_).Interface().(ForyEvolving)
+               return value.ForyEvolving(), true
+       }
+       return false, false
+}
diff --git a/go/fory/fory.go b/go/fory/fory.go
index 83dd60739..97b9dc4ae 100644
--- a/go/fory/fory.go
+++ b/go/fory/fory.go
@@ -214,12 +214,7 @@ func (f *Fory) RegisterStruct(type_ any, typeID uint32) 
error {
 
        // Determine the internal type ID based on config
        var internalTypeID TypeId
-       // Use COMPATIBLE_STRUCT when compatible mode is enabled (matches Java 
behavior)
-       if f.config.Compatible {
-               internalTypeID = COMPATIBLE_STRUCT
-       } else {
-               internalTypeID = STRUCT
-       }
+       internalTypeID = f.typeResolver.structTypeID(t, false)
 
        return f.typeResolver.RegisterStruct(t, internalTypeID, typeID)
 }
diff --git a/go/fory/struct_test.go b/go/fory/struct_test.go
index 7f345b724..d97bd1f3b 100644
--- a/go/fory/struct_test.go
+++ b/go/fory/struct_test.go
@@ -118,6 +118,34 @@ func TestOptionFieldSerialization(t *testing.T) {
        require.Equal(t, true, out.OptBool.Unwrap())
 }
 
+type evolvingStruct struct {
+       ID int32
+}
+
+type fixedStruct struct {
+       ID int32
+}
+
+func (fixedStruct) ForyEvolving() bool {
+       return false
+}
+
+func TestStructEvolvingOverride(t *testing.T) {
+       f := New(WithXlang(true), WithCompatible(true))
+       require.NoError(t, f.RegisterStruct(evolvingStruct{}, 100))
+       require.NoError(t, f.RegisterStruct(fixedStruct{}, 101))
+
+       resolver := f.GetTypeResolver()
+
+       evolvingInfo, err := 
resolver.GetTypeInfo(reflect.ValueOf(evolvingStruct{}), true)
+       require.NoError(t, err)
+       require.Equal(t, uint32(COMPATIBLE_STRUCT), evolvingInfo.TypeID)
+
+       fixedInfo, err := resolver.GetTypeInfo(reflect.ValueOf(fixedStruct{}), 
true)
+       require.NoError(t, err)
+       require.Equal(t, uint32(STRUCT), fixedInfo.TypeID)
+}
+
 func TestOptionFieldUnsupportedTypes(t *testing.T) {
        type Nested struct {
                Name string
diff --git a/go/fory/type_resolver.go b/go/fory/type_resolver.go
index 5fd1ca9b8..ebb4c416f 100644
--- a/go/fory/type_resolver.go
+++ b/go/fory/type_resolver.go
@@ -760,17 +760,9 @@ func (r *TypeResolver) RegisterNamedStruct(
        var internalTypeID TypeId
        userTypeID := invalidUserTypeID
        if typeId == 0 {
-               if r.metaShareEnabled() {
-                       internalTypeID = NAMED_COMPATIBLE_STRUCT
-               } else {
-                       internalTypeID = NAMED_STRUCT
-               }
+               internalTypeID = r.structTypeID(type_, true)
        } else {
-               if r.metaShareEnabled() {
-                       internalTypeID = COMPATIBLE_STRUCT
-               } else {
-                       internalTypeID = STRUCT
-               }
+               internalTypeID = r.structTypeID(type_, false)
                userTypeID = typeId
        }
        if registerById {
@@ -1169,11 +1161,11 @@ func (r *TypeResolver) getTypeInfo(value reflect.Value, 
create bool) (*TypeInfo,
           All other slice types are treated as lists (typeID 21).
        */
        if value.Kind() == reflect.Struct {
-               typeID = NAMED_STRUCT
+               typeID = uint32(r.structTypeID(value.Type(), true))
        } else if value.IsValid() && value.Kind() == reflect.Interface && 
value.Elem().Kind() == reflect.Struct {
-               typeID = NAMED_STRUCT
+               typeID = uint32(r.structTypeID(value.Elem().Type(), true))
        } else if value.IsValid() && value.Kind() == reflect.Ptr && 
value.Elem().Kind() == reflect.Struct {
-               typeID = NAMED_STRUCT
+               typeID = uint32(r.structTypeID(value.Elem().Type(), true))
        } else if value.Kind() == reflect.Map {
                typeID = MAP
        } else if value.Kind() == reflect.Array {
@@ -1418,6 +1410,25 @@ func (r *TypeResolver) metaShareEnabled() bool {
        return r.fory != nil && r.fory.metaContext != nil && 
r.fory.config.Compatible
 }
 
+func (r *TypeResolver) structTypeID(type_ reflect.Type, named bool) TypeId {
+       useCompatible := r.metaShareEnabled()
+       if useCompatible {
+               if evolving, ok := structEvolvingOverride(type_); ok && 
!evolving {
+                       useCompatible = false
+               }
+       }
+       if named {
+               if useCompatible {
+                       return NAMED_COMPATIBLE_STRUCT
+               }
+               return NAMED_STRUCT
+       }
+       if useCompatible {
+               return COMPATIBLE_STRUCT
+       }
+       return STRUCT
+}
+
 // WriteTypeInfo writes type info to buffer.
 // This is exported for use by generated code.
 func (r *TypeResolver) WriteTypeInfo(buffer *ByteBuffer, typeInfo *TypeInfo, 
err *Error) {
diff --git a/integration_tests/idl_tests/cpp/main.cc 
b/integration_tests/idl_tests/cpp/main.cc
index f1ed316da..79d2b3413 100644
--- a/integration_tests/idl_tests/cpp/main.cc
+++ b/integration_tests/idl_tests/cpp/main.cc
@@ -36,6 +36,8 @@
 #include "generated/collection.h"
 #include "generated/complex_fbs.h"
 #include "generated/complex_pb.h"
+#include "generated/evolving1.h"
+#include "generated/evolving2.h"
 #include "generated/graph.h"
 #include "generated/monster.h"
 #include "generated/optional_types.h"
@@ -159,6 +161,70 @@ fory::Result<void, fory::Error> ValidateGraph(const 
graph::Graph &graph_value) {
   return fory::Result<void, fory::Error>();
 }
 
+fory::Result<void, fory::Error> RunEvolvingRoundTrip() {
+  auto fory_v1 = fory::serialization::Fory::builder()
+                     .xlang(true)
+                     .compatible(true)
+                     .check_struct_version(false)
+                     .track_ref(false)
+                     .build();
+  auto fory_v2 = fory::serialization::Fory::builder()
+                     .xlang(true)
+                     .compatible(true)
+                     .check_struct_version(false)
+                     .track_ref(false)
+                     .build();
+  evolving1::register_types(fory_v1);
+  evolving2::register_types(fory_v2);
+
+  evolving1::EvolvingMessage msg_v1;
+  msg_v1.set_id(1);
+  msg_v1.set_name("Alice");
+  msg_v1.set_city("NYC");
+
+  FORY_TRY(bytes, fory_v1.serialize(msg_v1));
+  FORY_TRY(decoded, fory_v2.deserialize<evolving2::EvolvingMessage>(bytes));
+  if (decoded.id() != msg_v1.id() || decoded.name() != msg_v1.name() ||
+      decoded.city() != msg_v1.city()) {
+    return fory::Unexpected(fory::Error::invalid("evolving message mismatch"));
+  }
+  decoded.set_email("[email protected]");
+
+  FORY_TRY(round_bytes, fory_v2.serialize(decoded));
+  FORY_TRY(round_trip,
+           fory_v1.deserialize<evolving1::EvolvingMessage>(round_bytes));
+  if (!(round_trip == msg_v1)) {
+    return fory::Unexpected(
+        fory::Error::invalid("evolving roundtrip mismatch"));
+  }
+
+  evolving1::FixedMessage fixed_v1;
+  fixed_v1.set_id(10);
+  fixed_v1.set_name("Bob");
+  fixed_v1.set_score(90);
+  fixed_v1.set_note("note");
+
+  FORY_TRY(fixed_bytes, fory_v1.serialize(fixed_v1));
+  auto fixed_v2 = fory_v2.deserialize<evolving2::FixedMessage>(fixed_bytes);
+  if (!fixed_v2.ok()) {
+    return fory::Result<void, fory::Error>();
+  }
+  auto fixed_round = fory_v2.serialize(fixed_v2.value());
+  if (!fixed_round.ok()) {
+    return fory::Result<void, fory::Error>();
+  }
+  auto fixed_back =
+      fory_v1.deserialize<evolving1::FixedMessage>(fixed_round.value());
+  if (!fixed_back.ok()) {
+    return fory::Result<void, fory::Error>();
+  }
+  if (fixed_back.value() == fixed_v1) {
+    return fory::Unexpected(
+        fory::Error::invalid("fixed message unexpectedly compatible"));
+  }
+  return fory::Result<void, fory::Error>();
+}
+
 using StringMap = std::map<std::string, std::string>;
 
 fory::Result<void, fory::Error> RunRoundTrip(bool compatible) {
@@ -178,6 +244,10 @@ fory::Result<void, fory::Error> RunRoundTrip(bool 
compatible) {
   optional_types::register_types(fory);
   any_example::register_types(fory);
 
+  if (compatible) {
+    FORY_RETURN_IF_ERROR(RunEvolvingRoundTrip());
+  }
+
   FORY_RETURN_IF_ERROR(
       fory::serialization::register_any_type<bool>(fory.type_resolver()));
   FORY_RETURN_IF_ERROR(fory::serialization::register_any_type<std::string>(
diff --git a/integration_tests/idl_tests/generate_idl.py 
b/integration_tests/idl_tests/generate_idl.py
index e65628149..046de53b3 100755
--- a/integration_tests/idl_tests/generate_idl.py
+++ b/integration_tests/idl_tests/generate_idl.py
@@ -31,6 +31,8 @@ SCHEMAS = [
     IDL_DIR / "idl" / "tree.fdl",
     IDL_DIR / "idl" / "graph.fdl",
     IDL_DIR / "idl" / "root.idl",
+    IDL_DIR / "idl" / "evolving1.idl",
+    IDL_DIR / "idl" / "evolving2.idl",
     IDL_DIR / "idl" / "any_example.fdl",
     IDL_DIR / "idl" / "any_example.proto",
     IDL_DIR / "idl" / "monster.fbs",
@@ -55,6 +57,8 @@ GO_OUTPUT_OVERRIDES = {
     "tree.fdl": IDL_DIR / "go" / "tree" / "generated",
     "graph.fdl": IDL_DIR / "go" / "graph" / "generated",
     "root.idl": IDL_DIR / "go" / "root" / "generated",
+    "evolving1.idl": IDL_DIR / "go" / "evolving1" / "generated",
+    "evolving2.idl": IDL_DIR / "go" / "evolving2" / "generated",
     "any_example.fdl": IDL_DIR / "go" / "any_example" / "generated",
     "any_example.proto": IDL_DIR / "go" / "any_example_pb" / "generated",
     "complex_pb.proto": IDL_DIR / "go" / "complex_pb" / "generated",
diff --git a/integration_tests/idl_tests/go/idl_roundtrip_test.go 
b/integration_tests/idl_tests/go/idl_roundtrip_test.go
index acd5d3973..82bf4b415 100644
--- a/integration_tests/idl_tests/go/idl_roundtrip_test.go
+++ b/integration_tests/idl_tests/go/idl_roundtrip_test.go
@@ -31,6 +31,8 @@ import (
        collection 
"github.com/apache/fory/integration_tests/idl_tests/go/collection/generated"
        complexfbs 
"github.com/apache/fory/integration_tests/idl_tests/go/complex_fbs/generated"
        complexpb 
"github.com/apache/fory/integration_tests/idl_tests/go/complex_pb/generated"
+       evolving1 
"github.com/apache/fory/integration_tests/idl_tests/go/evolving1/generated"
+       evolving2 
"github.com/apache/fory/integration_tests/idl_tests/go/evolving2/generated"
        graphpkg 
"github.com/apache/fory/integration_tests/idl_tests/go/graph/generated"
        monster 
"github.com/apache/fory/integration_tests/idl_tests/go/monster/generated"
        optionaltypes 
"github.com/apache/fory/integration_tests/idl_tests/go/optional_types/generated"
@@ -104,6 +106,80 @@ func TestAutoIdRoundTripSchemaConsistent(t *testing.T) {
        runAutoIdRoundTrip(t, false)
 }
 
+func TestEvolvingRoundTrip(t *testing.T) {
+       foryV1 := fory.NewFory(
+               fory.WithXlang(true),
+               fory.WithRefTracking(false),
+               fory.WithCompatible(true),
+       )
+       if err := evolving1.RegisterTypes(foryV1); err != nil {
+               t.Fatalf("register evolving1 types: %v", err)
+       }
+       foryV2 := fory.NewFory(
+               fory.WithXlang(true),
+               fory.WithRefTracking(false),
+               fory.WithCompatible(true),
+       )
+       if err := evolving2.RegisterTypes(foryV2); err != nil {
+               t.Fatalf("register evolving2 types: %v", err)
+       }
+
+       msgV1 := evolving1.EvolvingMessage{
+               Id:   1,
+               Name: "Alice",
+               City: "NYC",
+       }
+       data, err := foryV1.Serialize(&msgV1)
+       if err != nil {
+               t.Fatalf("serialize evolving message v1: %v", err)
+       }
+       var msgV2 evolving2.EvolvingMessage
+       if err := foryV2.Deserialize(data, &msgV2); err != nil {
+               t.Fatalf("deserialize evolving message v2: %v", err)
+       }
+       if msgV2.Id != msgV1.Id || msgV2.Name != msgV1.Name || msgV2.City != 
msgV1.City {
+               t.Fatalf("evolving message mismatch: v1=%+v v2=%+v", msgV1, 
msgV2)
+       }
+       msgV2.Email = optional.Some("[email protected]")
+       roundBytes, err := foryV2.Serialize(&msgV2)
+       if err != nil {
+               t.Fatalf("serialize evolving message v2: %v", err)
+       }
+       var msgV1Round evolving1.EvolvingMessage
+       if err := foryV1.Deserialize(roundBytes, &msgV1Round); err != nil {
+               t.Fatalf("deserialize evolving message v1: %v", err)
+       }
+       if !reflect.DeepEqual(msgV1Round, msgV1) {
+               t.Fatalf("evolving round trip mismatch: %v vs %v", msgV1Round, 
msgV1)
+       }
+
+       fixedV1 := evolving1.FixedMessage{
+               Id:    10,
+               Name:  "Bob",
+               Score: 90,
+               Note:  "note",
+       }
+       fixedData, err := foryV1.Serialize(&fixedV1)
+       if err != nil {
+               t.Fatalf("serialize fixed message v1: %v", err)
+       }
+       var fixedV2 evolving2.FixedMessage
+       if err := foryV2.Deserialize(fixedData, &fixedV2); err != nil {
+               return
+       }
+       fixedRound, err := foryV2.Serialize(&fixedV2)
+       if err != nil {
+               return
+       }
+       var fixedV1Round evolving1.FixedMessage
+       if err := foryV1.Deserialize(fixedRound, &fixedV1Round); err != nil {
+               return
+       }
+       if reflect.DeepEqual(fixedV1Round, fixedV1) {
+               t.Fatalf("fixed message unexpectedly compatible: %v", 
fixedV1Round)
+       }
+}
+
 func runAddressBookRoundTrip(t *testing.T, compatible bool) {
        f := fory.NewFory(
                fory.WithXlang(true),
diff --git a/integration_tests/idl_tests/idl/evolving1.idl 
b/integration_tests/idl_tests/idl/evolving1.idl
new file mode 100644
index 000000000..961684bfa
--- /dev/null
+++ b/integration_tests/idl_tests/idl/evolving1.idl
@@ -0,0 +1,33 @@
+// 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.
+
+package evolving1;
+
+option evolving = false;
+
+message EvolvingMessage [id=1000, evolving=true] {
+    int32 id = 1;
+    string name = 2;
+    string city = 3;
+}
+
+message FixedMessage [id=1001, evolving=false] {
+    int32 id = 1;
+    string name = 2;
+    int32 score = 3;
+    string note = 4;
+}
diff --git a/integration_tests/idl_tests/idl/evolving2.idl 
b/integration_tests/idl_tests/idl/evolving2.idl
new file mode 100644
index 000000000..a62fcb028
--- /dev/null
+++ b/integration_tests/idl_tests/idl/evolving2.idl
@@ -0,0 +1,33 @@
+// 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.
+
+package evolving2;
+
+option evolving = false;
+
+message EvolvingMessage [id=1000, evolving=true] {
+    int32 id = 1;
+    string name = 2;
+    string city = 3;
+    optional string email = 4;
+}
+
+message FixedMessage [id=1001, evolving=false] {
+    int32 id = 1;
+    string name = 2;
+    optional string email = 4;
+}
diff --git 
a/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
 
b/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
index 87bc6b720..b35a5e97b 100644
--- 
a/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
+++ 
b/integration_tests/idl_tests/java/src/test/java/org/apache/fory/idl_tests/IdlRoundTripTest.java
@@ -48,6 +48,10 @@ import complex_fbs.Note;
 import complex_fbs.Payload;
 import complex_fbs.ScalarPack;
 import complex_fbs.Status;
+import evolving1.Evolving1ForyRegistration;
+import evolving1.EvolvingMessage;
+import evolving1.FixedMessage;
+import evolving2.Evolving2ForyRegistration;
 import graph.Edge;
 import graph.Graph;
 import graph.GraphForyRegistration;
@@ -114,6 +118,11 @@ public class IdlRoundTripTest {
     runAutoIdRoundTrip(false);
   }
 
+  @Test
+  public void testEvolvingRoundTrip() {
+    runEvolvingRoundTrip();
+  }
+
   private void runAddressBookRoundTrip(boolean compatible) throws Exception {
     Fory fory = buildFory(compatible);
     AddressbookForyRegistration.register(fory);
@@ -176,6 +185,48 @@ public class IdlRoundTripTest {
     }
   }
 
+  private void runEvolvingRoundTrip() {
+    Fory foryV1 = buildFory(true);
+    Fory foryV2 = buildFory(true);
+    Evolving1ForyRegistration.register(foryV1);
+    Evolving2ForyRegistration.register(foryV2);
+
+    EvolvingMessage messageV1 = new EvolvingMessage();
+    messageV1.setId(1);
+    messageV1.setName("Alice");
+    messageV1.setCity("NYC");
+
+    byte[] bytes = foryV1.serialize(messageV1);
+    Object decoded = foryV2.deserialize(bytes);
+    Assert.assertTrue(decoded instanceof evolving2.EvolvingMessage);
+    evolving2.EvolvingMessage messageV2 = (evolving2.EvolvingMessage) decoded;
+    Assert.assertEquals(messageV2.getId(), messageV1.getId());
+    Assert.assertEquals(messageV2.getName(), messageV1.getName());
+    Assert.assertEquals(messageV2.getCity(), messageV1.getCity());
+    messageV2.setEmail("[email protected]");
+
+    byte[] roundTripBytes = foryV2.serialize(messageV2);
+    Object roundTrip = foryV1.deserialize(roundTripBytes);
+    Assert.assertTrue(roundTrip instanceof EvolvingMessage);
+    Assert.assertEquals(roundTrip, messageV1);
+
+    FixedMessage fixedV1 = new FixedMessage();
+    fixedV1.setId(10);
+    fixedV1.setName("Bob");
+    fixedV1.setScore(90);
+    fixedV1.setNote("note");
+
+    byte[] fixedBytes = foryV1.serialize(fixedV1);
+    try {
+      Object fixedDecoded = foryV2.deserialize(fixedBytes);
+      byte[] fixedRoundTripBytes = foryV2.serialize(fixedDecoded);
+      Object fixedRoundTrip = foryV1.deserialize(fixedRoundTripBytes);
+      Assert.assertNotEquals(fixedRoundTrip, fixedV1);
+    } catch (Exception ignored) {
+      // Expected failure for non-evolving struct.
+    }
+  }
+
   @Test
   public void testToBytesFromBytes() {
     AddressBook book = buildAddressBook();
diff --git a/integration_tests/idl_tests/python/idl_tests/roundtrip.py 
b/integration_tests/idl_tests/python/idl_tests/roundtrip.py
index 52d503985..142a49af3 100644
--- a/integration_tests/idl_tests/python/idl_tests/roundtrip.py
+++ b/integration_tests/idl_tests/python/idl_tests/roundtrip.py
@@ -27,6 +27,8 @@ import any_example
 import complex_fbs
 import complex_pb
 import collection
+import evolving1
+import evolving2
 import monster
 import optional_types
 import graph
@@ -170,6 +172,35 @@ def file_roundtrip_auto_id(fory: pyfory.Fory, envelope: 
"auto_id.Envelope") -> N
     Path(data_file).write_bytes(fory.serialize(decoded))
 
 
+def local_roundtrip_evolving() -> None:
+    fory_v1 = pyfory.Fory(xlang=True, ref=False, compatible=True)
+    fory_v2 = pyfory.Fory(xlang=True, ref=False, compatible=True)
+    evolving1.register_evolving1_types(fory_v1)
+    evolving2.register_evolving2_types(fory_v2)
+
+    msg_v1 = evolving1.EvolvingMessage(id=1, name="Alice", city="NYC")
+    data = fory_v1.serialize(msg_v1)
+    msg_v2 = fory_v2.deserialize(data)
+    assert isinstance(msg_v2, evolving2.EvolvingMessage)
+    assert msg_v2.id == msg_v1.id
+    assert msg_v2.name == msg_v1.name
+    assert msg_v2.city == msg_v1.city
+    msg_v2.email = "[email protected]"
+    round_bytes = fory_v2.serialize(msg_v2)
+    msg_v1_round = fory_v1.deserialize(round_bytes)
+    assert msg_v1_round == msg_v1
+
+    fixed_v1 = evolving1.FixedMessage(id=10, name="Bob", score=90, note="note")
+    fixed_bytes = fory_v1.serialize(fixed_v1)
+    try:
+        fixed_v2 = fory_v2.deserialize(fixed_bytes)
+    except Exception:
+        return
+    round_fixed = fory_v2.serialize(fixed_v2)
+    fixed_v1_round = fory_v1.deserialize(round_fixed)
+    assert fixed_v1_round != fixed_v1
+
+
 def build_primitive_types() -> "complex_pb.PrimitiveTypes":
     contact = complex_pb.PrimitiveTypes.Contact.email("[email protected]")
     contact = complex_pb.PrimitiveTypes.Contact.phone(12345)
@@ -769,6 +800,9 @@ def run_roundtrip(compatible: bool) -> None:
     local_roundtrip_graph(ref_fory, graph_value)
     file_roundtrip_graph(ref_fory, graph_value)
 
+    if compatible:
+        local_roundtrip_evolving()
+
 
 def resolve_compatible_modes() -> list[bool]:
     value = os.environ.get("IDL_COMPATIBLE")
diff --git a/integration_tests/idl_tests/rust/Cargo.lock 
b/integration_tests/idl_tests/rust/Cargo.lock
index e569aad63..8a108d97d 100644
--- a/integration_tests/idl_tests/rust/Cargo.lock
+++ b/integration_tests/idl_tests/rust/Cargo.lock
@@ -78,7 +78,7 @@ checksum = 
"8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
 
 [[package]]
 name = "fory"
-version = "0.15.0"
+version = "0.15.0-alpha.0"
 dependencies = [
  "fory-core",
  "fory-derive",
@@ -86,7 +86,7 @@ dependencies = [
 
 [[package]]
 name = "fory-core"
-version = "0.15.0"
+version = "0.15.0-alpha.0"
 dependencies = [
  "byteorder",
  "chrono",
@@ -100,7 +100,7 @@ dependencies = [
 
 [[package]]
 name = "fory-derive"
-version = "0.15.0"
+version = "0.15.0-alpha.0"
 dependencies = [
  "fory-core",
  "proc-macro2",
@@ -141,7 +141,7 @@ dependencies = [
 
 [[package]]
 name = "idl_tests"
-version = "0.1.0"
+version = "0.15.0-alpha.0"
 dependencies = [
  "chrono",
  "fory",
diff --git a/integration_tests/idl_tests/rust/src/lib.rs 
b/integration_tests/idl_tests/rust/src/lib.rs
index 2a78cf5e2..5cbdfe7b6 100644
--- a/integration_tests/idl_tests/rust/src/lib.rs
+++ b/integration_tests/idl_tests/rust/src/lib.rs
@@ -30,6 +30,10 @@ pub mod generated {
     pub mod complex_fbs;
     #[path = "../generated/complex_pb.rs"]
     pub mod complex_pb;
+    #[path = "../generated/evolving1.rs"]
+    pub mod evolving1;
+    #[path = "../generated/evolving2.rs"]
+    pub mod evolving2;
     #[path = "../generated/graph.rs"]
     pub mod graph;
     #[path = "../generated/monster.rs"]
diff --git a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs 
b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
index 523e45127..c286f80d2 100644
--- a/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
+++ b/integration_tests/idl_tests/rust/tests/idl_roundtrip.rs
@@ -34,6 +34,8 @@ use idl_tests::generated::collection::{
 };
 use idl_tests::generated::complex_fbs::{self, Container, Note, Payload, 
ScalarPack, Status};
 use idl_tests::generated::complex_pb::{self, PrimitiveTypes};
+use idl_tests::generated::evolving1;
+use idl_tests::generated::evolving2;
 use idl_tests::generated::monster::{self, Color, Monster, Vec3};
 use idl_tests::generated::optional_types::{self, AllOptionalTypes, 
OptionalHolder, OptionalUnion};
 use idl_tests::generated::root;
@@ -503,6 +505,57 @@ fn test_address_book_roundtrip_schema_consistent() {
     run_address_book_roundtrip(false);
 }
 
+#[test]
+fn test_evolving_roundtrip() {
+    let mut fory_v1 = Fory::default().xlang(true).compatible(true);
+    evolving1::register_types(&mut fory_v1).expect("register evolving1 types");
+    let mut fory_v2 = Fory::default().xlang(true).compatible(true);
+    evolving2::register_types(&mut fory_v2).expect("register evolving2 types");
+
+    let msg_v1 = evolving1::EvolvingMessage {
+        id: 1,
+        name: "Alice".to_string(),
+        city: "NYC".to_string(),
+    };
+    let bytes = fory_v1.serialize(&msg_v1).expect("serialize evolving v1");
+    let mut msg_v2: evolving2::EvolvingMessage = fory_v2
+        .deserialize(&bytes)
+        .expect("deserialize evolving v2");
+    assert_eq!(msg_v1.id, msg_v2.id);
+    assert_eq!(msg_v1.name, msg_v2.name);
+    assert_eq!(msg_v1.city, msg_v2.city);
+
+    msg_v2.email = Some("[email protected]".to_string());
+    let round_bytes = fory_v2.serialize(&msg_v2).expect("serialize evolving 
v2");
+    let msg_v1_round: evolving1::EvolvingMessage = fory_v1
+        .deserialize(&round_bytes)
+        .expect("deserialize evolving v1");
+    assert_eq!(msg_v1, msg_v1_round);
+
+    let fixed_v1 = evolving1::FixedMessage {
+        id: 10,
+        name: "Bob".to_string(),
+        score: 90,
+        note: "note".to_string(),
+    };
+    let fixed_bytes = fory_v1
+        .serialize(&fixed_v1)
+        .expect("serialize fixed v1");
+    let fixed_v2 = 
fory_v2.deserialize::<evolving2::FixedMessage>(&fixed_bytes);
+    match fixed_v2 {
+        Err(_) => return,
+        Ok(value) => {
+            let round = fory_v2.serialize(&value);
+            if let Ok(round_bytes) = round {
+                let fixed_round = 
fory_v1.deserialize::<evolving1::FixedMessage>(&round_bytes);
+                if let Ok(fixed_round) = fixed_round {
+                    assert_ne!(fixed_round, fixed_v1);
+                }
+            }
+        }
+    }
+}
+
 fn run_address_book_roundtrip(compatible: bool) {
     let mut fory = Fory::default().xlang(true).compatible(compatible);
     complex_pb::register_types(&mut fory).expect("register complex pb types");
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java 
b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyObject.java
similarity index 50%
copy from 
java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java
copy to java/fory-core/src/main/java/org/apache/fory/annotation/ForyObject.java
index f5dcd3d4b..6089ca97d 100644
--- a/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java
+++ b/java/fory-core/src/main/java/org/apache/fory/annotation/ForyObject.java
@@ -17,20 +17,25 @@
  * under the License.
  */
 
-package org.apache.fory.resolver;
+package org.apache.fory.annotation;
 
-import static org.testng.Assert.assertNotNull;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
-import org.apache.fory.Fory;
-import org.apache.fory.config.Language;
-import org.testng.annotations.Test;
-
-public class TypeInfoTest {
-  @Test
-  public void testEncodePackageNameAndTypeName() {
-    Fory fory1 = 
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
-    TypeInfo info1 = 
fory1.getClassResolver().getTypeInfo(org.apache.fory.test.bean.Foo.class);
-    assertNotNull(info1.namespaceBytes);
-    assertNotNull(info1.typeNameBytes);
-  }
+/** Marker annotation for Fory-serializable types with optional serialization 
behavior settings. */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ForyObject {
+  /**
+   * Whether the annotated type should use schema evolution in compatible mode.
+   *
+   * <p>When {@code true} (default), compatible mode uses 
COMPATIBLE_STRUCT/NAMED_COMPATIBLE_STRUCT
+   * to include schema metadata for evolution. When {@code false}, 
STRUCT/NAMED_STRUCT is used to
+   * avoid that overhead.
+   */
+  boolean evolving() default true;
 }
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index e7c4ebfc9..ca6133a7b 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -649,7 +649,9 @@ public class ClassResolver extends TypeResolver {
     } else if (serializer != null && !isStructSerializer(serializer)) {
       return Types.EXT;
     } else {
-      return metaContextShareEnabled ? Types.COMPATIBLE_STRUCT : Types.STRUCT;
+      return metaContextShareEnabled && isStructEvolving(cls)
+          ? Types.COMPATIBLE_STRUCT
+          : Types.STRUCT;
     }
   }
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 7854601c7..1aacf976d 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -43,6 +43,7 @@ import java.util.function.Function;
 import org.apache.fory.Fory;
 import org.apache.fory.annotation.CodegenInvoke;
 import org.apache.fory.annotation.ForyField;
+import org.apache.fory.annotation.ForyObject;
 import org.apache.fory.annotation.Internal;
 import org.apache.fory.builder.CodecUtils;
 import org.apache.fory.builder.Generated.GeneratedMetaSharedSerializer;
@@ -909,12 +910,20 @@ public abstract class TypeResolver {
     if (serializer != null && !isStructSerializer(serializer)) {
       return Types.NAMED_EXT;
     }
-    if (fory.isCompatible()) {
+    if (fory.isCompatible() && isStructEvolving(cls)) {
       return metaContextShareEnabled ? Types.NAMED_COMPATIBLE_STRUCT : 
Types.NAMED_STRUCT;
     }
     return Types.NAMED_STRUCT;
   }
 
+  protected boolean isStructEvolving(Class<?> cls) {
+    if (cls == null) {
+      return true;
+    }
+    ForyObject annotation = cls.getAnnotation(ForyObject.class);
+    return annotation == null || annotation.evolving();
+  }
+
   protected static boolean isStructSerializer(Serializer<?> serializer) {
     return serializer instanceof GeneratedObjectSerializer
         || serializer instanceof GeneratedMetaSharedSerializer
@@ -1264,6 +1273,13 @@ public abstract class TypeResolver {
     if (foryField != null && foryField.id() >= 0) {
       return String.valueOf(foryField.id());
     }
+    String name = descriptor.getName();
+    if (name != null && name.startsWith("$tag")) {
+      String tagId = name.substring(4);
+      if (!tagId.isEmpty()) {
+        return tagId;
+      }
+    }
     return descriptor.getSnakeCaseName();
   }
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
index 6fb681b08..76d0366b4 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java
@@ -203,7 +203,8 @@ public class XtypeResolver extends TypeResolver {
     if (type.isEnum()) {
       typeId = Types.ENUM;
     } else {
-      int structTypeId = shareMeta ? Types.COMPATIBLE_STRUCT : Types.STRUCT;
+      int structTypeId =
+          shareMeta && isStructEvolving(type) ? Types.COMPATIBLE_STRUCT : 
Types.STRUCT;
       if (serializer != null) {
         if (isStructType(serializer)) {
           typeId = structTypeId;
@@ -248,7 +249,11 @@ public class XtypeResolver extends TypeResolver {
     short xtypeId;
     if (serializer != null) {
       if (isStructType(serializer)) {
-        xtypeId = (short) (shareMeta ? Types.NAMED_COMPATIBLE_STRUCT : 
Types.NAMED_STRUCT);
+        xtypeId =
+            (short)
+                (shareMeta && isStructEvolving(type)
+                    ? Types.NAMED_COMPATIBLE_STRUCT
+                    : Types.NAMED_STRUCT);
       } else if (serializer instanceof EnumSerializer) {
         xtypeId = Types.NAMED_ENUM;
       } else {
@@ -258,7 +263,11 @@ public class XtypeResolver extends TypeResolver {
       if (type.isEnum()) {
         xtypeId = Types.NAMED_ENUM;
       } else {
-        xtypeId = (short) (shareMeta ? Types.NAMED_COMPATIBLE_STRUCT : 
Types.NAMED_STRUCT);
+        xtypeId =
+            (short)
+                (shareMeta && isStructEvolving(type)
+                    ? Types.NAMED_COMPATIBLE_STRUCT
+                    : Types.NAMED_STRUCT);
       }
     }
     register(type, serializer, namespace, typeName, xtypeId, -1);
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java 
b/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java
index f5dcd3d4b..39a7352d9 100644
--- a/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/resolver/TypeInfoTest.java
@@ -19,13 +19,25 @@
 
 package org.apache.fory.resolver;
 
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 
 import org.apache.fory.Fory;
+import org.apache.fory.annotation.ForyObject;
 import org.apache.fory.config.Language;
+import org.apache.fory.type.Types;
 import org.testng.annotations.Test;
 
 public class TypeInfoTest {
+  public static class EvolvingStruct {
+    public int id;
+  }
+
+  @ForyObject(evolving = false)
+  public static class FixedStruct {
+    public int id;
+  }
+
   @Test
   public void testEncodePackageNameAndTypeName() {
     Fory fory1 = 
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
@@ -33,4 +45,18 @@ public class TypeInfoTest {
     assertNotNull(info1.namespaceBytes);
     assertNotNull(info1.typeNameBytes);
   }
+
+  @Test
+  public void testStructEvolvingOverride() {
+    Fory fory = 
Fory.builder().withLanguage(Language.XLANG).withCompatible(true).build();
+    fory.register(EvolvingStruct.class, "test", "EvolvingStruct");
+    fory.register(FixedStruct.class, "test", "FixedStruct");
+
+    TypeInfo evolvingInfo = 
fory.getTypeResolver().getTypeInfo(EvolvingStruct.class, false);
+    TypeInfo fixedInfo = fory.getTypeResolver().getTypeInfo(FixedStruct.class, 
false);
+    assertNotNull(evolvingInfo);
+    assertNotNull(fixedInfo);
+    assertEquals(evolvingInfo.getTypeId(), Types.NAMED_COMPATIBLE_STRUCT);
+    assertEquals(fixedInfo.getTypeId(), Types.NAMED_STRUCT);
+  }
 }
diff --git a/python/pyfory/__init__.py b/python/pyfory/__init__.py
index 7fcbd1b7a..08479adf5 100644
--- a/python/pyfory/__init__.py
+++ b/python/pyfory/__init__.py
@@ -69,7 +69,7 @@ from pyfory.serializer import (  # noqa: F401 # pylint: 
disable=unused-import
     StatefulSerializer,
 )
 from pyfory.struct import DataClassSerializer
-from pyfory.field import field  # noqa: F401 # pylint: disable=unused-import
+from pyfory.field import dataclass, field  # noqa: F401 # pylint: 
disable=unused-import
 from pyfory.types import (  # noqa: F401 # pylint: disable=unused-import
     TypeId,
     Ref,
@@ -130,6 +130,7 @@ __all__ = [
     "DeserializationPolicy",
     # Field metadata
     "field",
+    "dataclass",
     # Type utilities
     "record_class_factory",
     "get_qualified_classname",
diff --git a/python/pyfory/field.py b/python/pyfory/field.py
index fd67b740c..ccf408483 100644
--- a/python/pyfory/field.py
+++ b/python/pyfory/field.py
@@ -40,6 +40,7 @@ from typing import Any, Callable, Dict, Mapping, Optional
 
 # Key used to store Fory metadata in field.metadata
 FORY_FIELD_METADATA_KEY = "__fory__"
+FORY_OBJECT_METADATA_KEY = "__fory_object__"
 
 
 @dataclasses.dataclass(frozen=True)
@@ -69,6 +70,38 @@ class ForyFieldMeta:
         return self.id >= 0
 
 
[email protected](frozen=True)
+class ForyObjectMeta:
+    """Fory object metadata stored on dataclass types."""
+
+    evolving: bool = True
+
+
+def dataclass(_cls=None, *, evolving: bool = True, slots: bool = False, 
**kwargs):
+    """Create a dataclass with Fory-specific metadata."""
+
+    def wrap(cls):
+        if slots:
+            import inspect
+
+            supports_slots = "slots" in 
inspect.signature(dataclasses.dataclass).parameters
+            if supports_slots:
+                dc = dataclasses.dataclass(cls, slots=True, **kwargs)
+            else:
+                dc = dataclasses.dataclass(cls, **kwargs)
+                from pyfory.type_util import dataslots
+
+                dc = dataslots(dc)
+        else:
+            dc = dataclasses.dataclass(cls, **kwargs)
+        setattr(dc, FORY_OBJECT_METADATA_KEY, 
ForyObjectMeta(evolving=evolving))
+        return dc
+
+    if _cls is None:
+        return wrap
+    return wrap(_cls)
+
+
 def field(
     id: int = -1,
     *,
@@ -186,6 +219,11 @@ def extract_field_meta(dataclass_field: dataclasses.Field) 
-> Optional[ForyField
     return dataclass_field.metadata.get(FORY_FIELD_METADATA_KEY)
 
 
+def extract_object_meta(cls: type) -> Optional[ForyObjectMeta]:
+    """Extract ForyObjectMeta from a dataclass type if present."""
+    return getattr(cls, FORY_OBJECT_METADATA_KEY, None)
+
+
 def validate_field_metas(
     cls: type,
     field_metas: Dict[str, ForyFieldMeta],
diff --git a/python/pyfory/registry.py b/python/pyfory/registry.py
index 3d2394002..21038de29 100644
--- a/python/pyfory/registry.py
+++ b/python/pyfory/registry.py
@@ -29,6 +29,7 @@ from enum import Enum
 
 from pyfory import ENABLE_FORY_CYTHON_SERIALIZATION
 from pyfory.error import TypeUnregisteredError
+from pyfory.field import extract_object_meta
 
 from pyfory.serializer import (
     Serializer,
@@ -471,6 +472,10 @@ class TypeResolver:
         serializer=None,
         internal=False,
     ):
+        object_meta = extract_object_meta(cls)
+        evolving = True
+        if object_meta is not None:
+            evolving = object_meta.evolving
         if serializer is None:
             if issubclass(cls, enum.Enum):
                 serializer = EnumSerializer(self.fory, cls)
@@ -482,7 +487,7 @@ class TypeResolver:
                     type_id = TypeId.ENUM
             else:
                 serializer = None
-                if self.meta_share:
+                if self.meta_share and evolving:
                     if type_id is None:
                         type_id = TypeId.NAMED_COMPATIBLE_STRUCT
                         user_type_id = NO_USER_TYPE_ID
diff --git a/python/pyfory/tests/test_struct.py 
b/python/pyfory/tests/test_struct.py
index 265307111..9d2b4ad43 100644
--- a/python/pyfory/tests/test_struct.py
+++ b/python/pyfory/tests/test_struct.py
@@ -27,6 +27,7 @@ import pyfory
 from pyfory import Fory
 from pyfory.error import TypeUnregisteredError
 from pyfory.struct import DataClassSerializer
+from pyfory.types import TypeId
 
 
 def ser_de(fory, obj):
@@ -231,6 +232,24 @@ def test_data_class_serializer_xlang():
     assert obj_deserialized_none == obj_with_none_complex
 
 
+def test_struct_evolving_override():
+    @pyfory.dataclass
+    class EvolvingStruct:
+        f1: pyfory.int32 = 0
+
+    @pyfory.dataclass(evolving=False)
+    class FixedStruct:
+        f1: pyfory.int32 = 0
+
+    fory = Fory(xlang=True, compatible=True)
+    fory.register_type(EvolvingStruct, namespace="test", 
typename="EvolvingStruct")
+    fory.register_type(FixedStruct, namespace="test", typename="FixedStruct")
+    evolving_info = fory.type_resolver.get_type_info(EvolvingStruct)
+    fixed_info = fory.type_resolver.get_type_info(FixedStruct)
+    assert evolving_info.type_id == TypeId.NAMED_COMPATIBLE_STRUCT
+    assert fixed_info.type_id == TypeId.NAMED_STRUCT
+
+
 def test_data_class_serializer_xlang_codegen():
     """Test that DataClassSerializer generates xwrite/xread methods correctly 
in xlang mode."""
     fory = Fory(xlang=True, ref=True)
diff --git a/rust/fory-derive/src/lib.rs b/rust/fory-derive/src/lib.rs
index 017eb6f12..4ca12cbe6 100644
--- a/rust/fory-derive/src/lib.rs
+++ b/rust/fory-derive/src/lib.rs
@@ -111,6 +111,8 @@
 //! - **`#[fory(debug)]` / `#[fory(debug = true)]`**: Enables per-field debug 
instrumentation
 //!   for the annotated struct, allowing you to install custom hooks via
 //!   `fory_core::serializer::struct_`.
+//! - **`#[fory(evolving = false)]`**: Disables compatible struct type IDs for 
the annotated
+//!   struct, forcing STRUCT/NAMED_STRUCT even when compatible mode is enabled.
 //! - **`#[fory(skip)]`**: Marks an individual field (or enum variant) to be 
ignored by the
 //!   generated serializer, retaining compatibility with previous releases.
 //! - **`#[fory(generate_default)]`**: Enables the macro to generate `Default` 
implementation.
@@ -253,12 +255,14 @@ pub fn proc_macro_derive_fory_row(input: 
proc_macro::TokenStream) -> TokenStream
 pub(crate) struct ForyAttrs {
     pub debug_enabled: bool,
     pub generate_default: bool,
+    pub evolving: Option<bool>,
 }
 
 /// Parse fory attributes and return ForyAttrs
 fn parse_fory_attrs(attrs: &[Attribute]) -> syn::Result<ForyAttrs> {
     let mut debug_flag: Option<bool> = None;
     let mut generate_default_flag: Option<bool> = None;
+    let mut evolving_flag: Option<bool> = None;
 
     for attr in attrs {
         if attr.path().is_ident("fory") {
@@ -297,6 +301,23 @@ fn parse_fory_attrs(attrs: &[Attribute]) -> 
syn::Result<ForyAttrs> {
                         Some(_) => generate_default_flag,
                         None => Some(value),
                     };
+                } else if meta.path.is_ident("evolving") {
+                    let value = if meta.input.is_empty() {
+                        true
+                    } else {
+                        let lit: LitBool = meta.value()?.parse()?;
+                        lit.value
+                    };
+                    evolving_flag = match evolving_flag {
+                        Some(existing) if existing != value => {
+                            return Err(syn::Error::new(
+                                meta.path.span(),
+                                "conflicting `evolving` attribute values",
+                            ));
+                        }
+                        Some(_) => evolving_flag,
+                        None => Some(value),
+                    };
                 }
                 Ok(())
             })?;
@@ -306,5 +327,6 @@ fn parse_fory_attrs(attrs: &[Attribute]) -> 
syn::Result<ForyAttrs> {
     Ok(ForyAttrs {
         debug_enabled: debug_flag.unwrap_or(false),
         generate_default: generate_default_flag.unwrap_or(false),
+        evolving: evolving_flag,
     })
 }
diff --git a/rust/fory-derive/src/object/misc.rs 
b/rust/fory-derive/src/object/misc.rs
index 0e1cd8831..9919c4410 100644
--- a/rust/fory-derive/src/object/misc.rs
+++ b/rust/fory-derive/src/object/misc.rs
@@ -68,6 +68,16 @@ pub fn gen_actual_type_id() -> TokenStream {
     }
 }
 
+pub fn gen_actual_type_id_no_evolving() -> TokenStream {
+    quote! {
+        if register_by_name {
+            fory_core::types::TypeId::NAMED_STRUCT as u32
+        } else {
+            fory_core::types::TypeId::STRUCT as u32
+        }
+    }
+}
+
 pub fn gen_get_sorted_field_names(fields: &[&Field]) -> TokenStream {
     let static_field_names = get_sort_fields_ts(fields);
     quote! {
diff --git a/rust/fory-derive/src/object/serializer.rs 
b/rust/fory-derive/src/object/serializer.rs
index 5cdea36e3..b9dec57c8 100644
--- a/rust/fory-derive/src/object/serializer.rs
+++ b/rust/fory-derive/src/object/serializer.rs
@@ -75,8 +75,13 @@ pub fn derive_serializer(ast: &syn::DeriveInput, attrs: 
ForyAttrs) -> TokenStrea
         syn::Data::Struct(s) => {
             let source_fields = source_fields(&s.fields);
             let fields = extract_fields(&source_fields);
+            let actual_type_id_ts = if attrs.evolving == Some(false) {
+                misc::gen_actual_type_id_no_evolving()
+            } else {
+                misc::gen_actual_type_id()
+            };
             (
-                misc::gen_actual_type_id(),
+                actual_type_id_ts,
                 misc::gen_get_sorted_field_names(&fields),
                 misc::gen_field_fields_info(&source_fields),
                 quote! { Ok(Vec::new()) }, // No variants for structs
diff --git a/rust/tests/tests/test_simple_struct.rs 
b/rust/tests/tests/test_simple_struct.rs
index dac10a74c..f96f1a4d3 100644
--- a/rust/tests/tests/test_simple_struct.rs
+++ b/rust/tests/tests/test_simple_struct.rs
@@ -18,6 +18,7 @@
 use std::collections::HashMap;
 
 use fory_core::fory::Fory;
+use fory_core::TypeId;
 use fory_derive::ForyObject;
 
 // Test 1: Simple struct with one primitive field, non-compatible mode
@@ -78,6 +79,35 @@ fn test_compatible_field_type_change() {
     assert_eq!(result.value.unwrap(), 42i32);
 }
 
+#[test]
+fn test_struct_evolving_override() {
+    #[derive(ForyObject, Debug)]
+    struct Evolving {
+        id: i32,
+    }
+
+    #[derive(ForyObject, Debug)]
+    #[fory(evolving = false)]
+    struct Fixed {
+        id: i32,
+    }
+
+    let mut fory = Fory::default()
+        .xlang(true)
+        .compatible(true)
+        .track_ref(false);
+    fory.register::<Evolving>(100).unwrap();
+    fory.register::<Fixed>(101).unwrap();
+
+    let evolving_bytes = fory.serialize(&Evolving { id: 1 }).unwrap();
+    assert!(evolving_bytes.len() > 2);
+    assert_eq!(evolving_bytes[2], TypeId::COMPATIBLE_STRUCT as u8);
+
+    let fixed_bytes = fory.serialize(&Fixed { id: 1 }).unwrap();
+    assert!(fixed_bytes.len() > 2);
+    assert_eq!(fixed_bytes[2], TypeId::STRUCT as u8);
+}
+
 // Test 4: Compatible mode - serialize with field, deserialize with empty 
struct
 #[test]
 fn test_compatible_to_empty_struct() {


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

Reply via email to