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.

SerializationContext is the object that carries all per-operation configuration through the Prowl.Echo serialization pipeline. A single context instance is created at the start of a Serialize or Deserialize call and flows through every formatter and sub-object encountered during that call. You do not need to create one for simple use-cases — every Serializer method has an overload that accepts only the value and creates a default Auto-mode context internally. Create your own context when you need to control type embedding, intercept specific types, or track cross-object references yourself.

TypeMode

The TypeMode enum determines how aggressively Echo embeds .NET type information in the serialized graph.

Auto (default)

Embeds type information only when the actual runtime type differs from the declared field/parameter type. This is the recommended setting for most applications.

Aggressive

Always embeds $type in every compound object, regardless of whether the type matches the target. Use when every consumer needs to reconstruct the exact runtime type without any contextual hints.

None

Never embeds type information. Deserialization will fail if the concrete type cannot be inferred from the declared target type alone. Useful for simple, schema-stable value objects.

Circular Reference Tracking

SerializationContext maintains two dictionaries that Echo uses internally to detect and break circular references during serialization and to restore object identity during deserialization:
public Dictionary<object, int> objectToId; // object → stable integer ID
public Dictionary<int, object> idToObject; // ID → reconstructed object
public int nextId;                          // incremented for each new object
Echo populates these automatically as it walks your object graph. You do not need to interact with them directly in typical usage, but you can read objectToId after serialization to discover which objects were visited and in what order.
A single SerializationContext should not be reused across multiple independent Serialize or Deserialize calls. Create a fresh instance for each top-level operation so that the reference-tracking dictionaries start empty.

Creating a Context

// Default — TypeMode.Auto
var ctx = new SerializationContext();

// Explicit mode
var aggressiveCtx = new SerializationContext(TypeMode.Aggressive);
var noneCtx       = new SerializationContext(TypeMode.None);
Pass the context as the second argument to any Serializer method:
var ctx  = new SerializationContext(TypeMode.None);
var echo = Serializer.Serialize(myObject, ctx);

MyType result = Serializer.Deserialize<MyType>(echo, ctx)!;

OnSerialize: Intercepting Serialization

OnSerialize is a delegate of type SerializeOverride:
public delegate EchoObject? SerializeOverride(object value, SerializationContext context);
When set, Echo calls it for every object it visits before applying any built-in formatter. Return a non-null EchoObject to substitute your own representation; return null to let normal serialization proceed.

Example: Externalizing asset references

In a game engine, textures and meshes are often large objects that should be stored as file paths rather than inline data. OnSerialize lets you intercept those types and emit a lightweight reference instead:
var ctx = new SerializationContext();

ctx.OnSerialize = (value, context) =>
{
    if (value is Texture tex)
    {
        // Emit a compact reference instead of serializing pixel data
        var refNode = EchoObject.NewCompound();
        refNode.Add("assetPath", new EchoObject(tex.FilePath));
        return refNode;
    }

    // Let Echo handle everything else normally
    return null;
};

EchoObject scene = Serializer.Serialize(myScene, ctx);
Type-envelope wrapping (the $type field) is still applied after your delegate returns, so the round-trip type is preserved even for intercepted objects. You only need to produce the data compound; Echo handles the envelope.

OnDeserialize: Intercepting Deserialization

OnDeserialize is a delegate of type DeserializeOverride:
public delegate (bool handled, object? result) DeserializeOverride(
    EchoObject data, Type targetType, SerializationContext context);
Return (true, yourObject) to short-circuit Echo’s built-in deserialization for a given node. Return (false, null) to let normal deserialization proceed.

Example: Resolving asset references at load time

Paired with the OnSerialize example above, you can reload the actual Texture object when the scene is read back:
var ctx = new SerializationContext();

ctx.OnDeserialize = (data, targetType, context) =>
{
    if (targetType == typeof(Texture))
    {
        if (data.TryGet("assetPath", out var pathTag))
        {
            Texture tex = AssetDatabase.Load<Texture>(pathTag!.StringValue);
            return (true, tex);
        }
    }

    return (false, null);
};

MyScene scene = Serializer.Deserialize<MyScene>(echo, ctx)!;

TypeMode.None for Schema-Stable Data

When your data is entirely value-type-based — or when the declared types in your model already fully determine the concrete types — you can skip type embedding to produce smaller, cleaner output:
public class Config
{
    public string Host    { get; set; } = "localhost";
    public int    Port    { get; set; } = 8080;
    public bool   UseTLS  { get; set; } = true;
}

var cfg  = new Config { Host = "example.com", Port = 443, UseTLS = true };
var ctx  = new SerializationContext(TypeMode.None);
var echo = Serializer.Serialize(cfg, ctx);

// Deserialize by providing the concrete type explicitly
Config loaded = Serializer.Deserialize<Config>(echo, ctx)!;
With TypeMode.None, any field declared as an interface, abstract class, or object will fail to deserialize because Echo has no embedded type name to resolve. Reserve TypeMode.None for flat, schema-stable models where every field’s declared type is also its concrete type.

Complete Context Configuration Example

var ctx = new SerializationContext(TypeMode.Auto);

// Intercept external assets on the way out
ctx.OnSerialize = (value, _) =>
{
    if (value is IExternalAsset asset)
    {
        var node = EchoObject.NewCompound();
        node.Add("ref", new EchoObject(asset.Id.ToString()));
        return node;
    }
    return null;
};

// Resolve external assets on the way back
ctx.OnDeserialize = (data, targetType, _) =>
{
    if (typeof(IExternalAsset).IsAssignableFrom(targetType)
        && data.TryGet("ref", out var refTag))
    {
        Guid id = Guid.Parse(refTag!.StringValue);
        return (true, AssetRegistry.Resolve(id));
    }
    return (false, null);
};

EchoObject saved  = Serializer.Serialize(myWorld, ctx);
MyWorld    loaded = Serializer.Deserialize<MyWorld>(saved, new SerializationContext(TypeMode.Auto)
{
    OnDeserialize = ctx.OnDeserialize
})!;

Build docs developers (and LLMs) love