Dual Serialisation: Saving Monobehaviour and ScriptableObject At Runtime

All games, want to have a game engine that supports modding capacity, however… unity doesnt provide the tools (Runtime Inspector, Runtime Serialization).

99.999% of the unity answers say it is impossible and discourage you from going that direction. Go alone or go home. And judging from the outcome, whoever tried to “fix” unity serialization died in the end (example Vexe). Conclusion = go home (abandon) :frowning:

Yet the solution was given years ago : Unite 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution - YouTube

    public class TestDualSerialization : MonoBehaviour
        public string Json;
        public Object Object;

        [ContextMenu("ObjectToJson")] public void ObjectToJson()
            Json = JsonUtility.ToJson(Object);

        //must use FromJsonOverwrite for Monobehaviour or ScriptableObject
        [ContextMenu("ObjectFromJson")] public void ObjectFromJson()
            JsonUtility.FromJsonOverwrite(Json, Object);

I tried to code in the video, and it WORKS!!! So the question is, what are the limitations of this method, that everyone here undermines it ?

  1. Is the only problem the no “Reference-Sharing” capacity ? if so, I dont care, i could have a giant array with every object with ‘int’ array index as reference.

  2. Actually i might care. It creates references like “instanceID”: 12822 . Are these unique for each type?

Well, the JsonUtility is rather new when looking at the Unity history. (the talk you’ve linked was from Dec 2016 so not even a year ago).

The JsonUtility basically has the same [rules as the normal Unity serialization system][1] though with some restrictions.

As you said it does not support references to other UnityEngine.Object derived types as there is no general way how to get such references back during deserialization. You have to deserialize every UnityEngine.Object derived object on it’s own.

Instance IDs are only unique for one “session”. So deserializing a ScriptableObject at runtime will give that instance a new instance ID. The Instance ID actually is not serialized. It’s mainly used by the editor

I haven’t really used / tested the JsonUtility well to the bone. Though i hope that it also makes use of the [ISerializationCallbackReceiver interface][2]. It would allow to circumvent most limitations the serialization system has.

I may setup a test case and report back to give a certain answer on that matter.

So yes, the ISerializationCallbackReceiver interface does work when serializing / deserializing with the JsonUtility. So you can easily ship your own reference system if really necessary. Though the objects that you want to reference need to be managed in a static way. You can use a simple object tracker [like this one][3]. The only requirement is that each object you want to track has a unique ID string. You could simply use a GUID.

A tracked object would register “itself” to the tracker in “OnAfterDeserialize”. At this point the object has it’s ID loaded from the file (given your class has a string field for the ID). Likewise any class that holds references to other tracked objects will “request” an object reference based on a given ID. The Tracker allows “late binding”. So if you deserialize an object that has a reference to an object that hasn’t been loaded yet, it will initialize it once the object is registrated.

For a class to register itself to the tracker you would use:

ObjectTracker.Instance.AddObject(objID, this);

where “objID” would be the unique ID of this object.

To “deserialize” a reference you would use this pattern:

ObjectTracker.Instance.GetObject(refID, o=>yourMemberVariable = o);

Note that yourMemberVariable = o is the code that might be executed immediately or delayed. So make sure the code in that delegate is able to set the reference to the correct variable.

A very quick example:

public interface ITrackableObject
    string GetID();

public class TestObject : ScriptableObject, ITrackableObject, ISerializationCallbackReceiver
    public string myID;
    public List<ITrackableObject> someObjects;
    private List<string> m_TrackedIDs = new List<string>();
    public string GetID()
        return myID;

    public void OnAfterDeserialize()
        ObjectTracker.Instance.AddObject(myID, this);
        someObjects = new List<ITrackableObject>();
        for(int i = 0; i < m_TrackedIDs.Count; i++)
            int index = someObjects.Count;
            someObjects.Add(null); // add a null item to the list to prepare the "variable".
            ObjectTracker.Instance.GetObject(m_TrackedIDs*, o=> someObjects[index] = (ITrackableObject)o);*


public void OnBeforeSerialize()
foreach(var o in someObjects)
The “ITrackableObject” interface is not necessary but it provides a generic way for other classes to obtain the IDs of other objects they might reference.
Note: instead of adding a “null” item to the list and then using the index of that item in the delegate you could simply use:
ObjectTracker.Instance.GetObject(m_TrackedIDs*, o=> someObjects.Add( (ITrackableObject)o));
However this will not preserve the order of the objects in the list as the delegates might be called in a different order.
[1]: https://docs.unity3d.com/Manual/script-Serialization.html*_
_[2]: https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html*_
[3]: https://pastebin.com/T1g9HWpr*_