Cannot serialize a GUID field in class?

Hi All.

I have a MonoBehaviour that has two GUID fields in it. I set those through the custom inspector, but for some reason, it doesn’t store the changes into the component, so when I hit play or save the scene the values disappear.

I’m setting the target to be dirty in my custom inspector, and I have also tried to mark the GUIDs with [SerializeField].

Any one has any insight on this?

Can you get away with using a string to represent?

string guid = Guid.NewGuid();

May not be ideal, just a thought, structure the var however(in Start(), some method, etc).

Yeah, That is what i’ve done, however the var is linked to other information, so it isn’t really that ideal. I can probably get away with it, but i don’t understand WHY this is happening, which is the more frustrating part. It’s not like GUID is a complex class…

Unity has a limited range of types that can be serialized (here is the list), and there’s no easy workaround for that. You can still serialize a custom class though, with all the additional data you need, as long as the serialized fields are of one of those types.

1 Like

I’m use this solution

using System;
using System.Runtime.InteropServices;
using UnityEngine;

[StructLayout( LayoutKind.Explicit ), Serializable]
public class UnityGuid: IComparable, IComparable, IEquatable
{
public UnityGuid( )
{
Guid = Guid.NewGuid( );
}

[FieldOffset(0)]
public Guid Guid;
[FieldOffset(0), SerializeField]
private Int32 GuidPart1;
[FieldOffset(4), SerializeField]
private Int32 GuidPart2;
[FieldOffset(8), SerializeField]
private Int32 GuidPart3;
[FieldOffset(12), SerializeField]
private Int32 GuidPart4;

public static implicit operator Guid ( UnityGuid uGuid )
{
return uGuid.Guid;
}

public Int32 CompareTo ( object obj )
{
if( obj == null )
return -1;

if( obj is UnityGuid )
return ((UnityGuid)obj).Guid.CompareTo( Guid );

if( obj is Guid )
return ((Guid)obj).CompareTo( Guid );

return -1;
}
public Int32 CompareTo ( Guid other )
{
return Guid.CompareTo( other );
}
public Boolean Equals ( Guid other )
{
return Guid == other;
}

public override Boolean Equals ( object obj )
{
if( obj == null )
return false;

if( obj is UnityGuid )
return (UnityGuid)obj == Guid;

if( obj is Guid )
return (Guid)obj == Guid;

return false;
}
public override Int32 GetHashCode ( )
{
return Guid.GetHashCode( );
}
}

2 Likes

I’m use this Solution

using System;
using System.Runtime.InteropServices;
using UnityEngine;

[StructLayout( LayoutKind.Explicit ), Serializable]
public class UnityGuid: IComparable, IComparable, IEquatable
{
public UnityGuid( )
{
Guid = Guid.NewGuid( );
}

[FieldOffset(0)]
public Guid Guid;
[FieldOffset(0), SerializeField]
private Int32 GuidPart1;
[FieldOffset(4), SerializeField]
private Int32 GuidPart2;
[FieldOffset(8), SerializeField]
private Int32 GuidPart3;
[FieldOffset(12), SerializeField]
private Int32 GuidPart4;

public static implicit operator Guid ( UnityGuid uGuid )
{
return uGuid.Guid;
}

public Int32 CompareTo ( object obj )
{
if( obj == null )
return -1;

if( obj is UnityGuid )
return ((UnityGuid)obj).Guid.CompareTo( Guid );

if( obj is Guid )
return ((Guid)obj).CompareTo( Guid );

return -1;
}
public Int32 CompareTo ( Guid other )
{
return Guid.CompareTo( other );
}
public Boolean Equals ( Guid other )
{
return Guid == other;
}

public override Boolean Equals ( object obj )
{
if( obj == null )
return false;

if( obj is UnityGuid )
return (UnityGuid)obj == Guid;

if( obj is Guid )
return (Guid)obj == Guid;

return false;
}
public override Int32 GetHashCode ( )
{
return Guid.GetHashCode( );
}
}

1 Like

You can create wrapper type to store Guid data. Here is an example how to serailize\deserialize Guid

1 Like

Sorry for necroing this, but if anyone’s looking for a solution and doesn’t want to take the time to write their own wrapper, I’ve been using this one that I wrote. It includes a property drawer as well. I haven’t tested it a ton, but it’s simple enough it should work everywhere.

SerializableGuid class:

using System;
using UnityEngine;

/// <summary>
/// Serializable wrapper for System.Guid.
/// Can be implicitly converted to/from System.Guid.
///
/// Author: Searous
/// </summary>
[Serializable]
public struct SerializableGuid : ISerializationCallbackReceiver {
    private Guid guid;
    [SerializeField] private string serializedGuid;

