Copyright © 2026 Unity Technologies
When a serialized Dictionary[K, V] contains a duplicate key and the value type is a class carrying a [SerializeReference] field, saving and reloading the owning object silently corrupts the managed references — and not only on the duplicate row: a live entry loses its reference entirely.
This is data corruption in the managed serialization command path, so it affects all scripting backends (Mono / IL2CPP / CoreCLR), not just CoreCLR.
The SerializeReference gather pass registers a RefId for every reachable [SerializeReference] in the exact order the write pass later consumes them (an inline RefId cursor). For dictionaries, the write pass enumerates the merged set of rows — live entries plus preserved duplicate-key rows — while the gather pass must enumerate the same merged set.
The duplicate rows are looked up by a Field Unique Identifier (FUID) path that is read from a native thread-local stack (g_DictionaryFieldUniqueIdentifierStack). The write path pushes a frame onto this stack before enumerating; the gather path (GetDictionaryEntriesForGather in Runtime/Mono/SerializationBackend_DirectMemoryAccess/GatherDictionaryEntries.cpp) never pushes the frame. So during gather the FUID formats as the empty string, the duplicate-row lookup misses, and the preserved duplicate rows are skipped.
Result: the gather registers fewer RefIds than the write consumes → the inline RefId cursor is shifted by one for every row after the first duplicate → managed references are written with the wrong (or an exhausted) RefId.
Added an EditMode repro (Serialization test project):
}}, adds a preserved duplicate foo row whose reference is DerivedType3, saves the scene, reloads, and asserts each row keeps its own reference type.
Observed vs. expected after save/reload:
observed: (foo → DerivedType1), (foo → DerivedType2), (bar → null)
correct: (foo → DerivedType1), (foo → DerivedType3), (bar → DerivedType2)
The duplicate foo row consumed bar's RefId (DerivedType2 instead of DerivedType3) and the live bar row ran off the end of the cursor and became null. Confirmed failing on the Mono editor.
In GetDictionaryEntriesForGather, bracket the entry enumeration with PushDictionaryFieldUniqueIdentifierStackFrame(fuidCtx) / PopDictionaryFieldUniqueIdentifierStackFrame(), mirroring the write path, so the gather resolves the same duplicate-row FUID key and enumerates the preserved duplicate rows.
Silent, persistent data loss for any project that serializes a Dictionary with a class value containing [SerializeReference] and has (even transiently, via the Inspector) a duplicate key. Corrupts live data, not just the duplicate row. Requesting fix in 6.6.
Found while auditing the managed serialization command path for CoreCLR GC-safety.
Issues you vote on will appear here