Why am I seeing partial serialization of private members?

I’ve been encountering a strange problem that goes against my expectation and understanding of script serialization in Unity.

When I define private member variables in my MonoBehaviour or ScriptableObject classes, unless I explicitly attribute them NonSerializable, Unity is holding onto the data during hot reloading (after recompiling scripts).

Consider the following example:

using System;
using UnityEngine;

[ExecuteInEditMode]
public class SerializationTest : MonoBehaviour
{
    //[NonSerialized]
    private float value = 0f; 

    private void Awake()
    {
        Debug.Log($"Awake:{value}");
    }

    private void OnEnable()
    {
        Debug.Log($"OnEnable:{value}");
        value = UnityEngine.Random.value * 100f;
    }
}

By default, I would expect the private member ‘value’ not to be serialized and revert to 0, which is true when the scene is reloaded or entering play mode. However, after script recompilation the value persists and does not reset to 0, unless I specifically attribute it with [NonSerialized].

If you test the code above with NonSerialized commented out, you’ll see in the console that OnEnable is called and still has the previously assigned value, whereas if NonSerialized is uncommented the value is 0 (as is expected).

It seems there is a middle ground where the value is partially serialized (or at least stored by Unity) during script recompilation only. I have also noticed that private members are visible in the Inspector when debug mode is enabled, except if the NonSerialized attribute is applied.

This behavior has caused some bugginess in my code due to objects holding onto old values, leading to a state of partial initialization. The IDE may suggest [NonSerialized] is unnecessary for private members, however what I am experiencing is contrary to that idea and therefore to ensure my private data is fully reset upon reload I must qualify the members with the NonSerialized attribute.

I don’t recall this being the case in earlier versions of Unity, though it may have been something I overlooked. Here is the related docs which as far as I have found doesn’t explain the behavior I am seeing.

Does anyone have any insight into this? For now, my workaround is simply to be very explicit with my attributes in all variable declarations.

Remove ExecuteInEditMode. It causes OnEnable to run and setting the value of the member value.
If you don’t execute it in the editor after a domain reload, it will fall back to zero.

Using ExecuteInEditMode is required for most of my scripts since they operate in edit mode for authoring functionality, so that is non-negotiable and also necessary to demonstrate the point I am making.

OnEnable first outputs to the console the existing ‘value’ before assigning a new random value. So the first time you’ll get console output of “OnEnable:0” (as expected), but then each time thereafter recompiling scripts it outputs “OnEnable:83.32” or whatever random value was assigned previously. It should be 0 every single time if the value is not serialized.

The underlaying issue is that Unity is holding the private value even though technically it should not be. Using the NonSerialized attribute fixes it. I just want to understand why. I presume that Unity is copying component values using reflection or something during hot reloading, rather than reloading the scene, since OnAwake is not called after script recompile.

That behaviour, while very surprising and kinda illogical, is actually documented and by design. Quote from the docs:

Source: Unity - Manual: Script serialization

Though, as you have mentioned, it sadly does not explain WHY it’s done like that.

2 Likes

Ah thank you! I completely overlooked that part. At least I know now and it is for sure a documented and intended behavior. It’s an odd case, but I assume there is a good reason for it.

1 Like