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 is designed to be extended at two distinct levels. ISerializationFormat lets you intercept the object-to-EchoObject conversion for specific .NET types, giving you full control over how a type is represented in the intermediate tree. IFileFormat operates one layer above that, controlling how a completed EchoObject tree is encoded to and decoded from bytes — whether that’s a custom binary layout, a domain-specific text format, or anything else.

ISerializationFormat — Type-Level Handlers

The ISerializationFormat interface sits at the heart of Echo’s serialization pipeline. When Echo encounters a type it needs to serialize or deserialize, it walks its list of registered formats and delegates to the first one that claims the type via CanHandle.
public interface ISerializationFormat
{
    bool CanHandle(Type type);
    EchoObject Serialize(Type targetType, object value, SerializationContext context);
    object? Deserialize(EchoObject value, Type targetType, SerializationContext context);
}

Method Responsibilities

CanHandle(Type type)

Return true if this format is responsible for the given type. Keep this check fast — it is called for every type resolution. Checking type == typeof(MyType) or type.IsAssignableTo(typeof(ISomeInterface)) are both fine patterns.

Serialize(Type targetType, object value, SerializationContext context)

Convert value into an EchoObject and return it. The targetType is the declared field type (not necessarily the runtime type). Use context to recursively serialize nested values via Serializer.Serialize(fieldType, fieldValue, context).

Deserialize(EchoObject value, Type targetType, SerializationContext context)

Reconstruct and return an object from the provided EchoObject. Use context to recursively deserialize nested values via Serializer.Deserialize(echo, fieldType, context).

Registering a Custom Format

Call Serializer.RegisterFormat with an instance of your format. Registered formats are inserted at the front of the pipeline, giving them higher priority than all built-in formats except AnyObjectFormat, which is always kept last as the fallback.
Serializer.RegisterFormat(new Vector2Format());
Calling RegisterFormat clears the internal format cache, forcing re-resolution on the next serialization call for each type. This is expected — registration is an infrequent operation that typically happens once at startup.

Example: Custom Vector2 Format

The following format handles a hypothetical Vector2 struct, serializing its two components as a compact list rather than the default compound object.
public class Vector2Format : ISerializationFormat
{
    public bool CanHandle(Type type) => type == typeof(Vector2);

    public EchoObject Serialize(Type targetType, object value, SerializationContext context)
    {
        var v = (Vector2)value;
        var list = EchoObject.NewList();
        list.ListAdd(new EchoObject(v.X));
        list.ListAdd(new EchoObject(v.Y));
        return list;
    }

    public object? Deserialize(EchoObject value, Type targetType, SerializationContext context)
    {
        if (value.TagType != EchoType.List || value.Count < 2)
            return default(Vector2);

        return new Vector2(
            value.List[0].FloatValue,
            value.List[1].FloatValue
        );
    }
}
Then register it once at application startup:
Serializer.RegisterFormat(new Vector2Format());

// Now Vector2 fields serialize as [x, y] lists
var point = new Vector2(3.0f, 4.5f);
var echo = Serializer.Serialize(typeof(Vector2), point, new SerializationContext());
var restored = Serializer.Deserialize<Vector2>(echo);
When your format handles a family of types (e.g. all implementations of an interface), be careful about interaction with AnyObjectFormat. Returning true from CanHandle for broad type checks will shadow Echo’s built-in handling for those types. Prefer narrow, explicit checks where possible.

Built-In Format Precedence

Echo registers these formats in order, with AnyObjectFormat always anchored at the end:
PriorityFormatHandles
1PrimitiveFormatint, float, bool, string, etc.
2NullableFormatNullable<T>
3DateTimeFormatDateTime
4DateTimeOffsetFormatDateTimeOffset
5TimeSpanFormatTimeSpan
6GuidFormatGuid
7UriFormatUri
8VersionFormatVersion
9EnumFormatAll enum types
10TupleFormatTuple types
11AnonymousTypeFormatAnonymous types
12HashSetFormatHashSet<T>
13ArrayFormatArrays
14ListFormatList<T>
15QueueFormatQueue<T>
16StackFormatStack<T>
17LinkedListFormatLinkedList<T>
18CollectionFormatOther ICollection<T>
19DictionaryFormatIDictionary types
20FixedStructureFormat[FixedEchoStructure] types
LastAnyObjectFormatEverything else (field reflection)
Custom formats registered via RegisterFormat are prepended before index 1, making them the highest priority.