    public SerializableGuid(Guid guid) {
        this.guid = guid;
        serializedGuid = null;
    }

    public override bool Equals(object obj) {
        return obj is SerializableGuid guid &&
                this.guid.Equals(guid.guid);
    }

    public override int GetHashCode() {
        return -1324198676 + guid.GetHashCode();
    }

    public void OnAfterDeserialize() {
        try {
            guid = Guid.Parse(serializedGuid);
        } catch {
            guid = Guid.Empty;
            Debug.LogWarning($"Attempted to parse invalid GUID string '{serializedGuid}'. GUID will set to System.Guid.Empty");
        }
    }

    public void OnBeforeSerialize() {
        serializedGuid = guid.ToString();
    }

    public override string ToString() => guid.ToString();

    public static bool operator ==(SerializableGuid a, SerializableGuid b) => a.guid == b.guid;
    public static bool operator !=(SerializableGuid a, SerializableGuid b) => a.guid != b.guid;
    public static implicit operator SerializableGuid(Guid guid) => new SerializableGuid(guid);
    public static implicit operator Guid(SerializableGuid serializable) => serializable.guid;
    public static implicit operator SerializableGuid(string serializedGuid) => new SerializableGuid(Guid.Parse(serializedGuid));
    public static implicit operator string(SerializableGuid serializedGuid) => serializedGuid.ToString();
}

Property Drawer class:

using BadCupcake.Cyclic.Item;
using BadCupcake.Cyclic.Utility;
using System;
using UnityEditor;
using UnityEngine;

/// <summary>
/// Property drawer for SerializableGuid
///
/// Author: Searous
/// </summary>
[CustomPropertyDrawer(typeof(SerializableGuid))]
public class SerializableGuidPropertyDrawer : PropertyDrawer {

    private float ySep = 20;
    private float buttonSize;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        // Start property draw
        EditorGUI.BeginProperty(position, label, property);

        // Get property
        SerializedProperty serializedGuid = property.FindPropertyRelative("serializedGuid");

        // Draw label
        position = EditorGUI.PrefixLabel(new Rect(position.x, position.y + ySep / 2, position.width, position.height), GUIUtility.GetControlID(FocusType.Passive), label);
        position.y -= ySep / 2; // Offsets position so we can draw the label for the field centered

        buttonSize = position.width / 3; // Update size of buttons to always fit perfeftly above the string representation field

        // Buttons
        if(GUI.Button(new Rect(position.xMin, position.yMin, buttonSize, ySep - 2), "New")) {
            serializedGuid.stringValue = Guid.NewGuid().ToString();
        }
        if(GUI.Button(new Rect(position.xMin + buttonSize, position.yMin, buttonSize, ySep - 2), "Copy")) {
            EditorGUIUtility.systemCopyBuffer = serializedGuid.stringValue;
        }
        if(GUI.Button(new Rect(position.xMin + buttonSize * 2, position.yMin, buttonSize, ySep - 2), "Empty")) {
            serializedGuid.stringValue = ItemStack.emptyItemId.ToString();
        }

        // Draw fields - passs GUIContent.none to each so they are drawn without labels
        Rect pos = new Rect(position.xMin, position.yMin + ySep, position.width, ySep - 2);
        EditorGUI.PropertyField(pos, serializedGuid, GUIContent.none);

        // End property
        EditorGUI.EndProperty();
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
        // Field height never changes, so ySep * 2 will always return the proper hight of the field
        return ySep * 2;
    }
}

Hope this helps someone :slight_smile: If you find issues with it please tell me!

EDIT: Added a Debug.LogWarning() call when a GUID string fails to parse.
EDIT2: Here’s an example of what it looks like:

11 Likes

Thank you this is perfect!

Holy Crow, this looks like EXACTLY what I needed. Thanks.

You should absolutely make this a package on UPM/OpenUPM, or at least via Git.

1 Like

I made my SerializableGuid thingy a package as suggested. You can find it here: GitHub - Searous/Unity.SerializableGuid: A simple wrapper for System.Guid

It works locally, but I was having trouble with my git stuff so I couldn’t test adding it from the repo. If you have trouble with it, please tell me.

thank you for your help.In my project , I used GUids a lot, and I found that the performance of serialization and deserialization with String was very poor.Changing string to byte[ ] improves performance considerably
7482938--920438--upload_2021-9-9_16-54-31.png

6 Likes

Sorry for the necro but I am trying to use this serialized Guid and I can’t figure out how to assign to it in code.

7830042--991623--upload_2022-1-21_16-1-56.png

How would I assign to the serialied guid in code?

