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.

The Prowl.Echo source generator analyzes your types at compile time and emits optimized Serialize and Deserialize methods that implement ISerializable. By inlining primitive construction and bypassing the reflection pipeline entirely, source-generated types are significantly faster than reflection-based serialization — outperforming both System.Text.Json and Newtonsoft.Json across all benchmarks.

Getting Started

Add [GenerateSerializer] to any partial class or struct. The partial keyword is required so the generator can add the generated code alongside your definition.
using Prowl.Echo;

[GenerateSerializer]
public partial class Player
{
    public string Name = "Player";
    public int Health = 100;
    public float Speed = 5.0f;
    public List<string> Inventory = new();
}
The generator produces a Serialize and Deserialize method in a separate partial file. You use Serializer.Serialize and Serializer.Deserialize exactly as normal — the generated code is called automatically:
var player = new Player { Name = "Hero", Health = 200 };

// Serialize — calls the generated Serialize method
EchoObject echo = Serializer.Serialize(player);

// Deserialize — calls the generated Deserialize method
var loaded = Serializer.Deserialize<Player>(echo);
The source generator is bundled in the Prowl.Echo NuGet package under analyzers/dotnet/cs. No separate package or manual reference is needed.

Requirements

partial keyword

The class or struct must be declared partial — the generator adds a second partial declaration with the generated code.

Fields only

Only public fields (and private fields with [SerializeField]) are generated. Properties are never serialized.

What the Generator Inlines

The generator classifies each field into a FieldTypeCategory and emits the most efficient code for its type. Inline handling is used for the following categories — all other types, including Nullable<T>, fall back to Serializer.Serialize/Serializer.Deserialize:
Field TypeSerialize ExpressionDeserialize Expression
byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal, boolnew EchoObject(field)echo.ByteValue / .IntValue / .FloatValue etc.
charnew EchoObject((byte)field)(char)echo.ByteValue
stringnew EchoObject(field) (null-safe)echo.StringValue
byte[]new EchoObject(field) (null-safe)echo.ByteArrayValue
enumnew EchoObject(EchoType.Int, (int)field)(MyEnum)echo.IntValue
Guidnew EchoObject(EchoType.String, field.ToString())Guid.Parse(echo.StringValue)
DateTimecompound with "date" key (binary-encoded long via ToBinary())DateTime.FromBinary(echo.Get("date").LongValue)
TimeSpancompound with "ticks" key (long ticks)new TimeSpan(echo.Get("ticks").LongValue)
List<T> of known typeinline list iterationinline list construction
T[] of known typecompound with "array" keyinline array construction
Dictionary<string, T> of known typeinline compoundinline dictionary construction
All other types (including Nullable<T>)Serializer.Serialize(typeof(T), field, ctx)Serializer.Deserialize(echo, typeof(T), ctx)
For collection types (List<T>, T[], Dictionary<string, T>), the element type must itself be a simple known category (primitive, string, enum, or Guid). Nested collections or complex element types fall back to Serializer.Serialize.

Using Attributes with Generated Code

All serialization attributes work with [GenerateSerializer]:
[GenerateSerializer]
public partial class GameState
{
    public string MapName = "";

    [FormerlySerializedAs("hp")]
    public int HitPoints = 100;

    [IgnoreOnNull]
    public string? SessionId = null;

    [SerializeIf(nameof(IsDebugBuild))]
    public string DebugInfo = "";

    [SerializeIgnore]
    public string CachedPath = "";      // never written

    [SerializeField]
    private int _internalVersion = 1;   // included despite being private

    public bool IsDebugBuild => false;
}
The generated Serialize method wraps conditional fields in the appropriate if guards. [FormerlySerializedAs] generates additional else if (value.TryGet("hp", out var ...)) branches in the Deserialize method.

Performance Comparison

With [GenerateSerializer] and [FixedEchoStructure] on a complex object graph (20 nested objects, 100-element arrays, 50-entry dictionaries, collections):
Serialize:
  MessagePack          Avg:   0.0297 ms  (1.64x faster)
  Echo                 Avg:   0.0487 ms
  System.Text.Json     Avg:   0.0708 ms  (2.39x slower)
  Newtonsoft.Json      Avg:   0.1040 ms  (3.51x slower)

Deserialize:
  Echo                 Avg:   0.0200 ms
  MessagePack          Avg:   0.0470 ms  (2.35x slower)
  System.Text.Json     Avg:   0.1113 ms  (5.56x slower)
  Newtonsoft.Json      Avg:   0.1570 ms  (7.84x slower)

Round-trip:
  Echo                 Avg:   0.0644 ms
  MessagePack          Avg:   0.0714 ms  (1.11x slower)
  System.Text.Json     Avg:   0.1776 ms  (2.76x slower)
  Newtonsoft.Json      Avg:   0.2533 ms  (3.93x slower)
For maximum performance combine [GenerateSerializer] with [FixedEchoStructure] on small, stable value types like vectors and network packets. See Fixed Structure for details.

vs. Manual ISerializable

[GenerateSerializer] auto-generates ISerializable for you. You only need to implement it manually when you need logic the generator cannot express — for example, computing a derived field or migrating data between schema versions. See Custom Serialization for the manual approach.

Build docs developers (and LLMs) love