How to get ISerialization Callback Receiver's SerializedObject?

I have a regular class that implements ISerializationCallbackReceiver. The class inherits from System.Object, not MonoBehaviour or ScriptableObject. The class is intended to exist/serialize within a MonoBehaviour or ScriptableObject.

My question is: How can I get a reference to the SerializedObject that contains my class inside ISerializationCallbackReceiver’s OnBeforeSerialize and OnAfterDeserialize functions?

Assume there’s an issue during serialization – I want to call Debug.Log() with the context of the responsible object so developers can easily locate the source of the problem. I’ll even settle for just knowing the object’s path in the scene hierarchy.

Is it possible?

// DataHolderBehaviour.cs
public class MyDataHolderBehaviour : MonoBehaviour
{
    [SerializeField]
    protected MyData m_Example = default;
}
// Data.cs
[System.Serializable]
public class MyData : ISerializationCallbackReceiver
{
    public void OnBeforeSerialize()
    {
        Object context = // ???
        Debug.LogError("How can I get the SerializedObject for the object that owns MyData so its context can be logged here?", context);
    }

    public void OnAfterDeserialize()
    {
        // And here?
    }

    protected int m_SomeNonSerializedData;
}

The short answer is: You can’t. The issue here is that those callbacks are called whenever the object is serialized / deserialized. It’s actually called from native code from a separate thread. So you can not even interact with any of the Unity API from inside those callbacks. Those callbacks only purpose is to translate data that is not serializable into serializable data and vice versa.

Serialization in Unity does not necessarily originate from the managed code and does not necessarily involve a SerializedObject instance.

Apart from that seralizable classes which are not UntiyEngine.Object derived types (so neither MonoBehaviour nor ScriptableObject) do not have a SerializedObject but are represented by a SerializedProperty which would be part of the SerializedObject it is serialized under. Though as I said, inside those callbacks you can not access any of that and even if you could, you would run into race conditions / threading issues.

The best option would be to implement a custom property drawer for your type.

Here’s a kludge I found that offers at least some context about the object being (de)serialized. It lacks the ping feature when the log message is clicked and it does not log the object’s hierarchy/project path. This means it may still be challenging to locate the object in a busy project, but it’s better than nothing.

The trick is to intentionally call a scripting API, like GameObject.Find(), within ISerializationCallbackReceiver’s callbacks. This raises a UnityException holding a message with the type and object name of the offending instance:

UnityException: Find is not allowed to be called during serialization, call it from Awake or Start instead. Called from MonoBehaviour ‘MyDataHolderBehaviour’ on game object ‘MyTestObject’.
See “Script Serialization” page in the Unity Manual for further details.

This error is described in the Manual’s Script Serialization - Script serialization errors page, so some of us can pretend it’s “documented” :wink:

Until I learn how to submit feature suggestions to Unity, this is my solution. (Avert your eyes, @Bunny83!)

/// <summary>
///     Attempt to build a string containing the type and name of the
///     <see cref="UnityEngine.Object"/> that is actively being
///     (de)serialized.
///     <br /><br />
///
///     This function will only succeed if it is called during
///     serialization (e.g., from within <see
///     cref="ISerializationCallbackReceiver.OnBeforeSerialize"/>).
/// </summary>
/// <returns>
///     If serialization is occurring, return a string in the format:
///     "Called from MonoBehaviour '{TypeName}' on game object
///     '{GameObjectName}'". Otherwise, return <see langword="null"/>.
/// </returns>
public static string ExtractSerializationContext()
{
    string serializationContext = null;

    try
    {
        // If this executes during an ISerializationCallbackReceiver
        // callback, GameObject.Find will throw a UnityException, which
        // in this case, is desired. The exception is caught and its
        // message is parsed to obtain the context of the
        // (de)serializing object. This context is otherwise
        // inaccessible to us.

        GameObject.Find(null);
    }
    catch (UnityException e)
    {
        string exceptionMessage = e.Message;

        // Extract the serialization context from the exception
        // message.
        Match errorContextMatch =
            Regex.Match(exceptionMessage, @"Called from [^\r\n]+");

        if (errorContextMatch.Success)
        {
            serializationContext = errorContextMatch.Value;
        }
    }

    return serializationContext;
}

And it is used like this:

public void OnAfterDeserialize()
{
    string errorContext =
        MelcherUnityUtility.ExtractSerializationContext();

    Debug.LogError("Error! " + errorContext);

    // Prints: "Error! Called from MonoBehaviour '<TypeName>' on game object '<GameObjectName>'."
}

This trick appears to only work in OnAfterDeserialize. For OnBeforeSerialize, I’m able to use UnityEditor.Selection.activeObject to get the current object.