How can I make a SerializedProperty UnityEvent persist within a CustomInspector editing a ScriptableObject?

Question: With the following CustomInspector setup, the user can edit a UnityEvent. However once the inspected object is deselected in the hierarchy then reselected, the changes made by the user have been saved the ScriptableObject but are reset in the inspector. I need the changes to persist within the inspector.

I am going to use modified “Intractable” code from the comments because I think it best represents my setup:

Intractable.cs:

[Serializable]
public class Interactable : MonoBehaviour
{
    [SerializeField]
    public InteractableScriptableObject mySO;
}

InteractableScriptableObject:

[Serializable]
public class InteractableScriptableObject : ScriptableObject
{ 
    public List<InteractableCustomType1> customType1List;
}

InteractableCustomType1:

[Serializable]
public class InteractableCustomType1 : IComparable<InteractableCustomType1>
{

    public int myInt;
    public List<InteractableCustomType2> myCustomType2List = new List<InteractableCustomType2>();

    public InteractableCustomType1(int customInt, List<InteractableCustomType2> customType2List)
    {
        myCustomType2List = customType2List;
        myInt = customInt;
    }

    //Required by IComparable.
    public int CompareTo(InteractableCustomType1 other)
    {
        if (other == null)
        {
            return 1;
        }

        return 0;
    }
}

InteractableCustomType2:

[Serializable]
public class InteractableCustomType2 : IComparable<InteractableCustomType2>
{

    public string myString;
    public UnityEvent myUnityEvent;

    public InteractableCustomType2(string customString, UnityEvent customEvent)
    {
        myString = customString;
        myUnityEvent = customEvent;
    }

    //Required by IComparable.
    public int CompareTo(InteractableCustomType2 other)
    {
        if (other == null)
        {
            return 1;
        }

        return 0;
    }
}

InteractableInspector:

[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
    public override void OnInspectorGUI()
    {
        Interactable targetInteractable = (Interactable)target;

        if(targetInteractable != null)
        {
            InteractableScriptableObject SO = targetInteractable.mySO;

            if (GUILayout.Button("Create ScriptableObject"))
            {
                targetInteractable.mySO = CreateInstance<InteractableScriptableObject>();
            }

            if (SO.customType1List != null)
            {
                if (GUILayout.Button("Add a customType1"))
                {
                    SO.customType1List.Add(new InteractableCustomType1(0, new List<InteractableCustomType2>()));
                }

                if (SO.customType1List.Count > 0)
                {
                    for (int x = SO.customType1List.Count - 1; x >= 0; x--)
                    {
                        InteractableCustomType1 customType1 = SO.customType1List[x];//So it is easier to type out.

                        if (GUILayout.Button("Add a customType2"))
                        {
                            customType1.myCustomType2List.Add(new InteractableCustomType2("string", null));
                        }

                        if (customType1.myCustomType2List.Count > 0)
                        {
                            for (int y = customType1.myCustomType2List.Count - 1; y >= 0; y--)
                            {
                                SerializedObject serializedObject = new SerializedObject(targetInteractable.mySO);
                                SerializedProperty myCustomType1 = serializedObject.FindProperty("customType1List");
                                SerializedProperty customType1Element = myCustomType1.GetArrayElementAtIndex(x);
                                SerializedProperty targetCustomType2 = customType1Element.FindPropertyRelative("myCustomType2List").GetArrayElementAtIndex(y);
                                SerializedProperty myUnityEvent = targetCustomType2.FindPropertyRelative("myUnityEvent");
                                EditorGUILayout.PropertyField(myUnityEvent);

                                if (GUI.changed == true)
                                {
                                    serializedObject.ApplyModifiedProperties();
                                }
                            }
                        }
                    }
                }
                
            }
            else
            {
                SO.customType1List = new List<InteractableCustomType1>();
            }
        }
    }

}

The code I’m working with is so complex that I think my previous examples were confusing, from here with the new example; what would I change to make the changes made to the UnityEvent persist so the properties that were applied to the ScriptableObject are displayed again on the UnityEvent when reselected in the hierarchy.

