# Migrate a custom dictionary solution to built-in dictionary serialization

> Move a dictionary field from a custom or package serialization solution to Unity's built-in dictionary serialization, and copy its data to the new field.

Starting in Unity 6.6, Unity can serialize a field declared as the .NET [`Dictionary<TKey, TValue>`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2) type without custom serialization code. Apply the [`[SerializeField]`](xref:UnityEngine.SerializeField) attribute to the field, and Unity saves the dictionary with the object and shows it in the Editor window. For how dictionary serialization works, refer to [Dictionary serialization](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/dictionaries.md).

Earlier versions didn't serialize dictionary fields, so projects used one of the following solutions:

* A class marked with the [`[Serializable]`](https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute) attribute that stores two lists, one for keys and one for values, and rebuilds the dictionary in the [`ISerializationCallbackReceiver.OnAfterDeserialize`](/engine/6000.7/script-reference/unityengine/iserializationcallbackreceiver/onafterdeserialize.md) callback.
* A type that derives from the `Dictionary<TKey, TValue>` class and serializes its own key and value lists through the same callbacks. Most third-party serializable dictionary packages work this way.

This page describes how to migrate a field from one of those solutions to Unity's dictionary serialization, and how to copy its data to the new field.

## Fields that you don't need to migrate ##before-you-migrate

Check the following cases before you start.

### When you can't edit the type that stores the data ##cannot-edit

If the data is in a type that you can't edit, such as a `MonoBehaviour` or `ScriptableObject` inside a package, you can't migrate it, because there's nowhere to add the new field.

The solution keeps working, and Unity's dictionary serialization doesn't interfere with it. Consider contacting the package author and asking to adopt the built-in dictionary. You can still use serialized dictionaries in your own code.

### When you don't want to migrate a field yet ##not-migrated-yet

