Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ProwlEngine/Prowl.Echo/llms.txt
Use this file to discover all available pages before exploring further.
Prowl.Echo controls serialization behaviour through a set of declarative attributes. Most of these attributes live on fields and tell the serializer whether to include or skip a field, how to handle its name across data migrations, and under what conditions it should be written. One attribute — [GenerateSerializer] — operates at the type level and triggers source generation of a complete ISerializable implementation. A second type-level attribute, [FixedEchoStructure], switches a type to positional (index-based) serialization for maximum compactness and speed.
[GenerateSerializer]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
Inherited = false, AllowMultiple = false)]
public sealed class GenerateSerializerAttribute : Attribute { }
Marks a partial class or struct for automatic source-generated ISerializable implementation. The source generator reads the type’s fields at compile time and emits optimized Serialize and Deserialize methods, taking all other Echo attributes into account.
The generator is bundled in the Prowl.Echo NuGet package — no separate analyzer package reference is required. The type must be declared partial for the generator to emit code alongside it.
The source generator is under active development. Always review the generated output, especially for complex types with inheritance, generics, or unusual field patterns.
Usage
[GenerateSerializer]
public partial class PlayerData
{
public string name = "Player";
public int level = 1;
public float health = 100f;
[SerializeIgnore]
public bool isDirty = false; // runtime flag — not persisted
}
The generator produces the Serialize and Deserialize methods in a separate partial file at build time. You never need to write them by hand.
[FixedEchoStructure]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class FixedEchoStructureAttribute : Attribute { }
Marks a type for positional (ordinal) serialization. Instead of a named Compound ({"x": 1.0, "y": 2.0, "z": 3.0}), the type is serialized as an ordered List ([1.0, 2.0, 3.0]). This eliminates field-name overhead entirely, making it ideal for hot-path types like math primitives and network packets.
The field order must never change. Because values are stored by position rather than name, reordering, inserting, or removing fields will silently corrupt all existing serialized data. Only apply this attribute to types whose layout is truly stable.
[FixedEchoStructure] works especially well for types like Vector3, Quaternion, Color, and fixed-size network messages — types where you fully control the schema and can guarantee it will not evolve.
Usage
[FixedEchoStructure]
[GenerateSerializer]
public partial struct Vector3
{
public float x;
public float y;
public float z;
}
// Serializes as: [1.0, 2.0, 3.0] — no field names, no overhead.
[SerializeField]
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class SerializeFieldAttribute : Attribute { }
Forces a private or protected field to be included in serialization. By default, Echo only serializes public fields; [SerializeField] opts a non-public field into that set without changing its access modifier.
Usage
[GenerateSerializer]
public partial class Inventory
{
public string ownerName = "Hero";
[SerializeField]
private List<Item> _items = new(); // private, but must be persisted
private int _cachedWeight; // private and transient — not serialized
}
[SerializeIgnore]
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class SerializeIgnoreAttribute : Attribute { }
Excludes a public field from serialization. The effect is identical to marking the field with [System.NonSerialized] — both annotations cause Echo to skip the field entirely. Use [SerializeIgnore] when you want the exclusion to be expressed in Echo’s own vocabulary rather than the BCL’s attribute.
Usage
[GenerateSerializer]
public partial class Enemy
{
public string id = "goblin";
public int hp = 50;
[SerializeIgnore]
public bool isTargeted = false; // UI state — not saved
[SerializeIgnore]
public float distanceToPlayer; // computed at runtime — not saved
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
public class FormerlySerializedAsAttribute : Attribute
{
public string oldName { get; set; }
public FormerlySerializedAsAttribute(string name) => oldName = name;
}
Maps one or more legacy field names to the current field name during deserialization. When Echo reads a compound and cannot find the current field name, it checks whether any [FormerlySerializedAs] annotation matches a key that does exist in the stored data, and reads from that key instead.
AllowMultiple = true means you can stack the attribute to support an arbitrary chain of historical renames.
[FormerlySerializedAs] only affects deserialization. Serialization always writes the current field name.
Properties
The historical name that was used in previously serialized data.
Usage
[GenerateSerializer]
public partial class CharacterStats
{
// Renamed: "Strength" → "str" → "strength" over three versions.
[FormerlySerializedAs("str")]
[FormerlySerializedAs("Strength")]
public int strength = 10;
// Simple single rename
[FormerlySerializedAs("MaxHP")]
public int maxHealth = 100;
}
[IgnoreOnNull]
[AttributeUsage(AttributeTargets.Field,
Inherited = false, AllowMultiple = false)]
public sealed class IgnoreOnNullAttribute : Attribute { }
Skips writing a field to the output when its value is null. Without this attribute, a null field is written as an explicit null entry in the compound. With it, the key is omitted entirely, which reduces the size of the serialized output for sparsely-populated objects.
[IgnoreOnNull] pairs well with optional reference fields — configuration overrides, plugin extensions, or nullable metadata — where the absence of a value is the common case and the default is restored on deserialization without any stored entry.
Usage
[GenerateSerializer]
public partial class QuestEntry
{
public string questId = "";
[IgnoreOnNull]
public string? customTitle = null; // omitted when null
[IgnoreOnNull]
public List<string>? tags = null; // omitted when null
public bool isCompleted = false; // always written, even when false
}
[SerializeIf]
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class SerializeIfAttribute : Attribute
{
public string ConditionMemberName { get; }
public SerializeIfAttribute(string conditionMemberName)
=> ConditionMemberName = conditionMemberName;
}
Conditionally includes or excludes a field based on the runtime value of another member on the same type. The named member must return bool; it can be a field, a property, or a parameterless method. When the condition member evaluates to false at serialization time, the field is skipped.
[SerializeIf] is evaluated only during serialization, not during deserialization. If the condition was false when data was written, the field simply will not be present in the stored compound.
Properties
The name of the field, property, or method on the same type whose return value controls whether this field is serialized. The member must return bool.
Usage
[GenerateSerializer]
public partial class NetworkPacket
{
public bool HasPayload = true;
// Only written when HasPayload is true
[SerializeIf(nameof(HasPayload))]
public byte[] payload = Array.Empty<byte>();
// Multiple fields can reference the same condition
[SerializeIf(nameof(HasPayload))]
public int payloadVersion = 1;
}
You can also reference a computed property:
[GenerateSerializer]
public partial class SaveSlot
{
public int itemCount = 0;
// Derived bool property used as the condition
public bool HasItems => itemCount > 0;
[SerializeIf(nameof(HasItems))]
public List<Item> items = new();
}