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 exposes five interfaces that let you hook into and extend different layers of the serialization pipeline. ISerializable and ISerializationCallbackReceiver operate at the object level, giving you fine-grained control over how individual types are read and written. ISerializationFormat sits at the pipeline level, letting you register handlers for new categories of types. IFileFormat governs how EchoObject trees are persisted to bytes, strings, or files. IEchoLogger provides an optional structured logging hook for diagnostics. Together, these interfaces cover every extension point the library offers.

ISerializable

public interface ISerializable
{
    void Serialize(ref EchoObject compound, SerializationContext ctx);
    void Deserialize(EchoObject value, SerializationContext ctx);
}
Implement ISerializable when you need complete control over how a type is converted to and from an EchoObject. The most common reason to implement it manually is to handle types whose internal state cannot be expressed through simple field mapping — for example, types backed by a native handle, types that derive their serialized form from computed properties, or types with complex versioning logic.
[GenerateSerializer] auto-generates a correct ISerializable implementation for partial classes and structs at compile time. Only implement this interface manually when the generated code does not satisfy your requirements.

Methods

compound
ref EchoObject
The output compound container. Add named child EchoObject entries to it to record your type’s state. The compound is pre-created for you; you do not need to call EchoObject.NewCompound() yourself.
ctx
SerializationContext
The active serialization context. Pass this through to Serializer.Serialize / Serializer.Deserialize when you recurse into nested types so that settings such as TypeMode are propagated correctly.

Example

public partial class HitPoints : ISerializable
{
    private int _current;
    private int _max;

    public void Serialize(ref EchoObject compound, SerializationContext ctx)
    {
        compound.Add("current", new EchoObject(_current));
        compound.Add("max",     new EchoObject(_max));
    }

    public void Deserialize(EchoObject value, SerializationContext ctx)
    {
        _current = value["current"].IntValue;
        _max     = value["max"].IntValue;
    }
}

ISerializationCallbackReceiver

public interface ISerializationCallbackReceiver
{
    void OnBeforeSerialize();
    void OnAfterDeserialize();
}
ISerializationCallbackReceiver lets a type participate in the serialization lifecycle without taking over the entire read/write process. This is useful when most fields serialize normally but you need to prepare or restore transient, derived, or cached state around those operations.

Methods

OnBeforeSerialize
void
Called immediately before Echo serializes the object. Use this to populate any fields that must be written but are ordinarily derived from other state — for example, converting a runtime Dictionary into a serializable list of key–value pairs.
OnAfterDeserialize
void
Called immediately after Echo finishes writing deserialized values into the object’s fields. Use this to rebuild caches, re-establish object references, or validate the deserialized state.

Example — rebuilding a lookup dictionary after deserialization

[GenerateSerializer]
public partial class ItemDatabase : ISerializationCallbackReceiver
{
    // Serialized as a plain list
    public List<Item> items = new();

    // Runtime-only lookup — not serialized
    [SerializeIgnore]
    public Dictionary<string, Item> lookup = new();

    public void OnBeforeSerialize()
    {
        // Nothing needed; the list is already up-to-date.
    }

    public void OnAfterDeserialize()
    {
        // Rebuild the dictionary from the deserialized list.
        lookup = items.ToDictionary(item => item.Id);
    }
}

ISerializationFormat

public interface ISerializationFormat
{
    bool CanHandle(Type type);
    EchoObject Serialize(Type targetType, object value, SerializationContext context);
    object? Deserialize(EchoObject value, Type targetType, SerializationContext context);
}
ISerializationFormat is the extension point for teaching Echo how to serialize an entirely new category of types. Registered formats are consulted in registration order before the built-in formats, except for AnyObjectFormat, which is always evaluated last as the final fallback. Register a custom format with:
Serializer.RegisterFormat(new MyCustomFormat());
Registering a format clears the internal format cache, so a type that was previously handled by a built-in format will be re-evaluated against your new format on the next serialization call.

Methods

CanHandle
bool
Return true for every Type this format should handle. Echo calls CanHandle on each registered format in order and uses the first one that returns true. Keep the check fast — it is called on every unique type the first time that type is serialized.
Serialize
EchoObject
Convert value (which is guaranteed to be non-null and of a type for which CanHandle returned true) into an EchoObject. Return a Compound, List, or primitive EchoObject as appropriate.
Deserialize
object?
Convert an EchoObject back into an instance of targetType. Return null only if targetType is a reference type and the serialized value was null.

