Custom Serialization with ISerializationCallbackReceiver

Starting from Unity 4.5, any class can be made to inherit from ISerializationCallbackReceiver. This allows us to write methods that can be used to control seialization and deserialization, and it demands from the class to implement two public methods: OnBeforeSerialize() and OnAfterDeserialize(). These methods are automatically called when that class inherits from UnityEngine.Object or is marked with the [Serializable] attribute.

The idea behind this concept is both simple and powerful, but there are certain things you need to have in mind. Not only I will show you how to use this interface, I will also provide some information on things you should avoid or on things that won’t work as expected.

As we have seen in this article, serialization and deserialization can happen more often than we realize. Incorrect serialization can pose a problem when:

  • Saving the scene data (and carrying it into the game).
  • Instantiating objects.
  • Hot-reloading the code (i.e. changing code while the game is running).

In most cases you never have to implement your own editor, and if the default inspector displays data then you know that this data is serializable and will carry well into the new instance. Even if you do implement your own editor, it’s usually easy to make sure that most data can be serialized (see: C# Properties in Unity). Any data that cannot be serialized by default or by any conventional means (for example, delegates aren’t easy to serialize/deserialize) can also be generated in the Awake() or Start() methods. Generating non-serializable data during runtime seems to be a common practise among Unity folk.

You can see how the first two points above are easy to fix, but the third one is not: The Awake() and Start() methods are guaranteed to run only once during gameplay. If you change your code and then resume the game, any references to non-serializable data will become null or be reset to default values, which makes changing code while the game is running practically useless. You might not think this is a big deal at first, but you’ll actually waste so much time because of it that it’s better that you sort out this problem as soon as possible.

This is where ISerializationCallbackReceiver comes into play. In short, this is the process that you should follow:

  1. Save non-serializable data to equivalent fields that are serializable.
  2. Use serialized fields to generate non-serializable data.

The Dictionary example.

The Unity documentation has a good example on how you can use it to serialize a Dictionary<TKey, TValue>, which is (at the moment) not supported by default:

Since List<T> is serializable (as long as T is), we save the keys and values to lists. After deserialization, we use this data to construct the dictionary. This is fairly straightforward and there are no issues with it.

The only problem is that of performance. A dictionary with a built-in serialization would have been much faster at deserialization than manually creating it. This really shouldn’t concern you; the code to serialize/deserialize is not called often during the game (when performance matters). When it does get called, it’s usually not for any objects that contain a lot of data. If you do instantiate a lot of objects with a bunch of expensive-to-serialize data per instance, then your game is not engineered very well in the first place.

A serializable dictionary is very handy to have around, so lets make it its own class:

This works great, except it has a problem: List<T> is the only generic class that can be serialized by Unity. Anything else is non-serializable, even your custom classes. To use this in a class is simple:

  1. Inherit your generic class to a non-generic version (this needs not be public).
  2. Mark it as Serializable.  I tend to habitually forget this step, it’s easy to miss!.

So, now that everything works great, let us try using this class!

Note that the highlighted line (line 12) returns a StringIntDictionary instead of a Dictionary<string, int> . This is an important detail, and it is explained a bit in this article. Your code will still work, and it’d actually be nice to return a Dictionary instead (and keep StringIntDictionary hidden to avoid confusion from the user). The problem is when someone tries to keep a reference to the dictionary; if they do and store it as a Dictionary<string, int>, it won’t be correctly serialized.

Unfortunately, there’s nothing that can prevent someone from taking a StringIntDictionary and assigning it to a Dictionary<string, int> reference, so this is still not fool-proof. One thing we can do is to keep the actual dictionary completely hidden as part of our custom class, so that it will not be castable to a dictionary. However, we miss functionality there (such as LINQ extension methods) and we’ll be required to implement our own methods (which most of the time will just expose the dictionary methods), so that has its own problems.

Excluding this detail, everything will just work as expected now. Or will it?

Dealing with unpredictability

Now, consider the scenario in which we actually want to access the dictionary during OnAfterDeserialize(). Why? Well, to be honest, if you need to do that then you went deep. But you might need, for example, to access the dictionary of another script. There is a problem in this scenario, which is that we don’t know if the OnAfterDeserialize() method had been called for the dictionary yet! Indeed, the order can be quite unpredictable. It’s not exactly random, I believe, as the order will always be the same assuming the same scene layout, but it is undefined and should not be relied on.

One way to deal with this is to slightly modify our dictionary class to be something like this:

If the whole game is serializing/deserializing (for example, during hot-reloading), IsSerializing will return true. If you just insantiated an object during runtime that needs to access this dictionary (in which case the dictionary is in a valid state), then IsSerializing  will return  false. So, you can do this:

Sure, it’s not very optimized and the linear search beats the purpose of having a dictionary, but this is only during code reload. There won’t be a linear search during actual gameplay, the much preferred dictionary lookup will be used instead.

Therefore, if you do need to use deserialized data of another object during OnAfterDeserialize(), you can try implementing something similar to the above.

Some additional guidelines.

This is not over yet.

  • Do not depend on the order of serialization when programming OnBeforeSerialize(), even for objects which don’t implement the callback receiver.
  • When programming OnAfterDeserialize(), do not assume that any class which also implements the callback receiver has finished deserializing.
  • The order of serialization is not the same as the order of deserialization. If an object was the very first to be serialized, it doesn’t mean it will also be the first to deserialize.
  • Classes that do not inherit from MonoBehaviour, ScriptableObject (or  UnityEngine.Object  in general), aren’t very friendly with null references. Don’t check for null equivalency while serializing/deserializing, because the result will be unpredictable. Even if you do set the value to null before serializing or after deserializing, it will somehow end up with a non-null value eventually.
  • Do not call any Unity methods while using the callback receiver. Sadly, checking for equivalency between Unity objects is one of them, because even the == operator is overloaded to call a Unity method in the background. If you do call Unity methods, you’ll get an exception in certain cases.

Leave a Reply