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 2febfddf3 fix: keep skip reference ids aligned (#3823)
2febfddf3 is described below

commit 2febfddf3c8f03bbcbe6efc3b34ea83f6c78d2ee
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 8 10:55:09 2026 +0530

    fix: keep skip reference ids aligned (#3823)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    
    
    ## Related issues
    
    
    
    ## 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 or equivalent persisted links of the
    final clean AI review results from both fresh reviewers described in
    `AI_POLICY.md`, the Fory-guided reviewer and the independent general
    reviewer, 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
---
 AGENTS.md                                          |  13 --
 cpp/fory/serialization/ref_resolver.h              |  32 +++-
 cpp/fory/serialization/skip.cc                     |  29 ++--
 cpp/fory/serialization/struct_compatible_test.cc   | 178 ++++++++++++++++++++
 dart/packages/fory/lib/src/context/ref_reader.dart |   2 +
 go/fory/ref_resolver.go                            |  40 ++++-
 go/fory/ref_resolver_test.go                       |  28 +++-
 go/fory/skip.go                                    |  84 +++++-----
 go/fory/skip_test.go                               |  60 +++++++
 javascript/packages/core/lib/context.ts            |   2 +
 rust/fory-core/src/resolver/ref_resolver.rs        |   3 +
 rust/fory-core/src/serializer/collection.rs        |   2 +-
 rust/fory-core/src/serializer/map.rs               |   4 +-
 rust/fory-core/src/serializer/skip.rs              | 185 ++++++++++++++++-----
 rust/tests/tests/test_ref_resolver.rs              |  17 ++
 rust/tests/tests/test_skip_fields.rs               | 157 +++++++++++++++++
 16 files changed, 705 insertions(+), 131 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 83e6aa0c1..9039a2369 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -147,19 +147,6 @@ This is the entry point for AI guidance in Apache Fory. 
Read this file first, th
 - `docs/DEVELOPMENT.md` plus updates under `docs/guide/` and 
`docs/benchmarks/` are synced to `apache/fory-site`; other website content 
belongs there.
 - When benchmark logic, scripts, configuration, or compared serializers 
change, rerun the relevant benchmarks and refresh the artifacts under 
`docs/benchmarks/**`.
 
-## Network Error Command Log
-
-- 2026-06-26: `cargo check` from `rust/` failed while updating crates.io 
through
-  `127.0.0.1:7890`; retried as
-  `env -u all_proxy -u http_proxy -u https_proxy -u ALL_PROXY -u HTTP_PROXY -u 
HTTPS_PROXY cargo check`,
-  which still used the configured proxy. `cargo check --offline` succeeded 
using the local Cargo
-  cache.
-- 2026-06-26: `cmake -S . -B ../tasks/cpp-cmake-build -DFORY_BUILD_TESTS=ON
--DFORY_BUILD_SHARED=OFF -DFORY_BUILD_STATIC=ON` from `cpp/` failed while 
FetchContent tried to
-  clone googletest through `127.0.0.1:7890`; retried as
-  `env -u all_proxy -u http_proxy -u https_proxy -u ALL_PROXY -u HTTP_PROXY -u 
HTTPS_PROXY cmake -S . -B ../tasks/cpp-cmake-build -DFORY_BUILD_TESTS=ON 
-DFORY_BUILD_SHARED=OFF -DFORY_BUILD_STATIC=ON`,
-  which still used the configured proxy in the nested clone.
-
 ## Shared Engineering Expectations
 
 - Favor zero-copy techniques, JIT or codegen opportunities, and cache-friendly 
memory access patterns in performance-critical paths.
diff --git a/cpp/fory/serialization/ref_resolver.h 
b/cpp/fory/serialization/ref_resolver.h
index 01c2b8eec..1a6322ad0 100644
--- a/cpp/fory/serialization/ref_resolver.h
+++ b/cpp/fory/serialization/ref_resolver.h
@@ -102,8 +102,7 @@ public:
   RefReader() = default;
 
   template <typename T> uint32_t store_shared_ref(std::shared_ptr<T> ptr) {
-    // Store as shared_ptr<void> for type erasure, maintaining reference count
-    refs_.emplace_back(std::shared_ptr<void>(ptr, ptr.get()));
+    refs_.push_back(RefSlot{std::shared_ptr<void>(ptr, ptr.get()), 
&typeid(T)});
     return static_cast<uint32_t>(refs_.size() - 1);
   }
 
@@ -112,8 +111,7 @@ public:
     if (ref_id >= refs_.size()) {
       refs_.resize(ref_id + 1);
     }
-    // Store as shared_ptr<void> for type erasure
-    refs_[ref_id] = std::shared_ptr<void>(ptr, ptr.get());
+    refs_[ref_id] = RefSlot{std::shared_ptr<void>(ptr, ptr.get()), &typeid(T)};
   }
 
   template <typename T>
@@ -123,15 +121,26 @@ public:
                                            std::to_string(ref_id)));
     }
 
-    const std::shared_ptr<void> &stored = refs_[ref_id];
+    const RefSlot &slot = refs_[ref_id];
+    const std::shared_ptr<void> &stored = slot.ptr;
     if (!stored) {
       return Unexpected(Error::invalid_ref("Reference not resolved: " +
                                            std::to_string(ref_id)));
     }
 
-    // Alias constructor: create shared_ptr<T> that shares ownership with 
stored
-    // This works for polymorphic types because the void* points to the actual
-    // derived object
+    const std::type_info &requested_type = typeid(T);
+    // The stored void* is only safe to reinterpret as the same C++ static type
+    // that materialized the slot. Reject mismatches before a forged ref id can
+    // turn one shared_ptr<T> owner into an unrelated shared_ptr<U>.
+    if (slot.type == nullptr ||
+        (slot.type != &requested_type && !(*slot.type == requested_type))) {
+      std::string stored_type =
+          slot.type == nullptr ? "<unknown>" : slot.type->name();
+      return Unexpected(Error::invalid_ref(
+          "Reference type mismatch for ID " + std::to_string(ref_id) +
+          ": stored " + stored_type + ", requested " + requested_type.name()));
+    }
+
     return std::shared_ptr<T>(stored, static_cast<T *>(stored.get()));
   }
 
@@ -188,7 +197,12 @@ public:
   }
 
 private:
-  std::vector<std::shared_ptr<void>> refs_;
+  struct RefSlot {
+    std::shared_ptr<void> ptr;
+    const std::type_info *type = nullptr;
+  };
+
+  std::vector<RefSlot> refs_;
   std::vector<UpdateCallback> callbacks_;
 };
 
