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 768dee5a7 feat(python): support nested field schema encodings (#3643)
768dee5a7 is described below

commit 768dee5a7e27eb688a6e380dc5fdd98165543ba0
Author: Shawn Yang <[email protected]>
AuthorDate: Sat May 2 12:25:50 2026 +0800

    feat(python): support nested field schema encodings (#3643)
    
    ## Why?
    
    Python currently loses declared integer encoding aliases inside nested
    containers, so schemas such as `Dict[pyfory.fixed_int32,
    List[pyfory.tagged_int64]]` can fall back to runtime inference or
    incompatible local assignment behavior. This breaks cross-language
    schema fidelity for nested map/list/set elements, especially in
    compatible mode.
    
    ## What does this PR do?
    
    - Preserves declared Python field schema metadata for nested container
    keys, values, and elements in both pure Python and Cython serializers.
    - Extends primitive container fast paths to write/read fixed, varint,
    tagged, signed, unsigned, bool, and float schema-owned primitive
    encodings directly.
    - Adds compatible-read assignment planning and validation so remote
    nested integer encodings are consumed with remote metadata and assigned
    only when they satisfy the local schema.
    - Updates Python code generation to keep nested integer schema aliases
    in generic containers instead of converting those lists to numpy array
    hints.
    - Adds Python unit coverage, compiler generation coverage, and
    Java/Python xlang tests for signed and unsigned nested annotated
    containers.
    - Documents nested Python collection aliases and compatible-read
    behavior in README and Python guide pages.
    
    ## Related issues
    
    #1017
    #3630
    #3625
    #3630
    #3636
    #3639
    #3640
    #3642
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## 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
---
 README.md                                          |   4 +
 compiler/fory_compiler/generators/python.py        |  39 ++--
 .../fory_compiler/tests/test_generated_code.py     |  24 ++
 docs/guide/python/cross-language.md                |  15 +-
 docs/guide/python/field-configuration.md           |  32 ++-
 .../org/apache/fory/xlang/PythonXlangTest.java     |  27 +++
 .../java/org/apache/fory/xlang/XlangTestBase.java  |  74 ++++++
 python/README.md                                   |   5 +
 python/pyfory/collection.pxi                       | 116 +++++++++-
 python/pyfory/cpp/pyfory.cc                        |  33 ++-
 python/pyfory/cpp/pyfory.h                         |  12 +-
 python/pyfory/meta/typedef.py                      | 257 ++++++++++++++++++---
 python/pyfory/struct.pxi                           | 132 ++++++++++-
 python/pyfory/struct.py                            | 143 +++++++++---
 python/pyfory/tests/test_struct.py                 |  99 ++++++++
 python/pyfory/tests/test_typedef_encoding.py       |  27 +++
 python/pyfory/tests/xlang_test_main.py             |  75 ++++++
 17 files changed, 1010 insertions(+), 104 deletions(-)

diff --git a/README.md b/README.md
index 4ce534801..e4d995ae9 100644
--- a/README.md
+++ b/README.md
@@ -310,6 +310,10 @@ result = fory.deserialize(data)
 print(result.name, result.age)  # Output: chaokunyang 28
 ```
 
+Python schema aliases also apply inside declared containers, such as
+`Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]`, so nested keys and 
values use the requested
+wire encoding in both pure Python and Cython modes.
+
 For detailed Python usage including type hints, compatibility modes, and 
advanced features, see [Python Guide](docs/guide/python).
 
 #### Rust Serialization
diff --git a/compiler/fory_compiler/generators/python.py 
b/compiler/fory_compiler/generators/python.py
index 2065b8b5f..aaf356505 100644
--- a/compiler/fory_compiler/generators/python.py
+++ b/compiler/fory_compiler/generators/python.py
@@ -114,6 +114,14 @@ class PythonGenerator(BaseGenerator):
         PrimitiveKind.FLOAT32: "pyfory.float32_ndarray",
         PrimitiveKind.FLOAT64: "pyfory.float64_ndarray",
     }
+    LIST_ELEMENT_SCHEMA_PRIMITIVES = {
+        PrimitiveKind.INT32,
+        PrimitiveKind.INT64,
+        PrimitiveKind.TAGGED_INT64,
+        PrimitiveKind.UINT32,
+        PrimitiveKind.UINT64,
+        PrimitiveKind.TAGGED_UINT64,
+    }
 
     # Default values for primitive types
     DEFAULT_VALUES = {
@@ -645,6 +653,8 @@ class PythonGenerator(BaseGenerator):
         """Return True if a list should be represented as a numpy array."""
         if not isinstance(field_type.element_type, PrimitiveType):
             return False
+        if field_type.element_type.kind in self.LIST_ELEMENT_SCHEMA_PRIMITIVES:
+            return False
         return (
             field_type.element_type.kind in self.ARRAY_TYPE_HINTS
             and not element_optional
@@ -718,6 +728,7 @@ class PythonGenerator(BaseGenerator):
         element_optional: bool = False,
         element_ref: bool = False,
         parent_stack: Optional[List[Message]] = None,
+        prefer_numpy_array: bool = True,
     ) -> str:
         """Generate Python type hint."""
         if isinstance(field_type, PrimitiveType):
@@ -744,7 +755,9 @@ class PythonGenerator(BaseGenerator):
             if isinstance(field_type.element_type, PrimitiveType):
                 if not element_optional:
                     kind = field_type.element_type.kind
-                    if kind in self.ARRAY_TYPE_HINTS:
+                    if prefer_numpy_array and self.uses_numpy_array(
+                        field_type, element_optional
+                    ):
                         list_type = self.ARRAY_TYPE_HINTS[kind]
                     else:
                         element_type = self.generate_type(
@@ -753,6 +766,7 @@ class PythonGenerator(BaseGenerator):
                             False,
                             False,
                             parent_stack,
+                            False,
                         )
                         element_type = self.wrap_ref_type(
                             field_type.element_type,
@@ -769,6 +783,7 @@ class PythonGenerator(BaseGenerator):
                         False,
                         False,
                         parent_stack,
+                        False,
                     )
                     element_type = self.wrap_ref_type(
                         field_type.element_type,
@@ -785,6 +800,7 @@ class PythonGenerator(BaseGenerator):
                     False,
                     False,
                     parent_stack,
+                    False,
                 )
                 element_type = self.wrap_ref_type(
                     field_type.element_type,
@@ -800,10 +816,10 @@ class PythonGenerator(BaseGenerator):
 
         elif isinstance(field_type, MapType):
             key_type = self.generate_type(
-                field_type.key_type, False, False, False, parent_stack
+                field_type.key_type, False, False, False, parent_stack, False
             )
             value_type = self.generate_type(
-                field_type.value_type, False, False, False, parent_stack
+                field_type.value_type, False, False, False, parent_stack, False
             )
             value_type = self.wrap_ref_type(
                 field_type.value_type,
@@ -943,10 +959,9 @@ class PythonGenerator(BaseGenerator):
 
         elif isinstance(field_type, ListType):
             # Use numpy empty array for numeric types
-            if isinstance(field_type.element_type, PrimitiveType):
-                if field_type.element_type.kind in self.NUMPY_DTYPE_MAP:
-                    dtype = self.NUMPY_DTYPE_MAP[field_type.element_type.kind]
-                    return f"None  # Use np.array([], dtype={dtype}) to 
initialize"
+            if self.uses_numpy_array(field_type, element_optional=False):
+                dtype = self.NUMPY_DTYPE_MAP[field_type.element_type.kind]
+                return f"None  # Use np.array([], dtype={dtype}) to initialize"
             return "None"
 
         elif isinstance(field_type, MapType):
@@ -971,13 +986,9 @@ class PythonGenerator(BaseGenerator):
 
         elif isinstance(field_type, ListType):
             # Add numpy import for primitive arrays
-            if isinstance(field_type.element_type, PrimitiveType):
-                if (
-                    field_type.element_type.kind in self.ARRAY_TYPE_HINTS
-                    and not element_optional
-                ):
-                    imports.add("import numpy as np")
-                    return
+            if self.uses_numpy_array(field_type, element_optional):
+                imports.add("import numpy as np")
+                return
             self.collect_imports(field_type.element_type, imports)
 
         elif isinstance(field_type, MapType):
diff --git a/compiler/fory_compiler/tests/test_generated_code.py 
b/compiler/fory_compiler/tests/test_generated_code.py
index e92bf4ead..0197bb7b3 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -608,6 +608,30 @@ def 
test_java_nested_integer_annotations_in_generic_containers():
     )
 
 
+def test_python_nested_integer_schema_aliases_in_generic_containers():
+    schema = parse_fdl(
+        dedent(
+            """
+            package gen;
+
+            message NestedIntegerSchemaAliases {
+                map<fixed_int32, list<tagged_int64>> signed_values = 1;
+                map<fixed_uint32, list<tagged_uint64>> unsigned_values = 2;
+            }
+            """
+        )
+    )
+    python_output = render_files(generate_files(schema, PythonGenerator))
+    assert (
+        "signed_values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]"
+        in python_output
+    )
+    assert (
+        "unsigned_values: Dict[pyfory.fixed_uint32, 
List[pyfory.tagged_uint64]]"
+        in python_output
+    )
+
+
 def test_cpp_nested_integer_specs_in_generic_containers():
     schema = parse_fdl(
         dedent(
diff --git a/docs/guide/python/cross-language.md 
b/docs/guide/python/cross-language.md
index 306453836..fbe3e90bc 100644
--- a/docs/guide/python/cross-language.md
+++ b/docs/guide/python/cross-language.md
@@ -99,16 +99,23 @@ Use pyfory type annotations for explicit cross-language 
type mapping:
 
 ```python
 from dataclasses import dataclass
+from typing import Dict, List
 import pyfory
 
 @dataclass
 class TypedData:
-    int_value: pyfory.int32      # 32-bit integer
-    long_value: pyfory.int64     # 64-bit integer
-    float_value: pyfory.float32  # 32-bit float
-    double_value: pyfory.float64 # 64-bit float
+    int_value: pyfory.int32       # 32-bit integer
+    long_value: pyfory.int64      # 64-bit integer
+    float_value: pyfory.float32   # 32-bit float
+    double_value: pyfory.float64  # 64-bit float
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]
 ```
 
+Nested collection annotations are part of the field schema. For example,
+`Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]` writes map keys as fixed 
int32 values and the
+nested list elements as tagged int64 values. Compatible-mode reads consume 
bytes with the remote
+schema metadata, then assign only when the decoded value safely satisfies the 
local schema.
+
 ## Reduced-Precision Types
 
 `pyfory.serialization` exports Cython-only carrier types for xlang 
reduced-precision values:
diff --git a/docs/guide/python/field-configuration.md 
b/docs/guide/python/field-configuration.md
index 487913306..1e4e8f35c 100644
--- a/docs/guide/python/field-configuration.md
+++ b/docs/guide/python/field-configuration.md
@@ -278,6 +278,9 @@ class FloatingPoint:
 | `pyfory.int16`         | fixed    | 2 bytes    |
 | `pyfory.int32`         | varint   | 1-5 bytes  |
 | `pyfory.int64`         | varint   | 1-10 bytes |
+| `pyfory.fixed_int32`   | fixed    | 4 bytes    |
+| `pyfory.fixed_int64`   | fixed    | 8 bytes    |
+| `pyfory.tagged_int64`  | tagged   | 1-9 bytes  |
 | `pyfory.uint8`         | fixed    | 1 byte     |
 | `pyfory.uint16`        | fixed    | 2 bytes    |
 | `pyfory.uint32`        | varint   | 1-5 bytes  |
@@ -292,7 +295,32 @@ class FloatingPoint:
 
 - `varint`: Best for values that are often small (default for 
int32/int64/uint32/uint64)
 - `fixed`: Best for values that use full range (e.g., timestamps, hashes)
-- `tagged`: When type information needs to be preserved (uint64 only)
+- `tagged`: When type information needs to be preserved (int64/uint64 only)
+
+## Nested Container Type Annotations
+
+Integer encoding aliases can be used inside declared collection schemas. Fory 
uses the declared
+field schema for every nested element, key, and value in both pure Python and 
Cython modes:
+
+```python
+from dataclasses import dataclass, field
+from typing import Dict, List
+import pyfory
+
+
+@dataclass
+class Counters:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = 
field(default_factory=dict)
+```
+
+For `values`, map keys are written as fixed-width int32 values and each nested 
list element is
+written as tagged int64. Runtime type inference is used only for dynamic or 
unknown container
+schemas.
+
+In compatible mode, readers consume field bytes using the remote schema 
metadata. Python assigns the
+decoded value only when it can safely satisfy the local declared schema. 
Different integer encodings
+in the same signedness and width domain are compatible, and same-signedness 
narrowing is assigned
+only after range validation.
 
 ## Complete Example
 
@@ -481,6 +509,8 @@ class User:
 | `pyfory.field(dynamic=False)`                | Skip type info (use declared 
type)   |
 | `Optional[T]`                                | Type hint for nullable fields 
       |
 | `pyfory.int32`, `pyfory.int64`               | Signed integers (varint 
encoding)    |
+| `pyfory.fixed_int32`, `pyfory.fixed_int64`   | Fixed-size signed             
       |
+| `pyfory.tagged_int64`                        | Tagged encoding for int64     
       |
 | `pyfory.uint32`, `pyfory.uint64`             | Unsigned integers (varint 
encoding)  |
 | `pyfory.fixed_uint32`, `pyfory.fixed_uint64` | Fixed-size unsigned           
       |
 | `pyfory.tagged_uint64`                       | Tagged encoding for uint64    
       |
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/xlang/PythonXlangTest.java 
b/java/fory-core/src/test/java/org/apache/fory/xlang/PythonXlangTest.java
index 4bcca3a5e..b7c95fe64 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/PythonXlangTest.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/PythonXlangTest.java
@@ -232,6 +232,33 @@ public class PythonXlangTest extends XlangTestBase {
     super.testCollectionElementRefRemoteTracking(enableCodegen);
   }
 
+  @Override
+  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+  public void testSignedNestedAnnotatedContainerSchemaConsistent(boolean 
enableCodegen)
+      throws IOException {
+    super.testSignedNestedAnnotatedContainerSchemaConsistent(enableCodegen);
+  }
+
+  @Override
+  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+  public void testSignedNestedAnnotatedContainerCompatible(boolean 
enableCodegen)
+      throws IOException {
+    super.testSignedNestedAnnotatedContainerCompatible(enableCodegen);
+  }
+
+  @Override
+  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+  public void testNestedAnnotatedContainerSchemaConsistent(boolean 
enableCodegen)
+      throws IOException {
+    super.testNestedAnnotatedContainerSchemaConsistent(enableCodegen);
+  }
+
+  @Override
+  @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
+  public void testNestedAnnotatedContainerCompatible(boolean enableCodegen) 
throws IOException {
+    super.testNestedAnnotatedContainerCompatible(enableCodegen);
+  }
+
   @Override
   @Test(groups = "xlang", dataProvider = "enableCodegenParallel")
   public void testNullableFieldSchemaConsistentNotNull(boolean enableCodegen) 
throws IOException {
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java 
b/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java
index ba8556e45..4de245b33 100644
--- a/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java
+++ b/java/fory-core/src/test/java/org/apache/fory/xlang/XlangTestBase.java
@@ -39,6 +39,8 @@ import org.apache.fory.Fory;
 import org.apache.fory.ForyTestBase;
 import org.apache.fory.annotation.ForyField;
 import org.apache.fory.annotation.ForyStruct;
+import org.apache.fory.annotation.Int32Type;
+import org.apache.fory.annotation.Int64Type;
 import org.apache.fory.annotation.Ref;
 import org.apache.fory.annotation.UInt16Type;
 import org.apache.fory.annotation.UInt32Type;
@@ -1015,6 +1017,78 @@ public abstract class XlangTestBase extends ForyTestBase 
{
     Map<String, String> data;
   }
 
+  @Data
+  static class SignedNestedAnnotatedContainerSchemaConsistent {
+    Map<
+            @Int32Type(encoding = Int32Encoding.FIXED) Integer,
+            List<@Int64Type(encoding = Int64Encoding.TAGGED) Long>>
+        values;
+  }
+
+  protected void testSignedNestedAnnotatedContainerSchemaConsistent(boolean 
enableCodegen)
+      throws java.io.IOException {
+    String caseName = 
"test_signed_nested_annotated_container_schema_consistent";
+    Fory fory =
+        
Fory.builder().withXlang(true).withCompatible(false).withCodegen(enableCodegen).build();
+    fory.register(SignedNestedAnnotatedContainerSchemaConsistent.class, 803);
+
+    SignedNestedAnnotatedContainerSchemaConsistent obj =
+        new SignedNestedAnnotatedContainerSchemaConsistent();
+    obj.values = new LinkedHashMap<>();
+    obj.values.put(-7, Arrays.asList(7L, -1000000000L));
+    obj.values.put(3, Collections.singletonList(42L));
+
+    serDeCheck(fory, obj);
+
+    MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(512);
+    fory.serialize(buffer, obj);
+    ExecutionContext ctx = prepareExecution(caseName, buffer.getBytes(0, 
buffer.writerIndex()));
+    runPeer(ctx);
+
+    MemoryBuffer buffer2 = readBuffer(ctx.dataFile());
+    SignedNestedAnnotatedContainerSchemaConsistent result =
+        (SignedNestedAnnotatedContainerSchemaConsistent) 
fory.deserialize(buffer2);
+    Assert.assertEquals(result, obj);
+  }
+
+  @Data
+  static class SignedNestedAnnotatedContainerCompatible {
+    Map<
+            @Int32Type(encoding = Int32Encoding.FIXED) Integer,
+            List<@Int64Type(encoding = Int64Encoding.TAGGED) Long>>
+        values;
+  }
+
+  protected void testSignedNestedAnnotatedContainerCompatible(boolean 
enableCodegen)
+      throws java.io.IOException {
+    String caseName = "test_signed_nested_annotated_container_compatible";
+    Fory fory =
+        Fory.builder()
+            .withXlang(true)
+            .withCompatible(true)
+            .withCodegen(enableCodegen)
+            .withMetaCompressor(new NoOpMetaCompressor())
+            .build();
+    fory.register(SignedNestedAnnotatedContainerCompatible.class, 804);
+
+    SignedNestedAnnotatedContainerCompatible obj = new 
SignedNestedAnnotatedContainerCompatible();
+    obj.values = new LinkedHashMap<>();
+    obj.values.put(-7, Arrays.asList(7L, -1000000000L));
+    obj.values.put(3, Collections.singletonList(42L));
+
+    serDeCheck(fory, obj);
+
+    MemoryBuffer buffer = MemoryBuffer.newHeapBuffer(512);
+    fory.serialize(buffer, obj);
+    ExecutionContext ctx = prepareExecution(caseName, buffer.getBytes(0, 
buffer.writerIndex()));
+    runPeer(ctx);
+
+    MemoryBuffer buffer2 = readBuffer(ctx.dataFile());
+    SignedNestedAnnotatedContainerCompatible result =
+        (SignedNestedAnnotatedContainerCompatible) fory.deserialize(buffer2);
+    Assert.assertEquals(result, obj);
+  }
+
   @Data
   static class NestedAnnotatedContainerSchemaConsistent {
     Map<
diff --git a/python/README.md b/python/README.md
index d92bec3e5..9e3089d3c 100644
--- a/python/README.md
+++ b/python/README.md
@@ -454,6 +454,7 @@ Serialize data in Python and deserialize it in Java, Go, 
Rust, or other supporte
 **Python (Serializer)**
 
 ```python
