Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

[SerializeReference] data loss when class name is changed

Discussion in '2019.3 Beta' started by kvfreedom, Aug 30, 2019.

  1. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
    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?
     
    Flavelius and Peter77 like this.
  2. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,618
    Good catch. Unity Technologies, please support a mechanism that allows us to refactor code without breaking assets.
     
  3. julian-moschuering

    julian-moschuering

    Joined:
    Apr 15, 2014
    Posts:
    529
    There isn't a GUID for a type. Maybe support FormerlySerializedAs on class?
     
    KFIR_ONEHAMSA likes this.
  4. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
    Add a restriction: if the class wants to be referenced by [SerializeReference], the class must correspond to a script. Just like a MonoBehaviour.
     
    LucasHehir likes this.
  5. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,659
    Please file a bug report so we can track the problem, and we'll look at what we want to do about it.
     
  6. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,618
    Here is some bacon for you:
    (Case 1180719) [SerializeReference] data loss when class name is changed
     
    Sluggy and superpig like this.
  7. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,618
    kyuskoj and kvfreedom like this.
  8. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
    1.png
    I tested in 2020.1.0a15, serialization error when renaming class Jump1 to Jump2:
    2.png
    3.png
    renaming class Jump1 to Jump2
    4.png
    5.png
     
  9. LeonhardP

    LeonhardP

    Unity Technologies

    Joined:
    Jul 4, 2016
    Posts:
    3,136
  10. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
    (Case 1204407) [SerializeReference] serialized data loss when class name is changed
     
    LeonhardP and Peter77 like this.
  11. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
    Unity2020.1.0b1 can still reproduce the problem.
     

    Attached Files:

    Lyx5698 likes this.
  12. kvfreedom

    kvfreedom

    Joined:
    Mar 30, 2015
    Posts:
    37
  13. WAYNGames

    WAYNGames

    Joined:
    Mar 16, 2019
    Posts:
    992
  14. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Same problem
     
  15. DevionGames

    DevionGames

    Joined:
    Feb 22, 2010
    Posts:
    1,624
    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
    Code (CSharp):
    1.  /*
    2.              * I can't apply any changes to managedReferenceValue if it is null
    3.              * for (int i = 0; i < this.m_Actions.arraySize; i++) {
    4.                 SerializedProperty element = this.m_Actions.GetArrayElementAtIndex(i);
    5.                 if (element.GetValue() == null) {
    6.          
    7.                     element.managedReferenceValue = new MissingAction();
    8.                     serializedObject.ApplyModifiedPropertiesWithoutUndo();
    9.                 }
    10.             }*/
     
    ScriptsEngineer and Flavelius like this.
  16. LazloBonin

    LazloBonin

    Joined:
    Mar 6, 2015
    Posts:
    813
    For anyone getting this, the comment in the issue tracker suggests a fix:

    Add the
    MovedFrom
    attribute on top of your class:

    Code (CSharp):
    1. using UnityEngine.Scripting.APIUpdating;
    2.  
    3. namespace NewNamespace
    4. {
    5.     [MovedFrom(false, null, "OldNamespace", "OldClass")]
    6.     public class NewClass
    7.     {
    8.     }
    9. }
    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).
     
  17. David_296

    David_296

    Joined:
    Jun 26, 2015
    Posts:
    5
    It works
     
  18. Flavelius

    Flavelius

    Joined:
    Jul 8, 2012
    Posts:
    945
    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.
     
    JiongXia likes this.
  19. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    613
    Unfortunately you can only fix this by removing the broken components and adding them anew.

    So lets do it!

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Reflection;
    5. using UnityEngine;
    Code (CSharp):
    1.     public class MyClass : MonoBehaviour, ISerializationCallbackReceiver
    2.     {
    3.         [SerializeReference] protected IMyInterface myInterface;
    4.  
    5.         private string lastType;
    6.         public void OnBeforeSerialize()
    7.         {
    8.             lastType = myInterface?.GetType().AssemblyQualifiedName; // This measure to prevent our solution being called when myInterface is actually null, which would mess with Undo operations.
    9.         }
    10.  
    11.         private const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Default | BindingFlags.DeclaredOnly | BindingFlags.Instance;
    12.         public void OnAfterDeserialize()
    13.         {
    14. #if UNITY_EDITOR
    15.             if(!string.IsNullOrEmpty(lastType) && System.Type.GetType(lastType) == null)
    16.             {
    17.                 UnityEditor.EditorApplication.delayCall += () =>
    18.                 {
    19.                     var go = gameObject;
    20.  
    21.                     DestroyImmediate(this); // Destroying first handles cases with DisallowMultipleComponent.
    22.  
    23.                     // 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.
    24.  
    25.                     // Create a new component of the same type.
    26.                     var componentType = GetType();
    27.                     var component = go.AddComponent(componentType);
    28.  
    29.                     // Get all the fields from that component (up to MonoBehaviour otherwise we might mess up some internal stuff, IDs being mixed, big hassle).
    30.                     IEnumerable<FieldInfo> finfos = Enumerable.Empty<FieldInfo>();
    31.                     do
    32.                     {
    33.                         finfos = finfos.Concat(componentType.GetFields(bindingFlags));
    34.                         componentType = componentType.BaseType;
    35.                     }
    36.                     while (componentType != typeof(MonoBehaviour));
    37.  
    38.                     // Do some magic to make sure we only get serializable fields.
    39.                     finfos = from field in finfos
    40.                              let attributeTypes = field.CustomAttributes.Select(attribute => attribute.AttributeType)
    41.                              where !attributeTypes.Any(type => type == typeof(NonSerializedAttribute) || type == typeof(ObsoleteAttribute))
    42.                              where attributeTypes.Contains(typeof(SerializeReference))
    43.                                 || (
    44.                                     (field.IsPublic || attributeTypes.Contains(typeof(SerializeField)))
    45.                                     && (field.FieldType.IsSerializable || field.FieldType.IsSubclassOf(typeof(Object)))
    46.                                     )
    47.                              select field;
    48.  
    49.                     // Copy our serialized values.
    50.                     foreach (var finfo in finfos)
    51.                     {
    52.                         finfo.SetValue(component, finfo.GetValue(this));
    53.                     }
    54.                 };
    55.             }
    56.         }
    57. #endif
    58.     }
    This won't override [MovedFrom()] !
     
    Last edited: May 15, 2021
  20. The_MrX_

    The_MrX_

    Joined:
    Aug 25, 2019
    Posts:
    12
    This work if the interface data was a monobehaviour, but what if its simply data in a scriptableObject?
     
  21. HaipaDev

    HaipaDev

    Joined:
    Jun 30, 2017
    Posts:
    1
    yeah what about ScriptableObjects? /:
     
  22. Flavelius

    Flavelius

    Joined:
    Jul 8, 2012
    Posts:
    945
    What namespace/class format should the MovedFrom attribute have if the target was/is a nested class (inside a monobehaviour if that matters) ?
    The error message atleast shows something like namespace.subnamespace.monobehaviourname/nestedclassname
     
  23. AdamBebko

    AdamBebko

    Joined:
    Apr 8, 2016
    Posts:
    168
    Got this problem renaming namespaces in rider. My entire project is kaput now. Thank god for git. In git we trust, amen.
     
  24. Flavelius

    Flavelius

    Joined:
    Jul 8, 2012
    Posts:
    945
    Still relevant, how should the parameters be formatted for nested classes?
     
  25. GiftedMamba

    GiftedMamba

    Joined:
    Feb 25, 2017
    Posts:
    46
    Unity 2022.1.15f1 still has this issue. Is there any way to safely rename classes, serialized with [SerializedReference]?
    Or we should just avoid renaming of this classes(or avoid using [SerializedReference])?
     
  26. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    613
    I would avoid SerializedReference as much as possible. It's a neat trick, but you're usually better of writing custom Serializers using
    ISerializationCallbackReceiver
    . If that's not possible because you need to reference interfaces... well... don't. In my experience SerializedReference is more trouble than it's worth.

    Then again, there's many people smarter than me, so I would get a second opinion from someone who actually gets them if you decide on them.
     
    JiongXia likes this.
  27. Shrubokrant

    Shrubokrant

    Joined:
    Oct 2, 2016
    Posts:
    80
    I'm curious about your experience here would you mind elaborating a bit? I'm testing a new architecture for a new that uses SerializedReference: what trouble did it get you into, and why do you think it's not worth it?
     
  28. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    613
    Let me start by saying, back in 2021, I had a lot of trouble with it from the CustomPropertyDrawer / CustomEditor perspective. If you made one wrong move in code, something would happen where your reference would break and you had to reserialize it by renaming it, letting Unity load, renaming it back, letting Unity load again... it was a whole QoL thing that just made the experience rather bothersome.

    But maybe that has been fixed?

    Then there was working with managed references which was a tough experience, as you had to do some voodoo-magic with assemblies to understand what reference you were working with, since you only had typenames to work with.

    But that was in 2021.1, and doing a quick doc scan I see they've since added
    managedReferenceId
    as well as
    SerializationUtility
    tools for working with
    managedReferenceId
    ... so maybe this is better now?

    In the end I ended up doing so much work that I had to stop myself and ask "what am I even doing? Am I even still thinking like a Unity-dev at this point?" So I went back to my design and solved my problems thinking in Components and ScriptableObjects, which will always remain a valid approach. As it was much easier to work with, I could iterate faster and keep my inspectors simpler.

    As I side note, I am not sure how [SerializeReference] would fit into UI Elements but I don't see any reason why it would be any more complicated, just different.



    The bottom-line here is *gruff voice I'm old, kid, it's time for your generation to pick up the torch where we could carry it no further. (editors note: I'm not old just pretentious)

    For me I'm sticking to "Keep it Stupid Simple" as that has always worked best for me, but if you do end up using [SerializeReference] I'd love to see how you use it creatively in your work! I'm sure there's benefits I just don't know about because I stopped looking.
     
  29. ThisIsNik

    ThisIsNik

    Joined:
    Oct 28, 2019
    Posts:
    9
    I've been using [SerializeReference] for authoring behavior in my UtilityAI. Refactors are always scary and my data doesn't feel safe tho.

    Am considering switching to simply authoring the behavior in code instead for the same reasons as Casey.
     
  30. Goatilus

    Goatilus

    Joined:
    Oct 10, 2016
    Posts:
    13
    I'm working with Scriptable Objects that have serialized fields that I use for Info texts, visible ingame names and descriptions.
    I'm not deep into development with this stuff but already had issue with FormerlySerializedAs and [SerializeReference] and data loss because of that.

    Main question for me is, should I even keep such data in scriptable objects (of course I'm using VCS) or is there some other common approach to store such data, like in a database and then fetching it on the game load distributing the texts to the respective objects.

    To come more back to the main topic, I have a lot of different types of data objects stored as scriptable objects like "equipment", "damage types", "commodities", the list goes on, and all of them use the same scheme with the properties "string: description" and "string: displayedName" so I thought to put that into an interface that I can attach to the SO classes and "autogenerate" the necessary fields, and if I want the structure to change, I would only have to change the properties in the interface.

    This leads me to the scary point where I might actually change the property in the interface but would need a formerly serialized as on all the actual implementations and this seems all a bit too dangerous I think to store so much data like infotexts later, descriptions etc.

    How are you approaching that or how you work with FormerlySerializedAs in a "secure" way if there is one?
     
  31. CaseyHofland

    CaseyHofland

    Joined:
    Mar 18, 2016
    Posts:
    613
    You have to put
    FormerlySerializedAs
    on all the scripts that implement your interface and derive from type
    UnityEngine.Object
    . Everything in Unity that is serialized is an Object, and you can't serialize fields in an interface (which really wouldn't work if you think about their design).
     
  32. ThisIsNik

    ThisIsNik

    Joined:
    Oct 28, 2019
    Posts:
    9
    @Orokon
    Keep it simple and just use Unity's serialization. Your data is fine as long as you use VCS.
     
  33. LazloBonin

    LazloBonin

    Joined:
    Mar 6, 2015
    Posts:
    813
    Coming back to this.

    My own solution doesn't work if you rename the class twice. Unlike FormerlySerializedAs, MovedFrom has an AttributeUsage that prevents it from being applied more than once to any given object. And because seemingly doesn't reserialize the type name after seeing a MovedFrom, you're... screwed, for lack of a better word.

    Can anyone confirm whether this has indeed been fixed in 2021.3 as the Bug Tracker reports?