【Unity】Unity Attributes Guide: Metadata Markers That Give Your Code Special Behavior

Created: 2026-02-05Last updated: 2026-07-13

Learn how to customize Unity's Inspector with C# attributes. Covers practical usage of SerializeField, Header, Range, RequireComponent, and more.

Tested with: Unity 2022.3 LTS / Unity 6

"I want to show a private variable in the Inspector—how do I do that?" "My fields keep piling up and the Inspector has turned into one long, flat list..."

This is where attributes come in. Attributes are C# metadata markers—think of them as "instruction labels you stick onto your code." Just place one above a variable or class declaration in the form [...], and Unity will give it special behavior.

[HideInInspector]
public float strength;
Conceptual image of attributes: instruction label tags being attached to a code block

What You'll Learn

  • Go-to attributes that make the Inspector cleaner and safer (SerializeField, Header, Range, etc.)
  • The exact difference between HideInInspector and NonSerialized
  • What class-level attributes like RequireComponent and CreateAssetMenu do
  • How to build a custom attribute with a PropertyDrawer (and the Editor folder rules)

Sponsored

The 7 to Learn First: Making Weapon Data Hard to Break

There are a lot of attributes, but you'll use about 7 of them daily. Learn just these and you can build a genuinely practical Inspector. Our subject: weapon parameters — a WeaponConfig with attack power, fire interval, magazine size, and a description, hardened against typos and bad input.

// File name: WeaponConfig.cs — parameters for one weapon
using UnityEngine;

public class WeaponConfig : MonoBehaviour
{
    [Header("Attack Settings")]
    [Tooltip("Damage per shot")]
    [Range(1, 100)]
    [SerializeField] private int attackPower = 20;

    [Tooltip("Seconds between shots. Smaller = faster")]
    [Min(0.05f)]
    [SerializeField] private float fireInterval = 0.5f;

    [Tooltip("Magazine size")]
    [Min(0)]
    [SerializeField] private int magazineSize = 12;

    [Header("Flavor")]
    [TextArea(3, 6)]
    [SerializeField] private string description;

    [ContextMenu("Test Fire")]
    private void TestFire()
    {
        Debug.Log($"{name}: power {attackPower} / every {fireInterval}s / {magazineSize} rounds");
    }
}

Just 30 lines, but all seven attributes are pulling their weight:

AttributeRoleIts job on this weapon
[SerializeField]Show a private variable in the InspectorTweakable while encapsulation stays intact
[Header]Group fields under a headingThe "Attack Settings" / "Flavor" dividers
[Tooltip]Show an explanation on hoverEnds the "seconds or milliseconds?" confusion
[Range]Slider + range limitMakes a typo like attack power 9999 physically impossible
[Min]Minimum value limitPrevents negative ammo and a 0-second fire interval
[TextArea]Multi-line text inputFrees the description from a cramped one-line field
[ContextMenu]Run a method from the right-click menuTest-fire from the Inspector without pressing Play

Compare the Inspector before and after: the flat wall of fields becomes a control panel with headings, sliders, and explanations.

Comparison of the Inspector with and without attributes. Without attributes it's a flat list of fields; with Header, Range, and Tooltip it becomes an organized Inspector with headings and sliders

Note (attributes are not an all-purpose shield): Range and Min only restrict input from the Inspector. Assigning attackPower = 9999; from script sails right through (guard the code side with Mathf.Clamp), and they can't stop an empty description either. For "forgot to fill this in" checks, the handy tool is OnValidate, which the editor calls whenever a value changes:

void OnValidate()
{
    if (string.IsNullOrEmpty(description))
        Debug.LogWarning($"{name}: description is empty", this);
}

As a final test, hand WeaponConfig's Inspector to an imaginary teammate who's not great with numbers. Values outside the slider can't get in, and an empty description raises a warning — if it has become that kind of hard-to-break control panel, this article's main goal is met. From here on, treat the rest as a reference to pull up when you need it.

Attributes for the Inspector (Reference)

From here on, this is reference material — the full picture of Inspector attributes, including the seven above.

SerializeField: Show Private Variables in the Inspector

[SerializeField]
private float speed = 5.0f;

Designers and planners can tweak the value while your encapsulation stays intact.

field: SerializeField: Serialize a Property

This is the modern way to expose a C# auto-property in the Inspector.

[field: SerializeField] public float Speed { get; private set; } = 5.0f;