diff --git a/cpp/fory/serialization/skip.cc b/cpp/fory/serialization/skip.cc
index ec75d88e0..1f652261b 100644
--- a/cpp/fory/serialization/skip.cc
+++ b/cpp/fory/serialization/skip.cc
@@ -61,7 +61,15 @@ bool consume_ref_flag(ReadContext &ctx, bool tracking_ref, 
bool null_only) {
     (void)ctx.read_var_uint32(ctx.error());
     return false;
   }
-  if (ref_flag == NOT_NULL_VALUE_FLAG || ref_flag == REF_VALUE_FLAG) {
+  if (ref_flag == REF_VALUE_FLAG) {
+    // A skipped first occurrence still consumes a producer ref id. Reserve an
+    // empty slot so later Ref ids keep the same numbering as the wire stream.
+    if (tracking_ref) {
+      ctx.ref_reader().reserve_ref_id();
+    }
+    return true;
+  }
+  if (ref_flag == NOT_NULL_VALUE_FLAG) {
     return true;
   }
   ctx.set_error(Error::invalid_data(
@@ -605,22 +613,9 @@ void skip_union(ReadContext &ctx) {
   if (FORY_PREDICT_FALSE(ctx.has_error())) {
     return;
   }
-  // Read ref flag for the union value (Any-style)
-  int8_t ref_flag = ctx.read_int8(ctx.error());
-  if (FORY_PREDICT_FALSE(ctx.has_error())) {
-    return;
-  }
-  if (ref_flag == NULL_FLAG) {
-    return;
-  }
-  if (ref_flag == REF_FLAG) {
-    (void)ctx.read_var_uint32(ctx.error());
-    return;
-  }
-  if (ref_flag != NOT_NULL_VALUE_FLAG && ref_flag != REF_VALUE_FLAG) {
-    ctx.set_error(
-        Error::invalid_data("Unknown reference flag: " +
-                            std::to_string(static_cast<int>(ref_flag))));
+  // Read ref flag for the union value (Any-style).
+  bool has_value = consume_ref_flag(ctx, true, false);
+  if (FORY_PREDICT_FALSE(ctx.has_error()) || !has_value) {
     return;
   }
 
diff --git a/cpp/fory/serialization/struct_compatible_test.cc 
b/cpp/fory/serialization/struct_compatible_test.cc
index b062191ca..562e98626 100644
--- a/cpp/fory/serialization/struct_compatible_test.cc
+++ b/cpp/fory/serialization/struct_compatible_test.cc
@@ -277,6 +277,75 @@ struct CompatibleUnsignedExactV2 {
   FORY_STRUCT(CompatibleUnsignedExactV2, (id, fory::F(1)), (count, 
fory::F(2)));
 };
 
+struct SkippedRefItem {
+  int32_t value = 0;
+  FORY_STRUCT(SkippedRefItem, (value, fory::F(1)));
+};
+
+struct SkippedFieldRefWriter {
+  std::shared_ptr<SkippedRefItem> skipped;
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedFieldRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+              (alias, fory::F(3)));
+};
+
+struct SkippedFieldRefReader {
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedFieldRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct SkippedListRefWriter {
+  std::vector<std::shared_ptr<SkippedRefItem>> skipped;
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedListRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+              (alias, fory::F(3)));
+};
+
+struct SkippedListRefReader {
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedListRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct SkippedMapRefWriter {
+  std::map<int32_t, std::shared_ptr<SkippedRefItem>> skipped;
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedMapRefWriter, (skipped, fory::F(1)), (kept, fory::F(2)),
+              (alias, fory::F(3)));
+};
+
+struct SkippedMapRefReader {
+  std::shared_ptr<SkippedRefItem> kept;
+  std::shared_ptr<SkippedRefItem> alias;
+  FORY_STRUCT(SkippedMapRefReader, (kept, fory::F(2)), (alias, fory::F(3)));
+};
+
+struct RefTypeMismatchA {
+  int32_t value = 0;
+  FORY_STRUCT(RefTypeMismatchA, (value, fory::F(1)));
+};
+
+struct RefTypeMismatchB {
+  int32_t value = 0;
+  FORY_STRUCT(RefTypeMismatchB, (value, fory::F(1)));
+};
+
+struct RefTypeMismatchWriter {
+  std::shared_ptr<RefTypeMismatchA> first;
+  std::shared_ptr<RefTypeMismatchA> alias;
+  FORY_STRUCT(RefTypeMismatchWriter, (first, fory::F(1)), (alias, fory::F(2)));
+};
+
+struct RefTypeMismatchReader {
+  std::shared_ptr<RefTypeMismatchA> first;
+  std::shared_ptr<RefTypeMismatchB> alias;
+  FORY_STRUCT(RefTypeMismatchReader, (first, fory::F(1)), (alias, fory::F(2)));
+};
+
 struct ScalarBoolField {
   bool value = false;
   FORY_STRUCT(ScalarBoolField, (value, fory::F(1)));
@@ -731,6 +800,115 @@ TEST(SchemaEvolutionTest, 
ChangedSchemaReadsExactUnsignedFields) {
   EXPECT_EQ(decoded.value().count, value.count);
 }
 
+TEST(SchemaEvolutionTest, SkippedTrackedFieldKeepsRefIdsAligned) {
+  auto writer = Fory::builder().compatible(true).xlang(true).build();
+  auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+  constexpr uint32_t ITEM_TYPE_ID = 1010;
+  constexpr uint32_t WRAPPER_TYPE_ID = 1011;
+  ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(
+      writer.register_struct<SkippedFieldRefWriter>(WRAPPER_TYPE_ID).ok());
+  ASSERT_TRUE(
+      reader.register_struct<SkippedFieldRefReader>(WRAPPER_TYPE_ID).ok());
+
+  auto kept = std::make_shared<SkippedRefItem>();
+  kept->value = 42;
+  auto skipped = std::make_shared<SkippedRefItem>();
+  skipped->value = 7;
+  auto bytes = writer.serialize(SkippedFieldRefWriter{skipped, kept, kept});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+  auto decoded = reader.deserialize<SkippedFieldRefReader>(
+      bytes.value().data(), bytes.value().size());
+
+  ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+  ASSERT_NE(decoded.value().kept, nullptr);
+  EXPECT_EQ(decoded.value().kept->value, 42);
+  EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, SkippedListElementsKeepRefIdsAligned) {
+  auto writer = Fory::builder().compatible(true).xlang(true).build();
+  auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+  constexpr uint32_t ITEM_TYPE_ID = 1012;
+  constexpr uint32_t WRAPPER_TYPE_ID = 1013;
+  ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(
+      writer.register_struct<SkippedListRefWriter>(WRAPPER_TYPE_ID).ok());
+  ASSERT_TRUE(
+      reader.register_struct<SkippedListRefReader>(WRAPPER_TYPE_ID).ok());
+
+  auto kept = std::make_shared<SkippedRefItem>();
+  kept->value = 43;
+  auto skipped = std::make_shared<SkippedRefItem>();
+  skipped->value = 8;
+  auto bytes = writer.serialize(SkippedListRefWriter{{skipped}, kept, kept});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+  auto decoded = reader.deserialize<SkippedListRefReader>(bytes.value().data(),
+                                                          
bytes.value().size());
+
+  ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+  ASSERT_NE(decoded.value().kept, nullptr);
+  EXPECT_EQ(decoded.value().kept->value, 43);
+  EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, SkippedMapValuesKeepRefIdsAligned) {
+  auto writer = Fory::builder().compatible(true).xlang(true).build();
+  auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+  constexpr uint32_t ITEM_TYPE_ID = 1014;
+  constexpr uint32_t WRAPPER_TYPE_ID = 1015;
+  ASSERT_TRUE(writer.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(reader.register_struct<SkippedRefItem>(ITEM_TYPE_ID).ok());
+  ASSERT_TRUE(
+      writer.register_struct<SkippedMapRefWriter>(WRAPPER_TYPE_ID).ok());
+  ASSERT_TRUE(
+      reader.register_struct<SkippedMapRefReader>(WRAPPER_TYPE_ID).ok());
+
+  auto kept = std::make_shared<SkippedRefItem>();
+  kept->value = 44;
+  auto skipped = std::make_shared<SkippedRefItem>();
+  skipped->value = 9;
+  auto bytes =
+      writer.serialize(SkippedMapRefWriter{{{1, skipped}}, kept, kept});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+  auto decoded = reader.deserialize<SkippedMapRefReader>(bytes.value().data(),
+                                                         bytes.value().size());
+
+  ASSERT_TRUE(decoded.ok()) << decoded.error().to_string();
+  ASSERT_NE(decoded.value().kept, nullptr);
+  EXPECT_EQ(decoded.value().kept->value, 44);
+  EXPECT_EQ(decoded.value().kept, decoded.value().alias);
+}
+
+TEST(SchemaEvolutionTest, ReferenceTypeMismatchIsRejected) {
+  auto writer = Fory::builder().compatible(true).xlang(true).build();
+  auto reader = Fory::builder().compatible(true).xlang(true).build();
+
+  ASSERT_TRUE(writer.register_struct<RefTypeMismatchA>(1016).ok());
+  ASSERT_TRUE(reader.register_struct<RefTypeMismatchA>(1016).ok());
+  ASSERT_TRUE(reader.register_struct<RefTypeMismatchB>(1017).ok());
+  ASSERT_TRUE(writer.register_struct<RefTypeMismatchWriter>(1018).ok());
+  ASSERT_TRUE(reader.register_struct<RefTypeMismatchReader>(1018).ok());
+
+  auto shared = std::make_shared<RefTypeMismatchA>();
+  shared->value = 45;
+  auto bytes = writer.serialize(RefTypeMismatchWriter{shared, shared});
+  ASSERT_TRUE(bytes.ok()) << bytes.error().to_string();
+
+  auto decoded = reader.deserialize<RefTypeMismatchReader>(
+      bytes.value().data(), bytes.value().size());
+
+  ASSERT_FALSE(decoded.ok());
+  EXPECT_EQ(decoded.error().code(), ErrorCode::InvalidRef);
+  EXPECT_NE(decoded.error().message().find("Reference type mismatch"),
+            std::string::npos);
+}
+
 TEST(SchemaEvolutionTest, PimplPropertyNestedNamedStruct) {
   auto writer = Fory::builder().compatible(true).xlang(true).build();
   auto reader = Fory::builder().compatible(true).xlang(true).build();
diff --git a/dart/packages/fory/lib/src/context/ref_reader.dart 
b/dart/packages/fory/lib/src/context/ref_reader.dart
index 5b524bc2e..dafb8b695 100644
--- a/dart/packages/fory/lib/src/context/ref_reader.dart
+++ b/dart/packages/fory/lib/src/context/ref_reader.dart
@@ -21,6 +21,8 @@ import 'package:fory/src/memory/buffer.dart';
 import 'package:fory/src/context/ref_writer.dart';
 
 final class RefReader {
+  // Missing compatible structs may surface as null field values, but they are
+  // not published as reference targets; keep ref reads as direct lookups.
   final List<Object?> _refs = <Object?>[];
   final List<int> _preservedIds = <int>[];
   Object? _resolved;
diff --git a/go/fory/ref_resolver.go b/go/fory/ref_resolver.go
index 155c16791..1eb0d138b 100644
--- a/go/fory/ref_resolver.go
+++ b/go/fory/ref_resolver.go
@@ -202,8 +202,19 @@ func (r *RefResolver) ReadRefOrNull(buffer *ByteBuffer, 
ctxErr *Error) int8 {
        }
        if refTag == RefFlag {
                // read ref id and get object from ref resolver
-               refId := buffer.ReadVarUint32(ctxErr)
-               r.readObject = r.GetReadObject(int32(refId))
+               refId := int32(buffer.ReadVarUint32(ctxErr))
+               if refId < 0 || int(refId) >= len(r.readObjects) {
+                       ctxErr.SetError(InvalidRefIdError(refId))
+                       r.readObject = reflect.Value{}
+                       return RefFlag
+               }
+               object := r.readObjects[refId]
+               if !object.IsValid() {
+                       ctxErr.SetError(InvalidRefIdError(refId))
+                       r.readObject = reflect.Value{}
+                       return RefFlag
+               }
+               r.readObject = object
                return RefFlag
        } else {
                r.readObject = reflect.Value{}
@@ -228,6 +239,18 @@ func (r *RefResolver) PreserveRefId() (int32, error) {
        return nextReadRefId, nil
 }
 
+func (r *RefResolver) ReserveSkippedRefId() error {
+       if !r.refTracking {
+               return nil
+       }
+       nextReadRefId := len(r.readObjects)
+       if nextReadRefId > MaxInt32 {
+               return fmt.Errorf("referencable objects exceeds max int32")
+       }
+       r.readObjects = append(r.readObjects, reflect.Value{})
+       return nil
+}
+
 func (r *RefResolver) TryPreserveRefId(buffer *ByteBuffer) (int32, error) {
        var ctxErr Error
        headFlag := buffer.ReadInt8(&ctxErr)
@@ -236,11 +259,20 @@ func (r *RefResolver) TryPreserveRefId(buffer 
*ByteBuffer) (int32, error) {
        }
        if headFlag == RefFlag {
                // read ref id and get object from ref resolver
-               refId := buffer.ReadVarUint32(&ctxErr)
+               refId := int32(buffer.ReadVarUint32(&ctxErr))
                if ctxErr.HasError() {
                        return 0, ctxErr
                }
-               r.readObject = r.GetReadObject(int32(refId))
+               if refId < 0 || int(refId) >= len(r.readObjects) {
+                       r.readObject = reflect.Value{}
+                       return 0, InvalidRefIdError(refId)
+               }
+               object := r.readObjects[refId]
+               if !object.IsValid() {
+                       r.readObject = reflect.Value{}
+                       return 0, InvalidRefIdError(refId)
+               }
+               r.readObject = object
        } else {
                r.readObject = reflect.Value{}
                if headFlag == RefValueFlag {
diff --git a/go/fory/ref_resolver_test.go b/go/fory/ref_resolver_test.go
index 2e5157c2c..3b475c30f 100644
--- a/go/fory/ref_resolver_test.go
+++ b/go/fory/ref_resolver_test.go
@@ -44,9 +44,11 @@ func TestReferenceResolver(t *testing.T) {
        }
        refResolver.readObjects = make([]reflect.Value, 
len(refResolver.writtenObjects))
        ctxErr := &Error{}
-       for range values {
+       for i, data := range values {
                require.Equal(t, refResolver.ReadRefOrNull(buf, ctxErr), 
RefValueFlag)
+               refResolver.SetReadObject(int32(i), reflect.ValueOf(data))
                require.Equal(t, refResolver.ReadRefOrNull(buf, ctxErr), 
RefFlag)
+               require.False(t, ctxErr.HasError())
        }
        {
                s := []int{1, 2, 3}
@@ -200,6 +202,30 @@ func TestRefResolverOOBPanic(t *testing.T) {
        require.NotPanics(t, func() {
                resolver.ReadRefOrNull(buffer, &ctxErr)
        }, "RefResolver.GetReadObject should not panic on OOB index")
+       require.True(t, ctxErr.HasError())
+}
+
+func TestRefResolverRejectsSkippedSlotRef(t *testing.T) {
+       resolver := newRefResolver(true)
+       require.NoError(t, resolver.ReserveSkippedRefId())
+       keptID, err := resolver.PreserveRefId()
+       require.NoError(t, err)
+       kept := reflect.ValueOf("kept")
+       resolver.SetReadObject(keptID, kept)
+
+       skippedRef := NewByteBuffer(nil)
+       skippedRef.WriteInt8(RefFlag)
+       skippedRef.WriteVarUint32(0)
+       _, err = resolver.TryPreserveRefId(skippedRef)
+       require.Error(t, err)
+
+       keptRef := NewByteBuffer(nil)
+       keptRef.WriteInt8(RefFlag)
+       keptRef.WriteVarUint32(uint32(keptID))
+       refID, err := resolver.TryPreserveRefId(keptRef)
+       require.NoError(t, err)
+       require.Equal(t, int32(RefFlag), refID)
+       require.Equal(t, kept.String(), 
resolver.GetCurrentReadObject().String())
 }
 
 // TestRefResolverBoundaryRegression verifies that valid boundary indices
diff --git a/go/fory/skip.go b/go/fory/skip.go
index 82b03a505..23508a3cc 100644
--- a/go/fory/skip.go
+++ b/go/fory/skip.go
@@ -30,6 +30,37 @@ func skipSizedBytes(ctx *ReadContext, size uint64) {
        ctx.buffer.Skip(int(size), ctx.Err())
 }
 
+func consumeSkippedRefFlag(ctx *ReadContext, readRefFlag bool) bool {
+       if !readRefFlag {
+               return true
+       }
+       err := ctx.Err()
+       refFlag := ctx.buffer.ReadInt8(err)
+       if ctx.HasError() {
+               return false
+       }
+       switch refFlag {
+       case NullFlag:
+               return false
+       case RefFlag:
+               _ = ctx.buffer.ReadVarUint32(err)
+               return false
+       case RefValueFlag:
+               // A skipped first occurrence still consumes a producer ref id. 
Keep
+               // numbering aligned without adding a pending ref for the next 
materialized value.
+               if reserveErr := ctx.RefResolver().ReserveSkippedRefId(); 
reserveErr != nil {
+                       ctx.SetError(FromError(reserveErr))
+                       return false
+               }
+               return true
+       case NotNullValueFlag:
+               return true
+       default:
+               ctx.SetError(DeserializationErrorf("invalid ref flag %d", 
refFlag))
+               return false
+       }
+}
+
 // SkipFieldValue skips a field value in compatible mode when the field 
doesn't exist
 // or is incompatible with the local type.
 // Uses context error state for deferred error checking.
@@ -45,17 +76,8 @@ func SkipFieldValueWithTypeFlag(ctx *ReadContext, fieldDef 
FieldDef, readRefFlag
        if readTypeInfo {
                // Type info was written for this field (struct fields in 
compatible mode)
                // Read ref flag first if needed
-               if readRefFlag {
-                       refFlag := ctx.buffer.ReadInt8(err)
-                       if refFlag == NullFlag {
-                               return
-                       }
-                       if refFlag == RefFlag {
-                               // Reference to already-seen object, skip the 
reference index
-                               _ = ctx.buffer.ReadVarUint32(err)
-                               return
-                       }
-                       // RefValueFlag (0) or NotNullValueFlag (-1) means we 
need to read the actual object
+               if !consumeSkippedRefFlag(ctx, readRefFlag) {
+                       return
                }
 
                // Read type info (typeID + meta_index)
@@ -135,20 +157,8 @@ func isStructTypeId(id TypeId) bool {
 func SkipAnyValue(ctx *ReadContext, readRefFlag bool) {
        err := ctx.Err()
        // Handle ref flag first if needed
-       if readRefFlag {
-               refFlag := ctx.buffer.ReadInt8(err)
-               if ctx.HasError() {
-                       return
-               }
-               if refFlag == NullFlag {
-                       return
-               }
-               if refFlag == RefFlag {
-                       // Reference to already-seen object, skip the reference 
index
-                       _ = ctx.buffer.ReadVarUint32(err)
-                       return
-               }
-               // RefValueFlag (0) or NotNullValueFlag (-1) means we need to 
read the actual object
+       if !consumeSkippedRefFlag(ctx, readRefFlag) {
+               return
        }
 
        // ReadData type_id first
@@ -265,6 +275,7 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
                elemDef = FieldDef{
                        typeSpec: 
NewSimpleTypeSpec(TypeId(elemTypeInfo.TypeID)),
                        nullable: hasNull,
+                       trackRef: trackRef,
                }
        } else if isDeclared {
                // Use declared element type from the collection's field type
@@ -272,19 +283,22 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
                        elemDef = FieldDef{
                                typeSpec: fieldDef.typeSpec.elementType,
                                nullable: hasNull,
+                               trackRef: trackRef,
                        }
                } else {
                        // Fallback: use unknown type
                        elemDef = FieldDef{
                                typeSpec: NewSimpleTypeSpec(UNKNOWN),
                                nullable: true,
+                               trackRef: trackRef,
                        }
                }
        } else {
                // Not same type - each element has its own type info, use 
unknown
                elemDef = FieldDef{
                        typeSpec: NewSimpleTypeSpec(UNKNOWN),
-                       nullable: true,
+                       nullable: hasNull,
+                       trackRef: trackRef,
                }
        }
 
@@ -297,7 +311,7 @@ func skipCollection(ctx *ReadContext, fieldDef FieldDef) {
 
        for i := uint32(0); i < length; i++ {
                // Read ref flag if collection has ref tracking enabled
-               skipValue(ctx, elemDef, trackRef, false, elemTypeInfo)
+               skipValue(ctx, elemDef, trackRef || hasNull, false, 
elemTypeInfo)
                if ctx.HasError() {
                        return
                }
@@ -547,20 +561,8 @@ func skipStruct(ctx *ReadContext, info *TypeInfo) {
 // Uses context error state for deferred error checking.
 func skipValue(ctx *ReadContext, fieldDef FieldDef, readRefFlag bool, isField 
bool, typeInfo *TypeInfo) {
        err := ctx.Err()
-       if readRefFlag {
-               refFlag := ctx.buffer.ReadInt8(err)
-               if ctx.HasError() {
-                       return
-               }
-               if refFlag == NullFlag {
-                       return
-               }
-               if refFlag == RefFlag {
-                       // Reference to already-seen object, skip the reference 
index
-                       _ = ctx.buffer.ReadVarUint32(err)
-                       return
-               }
-               // RefValueFlag (0) or NotNullValueFlag (-1) means we need to 
read the actual object
+       if !consumeSkippedRefFlag(ctx, readRefFlag) {
+               return
        }
 
        typeIDNum := uint32(fieldDef.typeSpec.TypeId())
diff --git a/go/fory/skip_test.go b/go/fory/skip_test.go
index 1552a6ceb..3bae47359 100644
--- a/go/fory/skip_test.go
+++ b/go/fory/skip_test.go
@@ -130,3 +130,63 @@ func TestSkipMapRejectsInvalidChunkSize(t *testing.T) {
        )
        require.Error(t, f.readCtx.CheckError())
 }
+
+func TestSkipTrackedValueReservesRefId(t *testing.T) {
+       f := New(WithXlang(true), WithCompatible(true), WithTrackRef(true))
+       buf := NewByteBuffer(nil)
+       buf.WriteInt8(RefValueFlag)
+       buf.WriteLength(0)
+
+       f.readCtx.SetData(buf.Bytes())
+       skipValue(
+               f.readCtx,
+               FieldDef{
+                       typeSpec: NewCollectionTypeSpec(LIST, 
NewSimpleTypeSpec(STRING)),
+                       trackRef: true,
+               },
+               true,
+               false,
+               nil,
+       )
+       require.NoError(t, f.readCtx.CheckError())
+
+       resolver := f.readCtx.RefResolver()
+       require.Len(t, resolver.readObjects, 1)
+       require.Empty(t, resolver.readRefIds)
+
+       nextRefId, err := resolver.PreserveRefId()
+       require.NoError(t, err)
+       require.Equal(t, int32(1), nextRefId)
+}
+
+func TestSkipCollectionConsumesNullElementFlag(t *testing.T) {
+       tests := []struct {
+               name   string
+               header byte
+       }{
+               {name: "same_type_declared", header: CollectionDeclSameType | 
CollectionHasNull},
+               {name: "element_type_per_value", header: CollectionHasNull},
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       f := New(WithXlang(true), WithCompatible(false))
+                       buf := NewByteBuffer(nil)
+                       buf.WriteLength(1)
+                       buf.WriteByte(tc.header)
+                       buf.WriteInt8(NullFlag)
+                       buf.WriteByte(0x7f)
+
+                       f.readCtx.SetData(buf.Bytes())
+                       skipCollection(
+                               f.readCtx,
+                               FieldDef{
+                                       typeSpec: NewCollectionTypeSpec(LIST, 
NewSimpleTypeSpec(INT32)),
+                                       nullable: true,
+                               },
+                       )
+                       require.NoError(t, f.readCtx.CheckError())
+                       require.Equal(t, byte(0x7f), 
f.readCtx.Buffer().ReadByte(f.readCtx.Err()))
+               })
+       }
+}
diff --git a/javascript/packages/core/lib/context.ts 
b/javascript/packages/core/lib/context.ts
index a4cebfb35..f2613c7bc 100644
--- a/javascript/packages/core/lib/context.ts
+++ b/javascript/packages/core/lib/context.ts
@@ -241,6 +241,8 @@ export class RefReader {
   }
 
   getReadRef(refId: number) {
+    // Missing compatible structs may surface as null field values, but they 
are
+    // not published as reference targets; keep this hot path as a direct 
lookup.
     return this.readObjects[refId];
   }
 
diff --git a/rust/fory-core/src/resolver/ref_resolver.rs 
b/rust/fory-core/src/resolver/ref_resolver.rs
index 14ca87c58..963354da2 100644
--- a/rust/fory-core/src/resolver/ref_resolver.rs
+++ b/rust/fory-core/src/resolver/ref_resolver.rs
@@ -272,6 +272,9 @@ impl RefReader {
     #[inline(always)]
     pub fn reserve_ref_id(&mut self) -> u32 {
         let ref_id = self.refs.len() as u32;
+        // Skipped values reserve their producer ref id but intentionally stay
+        // unresolved. A later Ref to this slot must fail downcast instead of
+        // aliasing the next materialized object.
         self.refs.push(Box::new(()));
         ref_id
     }
diff --git a/rust/fory-core/src/serializer/collection.rs 
b/rust/fory-core/src/serializer/collection.rs
index eb3fc2ad8..2ca2342b5 100644
--- a/rust/fory-core/src/serializer/collection.rs
+++ b/rust/fory-core/src/serializer/collection.rs
@@ -26,7 +26,7 @@ use crate::serializer::{ForyDefault, Serializer};
 use crate::type_id::{self, need_to_write_type_for_field, TypeId, 
PRIMITIVE_ARRAY_TYPES};
 use crate::types::{bfloat16::bfloat16, float16::float16};
 
-const TRACKING_REF: u8 = 0b1;
+pub const TRACKING_REF: u8 = 0b1;
 
 pub const HAS_NULL: u8 = 0b10;
 
diff --git a/rust/fory-core/src/serializer/map.rs 
b/rust/fory-core/src/serializer/map.rs
index 8275cccbc..eb1b9089b 100644
--- a/rust/fory-core/src/serializer/map.rs
+++ b/rust/fory-core/src/serializer/map.rs
@@ -28,10 +28,10 @@ use std::rc::Rc;
 
 const MAX_CHUNK_SIZE: u8 = 255;
 
-const TRACKING_KEY_REF: u8 = 0b1;
+pub const TRACKING_KEY_REF: u8 = 0b1;
 pub const KEY_NULL: u8 = 0b10;
 pub const DECL_KEY_TYPE: u8 = 0b100;
-const TRACKING_VALUE_REF: u8 = 0b1000;
+pub const TRACKING_VALUE_REF: u8 = 0b1000;
 pub const VALUE_NULL: u8 = 0b10000;
 pub const DECL_VALUE_TYPE: u8 = 0b100000;
 
diff --git a/rust/fory-core/src/serializer/skip.rs 
b/rust/fory-core/src/serializer/skip.rs
index f4df4bf11..3a267429b 100644
--- a/rust/fory-core/src/serializer/skip.rs
+++ b/rust/fory-core/src/serializer/skip.rs
@@ -19,7 +19,7 @@ use crate::context::ReadContext;
 use crate::ensure;
 use crate::error::Error;
 use crate::meta::FieldType;
-use crate::serializer::collection::{DECL_ELEMENT_TYPE, HAS_NULL, IS_SAME_TYPE};
+use crate::serializer::collection::{DECL_ELEMENT_TYPE, HAS_NULL, IS_SAME_TYPE, 
TRACKING_REF};
 use crate::serializer::util;
 use crate::type_id as types;
 use crate::util::ENABLE_FORY_DEBUG_OUTPUT;
@@ -34,7 +34,14 @@ pub fn skip_field_value(
     field_type: &FieldType,
     read_ref_flag: bool,
 ) -> Result<(), Error> {
-    skip_value(context, field_type, read_ref_flag, true, &None)
+    skip_value(
+        context,
+        field_type,
+        field_type.track_ref,
+        read_ref_flag && !field_type.track_ref,
+        true,
+        &None,
+    )
 }
 
 #[inline(always)]
@@ -42,6 +49,21 @@ fn unknown_field_type() -> FieldType {
     FieldType::new(types::UNKNOWN, true, Vec::new())
 }
 
+#[inline(always)]
+fn has_unknown_element_type(field_type: &FieldType) -> bool {
+    field_type
+        .generics
+        .first()
+        .is_some_and(|elem_type| elem_type.type_id == types::UNKNOWN)
+}
+
+#[inline(always)]
+fn next_byte_is_null_only_ref_flag(context: &mut ReadContext) -> Result<bool, 
Error> {
+    let value = context.reader.read_i8()?;
+    context.reader.move_back(1);
+    Ok(value == RefFlag::Null as i8 || value == RefFlag::NotNullValue as i8)
+}
+
 #[inline(always)]
 fn skip_bytes(context: &mut ReadContext, len: usize) -> Result<(), Error> {
     context.reader.check_bound(len)?;
@@ -49,6 +71,39 @@ fn skip_bytes(context: &mut ReadContext, len: usize) -> 
Result<(), Error> {
     Ok(())
 }
 
+#[inline(always)]
+fn consume_ref_flag(
+    context: &mut ReadContext,
+    tracking_ref: bool,
+    null_only: bool,
+) -> Result<bool, Error> {
+    if !tracking_ref && !null_only {
+        return Ok(true);
+    }
+    let ref_flag = context.reader.read_i8()?;
+    if ref_flag == RefFlag::Null as i8 {
+        return Ok(false);
+    }
+    if ref_flag == RefFlag::Ref as i8 {
+        let _ref_index = context.reader.read_var_u32()?;
+        return Ok(false);
+    }
+    if ref_flag == RefFlag::RefValue as i8 {
+        // A skipped first occurrence still consumes a producer ref id. 
Reserve an
+        // empty slot so later Ref ids keep the same numbering as the wire 
stream.
+        if tracking_ref {
+            context.ref_reader.reserve_ref_id();
+        }
+        return Ok(true);
+    }
+    if ref_flag == RefFlag::NotNullValue as i8 {
+        return Ok(true);
+    }
+    Err(Error::invalid_ref(format!(
+        "Invalid reference flag: {ref_flag}"
+    )))
+}
+
 #[cold]
 #[inline(never)]
 fn skip_sized_bytes(context: &mut ReadContext, element_size: Option<usize>) -> 
Result<(), Error> {
@@ -117,18 +172,8 @@ fn skip_decimal(context: &mut ReadContext) -> Result<(), 
Error> {
 #[cold]
 #[inline(never)]
 pub fn skip_any_value(context: &mut ReadContext, read_ref_flag: bool) -> 
Result<(), Error> {
-    // Handle ref flag first if needed
-    if read_ref_flag {
-        let ref_flag = context.reader.read_i8()?;
-        if ref_flag == (RefFlag::Null as i8) {
-            return Ok(());
-        }
-        if ref_flag == (RefFlag::Ref as i8) {
-            // Reference to already-seen object, skip the reference index
-            let _ref_index = context.reader.read_var_u32()?;
-            return Ok(());
-        }
-        // RefValue (0) or NotNullValue (-1) means we need to read the actual 
object
+    if !consume_ref_flag(context, read_ref_flag, false)? {
+        return Ok(());
     }
 
     // Read type_id first
@@ -209,7 +254,7 @@ pub fn skip_any_value(context: &mut ReadContext, 
read_ref_flag: bool) -> Result<
     };
     // Don't read ref flag again in skip_value since we already handled it.
     // Pass type_info so skip_struct doesn't try to read type_id/meta_index 
again.
-    skip_value(context, &field_type, false, false, &type_info_opt)
+    skip_value(context, &field_type, false, false, false, &type_info_opt)
 }
 
 #[cold]
@@ -220,16 +265,31 @@ fn skip_collection(context: &mut ReadContext, field_type: 
&FieldType) -> Result<
         return Ok(());
     }
     let header = context.reader.read_u8()?;
+    let track_ref = (header & TRACKING_REF) != 0;
     let has_null = (header & HAS_NULL) != 0;
     let is_same_type = (header & IS_SAME_TYPE) != 0;
-    let skip_ref_flag = is_same_type && !has_null;
     let is_declared = (header & DECL_ELEMENT_TYPE) != 0;
     if field_type.generics.is_empty() {
         return Err(Error::invalid_data("empty generics"));
     }
     let default_elem_type = field_type.generics.first().unwrap();
+    if !is_same_type {
+        // Tuple fields use LIST<UNKNOWN> metadata and a collection-shaped 
prefix, but their
+        // compatible elements are still written as NullOnly values with 
inline type info.
+        let tuple_elements = header == 0
+            && has_unknown_element_type(field_type)
+            && next_byte_is_null_only_ref_flag(context)?;
+        let read_ref_flag = tuple_elements || track_ref || has_null;
+        context.inc_depth()?;
+        for _ in 0..length {
+            skip_any_value(context, read_ref_flag)?;
+        }
+        context.dec_depth();
+        return Ok(());
+    }
+
     let (type_info, elem_field_type);
-    let elem_type = if is_same_type && !is_declared {
+    let elem_type = if !is_declared {
         let type_info_rc = context.read_any_type_info()?;
         elem_field_type = FieldType::new_with_user_type_id(
             type_info_rc.get_type_id() as u32,
@@ -245,8 +305,9 @@ fn skip_collection(context: &mut ReadContext, field_type: 
&FieldType) -> Result<
         default_elem_type
     };
     context.inc_depth()?;
+    let null_only = has_null && !track_ref;
     for _ in 0..length {
-        skip_value(context, elem_type, !skip_ref_flag, false, &type_info)?;
+        skip_value(context, elem_type, track_ref, null_only, false, 
&type_info)?;
     }
     context.dec_depth();
     Ok(())
@@ -279,6 +340,7 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
         if header & crate::serializer::map::KEY_NULL != 0 {
             // Read value type info if not declared
             let value_declared = (header & 
crate::serializer::map::DECL_VALUE_TYPE) != 0;
+            let value_track_ref = (header & 
crate::serializer::map::TRACKING_VALUE_REF) != 0;
             let (value_type_info, value_field_type);
             let value_type = if !value_declared {
                 let type_info = context.read_any_type_info()?;
@@ -296,7 +358,14 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
                 default_value_type
             };
             context.inc_depth()?;
-            skip_value(context, value_type, false, false, &value_type_info)?;
+            skip_value(
+                context,
+                value_type,
+                value_track_ref,
+                false,
+                false,
+                &value_type_info,
+            )?;
             context.dec_depth();
             len_counter += 1;
             continue;
@@ -304,6 +373,7 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
         if header & crate::serializer::map::VALUE_NULL != 0 {
             // Read key type info if not declared
             let key_declared = (header & 
crate::serializer::map::DECL_KEY_TYPE) != 0;
+            let key_track_ref = (header & 
crate::serializer::map::TRACKING_KEY_REF) != 0;
             let (key_type_info, key_field_type);
             let key_type = if !key_declared {
                 let type_info = context.read_any_type_info()?;
@@ -321,7 +391,14 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
                 default_key_type
             };
             context.inc_depth()?;
-            skip_value(context, key_type, false, false, &key_type_info)?;
+            skip_value(
+                context,
+                key_type,
+                key_track_ref,
+                false,
+                false,
+                &key_type_info,
+            )?;
             context.dec_depth();
             len_counter += 1;
             continue;
@@ -330,6 +407,8 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
         let chunk_size = context.reader.read_u8()?;
         let key_declared = (header & crate::serializer::map::DECL_KEY_TYPE) != 
0;
         let value_declared = (header & 
crate::serializer::map::DECL_VALUE_TYPE) != 0;
+        let key_track_ref = (header & 
crate::serializer::map::TRACKING_KEY_REF) != 0;
+        let value_track_ref = (header & 
crate::serializer::map::TRACKING_VALUE_REF) != 0;
 
         // Read key type info if not declared
         let (key_type_info, key_field_type);
@@ -369,8 +448,22 @@ fn skip_map(context: &mut ReadContext, field_type: 
&FieldType) -> Result<(), Err
 
         context.inc_depth()?;
         for _ in 0..chunk_size {
-            skip_value(context, key_type, false, false, &key_type_info)?;
-            skip_value(context, value_type, false, false, &value_type_info)?;
+            skip_value(
+                context,
+                key_type,
+                key_track_ref,
+                false,
+                false,
+                &key_type_info,
+            )?;
+            skip_value(
+                context,
+                value_type,
+                value_track_ref,
+                false,
+                false,
+                &value_type_info,
+            )?;
         }
         context.dec_depth();
         len_counter += chunk_size as u32;
@@ -417,11 +510,20 @@ fn skip_struct(
                 field_info.field_name, field_info.field_type.type_id, 
field_info.field_type.type_id
             );
         }
-        let read_ref_flag = util::field_need_write_ref_into(
-            field_info.field_type.type_id,
-            field_info.field_type.nullable,
-        );
-        skip_value(context, &field_info.field_type, read_ref_flag, true, 
&None)?;
+        let tracking_ref = field_info.field_type.track_ref;
+        let null_only = !tracking_ref
+            && util::field_need_write_ref_into(
+                field_info.field_type.type_id,
+                field_info.field_type.nullable,
+            );
+        skip_value(
+            context,
+            &field_info.field_type,
+            tracking_ref,
+            null_only,
+            true,
+            &None,
+        )?;
     }
     context.dec_depth();
     Ok(())
@@ -458,21 +560,13 @@ fn skip_ext(
 fn skip_value(
     context: &mut ReadContext,
     field_type: &FieldType,
-    read_ref_flag: bool,
+    tracking_ref: bool,
+    null_only: bool,
     _is_field: bool,
     type_info: &Option<Rc<crate::TypeInfo>>,
 ) -> Result<(), Error> {
-    if read_ref_flag {
-        let ref_flag = context.reader.read_i8()?;
-        if ref_flag == (RefFlag::Null as i8) {
-            return Ok(());
-        }
-        if ref_flag == (RefFlag::Ref as i8) {
-            // Reference to already-seen object, skip the reference index
-            let _ref_index = context.reader.read_var_u32()?;
-            return Ok(());
-        }
-        // RefValue (0) or NotNullValue (-1) means we need to read the actual 
object
+    if !consume_ref_flag(context, tracking_ref, null_only)? {
+        return Ok(());
     }
     let type_id_num = field_type.type_id;
 
@@ -858,10 +952,15 @@ pub fn skip_enum_variant(
             Ok(())
         }
         0b1 => {
-            // Unnamed variant, skip tuple data (which is serialized as a 
collection)
-            // Tuple uses collection format but doesn't write type info, so 
skip directly
-            let field_type = FieldType::new(types::LIST, false, 
vec![unknown_field_type()]);
-            skip_collection(context, &field_type)
+            let len = context.reader.read_var_u32()? as usize;
+            let _header = context.reader.read_u8()?;
+            // Compatible tuple variants use a collection-shaped prefix, but 
each element is
+            // encoded as a NullOnly field with inline type info. The list 
skip path would read a
+            // shared element TypeInfo from the header and shift the stream by 
one byte.
+            for _ in 0..len {
+                skip_any_value(context, true)?;
+            }
+            Ok(())
         }
         0b10 => {
             // Named variant, skip struct-like data using skip_struct
diff --git a/rust/tests/tests/test_ref_resolver.rs 
b/rust/tests/tests/test_ref_resolver.rs
index 23c693947..a0842a90e 100644
--- a/rust/tests/tests/test_ref_resolver.rs
+++ b/rust/tests/tests/test_ref_resolver.rs
@@ -68,6 +68,23 @@ fn test_rc_storage_and_retrieval() {
     assert!(Rc::ptr_eq(&rc, &retrieved));
 }
 
+#[test]
+fn test_reserved_ref_slot_stays_unresolved() {
+    let mut ref_reader = RefReader::new();
+
+    let skipped_ref_id = ref_reader.reserve_ref_id();
+    let rc = Rc::new(String::from("kept"));
+    let kept_ref_id = ref_reader.store_rc_ref(rc.clone());
+
+    assert_eq!(skipped_ref_id, 0);
+    assert_eq!(kept_ref_id, 1);
+    assert!(ref_reader.get_rc_ref::<String>(skipped_ref_id).is_none());
+
+    let retrieved = ref_reader.get_rc_ref::<String>(kept_ref_id).unwrap();
+    assert_eq!(*retrieved, "kept");
+    assert!(Rc::ptr_eq(&rc, &retrieved));
+}
+
 #[test]
 fn test_arc_storage_and_retrieval() {
     let mut ref_reader = RefReader::new();
diff --git a/rust/tests/tests/test_skip_fields.rs 
b/rust/tests/tests/test_skip_fields.rs
index bb4a1f7dc..3f5b6114e 100644
--- a/rust/tests/tests/test_skip_fields.rs
+++ b/rust/tests/tests/test_skip_fields.rs
@@ -17,6 +17,9 @@
 
 use fory_core::{Fory, Reader};
 use fory_derive::{ForyEnum, ForyStruct};
+use std::any::Any;
+use std::collections::HashMap;
+use std::rc::Rc;
 
 #[derive(ForyStruct, Debug, PartialEq)]
 struct TestSkipFields {
@@ -70,6 +73,66 @@ struct ComplexNestedSkip {
     skipped_nested: TestSkipFields,
 }
 
+#[derive(ForyStruct, Debug, PartialEq)]
+struct RefSkipItem {
+    #[fory(id = 0)]
+    value: i32,
+}
+
+#[derive(ForyStruct)]
+struct SkippedAnyFieldWriter {
+    #[fory(id = 0)]
+    skipped: Rc<dyn Any>,
+    #[fory(id = 1)]
+    kept: Rc<dyn Any>,
+    #[fory(id = 2)]
+    alias: Rc<dyn Any>,
+}
+
+#[derive(ForyStruct)]
+struct SkippedAnyFieldReader {
+    #[fory(id = 1)]
+    kept: Rc<dyn Any>,
+    #[fory(id = 2)]
+    alias: Rc<dyn Any>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefListWriter {
+    #[fory(id = 0)]
+    skipped: Vec<Rc<RefSkipItem>>,
+    #[fory(id = 1)]
+    kept: Rc<RefSkipItem>,
+    #[fory(id = 2)]
+    alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefListReader {
+    #[fory(id = 1)]
+    kept: Rc<RefSkipItem>,
+    #[fory(id = 2)]
+    alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefMapWriter {
+    #[fory(id = 0)]
+    skipped: HashMap<i32, Rc<RefSkipItem>>,
+    #[fory(id = 1)]
+    kept: Rc<RefSkipItem>,
+    #[fory(id = 2)]
+    alias: Rc<RefSkipItem>,
+}
+
+#[derive(ForyStruct, Debug, PartialEq)]
+struct SkippedRefMapReader {
+    #[fory(id = 1)]
+    kept: Rc<RefSkipItem>,
+    #[fory(id = 2)]
+    alias: Rc<RefSkipItem>,
+}
+
 #[derive(ForyEnum, Debug, PartialEq)]
 enum TestEnumSkip {
     Pending,
@@ -204,6 +267,100 @@ fn test_complex_nested_skip() {
     assert_eq!(decoded.skipped_nested, TestSkipFields::fory_default());
 }
 
+#[test]
+fn skipped_ref_field_keeps_ref_ids_aligned() {
+    let mut writer = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    writer.register::<SkippedAnyFieldWriter>(1001).unwrap();
+    let mut reader = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    reader.register::<SkippedAnyFieldReader>(1001).unwrap();
+
+    let skipped: Rc<dyn Any> = Rc::new("skip".to_string());
+    let kept: Rc<dyn Any> = Rc::new("kept".to_string());
+    let bytes = writer
+        .serialize(&SkippedAnyFieldWriter {
+            skipped,
+            kept: kept.clone(),
+            alias: kept,
+        })
+        .unwrap();
+    let decoded: SkippedAnyFieldReader = reader.deserialize(&bytes).unwrap();
+
+    assert_eq!(decoded.kept.downcast_ref::<String>().unwrap(), "kept");
+    assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
+#[test]
+fn skipped_ref_list_keeps_ref_ids_aligned() {
+    let mut writer = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    writer.register::<RefSkipItem>(1002).unwrap();
+    writer.register::<SkippedRefListWriter>(1003).unwrap();
+    let mut reader = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    reader.register::<RefSkipItem>(1002).unwrap();
+    reader.register::<SkippedRefListReader>(1003).unwrap();
+
+    let skipped = Rc::new(RefSkipItem { value: 8 });
+    let kept = Rc::new(RefSkipItem { value: 43 });
+    let bytes = writer
+        .serialize(&SkippedRefListWriter {
+            skipped: vec![skipped],
+            kept: kept.clone(),
+            alias: kept,
+        })
+        .unwrap();
+    let decoded: SkippedRefListReader = reader.deserialize(&bytes).unwrap();
+
+    assert_eq!(decoded.kept.value, 43);
+    assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
+#[test]
+fn skipped_ref_map_keeps_ref_ids_aligned() {
+    let mut writer = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    writer.register::<RefSkipItem>(1004).unwrap();
+    writer.register::<SkippedRefMapWriter>(1005).unwrap();
+    let mut reader = Fory::builder()
+        .xlang(false)
+        .compatible(true)
+        .track_ref(true)
+        .build();
+    reader.register::<RefSkipItem>(1004).unwrap();
+    reader.register::<SkippedRefMapReader>(1005).unwrap();
+
+    let skipped = Rc::new(RefSkipItem { value: 9 });
+    let kept = Rc::new(RefSkipItem { value: 44 });
+    let bytes = writer
+        .serialize(&SkippedRefMapWriter {
+            skipped: HashMap::from([(1, skipped)]),
+            kept: kept.clone(),
+            alias: kept,
+        })
+        .unwrap();
+    let decoded: SkippedRefMapReader = reader.deserialize(&bytes).unwrap();
+
+    assert_eq!(decoded.kept.value, 44);
+    assert!(Rc::ptr_eq(&decoded.kept, &decoded.alias));
+}
+
 #[test]
 fn test_enum_skip() {
     let mut fory = Fory::builder().xlang(false).compatible(false).build();


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


Reply via email to