Serialize C# Properties (how-to with code)

Hi all!

There’s one particular C# feature I use a lot in my projects, and that is properties: They allow you to execute arbitrary code whenever a variable is read/written. Which is nice and leads to quite clean code.

However, Unity does not serialize properties by default. Which makes sense, since calling arbitrary code when serializing/deserializing objects probably is not a good idea.

Calling the getter/setter in editor automatically when tinkering with the inspector without the need to write a custom Editor has proven extremely useful though. I made my own implementation using PropertyDrawers. Here it is, feel free to copy and paste to your project:

using System;
using System.Reflection;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

     [System.AttributeUsage(System.AttributeTargets.Field)]
    public class SerializeProperty : PropertyAttribute
    {
        public string PropertyName { get; private set; }
 
        public SerializeProperty(string propertyName)
        {
            PropertyName = propertyName;
        }
    }
 
    #if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(SerializeProperty))]
    public class SerializePropertyAttributeDrawer : PropertyDrawer
    {
        private PropertyInfo propertyFieldInfo = null;
 
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            UnityEngine.Object target = property.serializedObject.targetObject;
 
            // Find the property field using reflection, in order to get access to its getter/setter.
            if (propertyFieldInfo == null)
                propertyFieldInfo = target.GetType().GetProperty(((SerializeProperty)attribute).PropertyName,
                                                     BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
 
            if (propertyFieldInfo != null){

                  // Retrieve the value using the property getter:
                object value = propertyFieldInfo.GetValue(target,null);
 
                // Draw the property, checking for changes:
                EditorGUI.BeginChangeCheck();
                value = DrawProperty(position,property.propertyType,propertyFieldInfo.PropertyType,value,label);
     
                // If any changes were detected, call the property setter:
                if (EditorGUI.EndChangeCheck() && propertyFieldInfo != null){
     
                    // Record object state for undo:
                    Undo.RecordObject(target, "Inspector");
     
                    // Call property setter:
                    propertyFieldInfo.SetValue(target,value,null);
                }

            }else{
                EditorGUI.LabelField(position,"Error: could not retrieve property.");
            }
        }
 
        private object DrawProperty(Rect position, SerializedPropertyType propertyType, Type type, object value, GUIContent label)
        {
            switch (propertyType) {
                case SerializedPropertyType.Integer:
                    return EditorGUI.IntField(position,label,(int)value);
                case SerializedPropertyType.Boolean:
                    return EditorGUI.Toggle(position,label,(bool)value);
                case SerializedPropertyType.Float:
                    return EditorGUI.FloatField(position,label,(float)value);
                case SerializedPropertyType.String:
                    return EditorGUI.TextField(position,label,(string)value);
                case SerializedPropertyType.Color:
                    return EditorGUI.ColorField(position,label,(Color)value);
                case SerializedPropertyType.ObjectReference:
                    return EditorGUI.ObjectField(position,label,(UnityEngine.Object)value,type,true);
                case SerializedPropertyType.ExposedReference:
                    return EditorGUI.ObjectField(position,label,(UnityEngine.Object)value,type,true);
                case SerializedPropertyType.LayerMask:
                    return EditorGUI.LayerField(position,label,(int)value);
                case SerializedPropertyType.Enum:
                    return EditorGUI.EnumPopup(position,label,(Enum)value);
                case SerializedPropertyType.Vector2:
                    return EditorGUI.Vector2Field(position,label,(Vector2)value);
                case SerializedPropertyType.Vector3:
                    return EditorGUI.Vector3Field(position,label,(Vector3)value);
                case SerializedPropertyType.Vector4:
                    return EditorGUI.Vector4Field(position,label,(Vector4)value);
                case SerializedPropertyType.Rect:
                    return EditorGUI.RectField(position,label,(Rect)value);
                case SerializedPropertyType.AnimationCurve:
                    return EditorGUI.CurveField(position,label,(AnimationCurve)value);
                case SerializedPropertyType.Bounds:
                    return EditorGUI.BoundsField(position,label,(Bounds)value);
                default:
                    throw new NotImplementedException("Unimplemented propertyType "+propertyType+".");
            }
        }
 
    }
    #endif

Usage:

class PropertySerializationTest : MonoBehaviour{

        [SerializeProperty("Test")] // pass the name of the property as a parameter
        public float test;
 
        public float Test{
            get{
                Debug.Log("Getter called");
                return test;
            }
            set{
                Debug.Log("Setter called");
                test = value;
            }
        }
}
20 Likes

Its working. I’m so happy about that ). Thank you @arkano22

With C# version > 7.3, there is a very neat trick to expose auto-properties. All auto properties have a backing field, so all you have to do is mark that field with SerializeField like this:

[field: SerializeField]
public float Speed { get; set; }

Note: u have to use "field: ". This syntax tells compiler that all attributes in that block refer to backing field.

21 Likes

Neat indeed! However, functionally speaking this is basically the same as having a public field, am I right? No custom set/get code called during serialization…

3 Likes

I agree that being able to expose properties is extremely useful!

One common use case is exposing settings properties, and applying their effects to all affected systems whenever their value is changed.