+from dataclasses import dataclass
 import pyfory
 
 # Cross-language mode for interoperability
@@ -472,6 +473,10 @@ binary_data = f.serialize(person)
 # binary_data can now be sent to Java, Go, etc.
 ```
 
+Nested collection aliases are declared schema. For example,
+`Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]` writes map keys with 
fixed int32 encoding and
+nested list elements with tagged int64 encoding in both pure Python and Cython 
modes.
+
 **Java (Deserializer)**
 
 ```java
diff --git a/python/pyfory/collection.pxi b/python/pyfory/collection.pxi
index a4b98118f..6a663cac8 100644
--- a/python/pyfory/collection.pxi
+++ b/python/pyfory/collection.pxi
@@ -947,29 +947,79 @@ cdef class MapSerializer(Serializer):
                 if not key_write_ref or not 
ref_writer.write_ref_or_null(c_buffer, key):
                     if key_cls is str:
                         write_context.write_string(key)
-                    elif key_serializer_type is Int64Serializer:
+                    elif key_serializer_type is Int64Serializer or 
key_serializer_type is Varint64Serializer:
                         write_context.write_varint64(key)
+                    elif key_serializer_type is FixedInt64Serializer:
+                        write_context.write_int64(key)
+                    elif key_serializer_type is TaggedInt64Serializer:
+                        write_context.write_tagged_int64(key)
                     elif key_serializer_type is Float64Serializer:
                         write_context.write_double(key)