It gives you a concise design: read-only from outside code, editable from the Inspector.

HideInInspector: Hide a Variable from the Inspector

[HideInInspector]
public float currentHealth;

The field is still serialized, so its value is saved.

NonSerialized: Disable Serialization

[System.NonSerialized]
public float tempValue;
AttributeSerializedShown in Inspector
HideInInspectorYesNo
NonSerializedNoNo

Which to use: Use NonSerialized for temporary variables whose values shouldn't be saved, and HideInInspector when you want the value saved but not editable.

Header: Display a Heading

[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5.0f;
[SerializeField] private float jumpForce = 10.0f;

[Header("Attack Settings")]
[SerializeField] private float attackPower = 20.0f;

Tooltip: Display a Tooltip

[Tooltip("Movement speed (in m/s)")]
[SerializeField] private float moveSpeed = 5.0f;

Range: Display a Slider

[Range(0, 100)]
[SerializeField] private float volume = 50.0f;

Note: Range only restricts input in the Inspector. If you assign a value directly from a script, out-of-range values can still get through. To enforce the range in code as well, combine it with Mathf.Clamp.

Min: Set a Minimum Value

[Min(0)]
[SerializeField] private float health = 100.0f;

Space: Add Spacing

[SerializeField] private float moveSpeed = 5.0f;
[Space(20)]
[SerializeField] private float attackPower = 20.0f;

TextArea: Multi-Line Text Input

[TextArea(3, 10)]
[SerializeField] private string description;
Sponsored

Attributes for Methods

ContextMenu: Add a Method to the Inspector's Context Menu

[ContextMenu("Restore Full Health")]
private void ResetHealth()
{
    currentHealth = maxHealth;
    Debug.Log("Health fully restored");
}

Right-click the component in the Inspector, and the menu item appears.

MenuItem: Add a Method to the Unity Editor Menu

Important: Scripts that use [MenuItem] must be placed inside an Editor folder. The UnityEditor namespace is editor-only, so placing the script outside an Editor folder will cause build errors.

// Editor/MyEditorTools.cs
using UnityEditor;
using UnityEngine;

public class MyEditorTools
{
    [MenuItem("Tools/Reset All Objects")]
    private static void ResetAllObjects()
    {
        Debug.Log("All objects have been reset");
    }
}

RuntimeInitializeOnLoadMethod: Initialize on Game Startup

using UnityEngine;

public class GameInitializer
{
    [RuntimeInitializeOnLoadMethod]
    private static void Initialize()
    {
        Debug.Log("Game started");
    }
}

This works even in static classes that don't inherit from MonoBehaviour.

Attributes for Classes

RequireComponent: Specify Required Components

Illustration of RequireComponent: attaching PlayerController automatically brings along a Rigidbody, chained to it
[RequireComponent(typeof(Rigidbody))]
public class PlayerController : MonoBehaviour
{
    private Rigidbody rb;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }
}

Attaching PlayerController automatically attaches a Rigidbody as well. On top of that, the Rigidbody can't be removed while PlayerController is attached, so the whole class of "someone removed a required component and now GetComponent returns null" accidents is prevented by design (see the GetComponent article).

DisallowMultipleComponent: Prevent Attaching the Same Component Twice

[DisallowMultipleComponent]
public class GameManager : MonoBehaviour
{
}

ExecuteAlways: Run in Edit Mode Too

[ExecuteAlways]
public class GridGenerator : MonoBehaviour
{
    void Update()
    {
        // Branch between edit mode and play mode
        if (Application.isPlaying)
        {
            // Logic while playing
        }
        else
        {
            // Logic while editing (e.g., visualization in the Scene view)
        }
    }
}

Note: With [ExecuteAlways], Update runs every frame even in edit mode. Heavy logic will slow down the editor, so either branch on Application.isPlaying or keep the work lightweight.

CreateAssetMenu: Add a Creation Menu for ScriptableObjects

[CreateAssetMenu(fileName = "NewWeapon", menuName = "MyGame/Weapon")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public int damage;
}

Attributes for Serialization

Serializable: Serialize a Custom Class