Awesome can you share the code for that?
I don’t have a lot of experience dealing with the editor properties.

While not the same person.

I personally don’t use the array and instead just mirrored the struct layout of the System.Guid, marked it serializable, and then had conversion methods to easily convert between the 2 types:

https://github.com/lordofduct/spacepuppy-unity-framework-4.0/blob/master/Framework/com.spacepuppy.core/Runtime/src/SerializableGuid.cs

You should note at this line specifically:
https://github.com/lordofduct/spacepuppy-unity-framework-4.0/blob/master/Framework/com.spacepuppy.core/Runtime/src/SerializableGuid.cs#L183

        public unsafe static implicit operator SerializableGuid(System.Guid guid)
        {
            return *(SerializableGuid*)&guid;
        }

This unsafe coercion relies on the fact that System.Guid and SerializableGuid have the same struct layout. This would fail otherwise. The rest of the struct doesn’t particularly care about layout. But by doing this we avoid creating the byte array all together.

And here is the PropertyDrawer I use, though mind you, there is some minor reliance on code that isn’t unity specific. So this property drawer will need massaging to get working in your own project.

https://github.com/lordofduct/spacepuppy-unity-framework-4.0/blob/master/Framework/com.spacepuppy.core/Editor/src/Core/SerializableGuidPropertyDrawer.cs

And here is an example usage:

public class SomeObject : ScriptableObject
{
    #region Fields
 
    [SerializeField]
    [SerializableGuid.Config(LinkToAsset = true)]
    private SerializableGuid _guid;
 
    #endregion
 
    #region Properties
  
    public System.Guid Guid => _guid; //implicitly is converted to System.Guid
  
    #endregion
 
}

No need to wrap serialization callbacks or anything.

Also the ‘config’ attribute offers the option to tether it to some existing guid. In this case the guid will reflect the asset guid of the ScriptableObject created on disk.

4 Likes

Very nice, much appreciated.

Here with my own solution: I’m gonna have to start naming this pattern because I use it a lot. How 'bout…

Attribute Serialization

    public class MyClass
    {
        [SerializeField, Guid] private string _guid;

        public Guid guid
        {
            get => new(_guid);
            set => _guid = value.ToString();
        }
    }

When you need to serialize something from .NET that can be serialized through basic Unity serialization, here’s what I do.

Create an attribute:
GuidAttribute

using UnityEngine;

public class GuidAttribute : PropertyAttribute { }

(Don’t worry it gets better.)

And a drawer:
GuidDrawer

using System;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;

    [CustomPropertyDrawer(typeof(GuidAttribute))]
    public class GuidDrawer : PropertyDrawer
    {
        public override VisualElement CreatePropertyGUI(SerializedProperty property)
        {
            if (property.propertyType != SerializedPropertyType.String)
            {
                throw new ArgumentException("This attribute is not supported on properties of this property type.", nameof(property.propertyType));
            }

            var textField = new TextField(property.displayName)
            {
                isDelayed = true,
            };
            textField.AddToClassList(TextField.alignedFieldUssClassName);
            textField.BindProperty(property);

            textField.RegisterValueChangedCallback(changed =>
            {
                // If the new value is null or empty, generate a new guid.
                // If the new value is a valid guid, set it as the new guid.
                // If the old value is not a valid guid, generate a new guid.
                if (string.IsNullOrEmpty(changed.newValue)
                    || (!Guid.TryParse(changed.newValue, out var guid)
                        && !Guid.TryParse(changed.previousValue, out guid)))
                {
                    guid = Guid.NewGuid();
                }

                textField.SetValueWithoutNotify(guid.ToString());

                // Since we're modifying the input, we also need to update the serialized object.
                property.stringValue = guid.ToString()
                property.serializedObject.ApplyModifiedProperties();
            });

            return textField;
        }
    }

The drawer will handle the “serialization”, or rather translation from string to guid.

Et voila, the above will forever serialize a valid guid string.

I would not recommend creating an intermediary for the above code as it bloats your codebase. Here, you’re always dealing with cold-hard .NET types and putting the onus on the developer to decide what they want to expose and what to keep private. The GuidAttribute acts more like a contract and keeping it simple is absolutely key, so that you can keep your complexity actually aligned with where you use the Guid.

Using the new UIElements value changed API, the editor performance is excellent.

I’ve used Attribute Serialization to make an interface selector (Object) and a Timespan drawer (long) amongst others. If there’s any sort of underlying value that can serialize your type, I would recommend this pattern as it’s very easy to implement and extremely cheap in production code.

4 Likes

This is nice, thanks. And I think I need to steal the property pattern for my editors. :stuck_out_tongue:

1 Like