If the runtime dictionary that the callbacks rebuild is a public field with neither the `[SerializeField]` nor the [`[NonSerialized]`](https://learn.microsoft.com/en-us/dotnet/api/system.nonserializedattribute) attribute, the [serialization rules analyzer](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/analyzer.md) reports the warning [UAC1015](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/analyzer.md#uac1015) on it. To stop the warning and keep your current solution, apply the `[NonSerialized]` attribute to the runtime dictionary. If no other class uses the field, you can make it private instead. Only do this if the callbacks rebuild the dictionary from key and value lists that the same class stores. If a package serializes the dictionary field itself, don't change the field, because it stores the data.

If the runtime dictionary has the `[SerializeField]` attribute while the callbacks are still in place, Unity serializes the dictionary field as well as the key and value lists, so the object stores the same data twice. The object still behaves the same way, because the `OnAfterDeserialize` callback rebuilds the dictionary from the lists after Unity reads the dictionary field. The extra copy takes up space in every scene, prefab, and asset that stores the object. To store one copy again, replace the `[SerializeField]` attribute with `[NonSerialized]`, or complete the migration.

### Check whether the data already loads ##data-already-loads

A solution that stores two separate lists writes them as two fields. A type that derives from `Dictionary<TKey, TValue>` writes its own fields nested under the field name:

```lang-yml
m_Prices:
  m_Keys:
  - sword
  - shield
  m_Values:
  - 0.5
  - 1.5
```

After you migrate, Unity stores the dictionary field as an array of entries, where each entry has a field named `key` and a field named `value`:

```lang-yml
m_Prices:
- key: sword
  value: 0.5
- key: shield
  value: 1.5
```

The structures are different, so data from these solutions can't load into a dictionary field.

A solution that stores one list of key/value pairs can match the structure exactly. If it does, its data might load into the dictionary field with no migration. Changing the field's declared type to `Dictionary<TKey, TValue>` is enough when all of the following are true:

* The new field has the same name as the old one.
* The old field is a list or an array of a `[Serializable]` entry type.
* That entry type has a serialized field named `key` whose type matches `TKey`, and a serialized field named `value` whose type matches `TValue`.

The order of the two fields doesn't matter. If the entry type has other fields, Unity ignores them and drops their values the next time it saves the object.

> **Note:**
>
> If the entry type's fields have other names, the data doesn't load, and Unity doesn't report the mismatch. Every entry gets default values, so Unity reports duplicate keys and keeps only the first entry.

Test this on a copy of your project before you rely on it. If the data doesn't load, follow the procedure in [Migrate the data](#migrate). It works whatever the solution you're replacing stores.

## Migrate the data ##migrate

> **Note:**
>
> Unity doesn't copy existing data into the new field. Until the migration method copies it, the data exists only in the old field. If you delete the old field first, you lose the data.

The general migration workflow is as follows:

1. Add the new `Dictionary<TKey, TValue>` field with the `[SerializeField]` attribute. Give it a different name from the old field, and keep the old field in place.

   If the old dictionary uses a custom key comparer, pass the same [`IEqualityComparer<TKey>`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.iequalitycomparer-1) implementation to the constructor in the new field's declaration. Unity doesn't serialize the comparer. Refer to [Custom key comparers](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/dictionaries.md#custom-key-comparers).

   If the class implements the `ISerializationCallbackReceiver` interface, don't use the runtime dictionary that the callbacks rebuild as the new field. Keep that dictionary as a separate field with the `[NonSerialized]` attribute, so the analyzer stops reporting it. Otherwise, the callbacks fill the new field when the object loads, and the migration method finds nothing to copy.

2. Add a migration method that copies the data from the old field into the new field. Make the method return whether it copied anything, so the caller knows which objects to save.

3. Run the migration method once from an Editor script, in the following order:

   1. [`ScriptableObject` assets](#scriptableobject). An asset stores its own values and inherits nothing.
   2. [Prefabs](#prefabs), in dependency order. A prefab variant inherits values from the prefab it derives from, so migrate that prefab first.
   3. [Scenes](#scenes). A prefab instance in a scene inherits values from its prefab asset, so migrate the prefab first.

4. Check that the migration copied the data on every asset, then delete the old field and the migration code. If the class implemented the `ISerializationCallbackReceiver` interface only to rebuild the dictionary, remove the interface and both callbacks as well.

The following sections describe the migration process for each object type.

### Migrate a dictionary on a ScriptableObject ##scriptableobject

A `ScriptableObject` asset stores its own values and doesn't inherit values from another asset, so the migration only has to load each asset, copy the data, and save the asset.

The following class has the new field next to the old one. The [`[HideInInspector]`](/engine/6000.7/script-reference/unityengine/hideininspector.md) attribute hides the old field in the **Inspector** window while Unity still serializes it:

```cs
using System.Collections.Generic;
    using UnityEngine;

    [CreateAssetMenu]
    public class LootTable : ScriptableObject
    {
        // The old field, serialized by the third-party solution.
        [SerializeField, HideInInspector]
        private ThirdPartyDictionary<string, int> m_LegacyWeights = new();

        // The new field, serialized by Unity.
        [SerializeField]
        private Dictionary<string, int> m_Weights = new();

        // Returns true when it copied something, so the caller knows which assets to save.
        public bool MigrateLegacyWeights()
        {
            bool copiedAnything = false;

            foreach (KeyValuePair<string, int> entry in m_LegacyWeights)
            {
                if (m_Weights.TryGetValue(entry.Key, out int existing) && existing == entry.Value)
                    continue;

                m_Weights[entry.Key] = entry.Value;
                copiedAnything = true;
            }

            return copiedAnything;
        }
    }
```

`ThirdPartyDictionary` stands for the dictionary type of the solution you're replacing. The migration method reads the data in the old field through whatever that type exposes.

The following Editor script loads each asset of that type, calls the method, marks the assets that changed as dirty, and saves them:

```cs
using UnityEditor;

    public static class LootTableMigration
    {
        [MenuItem("Tools/Migrate Loot Tables")]
        static void MigrateLootTables()
        {
            bool migratedAnything = false;

            foreach (string guid in AssetDatabase.FindAssets("t:LootTable"))
            {
                string path = AssetDatabase.GUIDToAssetPath(guid);
                var lootTable = AssetDatabase.LoadAssetAtPath<LootTable>(path);

                if (lootTable == null || !lootTable.MigrateLegacyWeights())
                    continue;

                EditorUtility.SetDirty(lootTable);
                migratedAnything = true;
            }

            if (migratedAnything)
                AssetDatabase.SaveAssets();
        }
    }
```

### Migrate a dictionary on a component ##component

The migration is the same whether the old field is a pair of lists or a dictionary type from a package. The only difference is how the migration method reads the data from the old field.

The component in the following example still has the two list fields from the solution you're replacing, next to the new dictionary field:

```cs
using System.Collections.Generic;
    using UnityEngine;

    public class Inventory : MonoBehaviour, ISerializationCallbackReceiver
    {
        // The old solution: two lists, rebuilt into a runtime dictionary.
        [SerializeField, HideInInspector] private List<string> m_LegacyKeys = new();
        [SerializeField, HideInInspector] private List<int> m_LegacyValues = new();

        // The new field, serialized by Unity.
        [SerializeField] private Dictionary<string, int> m_ItemCounts = new();

        // Returns true when it copied something, so the caller knows which objects to save.
        public bool MigrateLegacyItemCounts()
        {
            bool copiedAnything = false;

            for (int i = 0; i < m_LegacyKeys.Count && i < m_LegacyValues.Count; i++)
            {
                string key = m_LegacyKeys[i];
                int value = m_LegacyValues[i];

                if (m_ItemCounts.TryGetValue(key, out int existing) && existing == value)
                    continue;

                m_ItemCounts[key] = value;
                copiedAnything = true;
            }

            return copiedAnything;
        }

        public void OnBeforeSerialize() { }

        public void OnAfterDeserialize()
        {
            // The existing code that rebuilds your runtime dictionary.
            // It must not write to m_ItemCounts.
        }
    }
```

When the old field is a dictionary type from a package, only the migration method changes. It reads the entries through that type's public API, usually by enumerating it as key/value pairs:

```cs
using System.Collections.Generic;
    using UnityEngine;

    public class Inventory : MonoBehaviour
    {
        // The old field, serialized by the third-party solution.
        [SerializeField, HideInInspector]
        private ThirdPartyDictionary<string, int> m_LegacyItemCounts = new();

        // The new field, serialized by Unity.
        [SerializeField]
        private Dictionary<string, int> m_ItemCounts = new();

        public bool MigrateLegacyItemCounts()
        {
            bool copiedAnything = false;

            foreach (KeyValuePair<string, int> entry in m_LegacyItemCounts)
            {
                if (m_ItemCounts.TryGetValue(entry.Key, out int existing) && existing == entry.Value)
                    continue;

                m_ItemCounts[entry.Key] = entry.Value;
                copiedAnything = true;
            }

            return copiedAnything;
        }
    }
```

> **Note:**
>
> Call the migration method from an Editor script, not from the `OnAfterDeserialize` callback. The callback runs on every object as it loads, including prefab variants and prefab instances that only inherit the value. When Unity saves those objects, it records the copied data as an override on each of them. An Editor script can check whether an object stores the value or inherits it, and a deserialization callback can't.

### Migrate dictionaries in prefabs ##prefabs

Migrating a prefab needs more attention than migrating a `ScriptableObject` asset, because a prefab might inherit a value instead of storing it. A prefab variant inherits the values of the prefab it derives from, and a prefab that nests another prefab inherits the nested prefab's values. If the migration script writes to every component it finds, it also writes to those inherited values, and Unity records each write as a prefab override.

To migrate a component without recording an unwanted override, the migration script has to perform the following operations:

* **Check whether the component stores the value.** The `StoresLegacyItemCounts` method checks whether the component inherits from a prefab and, if it does, whether it overrides the old field. The script skips a component that only inherits the value.
* **Record the change as an override.** When you change a field directly on a prefab instance or a prefab variant, Unity doesn't record the change as an override until you call the [`PrefabUtility.RecordPrefabInstancePropertyModifications`](/engine/6000.7/script-reference/unityeditor/prefabutility/recordprefabinstancepropertymodifications.md) method.
* **Migrate prefabs in dependency order.** The `PrefabsInDependencyOrder` method visits a prefab only after the prefabs it derives from and the prefabs it nests. By the time the script reaches a prefab variant, it has already migrated the prefab that the variant derives from, so the variant inherits the migrated entries instead of storing its own copy.

The following example performs these operations:

```cs
using System;
    using System.Collections.Generic;
    using UnityEditor;
    using UnityEngine;

    public static class InventoryMigration
    {
        [MenuItem("Tools/Migrate Inventory Dictionaries")]
        static void MigrateInventoryDictionaries()
        {
            foreach (GameObject root in PrefabsInDependencyOrder())
            {
                bool migratedAnything = false;

                // Passing true also finds components on inactive GameObjects.
                foreach (Inventory inventory in root.GetComponentsInChildren<Inventory>(true))
                {
                    if (MigrateComponent(inventory))
                        migratedAnything = true;
                }

                if (migratedAnything)
                    PrefabUtility.SavePrefabAsset(root);
            }
        }

        // Migrates one component if the component stores the value itself.
        // The scene migration script reuses this method.
        internal static bool MigrateComponent(Inventory inventory)
        {
            bool inherited = PrefabUtility.GetCorrespondingObjectFromSource(inventory) != null;

            if (!StoresLegacyItemCounts(inventory, inherited, "m_LegacyKeys", "m_LegacyValues")
                || !inventory.MigrateLegacyItemCounts())
                return false;

            // Record the change as a prefab override.
            if (inherited)
                PrefabUtility.RecordPrefabInstancePropertyModifications(inventory);

            return true;
        }

        // Returns every prefab in the project, ordered so that each prefab comes after
        // the prefabs it derives from and the prefabs it nests.
        static List<GameObject> PrefabsInDependencyOrder()
        {
            var ordered = new List<GameObject>();
            var visited = new HashSet<string>();

            foreach (string guid in AssetDatabase.FindAssets("t:Prefab"))
                Visit(AssetDatabase.GUIDToAssetPath(guid), ordered, visited);

            return ordered;
        }

        static void Visit(string path, List<GameObject> ordered, HashSet<string> visited)
        {
            if (!visited.Add(path))
                return;

            foreach (string dependency in AssetDatabase.GetDependencies(path, false))
            {
                if (dependency != path
                    && dependency.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
                    Visit(dependency, ordered, visited);
            }

            var root = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            if (root != null)
                ordered.Add(root);
        }

        // True when the component stores the value itself: it doesn't inherit from a prefab,
        // or it overrides one of the fields of the solution you're replacing.
        // Pass the names of all those fields, for example both the key list and the value list.
        static bool StoresLegacyItemCounts(Component component, bool inherited,
            params string[] legacyFieldNames)
        {
            if (!inherited)
                return true;

            using (var serializedObject = new SerializedObject(component))
            {
                foreach (string fieldName in legacyFieldNames)
                {
                    SerializedProperty legacyField = serializedObject.FindProperty(fieldName);
                    if (legacyField == null)
                        continue;

                    if (legacyField.prefabOverride)
                        return true;

                    // A serializable class records overrides on its child properties.
                    // Use Next instead of NextVisible, because NextVisible skips fields that have the
                    // [HideInInspector] attribute, and the fields you're replacing often have it.
                    SerializedProperty end = legacyField.GetEndProperty();
                    SerializedProperty iterator = legacyField.Copy();

                    while (iterator.Next(true) && !SerializedProperty.EqualContents(iterator, end))
                    {
                        if (iterator.prefabOverride)
                            return true;
                    }
                }
            }

            return false;
        }
    }
```

The dependency order matters because Unity records an override by comparing the component against its source prefab. If the script migrates a prefab variant before the prefab it derives from, the variant doesn't inherit the entries yet, so Unity records the whole dictionary as an override on the variant. After that, if you change the dictionary on the source prefab, the variant keeps its own copy and doesn't receive the change. The same applies to nested prefabs, because a nested prefab instance takes its values from its source prefab, exactly as a variant does.

If a prefab variant already overrides the old field, the variant stores the value itself. In that case, the script migrates the variant too, and records a matching override of the new dictionary. Unity records that override by entry position, the same way as for lists, so adding or removing entries on the source prefab later can shift the override to a different entry. For more information, refer to [Overrides on arrays, lists, and dictionaries](/engine/6000.7/manual/working-with-gameobjects/prefabs/override/prefab-instance-overrides.md#collection-overrides).

Run the migration script once, then delete the old field. If you run the script again before you delete the old field, it doesn't copy anything as long as the new dictionary contains the same entries as the old field. If you edited the new dictionary in the meantime, the script overwrites your edit with the value from the old field.

### Migrate dictionaries in scenes ##scenes

Run the scene migration after the prefab migration. Scenes need the same check as prefabs, whether the component stores the value or inherits it. A scene object that isn't a prefab instance stores its own value, so the script migrates it. A prefab instance inherits its value from the prefab asset, so the script skips it unless it overrides the old field.

```cs
using UnityEditor;
    using UnityEditor.SceneManagement;
    using UnityEngine;
    using UnityEngine.SceneManagement;

    public static class InventorySceneMigration
    {
        [MenuItem("Tools/Migrate Inventory Dictionaries In Scenes")]
        static void MigrateInventoryDictionariesInScenes()
        {
            // Opening a scene discards unsaved changes in the current one.
            if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
                return;

            // Restore the scenes that were open when the migration finishes.
            SceneSetup[] sceneSetup = EditorSceneManager.GetSceneManagerSetup();

            try
            {
                // Only scenes under Assets. A scene in a read-only package can't be opened.
                foreach (string guid in AssetDatabase.FindAssets("t:Scene", new[] { "Assets" }))
                {
                    string path = AssetDatabase.GUIDToAssetPath(guid);
                    Scene scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
                    bool migratedAnything = false;

                    foreach (GameObject root in scene.GetRootGameObjects())
                    {
                        foreach (Inventory inventory in root.GetComponentsInChildren<Inventory>(true))
                        {
                            // The same per-component step as the prefab migration script.
                            if (!InventoryMigration.MigrateComponent(inventory))
                                continue;

                            // Unity saves a scene as a whole, so mark the changed object
                            // before you save the scene.
                            EditorUtility.SetDirty(inventory);
                            migratedAnything = true;
                        }
                    }

                    if (migratedAnything)
                    {
                        EditorSceneManager.MarkSceneDirty(scene);
                        EditorSceneManager.SaveScene(scene);
                    }
                }
            }
            finally
            {
                EditorSceneManager.RestoreSceneManagerSetup(sceneSetup);
            }
        }
    }
```

Because you migrated the prefabs first, a prefab instance in a scene inherits the entries and records only its own changes. Most scenes need no changes at all, because a prefab instance that overrides nothing inherits the migrated dictionary from its prefab.

## Additional resources

* [Dictionary serialization](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/dictionaries.md)
* [Serialization rules analyzer](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/analyzer.md)
* [Custom serialization](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/custom-serialization.md)
* [Dictionaries in prefabs](/engine/6000.7/manual/programming-environment/code-reload-serialization/script-serialization/dictionaries.md#dictionaries-in-prefabs)
* [Overrides on arrays, lists, and dictionaries](/engine/6000.7/manual/working-with-gameobjects/prefabs/override/prefab-instance-overrides.md#collection-overrides)
* [`ISerializationCallbackReceiver`](/engine/6000.7/script-reference/unityengine/iserializationcallbackreceiver.md)
