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.

Prowl.Echo serializes .NET objects into an intermediate EchoObject representation that can then be written to binary or text. The Serializer class is the single entry point for all serialization and deserialization work, and it requires no configuration to get started — just call Serialize and Deserialize.

The Core API

Serializer.Serialize

Converts a .NET object into an EchoObject. Accepts an optional TypeMode or a shared SerializationContext.

Serializer.Deserialize

Reconstructs a .NET object from an EchoObject. Generic and non-generic overloads available.

Serializer.DeserializeInto

Deserializes data into an existing object instance, overwriting only serialized fields and leaving all other state untouched.

Serializer.ClearCache

Clears all internal reflection caches. Necessary when hot-reloading assemblies at runtime.

Method Signatures

// Serialize — returns an EchoObject
EchoObject Serializer.Serialize(object? value, TypeMode typeMode = TypeMode.Auto);
EchoObject Serializer.Serialize(Type? targetType, object? value, TypeMode typeMode = TypeMode.Auto);
EchoObject Serializer.Serialize(object? value, SerializationContext context);
EchoObject Serializer.Serialize(Type? targetType, object? value, SerializationContext context);

// Deserialize — returns a typed or untyped object
T?      Serializer.Deserialize<T>(EchoObject? value);
object? Serializer.Deserialize(EchoObject? value, Type targetType);
T?      Serializer.Deserialize<T>(EchoObject? value, SerializationContext context);
object? Serializer.Deserialize(EchoObject? value, Type targetType, SerializationContext context);

// Deserialize into an existing instance
void Serializer.DeserializeInto(EchoObject? value, object target);
void Serializer.DeserializeInto(EchoObject? value, object target, SerializationContext context);

// Clear reflection caches
void Serializer.ClearCache();

Quickstart

1

Define your type

Echo serializes fields, not properties. Public fields are serialized automatically. Private fields require [SerializeField].
using Prowl.Echo;

public class Player
{
    public string Name = "Hero";
    public int    Health = 100;
    public float  Speed  = 5.0f;

    [SerializeField]
    private int _secretScore = 0;

    // Properties are NOT serialized — by design.
    public bool IsAlive => Health > 0;
}
2

Serialize the object

var player = new Player { Name = "Arden", Health = 200 };
EchoObject echo = Serializer.Serialize(player);
3

Deserialize back

Player? restored = Serializer.Deserialize<Player>(echo);
Console.WriteLine(restored?.Name); // "Arden"

Which Fields Are Serialized?

Echo’s reflection-based serializer walks the full inheritance hierarchy and collects fields that pass the following rules:
ConditionSerialized?
public field✅ Yes
private / protected field with [SerializeField]✅ Yes
private / protected field without [SerializeField]❌ No
Any field with [SerializeIgnore] or [NonSerialized]❌ No
Properties❌ No (by design)
public class Example
{
    public string VisibleField = "serialized";

    [SerializeField]
    private int _hiddenField = 42;        // included because of the attribute

    [SerializeIgnore]
    public string SkippedField = "gone";  // excluded even though it's public

    public string IgnoredProperty => "never serialized";
}
Prowl.Echo mimics Unity’s serializer in this regard: only fields matter, and the attribute controls opt-in for non-public members.

Serializing Classes, Structs, and Records

Classes

Standard reference-type classes work out of the box. All public fields, and any private fields marked [SerializeField], are serialized.
public class Weapon
{
    public string Name   = "Sword";
    public int    Damage = 25;
    public float  Range  = 1.5f;
}

EchoObject echo = Serializer.Serialize(new Weapon { Name = "Axe", Damage = 40 });
Weapon? weapon  = Serializer.Deserialize<Weapon>(echo);

Structs

Value types are handled identically. Echo’s fast path bypasses the full reflection pipeline for primitive structs.
public struct Point
{
    public float X;
    public float Y;
}

EchoObject echo = Serializer.Serialize(new Point { X = 3.0f, Y = 7.5f });
Point pt        = Serializer.Deserialize<Point>(echo);
For structs with a stable, never-changing field order (like Vector3), add [FixedEchoStructure] to enable compact positional serialization — great for network packets and binary-heavy workloads.

Records

Records expose their data through properties by default. To make record data serializable, declare backing fields explicitly, or use a record struct with public fields.
// Use public fields for Echo compatibility
public record class Config
{
    public string Host = "localhost";
    public int    Port = 8080;
}

EchoObject echo = Serializer.Serialize(new Config { Host = "example.com", Port = 443 });
Config? cfg     = Serializer.Deserialize<Config>(echo);

Null Handling

Serializing a null reference produces an EchoObject with TagType == EchoType.Null. Deserializing it returns null.
string? nullString = null;
EchoObject echo    = Serializer.Serialize(nullString); // EchoType.Null
string? result     = Serializer.Deserialize<string>(echo); // null

The Primitive Fast Path

For int, float, double, bool, string, long, byte, char, uint, short, ulong, ushort, sbyte, decimal, and byte[], Echo skips the full format-selection pipeline entirely and constructs the EchoObject in a single switch expression. This makes primitive round-trips as fast as possible.
// All of these hit the fast path:
EchoObject i = Serializer.Serialize(42);
EchoObject f = Serializer.Serialize(3.14f);
EchoObject b = Serializer.Serialize(true);
EchoObject s = Serializer.Serialize("hello");

Deserializing into an Existing Instance

DeserializeInto writes serialized data directly into an object you already have, without allocating a new one. Non-serialized fields (GPU handles, history buffers, event subscribers, etc.) are untouched.
var liveEnemy = new Enemy(); // already tracked by the game engine

EchoObject savedData = LoadSavedData();
Serializer.DeserializeInto(savedData, liveEnemy);

// liveEnemy now has restored field values, but its
// internal event subscriptions and caches are preserved.
DeserializeInto only works with compound (object) types. It will silently no-op on primitives, arrays, and null inputs.

Providing an Explicit Target Type

When you know the target type at compile time, pass it as the first argument to let Echo skip a runtime type check and produce cleaner serialized output (no unnecessary $type tag):
EchoObject echo = Serializer.Serialize(typeof(Player), player, new SerializationContext());
This is useful when storing objects in typed containers or when building custom serialization pipelines.

Clearing the Cache

Echo caches serializable field lists and type name lookups in ConcurrentDictionary instances for performance. If you dynamically load or unload assemblies at runtime (plug-in systems, hot-reload workflows), call ClearCache() afterwards:
// After hot-reloading an assembly:
Serializer.ClearCache();
Calling ClearCache() also clears TypeNameRegistry’s lookup tables, so the next serialization call for each type will re-compute everything from reflection. Avoid calling it in hot paths.

Saving and Loading EchoObject

An EchoObject is an in-memory IR. Convert it to bytes or text with the built-in format helpers:
// Text round-trip
EchoObject echo = Serializer.Serialize(player);
string     text = echo.WriteToString();
EchoObject fromText = EchoObject.ReadFromString(text);
Player? restored = Serializer.Deserialize<Player>(fromText);

// Binary round-trip
using var stream = File.OpenWrite("player.bin");
using var writer = new BinaryWriter(stream);
echo.WriteToBinary(writer);

Build docs developers (and LLMs) love