It also makes it trivial to expose EditorPrefs and PlayerPrefs in the inspector as if they were normal fields.

[field: SerializeField] is pretty useful, as it can be used to serialize properties and expose them in the inspector with minimal boilerplate code.

The main benefit it offers over using normal fields is that you can restrict the accessibility of the set accessor.

// You can look, but you can't touch!
[field: SerializeField]
public int Value
{
    get;
    private set;
}

So functionally it is very similar to having this:

[SerializeField]
private int _value;

// You can look, but you can't touch!
public int Value
{
    get
    {
        return _value;
    }

    private set
    {
        _value = value;
    }
}

Unfortunately the property backing field will have a very ugly label in the inspector by default, which is something you’ll need to fix before it really becomes a usable solution.

This is how it looks in the inspector by default (the one on the left):

5171399--513164--serialized-property.png

3 Likes

Any idea how to get this to work with a custom struct?

5918612--632306--upload_2020-5-30_10-27-32.png

Found that it’s not working like this, because

can’t found property, any suggestion how to fix?

row and column aren’t properties, nor backing fields of a property. They’re just public variables, you don’t need to do anything special for them to be serialized.

This method is intended to deal with properties.

1 Like

Interesting. @arkano22 does this need the backing field to be set to public? Doesn’t this defeat part of the point of using properties, which is keeping our fields private and controlling their access?

EDIT: Nevermind, I figured out I can just have my field private and add the [SerializeField] attribute together with the [SerializeProperty()] one.

Great stuff!

@arkano22 do the SerializeProperty and SerializePropertyAttributeDrawer classes need to be in the same script as the class in which you’d like to use SerializeProperty()?

I created a script with your first code block, then I created a second script following your PropertySerializationTest. The problem is I can’t use [SerializeProperty()] in that second script; I’m getting a “missing type or namespace” error for SerializePropertyAttribute and SerializeProperty.

I feel like I’m missing something basic here, but I’m at the edge of my programming knowledge with this one.

1 Like

@reinfeldx Scripts placed inside a folder named Editor are not visible to other scripts, that is probably the source of your issues.

Good practice would be to split the code into two separate files: one containing just the SerializeProperty class and another file one containing just the SerializePropertyAttributeDrawer class. The SerializePropertyAttributeDrawer.cs file should then be placed inside an Editor folder while the SerializeProperty.cs file should not be inside an Editor folder. This is because the drawer is only needed in the editor while the attribute might be used within classes that exist in builds too.

There’s an example in the manual under Customize the GUI of script members using Property Attributes.

1 Like

In Unity 2020 you can use [field: SerializeField]

5 Likes

I guess they fixed the backing-field name issue above?
When did they do that?

Why does private set is required? Resharper says it is redundant but unity won’t parse it without that (However, in versions before 2020 it worked). I had to switch that inspection off, pretty silly

Is there a way to use your SerializeProperty class with lists or arrays?

Thanks very much. Super useful!

Edit: @arkano22 I can’t get it working with int :confused: I might be doing something wrong though.

[SerializeProperty("Min")] public int minimum;
       
        public int Min
        {
            get => _min;
            set => Set(value, _max);
        }

it just returns the error of not being able to retrieve. I am using Unity 2019.4.14f1

Please start your on fresh post with all the information needed.

How to report your problem productively in the Unity3D forums:

http://plbm.com/?p=220

How to understand errors in general:

https://forum.unity.com/threads/assets-mouselook-cs-29-62-error-cs1003-syntax-error-expected.1039702/#post-6730855

The first error I see is, “What on earth is _min and _max in your code?”

1 Like

I had a similar issue. I had a scriptable object that was referencing properties from a namespaced script.
I thought it was the fact the properties where not in classes and instead just in namespaces, that that was the issue.
As others have said, it turned out to be just simply adding [field: SerializeField] to each property did the trick.
In the inspector, the label will look ugly, but just make a custom editor for that.

Also note that if your scriptable object is stored as a .asset file, don’t forget to set the object as dirty and save the asset database.

Has anyone found a way to use this together with validation code? Can’t seem to get this working when “expanding” the getter/setter methods.

For instance, I’ve tried to make a small class that makes sure an AudioClip is ambisonic (as that sometimes takes us by surprise during importing);

[Serializable]
public class AmbisonicAudioClip : ISerializationCallbackReceiver
{
    [SerializeField]
    private AudioClip clip;

    public AudioClip Clip
    {
        get => clip;
        set
        {
            clip = value;
            ValidateClip();
        }
    }

    public void OnAfterDeserialize()
    {
       
    }

    public void OnBeforeSerialize()
    {
        ValidateClip();
    }

    private void ValidateClip()
    {
        if(clip)
            if (!Clip.ambisonic)
                Debug.LogError("Clip " + clip.name + " is not ambisonic!");
    }
}

This works fine but looks bad in the editor, and I can’t serialize the public property…

1 Like

I just autocompleted my way to this thing here:

[field: SerializeField] public int CurrentVoxelCount { get; private set; }

That is exposed in the inspector.