The @ao_serialize annotation controls which fields are persisted and editable in the inspector.
@ao_serialize is the annotation you place on struct/class fields to opt them into the engine's serialization system. Without it, a field is runtime-only — it won't be saved, won't appear in the inspector, and won't be included when the scene is written to disk.
Enemy::class:Component{// Saved, shown in inspector, included in scene datamax_health:int @ao_serialize;patrol_radius:float @ao_serialize;// Runtime-only — not saved, not in inspectorcurrent_target:v2;aggro_timer:float;}
What it does
When you mark a field with @ao_serialize, the engine will:
Show it in the inspector so you can edit it on entities in the editor.
Save it with the scene so the value is restored when the scene loads.
Include it in JSON serialization (Save.set_json / Save.try_get_json).
Fields without @ao_serialize exist only in memory at runtime. They start at their default (zero) value each time the component is created or the scene is loaded.
Supported types
@ao_serialize works with all common CSL types:
Type
Example
Integers
s8, s16, s32, s64 / int
Unsigned
u8, u16, u32, u64 / uint
Floats
f32 / float, f64
Booleans
bool
Strings
string
Vectors
v2, v3, v4
Enums
Any user-defined enum
Fixed arrays
[N]T
Dynamic arrays
[..]T
Structs / classes
Nested types (their @ao_serialize fields are included recursively)
For nested structs/classes, only the fields marked @ao_serialize inside the nested type are serialized. The annotation doesn't cascade — you must mark each field individually.
Using with components
The most common use is on component fields. These become editable in the editor's inspector and are saved as part of the scene.
You can set capacity, loot_table, and is_locked per-entity in the editor. When the scene loads, those values are restored automatically.
Using with the Save system
@ao_serialize also controls which fields are included when you use the JSON save APIs. Only marked fields are written to JSON.
You can serialize any annotated type to/from a JSON string, independent of the Save system:
Default values and zero-filling
When deserializing, fields that are missing from the data (e.g. you added a new field after players already have saves) are zero-filled:
Numbers → 0
Booleans → false
Strings → ""
Arrays → empty
If zero isn't a sensible default, check and fill in your own defaults after loading:
What NOT to serialize
Not every field should be serialized. Leave @ao_serialize off fields that are:
Derived at runtime (positions computed each frame, cached lookups)
Temporary state (timers, cooldown counters, frame-local flags)
Large data that changes every frame (unnecessary save overhead)
A good rule of thumb: if the value is set once (in the editor or on load) and rarely changes, serialize it. If it's recomputed every frame, don't.
Common patterns
Enums for mode selection
Nested structs
Asset references
Some fields reference engine assets (textures, prefabs, sounds). These are serialized as asset identifiers and resolved automatically on load. See the built-in components (like Sprite_Renderer) for examples.
Chest :: class : Component {
capacity: int @ao_serialize;
loot_table: string @ao_serialize;
is_locked: bool @ao_serialize;
// Runtime state — no need to serialize
has_been_opened: bool;
}
Player_Progress :: class {
version: s64 @ao_serialize;
xp: s64 @ao_serialize;
level: s64 @ao_serialize;
unlocked_skins: [..]string @ao_serialize;
}
// Save
Save.set_json(player, "progress", ref progress);
// Load
progress: Player_Progress;
if !Save.try_get_json(player, "progress", ref progress) {
// New player — set defaults
progress.version = 1;
progress.level = 1;
}