Example — custom format for a Color struct

public readonly struct Color
{
    public readonly float R, G, B, A;
    public Color(float r, float g, float b, float a) => (R, G, B, A) = (r, g, b, a);
}

public class ColorFormat : ISerializationFormat
{
    public bool CanHandle(Type type) => type == typeof(Color);

    public EchoObject Serialize(Type targetType, object value, SerializationContext context)
    {
        var color = (Color)value;
        var compound = EchoObject.NewCompound();
        compound.Add("r", new EchoObject(color.R));
        compound.Add("g", new EchoObject(color.G));
        compound.Add("b", new EchoObject(color.B));
        compound.Add("a", new EchoObject(color.A));
        return compound;
    }

    public object? Deserialize(EchoObject value, Type targetType, SerializationContext context)
    {
        return new Color(
            value["r"].FloatValue,
            value["g"].FloatValue,
            value["b"].FloatValue,
            value["a"].FloatValue
        );
    }
}

// Registration (call once at startup):
Serializer.RegisterFormat(new ColorFormat());

IFileFormat

public interface IFileFormat
{
    void WriteTo(EchoObject tag, Stream stream);
    EchoObject ReadFrom(Stream stream);
}
IFileFormat defines how an EchoObject tree is encoded into bytes and decoded back again. The two core methods work with Stream, keeping the interface transport-agnostic. A suite of extension methods — defined in the static class FileFormatExtensions — wraps these core methods for the common cases of strings, byte arrays, and file paths.

Core methods

WriteTo
void
Write the complete EchoObject tree rooted at tag to stream. The stream is not closed or flushed by the implementation; callers are responsible for lifetime management.
ReadFrom
EchoObject
Read bytes from stream and reconstruct the EchoObject tree. Throws InvalidDataException if the stream does not contain valid data for this format.

Extension methods (FileFormatExtensions)

These extension methods are available on any IFileFormat implementation, including custom ones you write yourself.
MethodReturnsDescription
WriteToString(EchoObject tag)stringSerialize to a UTF-8 string. Best suited for text-based formats.
ReadFromString(string input)EchoObjectDeserialize from a UTF-8 string.
WriteToBytes(EchoObject tag)byte[]Serialize to a byte array. Works for both text and binary formats.
ReadFromBytes(byte[] data)EchoObjectDeserialize from a byte array.
WriteToFile(EchoObject tag, string path)voidSerialize and write directly to a file path.
ReadFromFile(string path)EchoObjectRead and deserialize from a file path.

Example

IFileFormat format = JsonFileFormat.Instance;

// Write to string
string json = format.WriteToString(echoObj);

// Read from string
EchoObject restored = format.ReadFromString(json);

// Write to file
format.WriteToFile(echoObj, "save.json");

// Read from file
EchoObject fromFile = format.ReadFromFile("save.json");

IEchoLogger

// namespace Echo.Logging
public interface IEchoLogger
{
    void Debug(string message);
    void Info(string message);
    void Warning(string message);
    void Error(string message, Exception? exception = null);
}
IEchoLogger is a thin structured-logging abstraction that Echo uses internally to report diagnostic information, type resolution warnings, and serialization errors. Implement it to route Echo’s log output into your application’s existing logging infrastructure. The default logger is NullEchoLogger, which silently discards all messages. No log output is produced unless you assign a custom logger.

Methods

Debug
void
Low-verbosity trace messages. Useful during development to understand how types are resolved and which formats are selected.
Info
void
General informational messages such as cache-clear notifications.
Warning
void
Non-fatal issues such as unrecognized type names or fields skipped during deserialization.
Error
void
Errors encountered during serialization or deserialization. The optional exception parameter carries the underlying exception when one is available.

Assigning a custom logger

public class ConsoleLogger : IEchoLogger
{
    public void Debug(string message)                        => Console.WriteLine($"[DBG] {message}");
    public void Info(string message)                         => Console.WriteLine($"[INF] {message}");
    public void Warning(string message)                      => Console.WriteLine($"[WRN] {message}");
    public void Error(string message, Exception? exception = null)
    {
        Console.WriteLine($"[ERR] {message}");
        if (exception != null) Console.WriteLine(exception);
    }
}

// Assign once at startup:
Serializer.Logger = new ConsoleLogger();
Serializer.Logger is a static property on Prowl.Echo.Serializer. The default value is new NullEchoLogger(), which is a no-op implementation bundled in the Echo.Logging namespace.

Build docs developers (and LLMs) love