Skip to main content

Serializable Type System

A class type as an ordinary field: Unity saves it, and the Inspector picks it from a list.

Quick start​

Before — Unity APIAfter — FastTools
[SerializeField]
private string _primaryWeaponName;

public System.Type PrimaryWeapon =>
string.IsNullOrEmpty(
_primaryWeaponName)
? null
: System.Type.GetType(
_primaryWeaponName, false);
[TypeSelector(Allow = TypeAllow.None)]
[SerializeField]
private SerializableType<Weapon>
_primaryWeapon;

public System.Type PrimaryWeapon =>
_primaryWeapon;

Selecting a serializable type in the InspectorSelecting a serializable type in the Inspector

SerializableType​

SerializableType stores an assembly-qualified name: the type name together with its assembly.

VariantSelection constraint
SerializableTypeNo base-type constraint
SerializableType<T>Types assignable to T

In code, the constructor creates a wrapper; a type incompatible with T throws ArgumentException:

var primary = new SerializableType<Weapon>(typeof(Sword));
System.Type type = primary;

var empty = new SerializableType<Weapon>(null);

For a resolved type, ToString() returns Type.Name, so a generic type reads Amplify`1, not the picker's caption.

Missing type​

A stored name that no longer resolves after a class, namespace, or assembly rename. The Inspector shows it as <Missing …>.

The missing Game.Combat.Spear type in an Inspector fieldThe missing Game.Combat.Spear type in an Inspector field

  • Type returns null.
  • AssemblyQualifiedName and ToString() return the stored name, from which the type can be restored.
warning

In a player, SerializableType and SerializableMonoScript find the type by its name, a string in the scene, prefab or asset data. Managed code stripping does not read such strings, so from Managed Stripping Level Low up a class chosen only in the Inspector may be dropped from the build, and .Type then returns null while the editor resolves it. Mark such classes [Preserve] (UnityEngine.Scripting) or list them in link.xml. The same applies to [TypeSelector] on a string.

SerializableMonoScript​

The same field, but the selection survives a class rename: the field remembers the script asset itself. Pick the type in the Inspector or drag a .cs file from Project onto the field.

After renaming Sword.cs → Blade.csSerializableTypeSerializableMonoScript
.Typenull, a missing typeBlade, in Play Mode too
Stored nameSwordBlade once the asset is saved again

Limitations:

  • only classes with their own .cs are listed: top-level, non-generic, named after the file;
  • generic types, nested classes and types from DLLs cannot be picked;
  • there is no public constructor, so the field cannot be created in code;
  • renaming the class without its file, or the file outside Unity without its .meta, breaks the link and the field shows a missing type.

TypeSelector​

FieldSelection result
stringStores the assembly-qualified name
SerializableType / SerializableMonoScriptConfigures the wrapper's selection
[SerializeReference]Creates an instance of the selected implementation — see SerializeReference Selector

Which types are offered​

public interface ITwoHanded { }

public abstract class Weapon { }
public abstract class MeleeWeapon : Weapon { }
public abstract class RangedWeapon : Weapon { }

public sealed class Sword : MeleeWeapon { }
public sealed class Axe : MeleeWeapon, ITwoHanded { }
public sealed class Bow : RangedWeapon, ITwoHanded { }
Field[TypeSelector(…, Allow = TypeAllow.None)]Types offered
stringtypeof(Weapon)Axe, Bow, Sword
SerializableType<MeleeWeapon>typeof(ITwoHanded)Axe, the only MeleeWeapon with ITwoHanded
SerializableType<Weapon>"MeleeWeapon, Assembly-CSharp"Axe, Sword
SerializableType<Weapon>typeof(Sword), typeof(Axe)Empty, AFT0009 warns: no class inherits both
SerializableType<Weapon>[]no argumentAxe, Bow, Sword for each entry

To allow a set of classes, give them a common interface or base class and pass it.

Properties​

PropertyDefaultBehaviour
AllowTypeAllow.AllLets abstract classes (Abstract), interfaces (Interface), both or neither into the list. Ignored on [SerializeReference]
RequiredfalseWarns about an empty type name or a null managed reference
note

In the Inspector of a runtime object, the picker leaves out types from editor-only assemblies (UnityEditor, Editor-only asmdefs and Editor folders): a player build cannot resolve them. The rule follows the object's class, so a runtime object's field declared under #if UNITY_EDITOR leaves them out too.

Required field​

[TypeSelector(typeof(Weapon), Required = true)]
[SerializeField] private string _secondaryWeapon;

An empty required field shows a warning below the pickerAn empty required field shows a warning below the picker

With Required = true, <None> remains selectable. For strings and wrappers, the check tests for an empty stored name; a missing type with a nonempty name passes this check.

For project-wide and CI validation, see required-field checks.

Constraint from another field​

Pass nameof(...) to let a field or property's current value control the candidate list:

[SerializeField] private SerializableType<Weapon> _weaponClass;

[TypeSelector(nameof(_weaponClass), Allow = TypeAllow.None)]
[SerializeField] private string _weaponName;

Choose MeleeWeapon in Weapon Class, and Weapon Name offers Sword and Axe. Changing a constraint does not clear an earlier selection.

Choosing MeleeWeapon in Weapon Class leaves only Axe and Sword in Weapon NameChoosing MeleeWeapon in Weapon Class leaves only Axe and Sword in Weapon Name

Constraint sourceConstrains to
System.TypeOne type
stringA type name resolved through Type.GetType()
SerializableType / SerializableMonoScriptThe resolved .Type value
An array of these valuesMultiple simultaneous constraints; List<T> is not supported
  • A string is first looked up among the instance fields and readable properties of the class that declares the field, inherited ones included, then as a type name.
  • For a field inside a [Serializable] class or a list element, the source is read from that same instance.
  • While the source is empty or unresolved it adds no constraint: the string Weapon Name then offers every concrete class in the project. A wrapper keeps its own T.

Errors in string arguments​

[TypeSelector("Spear, Assembly-CSharp")]
[SerializeField] private string _weaponName;

Analyzers catch mistakes in the strings:

  • AFT0006 — a one-word string that names no member of the class;
  • AFT0007 — the member cannot supply base types;
  • AFT0008 — the string is not a valid type name.

If a well-formed type name refers to a type that is not loaded, like Spear above, the Inspector shows a warning:

The constraint did not resolve, so the Inspector shows a warning below the fieldThe constraint did not resolve, so the Inspector shows a warning below the field

TypeSelectorDisplay​

[TypeSelectorDisplay] on a class changes only its row in the picker:

Parameter on SwordIn the picker
Name = "Longsword"Longsword in the list and the closed field; search still finds Sword
Group = "Weapons/Melee"Weapons → Melee → Longsword instead of the namespace
Tooltip = "A balanced blade"Tooltip on hover
Icon = "d_ScriptableObject Icon"Icon: a built-in one by name, an asset by path with extension, or one from Resources without extension
Hidden = trueNot listed; code assignment and an already stored value still work

Subclasses do not inherit these settings.

The Longsword name, icon, and Weapons/Melee group in the pickerThe Longsword name, icon, and Weapons/Melee group in the picker

note

[TypeSelector] and [TypeSelectorDisplay] are marked [Conditional("UNITY_EDITOR")]. Classes compiled into an external DLL without that symbol carry none of their settings, including Hidden.

The picker​

The picker groups types by namespace or Group and distinguishes identical names by assembly.

Favorites and Recent on the picker root pageFavorites and Recent on the picker root page

The picker's root page keeps the types you need most often:

  • Favorites: your picks. To add a type, click the star at the right of its row, or press Space while the row is selected.
  • Recent: the types chosen last.

Whether Favorites is shown and how long Recent is (0 hides it) are set in the FastTools window's Settings tab. The gear at the bottom right of the picker opens it, and both lists are cleared there too.

Generic types​

Picking an open generic type opens its argument pages and returns a constructed closed type:

public abstract class Enchantment { }
public sealed class Fire : Enchantment { }
public sealed class Frost : Enchantment { }

public sealed class Enchanted<T> : MeleeWeapon
where T : Enchantment { }

Choose Enchanted<T> in the _primaryWeapon field: the window offers the subclasses of Enchantment, and choosing Fire stores Enchanted<Fire>.

Choosing a generic type argument in the pickerChoosing a generic type argument in the picker

  • A generic argument can itself be generic: the window asks for its arguments first.
  • When every argument can be inferred from the field type, the closed type is returned immediately.
  • Interfaces, abstract classes, and hidden types are not offered as arguments.

TypeSelectorWindow​

TypeSelectorWindow opens the same picker from a custom inspector or editor window, for example from a UI Toolkit button:

using Aspid.FastTools.Types.Editors;

var button = new Button { text = "Select weapon" };
button.clicked += () => TypeSelectorWindow.Show(
GUIUtility.GUIToScreenRect(button.worldBound),
new TypeSelectorFilter { Types = new[] { typeof(Weapon) } },
currentAqn: selectedTypeName,
onSelected: aqn => selectedTypeName = aqn);

The callback receives an assembly-qualified name, or null for <None>; dismissing the window without a choice does not invoke it.

currentAqnMarked on opening
A name from the listThat type; the window opens in its group
"" (the default)<None>
nullNothing
A name missing from the listNothing: Enter right after opening cannot erase the stored name

TypeSelectorFilter​

PropertyDefaultPurpose
Typesempty: any typeBase types; a candidate must be assignable to each
AllowNone; All on [TypeSelector]Allowed categories: abstract classes and interfaces
PredicatenullA condition on top of Types and Allow
AdditionalTypesnullCandidates that bypass Types, Allow, and Predicate; Hidden filtering remains
ArgumentFilternullA condition on manually chosen arguments, on top of their where constraints
InferredArgumentFilternullA filter for arguments inferred from Types; receives the generic definition, the parameter and the argument
IncludeHiddenfalseOffer types marked Hidden = true, as generic arguments too
HideNoneOptionfalseHide <None> on the root page

Package sample​

For Inspector selection of enemy types and spawn patterns, see Types; for a picker opened from editor code, see EditorTools.

A wave of regular and elite enemies moves toward the center.A wave of regular and elite enemies moves toward the center.