External Reference Resolver


With the Odin Serializer you can specify custom way of handling and restoring external references for serialized data.

This is useful in many cases. For example, if you have objects that has references to project assets - ScriptableObjects, textures etc - you can emply a custom reference resolver to handle the serialization and deserialization of these references.

Odin offers 3 types of external reference resolvers, which allows you to use 3 different kinds of keys. All of them are implementing by inheriting from an interface, and have to be specified in the SerializationContext when you serialize and deserialize.

External String Reference Resolver

The first type of external reference resolver is the IExternalStringReferenceResolver. This resolver lets you specify a string id to serialize and deserialize references. For example, in the Unity Editor you could use this to serialize asset references by the asset GUID:

public class ScriptableObjectStringReferenceResolver : IExternalStringReferenceResolver
{
	// Multiple string reference resolvers can be chained together.
    public IExternalStringReferenceResolver NextResolver { get; set; } 

    public bool CanReference(object value, out string id)
    {
        if (value is ScriptableObject)
        {
            id = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject));
            return true;
        }

        id = null;
        return false;
    }

    public bool TryResolveReference(string id, out object value)
    {
        value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id));
        return value != null;
    }
}

When serializing and deserializing you need to specify the resolver in the serialization context:

byte[] Serialize(object obj)
{
    var context = new SerializationContext()
    {
        StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
    };
	return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}

object Deserialize(byte[] bytes)
{
    var context = new DeserializationContext()
    {
        StringReferenceResolver = new ScriptableObjectStringReferenceResolver(),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}

External GUID Reference Resolver

The external GUID reference resolver works much the same as the string resolver, but it uses strictly GUID keys.

The implementation is almost identical to the string resolver as well:

public class ScriptableObjectGuidReferenceResolver : IExternalGuidReferenceResolver
{
	// Multiple string reference resolvers can be chained together.
    public IExternalGuidReferenceResolver NextResolver { get; set; } 

    public bool CanReference(Guid value, out string id)
    {
        if (value is ScriptableObject)
        {
            id = new Guid(AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(value as ScriptableObject)));
            return true;
        }

        id = default(Guid);
        return false;
    }

    public bool TryResolveReference(Guid id, out object value)
    {
        value = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(id.ToString()));
        return value != null;
    }
}

And, of course, it needs to be specified when serializing and deserializing.

byte[] Serialize(object obj)
{
    var context = new SerializationContext()
    {
        GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    };
	return SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
}

object Deserialize(byte[] bytes)
{
    var context = new DeserializationContext()
    {
        GuidReferenceResolver = new ScriptableObjectGuidReferenceResolver(),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}

External Index Reference Resolver

Finally, the index resolver lets you make references with an index ID. This is used in the Serialized- classes, like the SerializedScriptableObject to reconstruct Unity references serialized by Odin.

It is important to keep the list the same both in between serialization and deserialization; at least if you want references to be reconstructed in the same way. A notable difference here is that you cannot chain multiple index resolvers together, unlike the string and guid resolvers.

public class IndexResolver : IExternalIndexReferenceResolver
{
	public List<UnityEngine.Object> ReferenceList;

	public IndexResolver()
	{
		this.referenceList = new List<UnityEngine.Object>();
	}

	public IndexResolver(List<UnityEngine.Object> references)
	{
		this.referenceList = references;
	}

	public bool CanReference(object value, out int index)
	{
		if (value is UnityEngine.Object)
		{
			index = this.referenceList.Count;
			this.referenceList.Add(value);
		}

		index = 0;
		return false;
	}

	public bool TryResolveReference(int index, out object value)
	{
	    value = this.referencedUnityObjects[index];
	    return true;
	}
}

And, of course, again, the notable difference is the adding of the list itself for the references.

byte[] Serialize(object obj, out List<UnityEngine.Object> references)
{
	var resolver = new IndexResolver();
    var context = new SerializationContext()
    {
		IndexReferenceResolver = resolver,
    };
	var bytes = SerializationUtility.SerializeValue(obj, DataFormat.Binary, context);
	references = resolver.ReferenceList;

	return bytes;
}

object Deserialize(byte[] bytes, List<UnityEngine.Object> references)
{
    var context = new DeserializationContext()
    {
        IndexReferenceResolver = new IndexResolver(references),
    };
	return SerializationUtility.DeserializeValue<object>(bytes, DataFormat.Binary, context);
}