-                    elif key_serializer_type is Int32Serializer:
+                    elif key_serializer_type is Int32Serializer or 
key_serializer_type is Varint32Serializer:
                         write_context.write_varint32(key)
+                    elif key_serializer_type is FixedInt32Serializer:
+                        write_context.write_int32(key)
                     elif key_serializer_type is Float32Serializer:
                         write_context.write_float(key)
+                    elif key_serializer_type is BooleanSerializer:
+                        write_context.write_bool(key)
+                    elif key_serializer_type is ByteSerializer:
+                        write_context.write_int8(key)
+                    elif key_serializer_type is Int16Serializer:
+                        write_context.write_int16(key)
+                    elif key_serializer_type is Uint8Serializer:
+                        write_context.write_uint8(key)
+                    elif key_serializer_type is Uint16Serializer:
+                        write_context.write_uint16(key)
+                    elif key_serializer_type is Uint32Serializer:
+                        write_context.write_uint32(key)
+                    elif key_serializer_type is VarUint32Serializer:
+                        write_context.write_var_uint32(key)
+                    elif key_serializer_type is Uint64Serializer:
+                        write_context.write_uint64(key)
+                    elif key_serializer_type is VarUint64Serializer:
+                        write_context.write_var_uint64(key)
+                    elif key_serializer_type is TaggedUint64Serializer:
+                        write_context.write_tagged_uint64(key)
                     else:
                         key_serializer.write(write_context, key)
                 if not value_write_ref or not 
ref_writer.write_ref_or_null(c_buffer, value):
                     if value_cls is str:
                         write_context.write_string(value)
-                    elif value_serializer_type is Int64Serializer:
+                    elif value_serializer_type is Int64Serializer or 
value_serializer_type is Varint64Serializer:
                         write_context.write_varint64(value)
+                    elif value_serializer_type is FixedInt64Serializer:
+                        write_context.write_int64(value)
+                    elif value_serializer_type is TaggedInt64Serializer:
+                        write_context.write_tagged_int64(value)
                     elif value_serializer_type is Float64Serializer:
                         write_context.write_double(value)
-                    elif value_serializer_type is Int32Serializer:
+                    elif value_serializer_type is Int32Serializer or 
value_serializer_type is Varint32Serializer:
                         write_context.write_varint32(value)
+                    elif value_serializer_type is FixedInt32Serializer:
+                        write_context.write_int32(value)
                     elif value_serializer_type is Float32Serializer:
                         write_context.write_float(value)
                     elif value_serializer_type is BooleanSerializer:
                         write_context.write_bool(value)
+                    elif value_serializer_type is ByteSerializer:
+                        write_context.write_int8(value)
+                    elif value_serializer_type is Int16Serializer:
+                        write_context.write_int16(value)
+                    elif value_serializer_type is Uint8Serializer:
+                        write_context.write_uint8(value)
+                    elif value_serializer_type is Uint16Serializer:
+                        write_context.write_uint16(value)
+                    elif value_serializer_type is Uint32Serializer:
+                        write_context.write_uint32(value)
+                    elif value_serializer_type is VarUint32Serializer:
+                        write_context.write_var_uint32(value)
+                    elif value_serializer_type is Uint64Serializer:
+                        write_context.write_uint64(value)
+                    elif value_serializer_type is VarUint64Serializer:
+                        write_context.write_var_uint64(value)
+                    elif value_serializer_type is TaggedUint64Serializer:
+                        write_context.write_tagged_uint64(value)
                     else:
                         value_serializer.write(write_context, value)
                 chunk_size += 1
@@ -1092,14 +1142,40 @@ cdef class MapSerializer(Serializer):
                 else:
                     if key_serializer_type is StringSerializer:
                         key = read_context.read_string()
-                    elif key_serializer_type is Int64Serializer:
+                    elif key_serializer_type is Int64Serializer or 
key_serializer_type is Varint64Serializer:
                         key = read_context.read_varint64()
+                    elif key_serializer_type is FixedInt64Serializer:
+                        key = read_context.read_int64()
+                    elif key_serializer_type is TaggedInt64Serializer:
+                        key = read_context.read_tagged_int64()
                     elif key_serializer_type is Float64Serializer:
                         key = read_context.read_double()
-                    elif key_serializer_type is Int32Serializer:
+                    elif key_serializer_type is Int32Serializer or 
key_serializer_type is Varint32Serializer:
                         key = read_context.read_varint32()
+                    elif key_serializer_type is FixedInt32Serializer:
+                        key = read_context.read_int32()
                     elif key_serializer_type is Float32Serializer:
                         key = read_context.read_float()
+                    elif key_serializer_type is BooleanSerializer:
+                        key = read_context.read_bool()
+                    elif key_serializer_type is ByteSerializer:
+                        key = read_context.read_int8()
+                    elif key_serializer_type is Int16Serializer:
+                        key = read_context.read_int16()
+                    elif key_serializer_type is Uint8Serializer:
+                        key = read_context.read_uint8()
+                    elif key_serializer_type is Uint16Serializer:
+                        key = read_context.read_uint16()
+                    elif key_serializer_type is Uint32Serializer:
+                        key = read_context.read_uint32()
+                    elif key_serializer_type is VarUint32Serializer:
+                        key = read_context.read_var_uint32()
+                    elif key_serializer_type is Uint64Serializer:
+                        key = read_context.read_uint64()
+                    elif key_serializer_type is VarUint64Serializer:
+                        key = read_context.read_var_uint64()
+                    elif key_serializer_type is TaggedUint64Serializer:
+                        key = read_context.read_tagged_uint64()
                     else:
                         key = read_context.read_non_ref(key_serializer)
                 if track_value_ref:
@@ -1114,16 +1190,40 @@ cdef class MapSerializer(Serializer):
                 else:
                     if value_serializer_type is StringSerializer:
                         value = read_context.read_string()
-                    elif value_serializer_type is Int64Serializer:
+                    elif value_serializer_type is Int64Serializer or 
value_serializer_type is Varint64Serializer:
                         value = read_context.read_varint64()
+                    elif value_serializer_type is FixedInt64Serializer:
+                        value = read_context.read_int64()
+                    elif value_serializer_type is TaggedInt64Serializer:
+                        value = read_context.read_tagged_int64()
                     elif value_serializer_type is Float64Serializer:
                         value = read_context.read_double()
-                    elif value_serializer_type is Int32Serializer:
+                    elif value_serializer_type is Int32Serializer or 
value_serializer_type is Varint32Serializer:
                         value = read_context.read_varint32()
+                    elif value_serializer_type is FixedInt32Serializer:
+                        value = read_context.read_int32()
                     elif value_serializer_type is Float32Serializer:
                         value = read_context.read_float()
                     elif value_serializer_type is BooleanSerializer:
                         value = read_context.read_bool()
+                    elif value_serializer_type is ByteSerializer:
+                        value = read_context.read_int8()
+                    elif value_serializer_type is Int16Serializer:
+                        value = read_context.read_int16()
+                    elif value_serializer_type is Uint8Serializer:
+                        value = read_context.read_uint8()
+                    elif value_serializer_type is Uint16Serializer:
+                        value = read_context.read_uint16()
+                    elif value_serializer_type is Uint32Serializer:
+                        value = read_context.read_uint32()
+                    elif value_serializer_type is VarUint32Serializer:
+                        value = read_context.read_var_uint32()
+                    elif value_serializer_type is Uint64Serializer:
+                        value = read_context.read_uint64()
+                    elif value_serializer_type is VarUint64Serializer:
+                        value = read_context.read_var_uint64()
+                    elif value_serializer_type is TaggedUint64Serializer:
+                        value = read_context.read_tagged_uint64()
                     else:
                         value = read_context.read_non_ref(value_serializer)
                 map_[key] = value
diff --git a/python/pyfory/cpp/pyfory.cc b/python/pyfory/cpp/pyfory.cc
index 0f35f8a64..06b587db8 100644
--- a/python/pyfory/cpp/pyfory.cc
+++ b/python/pyfory/cpp/pyfory.cc
@@ -582,10 +582,19 @@ static bool can_use_list_sequence_fastpath(PyObject 
**items, Py_ssize_t size,
     }
     return true;
   case TypeId::VARINT64:
+  case TypeId::INT64:
+  case TypeId::TAGGED_INT64:
   case TypeId::VARINT32:
+  case TypeId::INT32:
+  case TypeId::VAR_UINT64:
+  case TypeId::UINT64:
+  case TypeId::TAGGED_UINT64:
+  case TypeId::VAR_UINT32:
+  case TypeId::UINT32:
+  case TypeId::UINT8:
   case TypeId::INT8:
+  case TypeId::UINT16:
   case TypeId::INT16:
-  case TypeId::INT32:
     for (Py_ssize_t i = 0; i < size; ++i) {
       if (!PyLong_CheckExact(items[i])) {
         return false;
@@ -599,6 +608,7 @@ static bool can_use_list_sequence_fastpath(PyObject 
**items, Py_ssize_t size,
       }
     }
     return true;
+  case TypeId::FLOAT32:
   case TypeId::FLOAT64:
     for (Py_ssize_t i = 0; i < size; ++i) {
       if (!PyFloat_CheckExact(items[i])) {
@@ -1086,9 +1096,13 @@ static int write_primitive_sequence(PyObject **items, 
Py_ssize_t size,
     return 0;
   }
   default:
-    PyErr_Format(PyExc_ValueError, "unsupported primitive fastpath type id: 
%u",
-                 static_cast<unsigned>(type_id));
-    return -1;
+    for (Py_ssize_t i = 0; i < size; ++i) {
+      if (FORY_PREDICT_FALSE(write_primitive_item(buffer, items[i], type_id) !=
+                             0)) {
+        return -1;
+      }
+    }
+    return 0;
   }
 }
 
@@ -1434,9 +1448,14 @@ static int read_primitive_sequence_indexed(Buffer 
*buffer, Py_ssize_t size,
     }
     return 0;
   default:
-    PyErr_Format(PyExc_ValueError, "unsupported primitive fastpath type id: 
%u",
-                 static_cast<unsigned>(type_id));
-    return -1;
+    for (Py_ssize_t i = 0; i < size; ++i) {
+      PyObject *item = read_primitive_item(buffer, type_id);
+      if (FORY_PREDICT_FALSE(item == nullptr)) {
+        return -1;
+      }
+      set_item(i, item);
+    }
+    return 0;
   }
 }
 