Try this instead:

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
    SerializedProperty m_InteractableScriptableObjectProp;
    SerializedObject m_SerializedInteractableScriptableObject;

    void OnEnable()
    {
        m_InteractableScriptableObjectProp = this.serializedObject.FindProperty("mySO");
        if (m_InteractableScriptableObjectProp.objectReferenceValue != null)
        {
            m_SerializedInteractableScriptableObject =
                new SerializedObject(m_InteractableScriptableObjectProp.objectReferenceValue);
        }
    }

    public override void OnInspectorGUI()
    {
        this.serializedObject.Update();
        if (m_InteractableScriptableObjectProp.objectReferenceValue == null)
        {
            if (GUILayout.Button("Create ScriptableObject"))
            {
                var newInstance =CreateInstance<InteractableScriptableObject>();
                m_InteractableScriptableObjectProp.objectReferenceValue = newInstance;
                Undo.RegisterCreatedObjectUndo(newInstance, "Create New ScriptableObject");
                this.serializedObject.ApplyModifiedProperties();
                m_SerializedInteractableScriptableObject = new SerializedObject(newInstance);
                GUIUtility.ExitGUI();
            }
        }
        else
        {
            m_SerializedInteractableScriptableObject.Update();
            var customType1List = m_SerializedInteractableScriptableObject.FindProperty("customType1List");

            if (GUILayout.Button("Add a customType1"))
            {
                ++customType1List.arraySize;
                customType1List.serializedObject.ApplyModifiedProperties();
                customType1List.serializedObject.Update();
                GUIUtility.ExitGUI();
            }

            for (int t1Idx = customType1List.arraySize - 1; t1Idx >= 0; --t1Idx)
            {
                var t1Element = customType1List.GetArrayElementAtIndex(t1Idx);
                var customType2List = t1Element.FindPropertyRelative("myCustomType2List");

                if (GUILayout.Button("Add a customType2"))
                {
                    ++customType2List.arraySize;
                    customType2List.serializedObject.ApplyModifiedProperties();
                    var newElement = customType2List.GetArrayElementAtIndex(customType2List.arraySize - 1);
                    newElement.FindPropertyRelative("myString").stringValue = "string";
                    customType2List.serializedObject.ApplyModifiedProperties();
                    customType2List.serializedObject.Update();
                    GUIUtility.ExitGUI();
                }

                for (int t2Idx = customType2List.arraySize - 1; t2Idx >= 0; --t2Idx)
                {
                    var element = customType2List.GetArrayElementAtIndex(t2Idx);
                    var unityEvent = element.FindPropertyRelative("myUnityEvent");
                    EditorGUILayout.PropertyField(unityEvent);
                }
            }

            m_SerializedInteractableScriptableObject.ApplyModifiedProperties();
        }
    }
}

So if i understand that right you actually use a custom inspector of a different class(probably a MonoBehaviour?) to edit a completely different (referenced) ScriptableObject asset?

Well that’s a problem because Unity’s “SerializedObject” concept seems to have problems when the object that is altered isn’t currently inspected. That’s why you need to manually set it dirty by using.

EditorUtility.SetDirty( yourScriptableObject );

after you’ve applied some changes.

We had quite a few similar questions recently. Even it’s a more edge-case problem it would be great when they could fix it.

After finally realizing what @FirefightGI was experiencing, I too could very easily replicate his problem. There is little-to-no documentation on this specific thing, and I actually found a solution. The problem wasn’t solely a call to SetDirty, but the following code PROPERLY serializes the event data. So I do apologize for my initial commented answer about it working, that surely did not work.
I even walked the entire event system with serializedProperty.Move(). Even altering the data from the actual input zones didn’t help. SO here is a working solution sir.

    [CustomEditor(typeof(Interactable))]
    public class InteractableInspector : Editor
    {
        private Interactable interactable;
        private SerializedProperty onInteract;
        private SerializedObject serialObject;
        public override void OnInspectorGUI()
        {
            if (interactable == null)
            {
                interactable = target as Interactable;
            }
            if (serialObject == null)
            {
                serialObject = new UnityEditor.SerializedObject(interactable);
            }
            if (onInteract == null)
            {
                onInteract = serialObject.FindProperty("OnInteract");
            }
            EditorGUILayout.PropertyField(onInteract);
            onInteract.serializedObject.ApplyModifiedProperties();
            EditorUtility.SetDirty(interactable);
            onInteract.serializedObject.UpdateIfDirtyOrScript();
        }

    }

The actual problem?
Well Unity was not serializing the event handler when the event was altered, only when added. So adding a new event and removing it, would serialize the event handler, but this is of course not optimal. So we have to force it to serialize every Update or it will completely miss changes made to the serialized child properties of each event Call.
This may not be the most optimal solution, but it is certainly better than walking the entire SO and checking for changes.