[SerializeReference] data loss when class name is changed

Unity 2019.3b1 [SerializeReference] data is stored by class name. If the class name changes, the reference will be lost.
Is it possible to store by GUID instead of the class name? Or have other better ways to prevent loss?

2 Likes

Good catch. Unity Technologies, please support a mechanism that allows us to refactor code without breaking assets.

There isn’t a GUID for a type. Maybe support FormerlySerializedAs on class?

1 Like

Add a restriction: if the class wants to be referenced by [SerializeReference], the class must correspond to a script. Just like a MonoBehaviour.

1 Like

Please file a bug report so we can track the problem, and we’ll look at what we want to do about it.

Here is some bacon for you:
(Case 1180719) [SerializeReference] data loss when class name is changed

2 Likes

QA was able to reproduce the issue and it can be found in the public issue tracker now:

Thanks!

2 Likes

5260559--526181--1.png
I tested in 2020.1.0a15, serialization error when renaming class Jump1 to Jump2:
5260559--526184--2.png
5260559--526187--3.png
renaming class Jump1 to Jump2
5260559--526190--4.png
5260559--526193--5.png

If you’re still encountering issues like this, could you please submit a bug report with a reproduction project? The case that Peter submitted has been fixed in 2020.1.0a10.

(Case 1204407) [SerializeReference] serialized data loss when class name is changed

2 Likes

Unity2020.1.0b1 can still reproduce the problem.

5600734–579373–Assets.zip (2.85 KB)

1 Like

Issue Tracker

where did that issue go ?
https://issuetracker.unity3d.com/is…e-data-lost-when-the-class-name-is-refactored
I have the same thing with a struct.

Same problem

Is there a workaround for when a managedReferenceValue class has been deleted? I can identify when the value is null but replacing does not apply changes and they get reverted on assembly reload. The serializedObject.target seems to be corrupted, also setting any field on target does not apply changes. Target is a ScriptableObject. If target is MonoBehavior i can replace null referenced values.

Error on assembly reload: Unknown managed type referenced: [Assembly-CSharp] + Type

Unity 2019.4.10

 /*
             * I can't apply any changes to managedReferenceValue if it is null
             * for (int i = 0; i < this.m_Actions.arraySize; i++) {
                SerializedProperty element = this.m_Actions.GetArrayElementAtIndex(i);
                if (element.GetValue() == null) {
          
                    element.managedReferenceValue = new MissingAction();
                    serializedObject.ApplyModifiedPropertiesWithoutUndo();
                }
            }*/
2 Likes

For anyone getting this, the comment in the issue tracker suggests a fix:

Add the MovedFrom attribute on top of your class:

using UnityEngine.Scripting.APIUpdating;

namespace NewNamespace
{
    [MovedFrom(false, null, "OldNamespace", "OldClass")]
    public class NewClass
    {
    }
}

If you haven’t changed the namespace, you can leave the third parameter null, it seems.
If you have changed the assembly, you can fill in the first parameter with the old assembly name (untested).

18 Likes

It works

How to deal with deleted classes? The scriptableObject containing this invalid assignment is not correctly loaded if the class is missing. Any change to it is reverted on assembly reload, every field in the inspector is displayed as null, but the serialized data still contains the old values.

1 Like

Unfortunately you can only fix this by removing the broken components and adding them anew.

So lets do it!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
    public class MyClass : MonoBehaviour, ISerializationCallbackReceiver
    {
        [SerializeReference] protected IMyInterface myInterface;

        private string lastType;
        public void OnBeforeSerialize()
        {
            lastType = myInterface?.GetType().AssemblyQualifiedName; // This measure to prevent our solution being called when myInterface is actually null, which would mess with Undo operations.
        }

        private const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Default | BindingFlags.DeclaredOnly | BindingFlags.Instance;
        public void OnAfterDeserialize()
        {
#if UNITY_EDITOR
            if(!string.IsNullOrEmpty(lastType) && System.Type.GetType(lastType) == null)
            {
                UnityEditor.EditorApplication.delayCall += () =>
                {
                    var go = gameObject;

                    DestroyImmediate(this); // Destroying first handles cases with DisallowMultipleComponent.

                    // The next part looks scary if you're not versed in reflection, but in a nutshell it will copy all of our serialized data into the copy component.

                    // Create a new component of the same type.
                    var componentType = GetType();
                    var component = go.AddComponent(componentType);

                    // Get all the fields from that component (up to MonoBehaviour otherwise we might mess up some internal stuff, IDs being mixed, big hassle).
                    IEnumerable<FieldInfo> finfos = Enumerable.Empty<FieldInfo>();
                    do
                    {
                        finfos = finfos.Concat(componentType.GetFields(bindingFlags));
                        componentType = componentType.BaseType;
                    }
                    while (componentType != typeof(MonoBehaviour));

                    // Do some magic to make sure we only get serializable fields.
                    finfos = from field in finfos
                             let attributeTypes = field.CustomAttributes.Select(attribute => attribute.AttributeType)
                             where !attributeTypes.Any(type => type == typeof(NonSerializedAttribute) || type == typeof(ObsoleteAttribute))
                             where attributeTypes.Contains(typeof(SerializeReference))
                                || (
                                    (field.IsPublic || attributeTypes.Contains(typeof(SerializeField)))
                                    && (field.FieldType.IsSerializable || field.FieldType.IsSubclassOf(typeof(Object)))
                                    )
                             select field;

                    // Copy our serialized values.
                    foreach (var finfo in finfos)
                    {
                        finfo.SetValue(component, finfo.GetValue(this));
                    }
                };
            }
        }
#endif
    }

This won’t override [MovedFrom()] !

3 Likes

This work if the interface data was a monobehaviour, but what if its simply data in a scriptableObject?