- Home /
Serializing specialized subclasses of generic classes not working
Hello!
I am working on an editor tool and in the process I stumbled upon the following problem with Unity's data serialization:
If we have a custom generic class like
public class CustomGenericClass<T> {...}
and a subclass
[System.Serializable]
public class SpecializedCustomClass : CustomGenericClass<int> {...}
then SpecializedCustomClass will NOT serialize! Contrary to the answer here for example.
Of course, I prepared an example to demonstrate the faulty behaviour. Let's start with a working example without generics:
SerializableDictionary is a Unity-serializable version of a dictionary:
using UnityEngine;
using System;
using System.Collections.Generic;
public class SerializableDictionary
{
[Serializable]
public struct SerializableKeyValuePair
{
public int key;
public int value;
}
public Dictionary<int, int> dic;
public SerializableKeyValuePair[] pairs;
public SerializableDictionary()
{
dic = new Dictionary<int, int>();
pairs = new SerializableKeyValuePair[0];
}
public void Serialize()
{
Array.Resize<SerializableKeyValuePair>(ref pairs, dic.Count);
int i = 0;
foreach (KeyValuePair<int, int> kv in dic) {
pairs[i].key = kv.Key;
pairs[i].value = kv.Value;
++i;
}
}
public void Deserialize()
{
dic.Clear();
foreach (SerializableKeyValuePair pair in pairs)
dic.Add(pair.key, pair.value);
}
}
We also have a ScriptableObject-derived class using the dictionary:
using UnityEngine;
using System;
using System.Collections.Generic;
public class SomeWindowData : ScriptableObject, ISerializationCallbackReceiver
{
public SerializableDictionary someSerializedDic;
public SomeWindowData()
{
someSerializedDic = new IntToIntDictionary();
}
public void OnBeforeSerialize()
{
someSerializedDic.Serialize();
}
public void OnAfterDeserialize()
{
someSerializedDic.Deserialize();
}
}
In this first case, SomeWindowData uses a public SerializableDictionary variable.
Then, we finally have a window class using SomeWindowData, to test the serialization by displaying the dictionary contents:
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
public class SomeWindow : EditorWindow
{
private const string DefaultAssetPath = "Assets/somewindowdata.asset";
private SomeWindowData windowData;
public static SomeWindowData CreateSomeWindowDataAssetAtPath(string path)
{
SomeWindowData windowData = ScriptableObject.CreateInstance<SomeWindowData>();
AssetDatabase.CreateAsset(windowData, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return windowData;
}
[MenuItem("Window/Some ****** Up Window")]
static void ShowWindow()
{
EditorWindow.GetWindow(typeof(SomeWindow), false, "****");
}
void OnEnable()
{
windowData = AssetDatabase.LoadAssetAtPath(DefaultAssetPath, typeof(SomeWindowData)) as SomeWindowData;
if (!windowData) {
windowData = CreateSomeWindowDataAssetAtPath(DefaultAssetPath);
generateKeys(10);
}
}
void OnGUI()
{
foreach (KeyValuePair<int, int> kv in windowData.someSerializedDic.dic) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.IntField(kv.Key);
int newValue = EditorGUILayout.IntField(kv.Value);
EditorGUILayout.EndHorizontal();
if (newValue != kv.Value) {
windowData.someSerializedDic.dic[kv.Key] = newValue;
EditorUtility.SetDirty(windowData);
break;
}
}
}
public void generateKeys(int n)
{
if (windowData.someSerializedDic.dic == null)
windowData.someSerializedDic.dic = new Dictionary<int, int>();
windowData.someSerializedDic.dic.Clear();
for (int i = 1; i <= n; ++i)
windowData.someSerializedDic.dic.Add(i, i*i);
EditorUtility.SetDirty(windowData);
}
}
The window displays the key-value pairs and allows you to change the value entries (not the keys). It also has a generator function, which creates n keys for the dictionary. This example code is solely for testing the serialization.
To test the code, create a new project, put above three classes in separate .cs files and inside an Editor folder and you should see a new Window menu entry. Clicking on it, an asset named "Assets/somewindowdata.asset" is created and the following window should appear:
You can alter the values (right column) and entering/exiting play mode and/or restarting the Unity editor will save the values and you should see the same window again. So - serialization is working!
Now we move to subclasses of generic classes. As a transition, we slightly alter the class hierarchy to prepare for the next case and confirm that the serialization of subclasses works correctly.
We add following class:
[System.Serializable]
public class IntToIntDictionary : SerializableDictionary {}
and we adjust the SomeWindowData class to use the IntToIntDictionary subclass:
using UnityEngine; using System; using System.Collections.Generic;
public class SomeWindowData : ScriptableObject, ISerializationCallbackReceiver
{
public IntToIntDictionary someSerializedDic;
public SomeWindowData()
{
someSerializedDic = new IntToIntDictionary();
}
public void OnBeforeSerialize()
{
someSerializedDic.Serialize();
}
public void OnAfterDeserialize()
{
someSerializedDic.Deserialize();
}
}
We delete the somewindowdata.asset file and open our window again. The asset file gets created again, We set some values, restart Unity and all is well. Again, serialization working as expected.
Now it is time to move to the generic base class. We make the SerializableDictionary class generic with Key and Value type parameters:
using UnityEngine;
using System;
using System.Collections.Generic;
public class SerializableDictionary<T, U>
{
[Serializable]
public struct SerializableKeyValuePair
{
public T key;
public U value;
}
public Dictionary<T, U> dic;
public SerializableKeyValuePair[] pairs;
public SerializableDictionary()
{
dic = new Dictionary<T, U>();
pairs = new SerializableKeyValuePair[0];
}
public void Serialize()
{
Array.Resize<SerializableKeyValuePair>(ref pairs, dic.Count);
int i = 0;
foreach (KeyValuePair<T, U> kv in dic) {
pairs[i].key = kv.Key;
pairs[i].value = kv.Value;
++i;
}
}
public void Deserialize()
{
dic.Clear();
foreach (SerializableKeyValuePair pair in pairs)
dic.Add(pair.key, pair.value);
}
}
We adjust our derived class:
[System.Serializable]
public class IntToIntDictionary : SerializableDictionary<int, int> {}
The type of our SomeWindowData member is already set to IntToIntDictionary, so we should be ready to go. Again, we delete the somewindowdata.asset file and open another window. We modify some values, restart Unity and AAAAAAARGGHHH!!! We see an empty window appearing, meaning our data was not stored - serialization is broken!!!
Now my question is "WHY"?
A few important notes:
- [Serializable] attribute
+++ In the first case, it is enough to put the attribute on the SeralizedDictionary class for serialization to work.
+++ In the second case, you can omit the attribute on the SerializedDictionary and only put it on the IntToIntDictionary subclass for serialization to work.
+++ In the third example, even putting the attribute on all classes didn't make the magic happen...
- Number of generic type parameters
+++ Just in case, I tried with only one type parameter, creating a dictionary instead of . The result is the same - serialization does not work!
- But it should work!
+++ Again, according to this post it should work! For example, according to the answers of the gentlemen "andrey_bryzgalov" (top answer, 14 up-votes) and "Immanuel Scholz" this should be the way to do it and it should work without problems. Yet, this is not the case!
+++ Let's assume the above would work, would it have been so hard for Unity's programmers to make some preprocessing and to create the empty subclasses on the fly before compiling? A small and simple step, but it would allow you to serialize custom generic classes!
+++ Again, if this would actually not fail, I could put the subclass definition inside the SomeWindowData class as a nested class and omit the creation of a two-line source file.
So why is this not working? Am I doing something wrong? Did this work at some point in time with some versions of Unity, but is broken now? Also, in given link above, there were a few people complaining that it doesn't work, but either no one answered this or it was nothing constructive. So is this being effectively ignored by Unity Team?
For now, I will just duplicate the SerializableDictionary code for each specialization, at least at that level it works. Unity's crappy and unstable serialization really humbles you and you are even happy with some working fragments :)
So anyway, I would love to hear some opinions, suggestions and hopefully solutions of the described problem. Thanks in advance, everyone! And sorry for the long post :)
For information, the code was tested (and failure confirmed) on Unity v4.6.3f1 and v5.0.1f1.
I know this is an old thread. I've just ran into the same issue and I don't know how to solve it. Have you managed to find a solution?