This is a question often seen out there. Maybe you already know that Unity doesn’t display, or serialize, dictionaries in the inspector but it’s so easy to forget. You’re planning a system and you want to group up user entered data or make things easy to review just by having the data visible right in the editor. This article will give you two great solutions that you can do very easily, and now another solution that is a bit more complicated but with the code provided already is easiest to use.
Solution 1:
Add-on assets are not needed here. There ARE assets that will allow you to serialize dictionaries in the editor which use lists in the background by adding an attribute or creating a specific type. If you look at the code, it’s quite long and a lot of it is for drawing the custom inspector. There are also intricacies for using these assets. Such as needing to define each dictionary type specifically, required to inherit from another class, has to implement an interface, prefab compatibility, working with custom inspectors…
I like simple, and I like for Unity to handle as much of it as possible. To that end, the solution is to just create a class or struct as a data container and make a List of that class or struct. Preferably using generics which I’ll provide in the templates below. Where this becomes a better solution is that this new class can hold all the data you want and as long as the types can be serialized individually by Unity, then it will all be displayed in the inspector. Then you maintain fast dictionary lookups by writing a quick foreach loop that will insert the data into a dictionary, in a way that works best for you to use at runtime. I’ll provide an example below.
Very quickly let’s discuss when to choose a class or a struct. You can look up the guidelines with a google search but you’ll find that only a couple of the rules really apply as most are routinely broken. The main one broken being the data size in a struct.
From what I can parse, the most important aspect is usage. Passing data around and data types.
How will this new class or struct be used? If the data will be passed around then use a class. If the data will not be passed around then use a struct. The reason being, a List of structs will be a continuous chunk of memory that does not add overhead to garbage collection. But will be copied in full when passed elsewhere. A class on the other hand will be a List that points to different chunks of memory where the class data exists on the heap. When passed there is no copying of the data, just of the reference pointer.
Ok let’s show some code examples.
Struct Template
[Serializable]
public struct TwoStruct<T1, T2> {
// Constructor
public TwoStruct(T1 item1, T2 item2) => (Item1, Item2) = (item1, item2);
// Serialized Public Properties
[field: SerializeField] public T1 Item1 { get; private set; }
[field: SerializeField] public T2 Item2 { get; private set; }
// Deconstructor
public void Destructor(out T1 item1, out T2 item2) => (item1, item2) = (Item1, Item2);
}
Class Template
[Serializable]
public class TwoClass<T1, T2> {
// Constructor
public TwoClass(T1 item1, T2 item2) => (Item1, Item2) = (item1, item2);
// Serialized Public Properties
[field: SerializeField] public T1 Item1 { get; private set; }
[field: SerializeField] public T2 Item2 { get; private set; }
// Deconstructor
public void Destructor(out T1 item1, out T2 item2) => (item1, item2) = (Item1, Item2);
}
Using this template you could come back and specify types or rename the field and item names which will display in the inspector. Such as “Key” & “Value”, or the class name could be “WeaponList” with a name variable for the key… What I like to do is pair this with a code snippet extension for Visual Studio called Snippet Designer, that I found and have these ready to go. In the end it looks like this:

Code Setup
[SerializeField]
private List<TwoStruct<WindowKey, CanvasGroup>> windows
= new List<TwoStruct<WindowKey, CanvasGroup>>();
private Dictionary<WindowKey, CanvasGroup> windowLookup;
...
private void Awake() {
windowLookup = new Dictionary<WindowKey, CanvasGroup>();
foreach (var entry in windows) {
windowLookup[entry.Item1] = entry.Item2;
}
}
Declare our list and dictionary as normal. Then in Awake we filter that list, in any way we would like, into the dictionary for lookups. It’s really very simple and more just knowing the road to take to reach where you want to get. Just make sure all your keys end up being unique. The method used above will insert the dictionary entry and if it already exists, it will just set the new value to that key. If you need to do checking or want errors to be thrown use “.contains” or “.Add” as needed.
Solution 2:
A mirror of the C# generic Dictionary called InspectableDictionary which is used in exactly the same way as normal. You create the class InspectableDictionary, implement the IDictionary interface, add the serialized lists and a way to sync back and forth to the dictionary for editor set values(not in play mode) and inspection as things are added by script in play mode. No custom editor, no attributes, type safety. This is a wrapper class on the standard Dictionary.
Inspectable Dictionary
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace BuildingBlocks.DataTypes {
/// <summary>Same usage as generic dictionary. But will create serialized fields in the inspector for this dictionary.<br> Will synchronize dictionary and inspector during play mode for inspection. No manual adding in editor during play mode.</br></summary>
[Serializable]
public class InspectableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IEnumerable {
//---
// Inspectable Lists
[SerializeField, Tooltip("Items can be added in the inspector, only prior to play mode not at runtime, and will be exposed through a dictionary.")]
private List<TKey> ItemKeys = new List<TKey>();
[SerializeField, Tooltip("Items can be added in the inspector, only prior to play mode not at runtime, and will be exposed through a dictionary.")]
private List<TValue> ItemValues = new List<TValue>();
//---
// Internal Use
private bool inspectableSync;
private readonly Dictionary<TKey, TValue> lookup = new Dictionary<TKey, TValue>();
public TValue this[TKey key] {
get {
InspectorToDictionary();
return lookup[key];
}
set {
lookup[key] = value;
DictionaryToInspectorAdditive(key, value);
}
}
public TValue this[int index] {
get {
DictionaryToInspector();
return ItemValues[index];
}
set {
ItemValues[index] = value;
inspectableSync = false;
InspectorToDictionary();
}
}
//---
// Synchronization Methods
/// <summary>Copies in full the dictionary as lists to the inspector lists.</summary>
private void DictionaryToInspector() {
ItemKeys.AddRange(lookup.Keys.ToList());
ItemValues.AddRange(lookup.Values.ToList());
}
/// <summary>Adds a Key-Value pair to the inspector lists.</summary>
private void DictionaryToInspectorAdditive(TKey key, TValue value) {
ItemKeys.Add(key);
ItemValues.Add(value);
}
/// <summary>Removes a Key-Value pair to the inspector lists.</summary>
private void DictionaryToInspectorNegative(TKey key, TValue value) {
ItemKeys.Remove(key);
ItemValues.Remove(value);
}
/// <summary>Clears dictionary, rebuilds from serialized inspector lists. Removes duplicates by copying back to lists.</summary>
private void InspectorToDictionary() {
if (inspectableSync is false) {
lookup.Clear();
for (int i = ItemKeys.Count - 1; i >= 0; i--) {
var item = ItemKeys[i];
var value = ItemValues[i];
if (item != null && value != null) {
lookup[item] = value;
}
else {
ItemKeys.RemoveAt(i); ItemValues.RemoveAt(i);
}
}
ItemKeys = lookup.Keys.ToList();
ItemValues = lookup.Values.ToList();
inspectableSync = true;
}
}
//---
// IDictionary, ICollection, IEnumerable
public bool IsFixedSize => false;
public bool IsReadOnly => false;
public ICollection<TKey> Keys => lookup.Keys;
public ICollection<TValue> Values => lookup.Values;
public int Count => lookup.Count;
public void Add(TKey key, TValue value) {
InspectorToDictionary();
lookup.Add(key, value);
DictionaryToInspectorAdditive(key, value);
}
public void Clear() {
lookup.Clear();
ItemKeys.Clear();
ItemValues.Clear();
}
public bool Contains(KeyValuePair<TKey, TValue> item) {
InspectorToDictionary();
return lookup.Contains(item);
}
public bool ContainsKey(TKey key) {
InspectorToDictionary();
return lookup.ContainsKey(key);
}
public bool TryGetValue(TKey key, out TValue value) {
InspectorToDictionary();
var exists = lookup.TryGetValue(key, out TValue backer);
value = backer;
return exists;
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
InspectorToDictionary();
return lookup.GetEnumerator();
}
public bool Remove(TKey key) {
var success = lookup.Remove(key);
DictionaryToInspectorNegative(key, lookup[key]);
return success;
}
IEnumerator IEnumerable.GetEnumerator() {
InspectorToDictionary();
return lookup.GetEnumerator();
}
public void Add(KeyValuePair<TKey, TValue> item) {
lookup.Add(item.Key, item.Value);
DictionaryToInspectorAdditive(item.Key, item.Value);
}
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) {
foreach (var kvp in lookup) {
array[arrayIndex++] = kvp;
}
}
public bool Remove(KeyValuePair<TKey, TValue> item) {
var success = lookup.Remove(item.Key);
DictionaryToInspectorNegative(item.Key, item.Value);
return success;
}
}
}