To display a custom class (one that isn't a MonoBehaviour or ScriptableObject) in the Inspector, you need [Serializable].

[System.Serializable]
public class EnemyData
{
    public string name;
    public int hp;
    public float speed;
}

public class EnemySpawner : MonoBehaviour
{
    [SerializeField] private EnemyData[] enemies;
}

SerializeReference: Polymorphic Serialization

Lets a base class or interface field hold instances of derived classes.

public interface ISkill
{
    void Execute();
}

[System.Serializable]
public class FireSkill : ISkill
{
    public float damage;
    public void Execute() { /* Fire attack */ }
}

[System.Serializable]
public class IceSkill : ISkill
{
    public float slowDuration;
    public void Execute() { /* Ice attack */ }
}

public class Player : MonoBehaviour
{
    [SerializeReference] private ISkill skill;  // Can hold a FireSkill or IceSkill
}

Note: To pick the concrete type in the Inspector with SerializeReference, you need a custom editor or an asset like Odin Inspector.

Sponsored

Creating Custom Attributes

Defining a Custom Attribute

To create a custom attribute that affects the Inspector, inherit from PropertyAttribute.

using UnityEngine;

[System.AttributeUsage(System.AttributeTargets.Field)]
public class ReadOnlyAttribute : PropertyAttribute
{
}

Note: You must inherit from UnityEngine.PropertyAttribute, not System.Attribute. If you inherit from System.Attribute, it won't work with PropertyDrawers.

Pairing It with an Editor Extension

The PropertyDrawer must be placed inside an Editor folder.

Assets/
├── Scripts/
│   └── ReadOnlyAttribute.cs    // Attribute definition
└── Editor/
    └── ReadOnlyDrawer.cs       // PropertyDrawer (inside the Editor folder)
// Editor/ReadOnlyDrawer.cs
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        GUI.enabled = false;
        EditorGUI.PropertyField(position, property, label);
        GUI.enabled = true;
    }
}

Important: Placing a PropertyDrawer outside an Editor folder causes build errors. The UnityEditor namespace is editor-only and cannot be included in builds.

Practical Example

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class PlayerController : MonoBehaviour
{
    [Header("Movement Settings")]
    [Tooltip("Movement speed (in m/s)")]
    [SerializeField] private float moveSpeed = 5.0f;

    [Tooltip("Jump force")]
    [SerializeField] private float jumpForce = 10.0f;

    [Space(20)]

    [Header("Attack Settings")]
    [Range(10, 100)]
    [SerializeField] private float attackPower = 20.0f;

    [Min(0)]
    [SerializeField] private float attackRange = 2.0f;

    [Space(20)]

    [Header("Status")]
    [HideInInspector]
    public float currentHealth;

    [SerializeField] private float maxHealth = 100.0f;

    [ContextMenu("Restore Full Health")]
    private void ResetHealth()
    {
        currentHealth = maxHealth;
    }
}

Best Practices

  • Combine SerializeField with private - Tweak values in the Inspector while keeping encapsulation
  • Use Header and Space for readability - Group variables to keep the Inspector organized
  • Add descriptions with Tooltip - Makes fields easier for teammates to understand
  • Set sensible limits with Range and Min - Prevents invalid values from being entered
  • Declare dependencies with RequireComponent - Makes required components explicit

Bonus: Good to Know for Later

Once you're comfortable with attributes, these are the next things worth knowing.

  • [FormerlySerializedAs] — stop losing values when you rename a variable: If you rename a serialized variable, the values set in the Inspector disappear because they're tied to the old name. Add [FormerlySerializedAs("oldName")] when renaming, and the old data carries over to the new name (requires using UnityEngine.Serialization;). This is essential knowledge for renaming in team projects.

    using UnityEngine.Serialization;
    
    // Carry over the value saved under the old name "speed" to "moveSpeed"
    [FormerlySerializedAs("speed")]
    [SerializeField] private float moveSpeed = 5.0f;
    
  • [DefaultExecutionOrder] — pin down script execution order: Adding [DefaultExecutionOrder(-100)] to a class lets you specify the Script Execution Order setting in code. Handy when manager classes need to initialize first (for execution-order issues, see pitfall 2 in the Singleton article).

  • Editor extensions beyond PropertyDrawer: When custom attributes aren't enough, the next step is CustomEditor, which lets you rebuild the entire Inspector. Take that step with the intro to editor extensions.

Summary

Attributes are C# metadata markers that give your code special behavior.

  • SerializeField - Show private variables in the Inspector
  • Header, Space - Improve Inspector readability
  • Range, Min - Constrain value ranges
  • RequireComponent - Auto-attach required components
  • ContextMenu - Run methods from the editor

Put attributes to work, and your code will become more readable and easier to maintain.

Further Reading