Duplicating a [SerializeReference] property?

I’m building an editor tool that involves a graph with nodes, and a custom Editor. I’m utilizing the [SerializeReference] attribute for the nodes, which has worked really well thus far, until I wanted to duplicate a node. That is, create a new node with all the same values as the previous node. Here are some things I’ve tried:

  • Duplicating the element in the list of nodes. This simply creates another reference to the same node object.

  • Using BinaryFormatter to create a deep copy of the node, and assigning that to the new node. This doesn’t seem to work well with some built-in Unity classes like Vector2, and threw some errors.

  • Creating a shallow copy with MemberwiseClone(). This was the closest I’ve gotten, however reference types, like Lists, were not cloned, and would reference the same List. So if I changed the list on one node, it would change on the other as well.

One thing to note is that I cannot manually go through each value in the node and assign them on the new node, because there are many derived versions of the nodes, and I do not want to implement a sort of .Clone() function on each child class.

Any thoughts?

First way:

Use reflection to iterate over all public or private variables that have attribute [SerializeField]
(Remember to ignore the public variable with attribute [NonSerialized])
Because when using [SerializeReference] you can get exactly the derived type of node, instead of the base type.

But be careful, because reflection also causes some performance problems if used in build.

Second way:

Use a ScriptableObject as a clone machine

//NodeCloner is custom your class
NodeCloner nodeCloner = ScriptableObject.CreateInstance<NodeCloner>();//You can cache it
nodeCloner.nodeData = yourNodeData;
  
NodeCloner newNodeCloner = ScriptableObject.Instantiate(nodeCloner);
var newNodeData = newNodeCloner.nodeData;

However, as for performance in the build vs. reflection, I haven’t tested it yet.

A couple of years later and still no direct solution to this, so I made this script, invoke SerializeReferenceDuplicationAnchor.Validate with your Unity object that contains serialized references.

Usage:

#if UNITY_EDITOR
void OnValidate()
{
    SerializeReferenceDuplicationAnchor.Validate(this);
}
#endif

Script:

#if UNITY_EDITOR
using System.Collections.Generic;

using UnityEditor;

using UnityEngine;

using CObject = System.Object;
using UObject = UnityEngine.Object;

/// <summary>
/// A tool that will properly duplicate all serialized references inside a unity object
/// </summary>
static class SerializeReferenceDuplicationAnchor
{
    static HashSet<CObject> References = new(200, ReferenceEqualityComparer.Default);
    class ReferenceEqualityComparer : IEqualityComparer<CObject>
    {
        //Ensure we always do a reference check
        public new bool Equals(CObject x, CObject y) => ReferenceEquals(x, y);

        //Default hashcode implementation of the type is good enough
        //I wanted to use the CLRs' internal hashcode mechanism, but I couldn't find a public API for it
        public int GetHashCode(CObject obj) => obj.GetHashCode();

        public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer();
    }

    public static void Validate(UObject target)
    {
        References.Clear();

        var managedObject = new SerializedObject(target);

        var iterator = managedObject.GetIterator();

        while (iterator.NextVisible(true))
        {
            if (iterator.propertyType is not SerializedPropertyType.ManagedReference)
                continue;

            if (iterator.managedReferenceValue == null)
                continue;

            if (References.Add(iterator.managedReferenceValue))
                continue;

            iterator.managedReferenceValue = DuplicateReference(iterator.managedReferenceValue);
        }

        managedObject.ApplyModifiedProperties();
    }

    static CObject DuplicateReference(CObject original)
    {
        //Yeah, not the most optimal solution, but not many options that Unity allows us

        var type = original.GetType();

        //Json serialization uses the same serialization engine that the inspector uses
        //Ie, we will get all the values we are expecting
        var json = JsonUtility.ToJson(original);

        var clone = JsonUtility.FromJson(json, type);

        return clone;
    }
}
#endif