IFileFormat — Output Encoding

IFileFormat defines how a completed EchoObject tree is written to and read from a Stream. This is the layer for custom binary layouts, proprietary text formats, or any encoding not already provided by Echo.
public interface IFileFormat
{
    void WriteTo(EchoObject tag, Stream stream);
    EchoObject ReadFrom(Stream stream);
}

Extension Methods

Once you implement IFileFormat, a full set of convenience methods is available automatically via FileFormatExtensions:
MethodDescription
WriteToString(EchoObject tag)Writes to a UTF-8 string (best for text formats)
ReadFromString(string input)Reads from a UTF-8 string
WriteToBytes(EchoObject tag)Writes to a byte[] (works for text and binary)
ReadFromBytes(byte[] data)Reads from a byte[]
WriteToFile(EchoObject tag, string path)Writes to a file at the given path
ReadFromFile(string path)Reads from a file at the given path
All of these are derived from the two core WriteTo/ReadFrom methods — you only need to implement those two.

Example: Key=Value Text Format

The following format encodes a flat compound EchoObject as a simple key=value text file. It demonstrates the full IFileFormat contract.
public class KeyValueFormat : IFileFormat
{
    public static readonly KeyValueFormat Instance = new();

    public void WriteTo(EchoObject tag, Stream stream)
    {
        if (tag.TagType != EchoType.Compound)
            throw new InvalidOperationException("KeyValueFormat only supports compound objects.");

        using var writer = new StreamWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true);
        foreach (var entry in tag.Tags)
            writer.WriteLine($"{entry.Key}={entry.Value.StringValue}");
    }

    public EchoObject ReadFrom(Stream stream)
    {
        var compound = EchoObject.NewCompound();
        using var reader = new StreamReader(stream, System.Text.Encoding.UTF8, leaveOpen: true);

        string? line;
        while ((line = reader.ReadLine()) != null)
        {
            var separatorIndex = line.IndexOf('=');
            if (separatorIndex < 0) continue;

            var key = line[..separatorIndex].Trim();
            var value = line[(separatorIndex + 1)..].Trim();
            compound[key] = new EchoObject(value);
        }

        return compound;
    }
}
Using the format via the extension methods:
var echo = Serializer.Serialize(mySettings, new SerializationContext(TypeMode.None));

// Write to a file
KeyValueFormat.Instance.WriteToFile(echo, "settings.cfg");

// Read back as a string
string text = KeyValueFormat.Instance.WriteToString(echo);

// Round-trip from bytes
byte[] bytes = KeyValueFormat.Instance.WriteToBytes(echo);
EchoObject restored = KeyValueFormat.Instance.ReadFromBytes(bytes);
var settings = Serializer.Deserialize<MySettings>(restored);
IFileFormat operates on the EchoObject tree, not on raw .NET objects. Serialization (object → EchoObject) and encoding (EchoObject → bytes) are always separate steps in Echo’s pipeline.

Built-In File Formats

Echo ships with several ready-to-use IFileFormat implementations:

EchoBinaryFormat

Echo’s native binary format. Supports BinaryEncodingMode.Performance (fixed-width integers) and BinaryEncodingMode.Size (LEB128 + LZW string compression). Recommended for production I/O.

EchoTextFormat

Human-readable text format. Good for debugging and hand-editing saved data.

JsonFileFormat

JSON encoding via WriteToJson() / ReadFromJson() on EchoObject.

YamlFileFormat

YAML encoding via WriteToYaml() / ReadFromYaml() on EchoObject.

XmlFileFormat

XML encoding via WriteToXml() / ReadFromXml() on EchoObject.

BsonFileFormat

BSON binary encoding via WriteToBson() / ReadFromBson() on EchoObject.

Build docs developers (and LLMs) love