diff --git a/python/pyfory/cpp/pyfory.h b/python/pyfory/cpp/pyfory.h
index 3801929c9..619ad9b77 100644
--- a/python/pyfory/cpp/pyfory.h
+++ b/python/pyfory/cpp/pyfory.h
@@ -49,12 +49,22 @@ inline constexpr bool 
Fory_CanUsePrimitiveCollectionFastpath(uint8_t type_id) {
   switch (static_cast<TypeId>(type_id)) {
   case TypeId::STRING:
   case TypeId::VARINT64:
+  case TypeId::INT64:
+  case TypeId::TAGGED_INT64:
   case TypeId::VARINT32:
+  case TypeId::INT32:
+  case TypeId::VAR_UINT64:
+  case TypeId::UINT64:
+  case TypeId::TAGGED_UINT64:
+  case TypeId::VAR_UINT32:
+  case TypeId::UINT32:
   case TypeId::BOOL:
+  case TypeId::FLOAT32:
   case TypeId::FLOAT64:
+  case TypeId::UINT8:
   case TypeId::INT8:
+  case TypeId::UINT16:
   case TypeId::INT16:
-  case TypeId::INT32:
     return true;
   default:
     return false;
diff --git a/python/pyfory/meta/typedef.py b/python/pyfory/meta/typedef.py
index 759fbabdc..521cbf8de 100644
--- a/python/pyfory/meta/typedef.py
+++ b/python/pyfory/meta/typedef.py
@@ -71,16 +71,18 @@ class TypeDef:
         self.encoded = encoded
         self.is_compressed = is_compressed
 
-    def create_fields_serializer(self, resolver, resolved_field_names=None):
+    def create_fields_serializer(self, resolver, resolved_field_names=None, 
local_field_types=None):
         """Create serializers for each field.
 
         Args:
             resolver: The type resolver
             resolved_field_names: Optional list of resolved field names (for 
TAG_ID encoding).
                                   If None, uses field_info.name directly.
+            local_field_types: Optional precomputed local field type map.
         """
-        field_nullable = resolver.field_nullable
-        field_types = infer_field_types(self.cls, 
field_nullable=field_nullable)
+        field_types = local_field_types
+        if field_types is None:
+            field_types = infer_field_types(self.cls, 
field_nullable=resolver.field_nullable)
         serializers = []
         for i, field_info in enumerate(self.fields):
             # Use resolved name if provided, otherwise use original name
@@ -155,33 +157,56 @@ class TypeDef:
             return resolver.get_type_info_by_name(self.namespace, 
self.typename).serializer
 
         from pyfory.struct import DataClassSerializer
+        from pyfory.struct import FieldInfo as StructFieldInfo
+        from pyfory.type_util import get_type_hints, unwrap_optional
 
         # Resolve actual field names from TAG_ID encoding if needed
         field_names = self._resolve_field_names_from_tag_ids()
 
-        # Build nullable_fields using resolved field names
-        nullable_fields = {}
+        local_field_infos = build_field_infos(resolver, self.cls)
+        local_infos_by_name = {field_info.name: field_info for field_info in 
local_field_infos}
+        local_infos_by_tag = {field_info.tag_id: field_info for field_info in 
local_field_infos if field_info.tag_id >= 0}
+        local_field_types = infer_field_types(self.cls, 
field_nullable=resolver.field_nullable)
+        type_hints = get_type_hints(self.cls)
+        serializers = self.create_fields_serializer(resolver, field_names, 
local_field_types)
+        runtime_field_infos = []
         for i, field_info in enumerate(self.fields):
             resolved_name = field_names[i]
-            nullable_fields[resolved_name] = field_info.field_type.is_nullable
-
-        dynamic_fields = {}
-        ref_fields = {}
-        for i, field_info in enumerate(self.fields):
-            resolved_name = field_names[i]
-            type_id = field_info.field_type.type_id
-            ref_fields[resolved_name] = field_info.field_type.is_tracking_ref
-            if is_polymorphic_type(type_id):
-                dynamic_fields[resolved_name] = True
+            local_info = None
+            if field_info.tag_id >= 0:
+                local_info = local_infos_by_tag.get(field_info.tag_id)
+            if local_info is None:
+                local_info = local_infos_by_name.get(resolved_name)
+            can_assign, validation_field_type = plan_field_assignment(
+                field_info.field_type,
+                local_info.field_type if local_info is not None else None,
+            )
+            type_hint = type_hints.get(resolved_name, typing.Any)
+            unwrapped_type, _ = unwrap_optional(type_hint, 
field_nullable=resolver.field_nullable)
+            runtime_field_infos.append(
+                StructFieldInfo(
+                    name=resolved_name,
+                    index=i,
+                    type_hint=type_hint,
+                    tag_id=field_info.tag_id,
+                    nullable=field_info.field_type.is_nullable,
+                    ref=field_info.field_type.is_tracking_ref,
+                    dynamic=is_polymorphic_type(field_info.field_type.type_id),
+                    runtime_ref_tracking=field_info.field_type.is_tracking_ref,
+                    type_id=field_info.field_type.type_id,
+                    serializer=serializers[i],
+                    unwrapped_type=unwrapped_type,
+                    field_type=field_info.field_type,
+                    assign=can_assign,
+                    validation_field_type=validation_field_type,
+                )
+            )
 
         return DataClassSerializer(
             resolver,
             self.cls,
-            field_names=field_names,
-            serializers=self.create_fields_serializer(resolver, field_names),
-            nullable_fields=nullable_fields,
-            dynamic_fields=dynamic_fields,
-            ref_fields=ref_fields,
+            field_infos=runtime_field_infos,
+            fields_from_typedef=True,
         )
 
     def __repr__(self):
@@ -442,12 +467,126 @@ class DynamicFieldType(FieldType):
             "Union fields don't write field type info, \
             they are not dynamic field types"
         )
+        if self.type_id != TypeId.UNKNOWN:
+            return FieldType.create_serializer(self, resolver, type_)
         return None
 
     def __repr__(self):
         return f"DynamicFieldType(type_id={self.type_id}, 
is_monomorphic={self.is_monomorphic}, is_nullable={self.is_nullable}, 
is_tracking_ref={self.is_tracking_ref})"
 
 
+_SIGNED_INT32_TYPE_IDS = frozenset((TypeId.INT32, TypeId.VARINT32))
+_SIGNED_INT64_TYPE_IDS = frozenset((TypeId.INT64, TypeId.VARINT64, 
TypeId.TAGGED_INT64))
+_UNSIGNED_INT32_TYPE_IDS = frozenset((TypeId.UINT32, TypeId.VAR_UINT32))
+_UNSIGNED_INT64_TYPE_IDS = frozenset((TypeId.UINT64, TypeId.VAR_UINT64, 
TypeId.TAGGED_UINT64))
+_INT_TYPE_DOMAINS = {type_id: (True, 32) for type_id in _SIGNED_INT32_TYPE_IDS}
+_INT_TYPE_DOMAINS.update({type_id: (True, 64) for type_id in 
_SIGNED_INT64_TYPE_IDS})
+_INT_TYPE_DOMAINS.update({type_id: (False, 32) for type_id in 
_UNSIGNED_INT32_TYPE_IDS})
+_INT_TYPE_DOMAINS.update({type_id: (False, 64) for type_id in 
_UNSIGNED_INT64_TYPE_IDS})
+_INT_RANGES = {
+    (True, 32): (-(1 << 31), (1 << 31) - 1),
+    (True, 64): (-(1 << 63), (1 << 63) - 1),
+    (False, 32): (0, (1 << 32) - 1),
+    (False, 64): (0, (1 << 64) - 1),
+}
+
+
+def _requires_nullable_validation(remote_field_type: FieldType, 
local_field_type: FieldType) -> bool:
+    return remote_field_type.is_nullable and not local_field_type.is_nullable
+
+
+def _field_type_assignment(remote_field_type: FieldType, local_field_type: 
FieldType) -> typing.Tuple[bool, bool]:
+    if local_field_type is None:
+        return False, False
+    needs_validation = _requires_nullable_validation(remote_field_type, 
local_field_type)
+    remote_type_id = remote_field_type.type_id
+    local_type_id = local_field_type.type_id
+    if local_type_id == TypeId.UNKNOWN:
+        return True, needs_validation
+    if remote_type_id == TypeId.UNKNOWN:
+        return True, True
+    if remote_type_id in (TypeId.LIST, TypeId.SET):
+        if local_type_id != remote_type_id:
+            return False, False
+        child_assignable, child_needs_validation = _field_type_assignment(
+            remote_field_type.element_type,
+            local_field_type.element_type,
+        )
+        return child_assignable, needs_validation or child_needs_validation
+    if remote_type_id == TypeId.MAP:
+        if local_type_id != TypeId.MAP:
+            return False, False
+        key_assignable, key_needs_validation = _field_type_assignment(
+            remote_field_type.key_type,
+            local_field_type.key_type,
+        )
+        value_assignable, value_needs_validation = _field_type_assignment(
+            remote_field_type.value_type,
+            local_field_type.value_type,
+        )
+        return key_assignable and value_assignable, needs_validation or 
key_needs_validation or value_needs_validation
+    remote_int_domain = _INT_TYPE_DOMAINS.get(remote_type_id)
+    local_int_domain = _INT_TYPE_DOMAINS.get(local_type_id)
+    if remote_int_domain is not None or local_int_domain is not None:
+        if remote_int_domain is None or local_int_domain is None:
+            return False, False
+        remote_signed, remote_width = remote_int_domain
+        local_signed, local_width = local_int_domain
+        if remote_signed != local_signed:
+            return False, False
+        return True, needs_validation or remote_width > local_width
+    if remote_type_id == local_type_id:
+        return True, needs_validation
+    return False, False
+
+
+def plan_field_assignment(
+    remote_field_type: FieldType, local_field_type: typing.Optional[FieldType]
+) -> typing.Tuple[bool, typing.Optional[FieldType]]:
+    assignable, needs_validation = _field_type_assignment(remote_field_type, 
local_field_type)
+    if not assignable:
+        return False, None
+    return True, local_field_type if needs_validation else None
+
+
+def _validate_int_value(value, type_id: int) -> bool:
+    domain = _INT_TYPE_DOMAINS.get(type_id)
+    if domain is None:
+        return True
+    if type(value) is not int:
+        return False
+    min_value, max_value = _INT_RANGES[domain]
+    return min_value <= value <= max_value
+
+
+def is_value_assignable(value, local_field_type: FieldType) -> bool:
+    if value is None:
+        return local_field_type.is_nullable
+    type_id = local_field_type.type_id
+    if type_id == TypeId.UNKNOWN:
+        return True
+    if type_id in (TypeId.LIST, TypeId.SET):
+        if not isinstance(value, (list, tuple, set)):
+            return False
+        return all(is_value_assignable(element, local_field_type.element_type) 
for element in value)
+    if type_id == TypeId.MAP:
+        if not isinstance(value, dict):
+            return False
+        return all(
+            is_value_assignable(key, local_field_type.key_type) and 
is_value_assignable(map_value, local_field_type.value_type)
+            for key, map_value in value.items()
+        )
+    if type_id in _INT_TYPE_DOMAINS:
+        return _validate_int_value(value, type_id)
+    if type_id == TypeId.BOOL:
+        return type(value) is bool
+    if type_id in (TypeId.FLOAT32, TypeId.FLOAT64):
+        return type(value) is float
+    if type_id == TypeId.STRING:
+        return type(value) is str
+    return True
+
+
 def build_field_infos(type_resolver, cls):
     """Build field information for the class.
 
@@ -511,7 +650,7 @@ def build_field_infos(type_resolver, cls):
         field_info = FieldInfo(field_name, field_type, cls.__name__, tag_id)
         field_infos.append(field_info)
 
-    field_types = infer_field_types(cls)
+    field_types = infer_field_types(cls, field_nullable=field_nullable)
     serializers = [field_info.field_type.create_serializer(type_resolver, 
field_types.get(field_info.name, None)) for field_info in field_infos]
 
     # Get just the field names for sorting
@@ -559,6 +698,7 @@ def build_field_type_from_type_ids_with_ref(
 ):
     """Build field type from type IDs with explicit ref tracking control."""
     from pyfory.type_util import unwrap_ref
+    from pyfory.type_util import unwrap_optional
 
     type_id = type_ids[0]
     if type_id is None:
@@ -571,6 +711,7 @@ def build_field_type_from_type_ids_with_ref(
     morphic = not is_polymorphic_type(type_id)
     if type_id in [TypeId.SET, TypeId.LIST]:
         elem_hint = None
+        elem_nullable = False
         elem_ref_override = None
         if type_hint is not None:
             origin = typing.get_origin(type_hint) if hasattr(typing, 
"get_origin") else getattr(type_hint, "__origin__", None)
@@ -578,19 +719,21 @@ def build_field_type_from_type_ids_with_ref(
                 args = typing.get_args(type_hint) if hasattr(typing, 
"get_args") else getattr(type_hint, "__args__", ())
                 if args:
                     elem_hint, elem_ref_override = unwrap_ref(args[0])
+                    elem_hint, elem_nullable = unwrap_optional(elem_hint)
             elif origin in (tuple, typing.Tuple):
                 tuple_elem_hint = get_homogeneous_tuple_elem_type(type_hint)
                 if tuple_elem_hint is not None:
                     elem_hint, elem_ref_override = unwrap_ref(tuple_elem_hint)
+                    elem_hint, elem_nullable = unwrap_optional(elem_hint)
         elem_tracking_ref = is_tracking_ref
         if elem_ref_override is not None:
-            elem_tracking_ref = elem_ref_override and is_tracking_ref
+            elem_tracking_ref = elem_ref_override and type_resolver.track_ref
         elem_type = build_field_type_from_type_ids_with_ref(
             type_resolver,
             field_name,
             type_ids[1],
             visitor,
-            is_nullable=False,
+            is_nullable=elem_nullable,
             is_tracking_ref=elem_tracking_ref,
             type_hint=elem_hint,
         )
@@ -600,6 +743,8 @@ def build_field_type_from_type_ids_with_ref(
     elif type_id == TypeId.MAP:
         key_hint = None
         value_hint = None
+        key_nullable = False
+        value_nullable = False
         key_ref_override = None
         value_ref_override = None
         if type_hint is not None:
@@ -608,19 +753,21 @@ def build_field_type_from_type_ids_with_ref(
                 args = typing.get_args(type_hint) if hasattr(typing, 
"get_args") else getattr(type_hint, "__args__", ())
                 if len(args) >= 2:
                     key_hint, key_ref_override = unwrap_ref(args[0])
+                    key_hint, key_nullable = unwrap_optional(key_hint)
                     value_hint, value_ref_override = unwrap_ref(args[1])
+                    value_hint, value_nullable = unwrap_optional(value_hint)
         key_tracking_ref = is_tracking_ref
         if key_ref_override is not None:
-            key_tracking_ref = key_ref_override and is_tracking_ref
+            key_tracking_ref = key_ref_override and type_resolver.track_ref
         value_tracking_ref = is_tracking_ref
         if value_ref_override is not None:
-            value_tracking_ref = value_ref_override and is_tracking_ref
+            value_tracking_ref = value_ref_override and type_resolver.track_ref
         key_type = build_field_type_from_type_ids_with_ref(
             type_resolver,
             field_name,
             type_ids[1],
             visitor,
-            is_nullable=False,
+            is_nullable=key_nullable,
             is_tracking_ref=key_tracking_ref,
             type_hint=key_hint,
         )
@@ -629,7 +776,7 @@ def build_field_type_from_type_ids_with_ref(
             field_name,
             type_ids[2],
             visitor,
-            is_nullable=False,
+            is_nullable=value_nullable,
             is_tracking_ref=value_tracking_ref,
             type_hint=value_hint,
         )
@@ -664,6 +811,8 @@ def build_field_type(type_resolver, field_name: str, 
type_hint, visitor, is_null
 
 
 def build_field_type_from_type_ids(type_resolver, field_name: str, type_ids, 
visitor, is_nullable=False, type_hint=None):
+    from pyfory.type_util import unwrap_optional, unwrap_ref
+
     tracking_ref = type_resolver.track_ref
     type_id = type_ids[0]
     if type_id is None:
@@ -675,11 +824,59 @@ def build_field_type_from_type_ids(type_resolver, 
field_name: str, type_ids, vis
     assert type_id >= 0, f"Unknown type: {type_id} for field: {field_name}"
     morphic = not is_polymorphic_type(type_id)
     if type_id in [TypeId.SET, TypeId.LIST]:
-        elem_type = build_field_type_from_type_ids(type_resolver, field_name, 
type_ids[1], visitor, is_nullable=False)
+        elem_hint = None
+        elem_nullable = False
+        if type_hint is not None:
+            origin = typing.get_origin(type_hint) if hasattr(typing, 
"get_origin") else getattr(type_hint, "__origin__", None)
+            if origin in (list, typing.List, set, typing.Set):
+                args = typing.get_args(type_hint) if hasattr(typing, 
"get_args") else getattr(type_hint, "__args__", ())
+                if args:
+                    elem_hint, _ = unwrap_ref(args[0])
+                    elem_hint, elem_nullable = unwrap_optional(elem_hint)
+            elif origin in (tuple, typing.Tuple):
+                tuple_elem_hint = get_homogeneous_tuple_elem_type(type_hint)
+                if tuple_elem_hint is not None:
+                    elem_hint, _ = unwrap_ref(tuple_elem_hint)
+                    elem_hint, elem_nullable = unwrap_optional(elem_hint)
+        elem_type = build_field_type_from_type_ids(
+            type_resolver,
+            field_name,
+            type_ids[1],
+            visitor,
+            is_nullable=elem_nullable,
+            type_hint=elem_hint,
+        )
         return CollectionFieldType(type_id, morphic, is_nullable, 
tracking_ref, elem_type)
     elif type_id == TypeId.MAP:
-        key_type = build_field_type_from_type_ids(type_resolver, field_name, 
type_ids[1], visitor, is_nullable=False)
-        value_type = build_field_type_from_type_ids(type_resolver, field_name, 
type_ids[2], visitor, is_nullable=False)
+        key_hint = None
+        value_hint = None
+        key_nullable = False
+        value_nullable = False
+        if type_hint is not None:
+            origin = typing.get_origin(type_hint) if hasattr(typing, 
"get_origin") else getattr(type_hint, "__origin__", None)
+            if origin in (dict, typing.Dict):
+                args = typing.get_args(type_hint) if hasattr(typing, 
"get_args") else getattr(type_hint, "__args__", ())
+                if len(args) >= 2:
+                    key_hint, _ = unwrap_ref(args[0])
+                    key_hint, key_nullable = unwrap_optional(key_hint)
+                    value_hint, _ = unwrap_ref(args[1])
+                    value_hint, value_nullable = unwrap_optional(value_hint)
+        key_type = build_field_type_from_type_ids(
+            type_resolver,
+            field_name,
+            type_ids[1],
+            visitor,
+            is_nullable=key_nullable,
+            type_hint=key_hint,
+        )
+        value_type = build_field_type_from_type_ids(
+            type_resolver,
+            field_name,
+            type_ids[2],
+            visitor,
+            is_nullable=value_nullable,
+            type_hint=value_hint,
+        )
         return MapFieldType(type_id, morphic, is_nullable, tracking_ref, 
key_type, value_type)
     elif type_id in [
         TypeId.UNKNOWN,
diff --git a/python/pyfory/struct.pxi b/python/pyfory/struct.pxi
index 7b4ee4259..129a438f0 100644
--- a/python/pyfory/struct.pxi
+++ b/python/pyfory/struct.pxi
@@ -30,8 +30,10 @@ cdef struct FieldRuntimeInfo:
     uint8_t track_ref
     uint8_t is_dynamic
     uint8_t field_exists
+    uint8_t assign
     PyObject *field_name
     PyObject *serializer
+    PyObject *validation_field_type
 
 
 @cython.final
@@ -42,6 +44,7 @@ cdef class DataClassSerializer(Serializer):
     cdef public bint _has_slots
     cdef public bint _fields_from_typedef
     cdef public bint _has_missing_fields
+    cdef bint _has_validation_fields
     cdef public list _field_names
     cdef public list _serializers
     cdef public dict _nullable_fields
@@ -53,8 +56,12 @@ cdef class DataClassSerializer(Serializer):
     cdef public int32_t _hash
     cdef public tuple _field_name_interned
     cdef tuple _serializer_owner
+    cdef tuple _validation_field_type_owner
     cdef public dict _default_values_factory
     cdef tuple _missing_field_defaults
+    cdef public object _assign_fields
+    cdef public object _assigned_field_names
+    cdef public object _value_assignable_checker
     cdef vector[FieldRuntimeInfo] _field_runtime_infos
 
     def __init__(
@@ -66,6 +73,8 @@ cdef class DataClassSerializer(Serializer):
         nullable_fields: dict = None,
         dynamic_fields: dict = None,
         ref_fields: dict = None,
+        field_infos: list = None,
+        fields_from_typedef: bint = False,
     ):
         super().__init__(type_resolver, clz)
 
@@ -83,8 +92,16 @@ cdef class DataClassSerializer(Serializer):
         self._type_hints = get_type_hints(clz)
         self._has_slots = hasattr(clz, "__slots__")
 
-        self._fields_from_typedef = field_names is not None and serializers is 
not None
-        if self._fields_from_typedef:
+        self._fields_from_typedef = fields_from_typedef or (field_names is not 
None and serializers is not None)
+        if field_infos is not None:
+            self._field_infos = list(field_infos)
+            self._field_names = [fi.name for fi in self._field_infos]
+            self._serializers = [fi.serializer for fi in self._field_infos]
+            self._nullable_fields = {fi.name: fi.nullable for fi in 
self._field_infos}
+            self._ref_fields = {fi.name: fi.runtime_ref_tracking for fi in 
self._field_infos}
+            self._dynamic_fields = {fi.name: fi.dynamic for fi in 
self._field_infos}
+            self._field_metas = {}
+        elif self._fields_from_typedef:
             self._field_names = list(field_names)
             self._serializers = list(serializers)
             self._nullable_fields = dict(nullable_fields) if nullable_fields 
is not None else {}
@@ -157,6 +174,13 @@ cdef class DataClassSerializer(Serializer):
         self._build_fastpath_metadata()
         self._build_missing_field_defaults()
 
+        if self._has_validation_fields:
+            from pyfory.meta.typedef import is_value_assignable
+
+            self._value_assignable_checker = is_value_assignable
+        else:
+            self._value_assignable_checker = None
+
     cdef object _intern_field_name(self, str name):
         cdef bytes encoded = name.encode("utf-8")
         cdef const char *ptr = encoded
@@ -233,12 +257,22 @@ cdef class DataClassSerializer(Serializer):
         cdef bint is_dynamic
         cdef bint is_nullable
         cdef bint is_tracking_ref
+        cdef bint assign
         cdef FieldRuntimeInfo runtime_info
+        cdef list validation_field_types
+        cdef object validation_field_type
 
         self._field_runtime_infos.clear()
         self._has_missing_fields = False
+        self._has_validation_fields = False
         current_fields = set(self._get_field_names(self.type_))
         self._field_runtime_infos.reserve(len(self._field_names))
+        self._assign_fields = [
+            bool(getattr(self._field_infos[i], "assign", True)) if i < 
len(self._field_infos) else True
+            for i in range(len(self._field_names))
+        ]
+        self._assigned_field_names = set()
+        validation_field_types = []
 
         for i in range(len(self._field_names)):
             field_name = self._field_names[i]
@@ -246,16 +280,30 @@ cdef class DataClassSerializer(Serializer):
             is_nullable = bool(self._nullable_fields.get(field_name, False))
             is_tracking_ref = bool(self._ref_fields.get(field_name, False))
             is_dynamic = bool(self._dynamic_fields.get(field_name, False))
+            assign = bool(self._assign_fields[i])
+            validation_field_type = (
+                getattr(self._field_infos[i], "validation_field_type", None)
+                if i < len(self._field_infos)
+                else None
+            )
+            validation_field_types.append(validation_field_type)
+            if validation_field_type is not None:
+                self._has_validation_fields = True
             runtime_info.basic_type_id = 
self._resolve_basic_type_id(serializer, is_dynamic)
             runtime_info.is_nullable = 1 if is_nullable else 0
             runtime_info.track_ref = 1 if is_tracking_ref else 0
             runtime_info.is_dynamic = 1 if is_dynamic else 0
             runtime_info.field_exists = 1 if field_name in current_fields else 0
-            if runtime_info.field_exists == 0:
+            runtime_info.assign = 1 if assign else 0
+            if runtime_info.field_exists == 0 or runtime_info.assign == 0:
                 self._has_missing_fields = True
+            if runtime_info.field_exists != 0 and runtime_info.assign != 0:
+                self._assigned_field_names.add(field_name)
             runtime_info.field_name = <PyObject *>self._field_name_interned[i]
             runtime_info.serializer = <PyObject *>serializer
+            runtime_info.validation_field_type = <PyObject 
*>validation_field_type if validation_field_type is not None else NULL
             self._field_runtime_infos.push_back(runtime_info)
+        self._validation_field_type_owner = tuple(validation_field_types)
 
     cdef void _build_missing_field_defaults(self):
         cdef object missing_fields
@@ -267,7 +315,7 @@ cdef class DataClassSerializer(Serializer):
         if not self.type_resolver.compatible or not 
self._default_values_factory:
             return
 
-        missing_fields = set(self._get_field_names(self.type_)) - 
set(self._field_names)
+        missing_fields = set(self._get_field_names(self.type_)) - 
self._assigned_field_names
         if not missing_fields:
             return
 
@@ -389,21 +437,45 @@ cdef class DataClassSerializer(Serializer):
         cdef FieldRuntimeInfo *field_info
 
         if not self._has_missing_fields:
+            if not self._has_validation_fields:
+                for i in range(field_count):
+                    field_info = &self._field_runtime_infos[i]
+                    field_value = self._read_field_value(read_context, 
field_info)
+                    field_name = <object>field_info.field_name
+                    obj_dict[field_name] = field_value
+                return
             for i in range(field_count):
                 field_info = &self._field_runtime_infos[i]
                 field_value = self._read_field_value(read_context, field_info)
                 field_name = <object>field_info.field_name
+                if field_info.validation_field_type == NULL:
+                    obj_dict[field_name] = field_value
+                else:
+                    obj_dict[field_name] = 
self._validate_or_default(field_name, field_value, field_info)
+            return
+
+        if not self._has_validation_fields:
+            for i in range(field_count):
+                field_info = &self._field_runtime_infos[i]
+                if field_info.field_exists == 0 or field_info.assign == 0:
+                    self._read_missing_field_value(read_context, field_info)
+                    continue
+                field_value = self._read_field_value(read_context, field_info)
+                field_name = <object>field_info.field_name
                 obj_dict[field_name] = field_value
             return
 
         for i in range(field_count):
             field_info = &self._field_runtime_infos[i]
-            if field_info.field_exists == 0:
+            if field_info.field_exists == 0 or field_info.assign == 0:
                 self._read_missing_field_value(read_context, field_info)
                 continue
             field_value = self._read_field_value(read_context, field_info)
             field_name = <object>field_info.field_name
-            obj_dict[field_name] = field_value
+            if field_info.validation_field_type == NULL:
+                obj_dict[field_name] = field_value
+            else:
+                obj_dict[field_name] = self._validate_or_default(field_name, 
field_value, field_info)
 
     cdef inline void _read_slots(self, ReadContext read_context, object obj):
         cdef Py_ssize_t i
@@ -413,21 +485,53 @@ cdef class DataClassSerializer(Serializer):
         cdef FieldRuntimeInfo *field_info
 
         if not self._has_missing_fields:
+            if not self._has_validation_fields:
+                for i in range(field_count):
+                    field_info = &self._field_runtime_infos[i]
+                    field_value = self._read_field_value(read_context, 
field_info)
+                    field_name = <object>field_info.field_name
+                    PyObject_SetAttr(obj, field_name, field_value)
+                return
             for i in range(field_count):
                 field_info = &self._field_runtime_infos[i]
                 field_value = self._read_field_value(read_context, field_info)
                 field_name = <object>field_info.field_name
+                if field_info.validation_field_type == NULL:
+                    PyObject_SetAttr(obj, field_name, field_value)
+                else:
+                    PyObject_SetAttr(
+                        obj,
+                        field_name,
+                        self._validate_or_default(field_name, field_value, 
field_info),
+                    )
+            return
+
+        if not self._has_validation_fields:
+            for i in range(field_count):
+                field_info = &self._field_runtime_infos[i]
+                if field_info.field_exists == 0 or field_info.assign == 0:
+                    self._read_missing_field_value(read_context, field_info)
+                    continue
+                field_value = self._read_field_value(read_context, field_info)
+                field_name = <object>field_info.field_name
                 PyObject_SetAttr(obj, field_name, field_value)
             return
 
         for i in range(field_count):
             field_info = &self._field_runtime_infos[i]
-            if field_info.field_exists == 0:
+            if field_info.field_exists == 0 or field_info.assign == 0:
                 self._read_missing_field_value(read_context, field_info)
                 continue
             field_value = self._read_field_value(read_context, field_info)
             field_name = <object>field_info.field_name
-            PyObject_SetAttr(obj, field_name, field_value)
+            if field_info.validation_field_type == NULL:
+                PyObject_SetAttr(obj, field_name, field_value)
+            else:
+                PyObject_SetAttr(
+                    obj,
+                    field_name,
+                    self._validate_or_default(field_name, field_value, 
field_info),
+                )
 
     cdef inline object _read_missing_field_value(self, ReadContext 
read_context, FieldRuntimeInfo *field_info):
         cdef object resolver = self.type_resolver.resolver
@@ -461,6 +565,18 @@ cdef class DataClassSerializer(Serializer):
             return read_context.read_no_ref()
         return read_context.read_non_ref(serializer)
 
+    cdef inline object _default_field_value(self, object field_name):
+        cdef object default_factory = 
self._default_values_factory.get(field_name)
+        if default_factory is None:
+            return None
+        return default_factory()
+
+    cdef inline object _validate_or_default(self, object field_name, object 
field_value, FieldRuntimeInfo *field_info):
+        if field_info.validation_field_type != NULL:
+            if not self._value_assignable_checker(field_value, 
<object>field_info.validation_field_type):
+                return self._default_field_value(field_name)
+        return field_value
+
     cdef inline void _apply_missing_defaults_dict(self, dict obj_dict):
         cdef object field_name
         cdef object default_factory
diff --git a/python/pyfory/struct.py b/python/pyfory/struct.py
index 857a2e3a2..14572e36e 100644
--- a/python/pyfory/struct.py
+++ b/python/pyfory/struct.py
@@ -60,6 +60,7 @@ from pyfory.type_util import (
     _get_args,
     _get_origin,
     infer_field,
+    infer_field_types,
     get_homogeneous_tuple_elem_type,
     is_subclass,
     get_type_hints,
@@ -138,6 +139,9 @@ class FieldInfo:
     type_id: int  # Fory TypeId
     serializer: Serializer  # Field serializer
     unwrapped_type: type  # Type with Optional unwrapped
+    field_type: object = None  # Recursive TypeDef schema node for this field
+    assign: bool = True  # Whether a remote field value may be assigned locally
+    validation_field_type: object = None  # Local schema used for 
compatible-read validation
 
 
 def _is_abstract_type(type_hint: type) -> bool:
@@ -234,7 +238,10 @@ def _extract_field_infos(
 
     # Build FieldInfo list
     field_infos: List[FieldInfo] = []
-    visitor = StructFieldSerializerVisitor(fory)
+    from pyfory.meta.typedef import build_field_type_with_ref
+
+    visitor = StructTypeIdVisitor(fory, clz)
+    field_types = infer_field_types(clz, field_nullable=global_field_nullable)
     global_ref_tracking = fory.track_ref
 
     for index, (field_name, dc_field) in enumerate(active_fields):
@@ -248,21 +255,28 @@ def _extract_field_infos(
         # Compute runtime ref tracking: field.ref AND global config
         runtime_ref = meta.ref and global_ref_tracking
 
-        # Infer serializer
-        serializer = infer_field(field_name, unwrapped_type, visitor, 
types_path=[])
+        field_type = build_field_type_with_ref(
+            fory,
+            field_name,
+            unwrapped_type,
+            visitor,
+            effective_nullable,
+            runtime_ref,
+        )
+        serializer = field_type.create_serializer(fory, 
field_types.get(field_name, None))
 
-        # Get type_id from serializer
-        if serializer is not None:
-            type_id = fory.get_type_info(serializer.type_).type_id
-        else:
-            type_id = TypeId.UNKNOWN
+        type_id = field_type.type_id
 
         # Compute effective dynamic based on type.
         # - Abstract classes: always True (type info must be written)
-        # - If explicitly set (not None): use that value
+        # - Compatible ref-tracked struct fields carry type info for xlang
+        #   parity with other runtimes
+        # - If explicitly set (not None): use that value for non-ref fields
         # - Otherwise: write type info for polymorphic types that are not 
registered by id
         is_abstract = _is_abstract_type(unwrapped_type)
-        if is_abstract:
+        if fory.compatible and runtime_ref and is_polymorphic_type(type_id):
+            effective_dynamic = True
+        elif is_abstract:
             # Abstract classes always need type info
             effective_dynamic = True
         elif meta.dynamic is not None:
@@ -284,6 +298,7 @@ def _extract_field_infos(
             type_id=type_id,
             serializer=serializer,
             unwrapped_type=unwrapped_type,
+            field_type=field_type,
         )
         field_infos.append(field_info)
 
@@ -367,14 +382,24 @@ class DataClassSerializer(Serializer):
         nullable_fields: Dict[str, bool] = None,
         dynamic_fields: Dict[str, bool] = None,
         ref_fields: Dict[str, bool] = None,
+        field_infos: List[FieldInfo] = None,
+        fields_from_typedef: bool = False,
     ):
         super().__init__(type_resolver, clz)
 
         self._type_hints = get_type_hints(clz)
         self._has_slots = hasattr(clz, "__slots__")
 
-        self._fields_from_typedef = field_names is not None and serializers is 
not None
-        if self._fields_from_typedef:
+        self._fields_from_typedef = fields_from_typedef or (field_names is not 
None and serializers is not None)
+        if field_infos is not None:
+            self._field_infos = list(field_infos)
+            self._field_names = [fi.name for fi in self._field_infos]
+            self._serializers = [fi.serializer for fi in self._field_infos]
+            self._nullable_fields = {fi.name: fi.nullable for fi in 
self._field_infos}
+            self._ref_fields = {fi.name: fi.runtime_ref_tracking for fi in 
self._field_infos}
+            self._dynamic_fields = {fi.name: fi.dynamic for fi in 
self._field_infos}
+            self._field_metas = {}
+        elif self._fields_from_typedef:
             self._field_names = list(field_names)
             self._serializers = list(serializers)
             self._nullable_fields = nullable_fields or {}
@@ -426,7 +451,23 @@ class DataClassSerializer(Serializer):
 
         self._field_name_interned = {name: sys.intern(name) for name in 
self._field_names}
         self._current_class_field_names = 
set(self._get_field_names(self.type_))
-        self._has_missing_fields = any(field_name not in 
self._current_class_field_names for field_name in self._field_names)
+        self._assign_fields = [
+            bool(getattr(self._field_infos[index], "assign", True)) if index < 
len(self._field_infos) else True
+            for index in range(len(self._field_names))
+        ]
+        self._validation_field_types = [
+            getattr(self._field_infos[index], "validation_field_type", None) 
if index < len(self._field_infos) else None
+            for index in range(len(self._field_names))
+        ]
+        self._has_validation_fields = any(field_type is not None for 
field_type in self._validation_field_types)
+        self._assigned_field_names = {
+            field_name
+            for index, field_name in enumerate(self._field_names)
+            if self._assign_fields[index] and field_name in 
self._current_class_field_names
+        }
+        self._has_missing_fields = any(
+            field_name not in self._current_class_field_names or not 
self._assign_fields[index] for index, field_name in enumerate(self._field_names)
+        )
         self._default_values_factory = (
             build_default_values_factory(self.type_resolver, self._type_hints, 
dataclasses.fields(self.type_))
             if dataclasses.is_dataclass(self.type_)
@@ -456,7 +497,7 @@ class DataClassSerializer(Serializer):
     def _build_missing_field_defaults(self):
         if not self.type_resolver.compatible or not 
self._default_values_factory:
             return []
-        missing_fields = self._current_class_field_names - 
set(self._field_names)
+        missing_fields = self._current_class_field_names - 
self._assigned_field_names
         if not missing_fields:
             return []
         return [(field_name, default_factory) for field_name, default_factory 
in self._default_values_factory.items() if field_name in missing_fields]
@@ -500,6 +541,24 @@ class DataClassSerializer(Serializer):
             return read_context.read_no_ref()
         return read_context.read_no_ref(serializer=serializer)
 
+    def _default_field_value(self, field_name):
+        default_factory = self._default_values_factory.get(field_name)
+        if default_factory is None:
+            return None
+        return default_factory()
+
+    def _assign_read_field_value(self, obj, obj_dict, field_name, field_value, 
validation_field_type):
+        if validation_field_type is not None:
+            from pyfory.meta.typedef import is_value_assignable
+
+            if not is_value_assignable(field_value, validation_field_type):
+                field_value = self._default_field_value(field_name)
+        interned_name = self._field_name_interned[field_name]
+        if obj_dict is not None:
+            obj_dict[interned_name] = field_value
+        else:
+            setattr(obj, interned_name, field_value)
+
     def write(self, write_context: Buffer, value):
         if not self.type_resolver.compatible:
             write_context.write_int32(self._hash)
@@ -567,28 +626,50 @@ class DataClassSerializer(Serializer):
                 is_dynamic = self._dynamic_fields.get(field_name, False)
                 is_tracking_ref = self._ref_fields.get(field_name, False)
                 is_basic = self._basic_field_flags[index]
-                if field_name not in self._current_class_field_names:
+                if field_name not in self._current_class_field_names or not 
self._assign_fields[index]:
                     self._read_missing_field_value(read_context, serializer, 
is_nullable, is_dynamic, is_basic, is_tracking_ref)
                     continue
                 field_value = self._read_field_value(read_context, serializer, 
is_nullable, is_dynamic, is_basic, is_tracking_ref)
-                interned_name = self._field_name_interned[field_name]
-                if obj_dict is not None:
-                    obj_dict[interned_name] = field_value
+                validation_field_type = self._validation_field_types[index]
+                if validation_field_type is None:
+                    interned_name = self._field_name_interned[field_name]
+                    if obj_dict is not None:
+                        obj_dict[interned_name] = field_value
+                    else:
+                        setattr(obj, interned_name, field_value)
                 else:
-                    setattr(obj, interned_name, field_value)
+                    self._assign_read_field_value(obj, obj_dict, field_name, 
field_value, validation_field_type)
         else:
-            for index, field_name in enumerate(self._field_names):
-                serializer = self._serializers[index]
-                is_nullable = self._nullable_fields.get(field_name, False)
-                is_dynamic = self._dynamic_fields.get(field_name, False)
-                is_tracking_ref = self._ref_fields.get(field_name, False)
-                is_basic = self._basic_field_flags[index]
-                field_value = self._read_field_value(read_context, serializer, 
is_nullable, is_dynamic, is_basic, is_tracking_ref)
-                interned_name = self._field_name_interned[field_name]
-                if obj_dict is not None:
-                    obj_dict[interned_name] = field_value
-                else:
-                    setattr(obj, interned_name, field_value)
+            if not self._has_validation_fields:
+                for index, field_name in enumerate(self._field_names):
+                    serializer = self._serializers[index]
+                    is_nullable = self._nullable_fields.get(field_name, False)
+                    is_dynamic = self._dynamic_fields.get(field_name, False)
+                    is_tracking_ref = self._ref_fields.get(field_name, False)
+                    is_basic = self._basic_field_flags[index]
+                    field_value = self._read_field_value(read_context, 
serializer, is_nullable, is_dynamic, is_basic, is_tracking_ref)
+                    interned_name = self._field_name_interned[field_name]
+                    if obj_dict is not None:
+                        obj_dict[interned_name] = field_value
+                    else:
+                        setattr(obj, interned_name, field_value)
+            else:
+                for index, field_name in enumerate(self._field_names):
+                    serializer = self._serializers[index]
+                    is_nullable = self._nullable_fields.get(field_name, False)
+                    is_dynamic = self._dynamic_fields.get(field_name, False)
+                    is_tracking_ref = self._ref_fields.get(field_name, False)
+                    is_basic = self._basic_field_flags[index]
+                    field_value = self._read_field_value(read_context, 
serializer, is_nullable, is_dynamic, is_basic, is_tracking_ref)
+                    validation_field_type = self._validation_field_types[index]
+                    if validation_field_type is None:
+                        interned_name = self._field_name_interned[field_name]
+                        if obj_dict is not None:
+                            obj_dict[interned_name] = field_value
+                        else:
+                            setattr(obj, interned_name, field_value)
+                    else:
+                        self._assign_read_field_value(obj, obj_dict, 
field_name, field_value, validation_field_type)
 
         if self._missing_field_defaults:
             for field_name, default_factory in self._missing_field_defaults:
diff --git a/python/pyfory/tests/test_struct.py 
b/python/pyfory/tests/test_struct.py
index 23e24e7f9..699756f34 100644
--- a/python/pyfory/tests/test_struct.py
+++ b/python/pyfory/tests/test_struct.py
@@ -36,6 +36,14 @@ def ser_de(fory, obj):
     return fory.deserialize(binary)
 
 
+def compat_ser_de(remote_cls, local_cls, value, type_id):
+    writer = Fory(xlang=True, compatible=True, ref=False)
+    reader = Fory(xlang=True, compatible=True, ref=False)
+    writer.register_type(remote_cls, type_id=type_id)
+    reader.register_type(local_cls, type_id=type_id)
+    return reader.deserialize(writer.serialize(value))
+
+
 @dataclass
 class SimpleObject:
     f1: Optional[Dict[pyfory.int32, pyfory.float64]] = None
@@ -87,6 +95,97 @@ def test_struct():
         assert ser_de(fory, ComplexObject(f6=2**64)) == ComplexObject(f6=2**64)
 
 
+@dataclass
+class NestedDeclaredEncodingObject:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = 
dataclasses.field(default_factory=dict)
+    flags: Set[pyfory.fixed_uint32] = dataclasses.field(default_factory=set)
+
+
+def test_nested_declared_field_type_drives_local_roundtrip():
+    fory = Fory(xlang=True, compatible=True, ref=False)
+    fory.register_type(NestedDeclaredEncodingObject, type_id=701)
+    serializer = 
fory.type_resolver.get_serializer(NestedDeclaredEncodingObject)
+    field_infos = {field_info.name: field_info for field_info in 
serializer._field_infos}
+
+    values_type = field_infos["values"].field_type
+    assert values_type.type_id == TypeId.MAP
+    assert values_type.key_type.type_id == TypeId.INT32
+    assert values_type.value_type.type_id == TypeId.LIST
+    assert values_type.value_type.element_type.type_id == TypeId.TAGGED_INT64
+    assert field_infos["flags"].field_type.element_type.type_id == 
TypeId.UINT32
+
+    value = NestedDeclaredEncodingObject(values={1: [2, -3], -4: [5]}, 
flags={1, 2**32 - 1})
+    assert ser_de(fory, value) == value
+
+
+@dataclass
+class RemoteNestedFixedTagged:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = 
dataclasses.field(default_factory=dict)
+
+
+@dataclass
+class LocalNestedVarint:
+    values: Dict[pyfory.int32, List[pyfory.int64]] = 
dataclasses.field(default_factory=dict)
+
+
+@dataclass
+class RemoteNestedWide:
+    values: Dict[pyfory.fixed_int64, List[pyfory.tagged_int64]] = 
dataclasses.field(default_factory=dict)
+
+
+@dataclass
+class LocalNestedNarrow:
+    values: Dict[pyfory.fixed_int32, List[pyfory.int32]] = 
dataclasses.field(default_factory=lambda: {7: [8]})
+
+
+@dataclass
+class RemoteNestedUnsigned:
+    values: Dict[pyfory.fixed_uint32, List[pyfory.tagged_uint64]] = 
dataclasses.field(default_factory=dict)
+
+
+@dataclass
+class LocalNestedSignedDefault:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = 
dataclasses.field(default_factory=lambda: {-1: [-1]})
+
+
+def test_compatible_read_accepts_nested_same_domain_integer_encoding():
+    result = compat_ser_de(
+        RemoteNestedFixedTagged,
+        LocalNestedVarint,
+        RemoteNestedFixedTagged(values={1: [2, -3], -4: [5]}),
+        702,
+    )
+    assert result == LocalNestedVarint(values={1: [2, -3], -4: [5]})
+
+
+def test_compatible_read_validates_nested_integer_narrowing():
+    result = compat_ser_de(
+        RemoteNestedWide,
+        LocalNestedNarrow,
+        RemoteNestedWide(values={1: [2, -3]}),
+        703,
+    )
+    assert result == LocalNestedNarrow(values={1: [2, -3]})
+
+    result = compat_ser_de(
+        RemoteNestedWide,
+        LocalNestedNarrow,
+        RemoteNestedWide(values={1: [1 << 40]}),
+        704,
+    )
+    assert result == LocalNestedNarrow()
+
+
+def test_compatible_read_skips_nested_signed_unsigned_mismatch():
+    result = compat_ser_de(
+        RemoteNestedUnsigned,
+        LocalNestedSignedDefault,
+        RemoteNestedUnsigned(values={1: [2]}),
+        705,
+    )
+    assert result == LocalNestedSignedDefault()
+
+
 @dataclass
 class SuperClass1:
     f1: Optional[Any] = None
diff --git a/python/pyfory/tests/test_typedef_encoding.py 
b/python/pyfory/tests/test_typedef_encoding.py
index 7496b9f9b..6a68c4849 100644
--- a/python/pyfory/tests/test_typedef_encoding.py
+++ b/python/pyfory/tests/test_typedef_encoding.py
@@ -21,6 +21,7 @@ Tests for xlang TypeDef implementation.
 
 from dataclasses import dataclass
 from typing import List, Dict
+import pyfory
 from pyfory.serialization import Buffer
 from pyfory.meta.typedef import (
     TypeDef,
@@ -53,6 +54,13 @@ class SimpleTypeDef:
     value: int
 
 
+@dataclass
+class NestedEncodingTypeDef:
+    """TypeDef with nested primitive encoding overrides."""
+
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]]
+
+
 def test_collection_field_type():
     """Test collection field type creation and serialization."""
     element_type = FieldType(TypeId.INT32, True, True, False)
@@ -146,6 +154,25 @@ def test_encode_decode_typedef():
             assert field.field_type.is_nullable == 
typedef.fields[i].field_type.is_nullable
 
 
+def test_nested_container_typedef_preserves_declared_encoding():
+    fory = Fory(xlang=True)
+    fory.register(NestedEncodingTypeDef, namespace="example", 
typename="NestedEncodingTypeDef")
+
+    typedef = encode_typedef(fory.type_resolver, NestedEncodingTypeDef)
+    values_field = next(field for field in typedef.fields if field.name == 
"values")
+    assert values_field.field_type.type_id == TypeId.MAP
+    assert values_field.field_type.key_type.type_id == TypeId.INT32
+    assert values_field.field_type.value_type.type_id == TypeId.LIST
+    assert values_field.field_type.value_type.element_type.type_id == 
TypeId.TAGGED_INT64
+
+    decoded_typedef = decode_typedef(Buffer(typedef.encoded), 
fory.type_resolver)
+    decoded_values_field = next(field for field in decoded_typedef.fields if 
field.name == "values")
+    assert decoded_values_field.field_type.type_id == TypeId.MAP
+    assert decoded_values_field.field_type.key_type.type_id == TypeId.INT32
+    assert decoded_values_field.field_type.value_type.type_id == TypeId.LIST
+    assert decoded_values_field.field_type.value_type.element_type.type_id == 
TypeId.TAGGED_INT64
+
+
 if __name__ == "__main__":
     test_collection_field_type()
     test_map_field_type()
diff --git a/python/pyfory/tests/xlang_test_main.py 
b/python/pyfory/tests/xlang_test_main.py
index 8ada5dea6..2a1afed53 100644
--- a/python/pyfory/tests/xlang_test_main.py
+++ b/python/pyfory/tests/xlang_test_main.py
@@ -230,6 +230,26 @@ class RefOverrideContainer:
     map_field: Dict[str, Ref[RefOverrideElement]] = None
 
 
+@dataclass
+class SignedNestedAnnotatedContainerSchemaConsistent:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = None
+
+
+@dataclass
+class SignedNestedAnnotatedContainerCompatible:
+    values: Dict[pyfory.fixed_int32, List[pyfory.tagged_int64]] = None
+
+
+@dataclass
+class NestedAnnotatedContainerSchemaConsistent:
+    values: Dict[pyfory.fixed_uint32, List[pyfory.tagged_uint64]] = None
+
+
+@dataclass
+class NestedAnnotatedContainerCompatible:
+    values: Dict[pyfory.fixed_uint32, List[pyfory.tagged_uint64]] = None
+
+
 # ============================================================================
 # Nullable Field Test Types
 # ============================================================================
@@ -1424,6 +1444,61 @@ def test_collection_element_ref_remote_tracking():
         f.write(fory.serialize(outer))
 
 
+def _test_nested_annotated_container(cls, type_id: int, expected, compatible: 
bool):
+    data_file = get_data_file()
+    with open(data_file, "rb") as f:
+        data_bytes = f.read()
+
+    kwargs = {"xlang": True, "compatible": compatible}
+    if compatible:
+        kwargs["meta_compressor"] = NoOpMetaCompressor()
+    fory = pyfory.Fory(**kwargs)
+    fory.register_type(cls, type_id=type_id)
+
+    obj = fory.deserialize(data_bytes)
+    debug_print(f"Deserialized nested annotated container: {obj}")
+    assert obj == expected, f"Mismatch: {obj} != {expected}"
+
+    with open(data_file, "wb") as f:
+        f.write(fory.serialize(obj))
+
+
+def test_signed_nested_annotated_container_schema_consistent():
+    _test_nested_annotated_container(
+        SignedNestedAnnotatedContainerSchemaConsistent,
+        803,
+        SignedNestedAnnotatedContainerSchemaConsistent(values={-7: [7, 
-1000000000], 3: [42]}),
+        compatible=False,
+    )
+
+
+def test_signed_nested_annotated_container_compatible():
+    _test_nested_annotated_container(
+        SignedNestedAnnotatedContainerCompatible,
+        804,
+        SignedNestedAnnotatedContainerCompatible(values={-7: [7, -1000000000], 
3: [42]}),
+        compatible=True,
+    )
+
+
+def test_nested_annotated_container_schema_consistent():
+    _test_nested_annotated_container(
+        NestedAnnotatedContainerSchemaConsistent,
+        801,
+        NestedAnnotatedContainerSchemaConsistent(values={4000000000: [7, 
1000000000], 3: [42]}),
+        compatible=False,
+    )
+
+
+def test_nested_annotated_container_compatible():
+    _test_nested_annotated_container(
+        NestedAnnotatedContainerCompatible,
+        802,
+        NestedAnnotatedContainerCompatible(values={4000000000: [7, 
1000000000], 3: [42]}),
+        compatible=True,
+    )
+
+
 # ============================================================================
 # Circular Reference Test Types
 # ============================================================================


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


Reply via email to