Skip to main content

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.

[FixedEchoStructure] tells Prowl.Echo that a type’s field order is stable and will never change. Instead of writing a named compound (dictionary-like) with field name strings, Echo serializes the fields positionally into a List — no names, just values in order. This produces smaller output and eliminates string key encoding during both write and read.

When to Use

Small stable value types

Vector2, Vector3, Quaternion, Color — types where the fields are fixed by definition and you own the type completely

Network packets

Message structs where the sender and receiver are always compiled from the same code and fields never drift
If you add, remove, or reorder fields on a type marked [FixedEchoStructure], all existing serialized data becomes unreadable. Only apply it to types you control completely and whose shape will not change.

Basic Usage

Combine [FixedEchoStructure] with [GenerateSerializer] for the best performance:
using Prowl.Echo;

[GenerateSerializer]
[FixedEchoStructure]
public partial struct Vector3
{
    public float X;
    public float Y;
    public float Z;
}
Usage is identical to any other serializable type:
var v = new Vector3 { X = 1.0f, Y = 2.5f, Z = -0.5f };

EchoObject echo = Serializer.Serialize(v);
var loaded = Serializer.Deserialize<Vector3>(echo);

How the Output Differs

Without [FixedEchoStructure], a Vector3 serializes to a Compound (named dictionary):
Compound {
  "X": 1.0f,
  "Y": 2.5f,
  "Z": -0.5f
}
With [FixedEchoStructure], the same type serializes to a List (positional):
List [1.0f, 2.5f, -0.5f]
The list is smaller on the wire and faster to write and read because there are no string keys to encode or decode.

Generated Code Behavior

When both [GenerateSerializer] and [FixedEchoStructure] are applied, the generator emits a different serialize/deserialize pattern than the default named-compound path. Serialize — builds a list and assigns it to compound:
public void Serialize(ref EchoObject compound, SerializationContext ctx)
{
    var list = EchoObject.NewList();
    list.ListAdd(new EchoObject(X));
    list.ListAdd(new EchoObject(Y));
    list.ListAdd(new EchoObject(Z));
    compound = list;
}
Deserialize — reads by position, validates the EchoType is List, and throws on field count mismatch:
public void Deserialize(EchoObject value, SerializationContext ctx)
{
    if (value.TagType != EchoType.List)
        throw new System.InvalidOperationException("Expected list for fixed structure deserialization");

    var listValue = (System.Collections.Generic.List<EchoObject>)value.Value!;

    if (listValue.Count != 3)
        throw new System.InvalidOperationException($"Field count mismatch. Expected 3 but got {listValue.Count}");

    X = listValue[0].FloatValue;
    Y = listValue[1].FloatValue;
    Z = listValue[2].FloatValue;
}
Any mismatch in field count throws immediately, which makes schema violations easy to detect.

Without [GenerateSerializer]

[FixedEchoStructure] also works without source generation. The FixedStructureFormat formatter handles it via reflection, reading and writing fields in declaration order (sorted by MetadataToken). This is slower than source-generated code but still produces the positional List format:
// Works without [GenerateSerializer], but uses reflection
[FixedEchoStructure]
public struct Color
{
    public float R;
    public float G;
    public float B;
    public float A;
}
Add [GenerateSerializer] alongside [FixedEchoStructure] whenever possible. The source-generated path avoids reflection entirely and is the fastest option available.

Combining with Binary Format for Minimum Size

For the absolute smallest serialized size, combine fixed structure with the binary format’s Size encoding mode:
var options = new BinarySerializationOptions
{
    EncodingMode = BinaryEncodingMode.Size  // LEB128 integer encoding
};

var v = new Vector3 { X = 1.0f, Y = 2.5f, Z = -0.5f };
EchoObject echo = Serializer.Serialize(v);

using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
echo.WriteToBinary(writer, options);
This combination — positional list + LEB128 encoding — produces the most compact representation Echo can generate.

Backward Compatibility

Because [FixedEchoStructure] serializes by position rather than by name, schema evolution requires special care:
  • [FormerlySerializedAs] has no effect — there are no names to remap
  • Adding a field in the middle of the list breaks deserialization of all existing data
  • Only appending fields to the end is safe if you also update the reader
For types that need to evolve over time, use the default compound serialization instead. Reserve [FixedEchoStructure] for types that are truly immutable in structure.

Build docs developers (and LLMs) love