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 configuration and state container that travels through the entire serialization or deserialization pipeline for a given operation. It controls how type metadata is embedded via TypeMode, exposes optional hook delegates that let you intercept and replace the default behavior for specific objects, and maintains the reference-tracking dictionaries used when the same object instance appears more than once in an object graph.

TypeMode Enum

TypeMode governs when Serializer embeds type metadata ($type or $t/$v keys) in the serialized output.
Aggressive
TypeMode
Always embed type information on every serialized object, regardless of whether the runtime type matches the declared type. Useful when the consumer of the data cannot know the declared type ahead of time, or when you need every node to be self-describing.
var ctx = new SerializationContext(TypeMode.Aggressive);
EchoObject echo = Serializer.Serialize(new Animal(), ctx);
// echo always contains "$type" even for exact-match types.
Auto
TypeMode
default:"TypeMode.Auto"
Embed type information only when the value’s runtime type differs from the declared (target) type. This is the default and produces the most compact output while still supporting polymorphism wherever it is actually needed.
// No type info written — Animal is the declared and actual type.
var ctx = new SerializationContext(TypeMode.Auto);
EchoObject echo = Serializer.Serialize(typeof(Animal), new Animal(), ctx);

// Type info IS written — Dog is a subtype of Animal.
EchoObject echo2 = Serializer.Serialize(typeof(Animal), new Dog(), ctx);
None
TypeMode
Never embed type information. The smallest possible output, but deserialization must be told the exact concrete type — if the type is wrong the result will be incorrect or an exception will be thrown. Suitable for tightly-coupled scenarios where both sides share a fixed schema.
var ctx = new SerializationContext(TypeMode.None);
EchoObject echo = Serializer.Serialize(new Player(), ctx);

// Caller must supply the exact type; no $type key to fall back on.
Player p = Serializer.Deserialize<Player>(echo, new SerializationContext(TypeMode.None))!;
Using TypeMode.None with polymorphic types (interfaces, abstract classes, or base class references holding derived instances) will produce data that cannot be correctly deserialized unless you always supply the exact concrete type at the call site.

SerializeOverride Delegate

public delegate EchoObject? SerializeOverride(object value, SerializationContext context);
A callback invoked by Serializer before any built-in formatter runs, giving you the opportunity to take full control of how a specific object is serialized.
value
object
The object that is about to be serialized.
context
SerializationContext
The active serialization context. You may call Serializer.Serialize recursively within the delegate using this same context.
Return value: Return a non-null EchoObject to use that as the serialized representation (normal type-wrapping rules still apply around it). Return null to let the serializer proceed with its built-in formatting pipeline.

DeserializeOverride Delegate

public delegate (bool handled, object? result) DeserializeOverride(
    EchoObject data, Type targetType, SerializationContext context);
A callback invoked by Serializer before any built-in formatter runs during deserialization. The return type is a value-tuple that explicitly signals whether the delegate handled the object.
data
EchoObject
The EchoObject node that is about to be deserialized. Type metadata has already been extracted — data is the payload, not a type-envelope wrapper.
targetType
Type
The resolved target type (after any $type metadata has been applied).
context
SerializationContext
The active deserialization context.
Return value:
  • Return (true, result) to use result as the deserialized object; the built-in pipeline is skipped entirely.
  • Return (false, null) to let the serializer proceed with its built-in formatting pipeline.

SerializationContext Class

public class SerializationContext

Constructor

public SerializationContext(TypeMode typeMode = TypeMode.Auto)
Creates a new context and initializes the internal reference-tracking tables. A sentinel NullKey instance is pre-registered at id 0 so that null values round-trip correctly without special-casing.
typeMode
TypeMode
default:"TypeMode.Auto"
Determines when type metadata is embedded. See the TypeMode section above.

OnSerialize Property

public SerializeOverride? OnSerialize { get; set; }
When set, this delegate is called during Serializer.Serialize for every non-primitive, non-null object, before the built-in formatter selection runs. Assign it to intercept specific types — for example, to replace a live asset reference with a path identifier instead of serializing the full asset data.

OnDeserialize Property

public DeserializeOverride? OnDeserialize { get; set; }
When set, this delegate is called during Serializer.Deserialize for every non-null node, before the built-in formatter selection runs. Assign it to reconstruct objects from external references or apply custom construction logic.

Reference Tracking Fields

public Dictionary<object, int> objectToId   // object → integer id
public Dictionary<int, object> idToObject   // integer id → object
public int nextId                           // next available id (starts at 1)
These dictionaries are used internally by the serialization pipeline to detect and encode shared object references (the same instance appearing at multiple locations in an object graph). objectToId uses ReferenceEqualityComparer.Instance, so identity — not equality — determines whether two references point to the same object.
You rarely need to interact with objectToId, idToObject, or nextId directly. They are public so that advanced scenarios — such as custom ISerializationFormat implementations that need to participate in reference tracking — can do so.

Code Examples

1. Creating a context with TypeMode.None

Use this when both the writing side and the reading side share the same schema and type information in the payload would be wasteful.
var writeCtx = new SerializationContext(TypeMode.None);
EchoObject echo = Serializer.Serialize(typeof(PlayerStats), stats, writeCtx);

// Must provide exact type on the read side
var readCtx = new SerializationContext(TypeMode.None);
PlayerStats restored = Serializer.Deserialize<PlayerStats>(echo, readCtx)!;

2. Using OnSerialize to intercept serialization

A common pattern in game engines is to serialize asset references as lightweight path identifiers rather than embedding the full asset data inline.
var ctx = new SerializationContext();

ctx.OnSerialize = (value, context) =>
{
    // Replace any Texture object with just its registry path
    if (value is Texture tex)
    {
        var tag = EchoObject.NewCompound();
        tag["assetPath"] = new EchoObject(AssetRegistry.GetPath(tex));
        return tag; // non-null → use this instead of normal serialization
    }

    return null; // null → proceed with built-in serialization
};

EchoObject echo = Serializer.Serialize(material, ctx);

3. Using OnDeserialize to intercept deserialization

The matching read-side hook retrieves the live asset from the registry rather than constructing a new one.
var ctx = new SerializationContext();

ctx.OnDeserialize = (data, targetType, context) =>
{
    // Reconstruct a Texture by looking it up from the registry
    if (targetType == typeof(Texture) && data.TryGet("assetPath", out var pathTag))
    {
        Texture? tex = AssetRegistry.Load<Texture>(pathTag!.StringValue);
        return (handled: true, result: tex);
    }

    return (handled: false, result: null); // let normal deserialization proceed
};

Material mat = Serializer.Deserialize<Material>(echo, ctx)!;

4. Reusing a context across multiple objects

When serializing a group of related objects (e.g., an entire scene), pass the same context to every Serialize call so that shared object instances are tracked consistently across the whole batch.
var ctx = new SerializationContext(TypeMode.Aggressive);

var echoObjects = scene.Objects
    .Select(obj => Serializer.Serialize(obj, ctx))
    .ToList();

// All echoObjects share the same reference table from ctx.

Build docs developers (and LLMs) love