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.
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.
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 IDpublic Dictionary<int, object> idToObject; // ID → reconstructed objectpublic 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.
// Default — TypeMode.Autovar ctx = new SerializationContext();// Explicit modevar 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 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.
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 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.
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 explicitlyConfig 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.
var ctx = new SerializationContext(TypeMode.Auto);// Intercept external assets on the way outctx.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 backctx.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})!;