When Prowl.Echo serializes an object graph, it only knows the declared types of fields and parameters — the types that appear in your source code. At runtime, those slots can hold derived classes, interface implementations, or any other subtype. Without recording the actual runtime type, deserialization has no way to know which concrete class to instantiate. Type preservation is how Echo bridges that gap: by embedding type names directly in the serializedDocumentation 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.
EchoObject, it guarantees that Dog stays a Dog even after a round-trip through a field declared as Animal.
Why It Matters: The Polymorphism Problem
Consider a list of animals serialized without any type information:Animal, but the runtime types are Dog and Cat. Without embedding those names, deserialization would try to instantiate Animal — an abstract class — and throw. Type preservation records "MyAssembly.Dog, MyAssembly" and "MyAssembly.Cat, MyAssembly" in the graph so that the correct constructor is called on the way back.
The Three TypeMode Values
TypeMode.Auto — embed only when necessary (default)
Echo compares the actual runtime type of each value against the declared target type. If they differ, a type envelope is added. If they match exactly, no envelope is written — keeping the output lean for the common case.
TypeMode.Aggressive — always embed type
Every compound object receives a
$type field regardless of whether the declared and runtime types match. Use this when consumers cannot rely on schema knowledge and need to reconstruct the exact type unconditionally.How Type Information Is Stored
Echo uses two distinct storage formats depending on whether the preserved type is “simple” (primitives,string, decimal, DateTime, Guid, enums) or “complex” (any reference type or struct that is serialized as a compound).
Compact wrapper for simple types ($t / $v)
When a primitive or simple value must carry type information, Echo wraps it in a two-key compound:
$t holds the compact type token ("i" means System.Int32). $v holds the raw EchoObject value.
Full wrapper for complex types ($type)
For reference types serialized as compound objects, the type name is injected directly as a peer key:
$value key to hold the data alongside $type.
You can inspect the stored type of any
EchoObject before deserializing by calling GetStoredType(). It reads the $type field and resolves it through TypeNameRegistry without triggering any deserialization. See the EchoObject reference for the security implications.TypeNameRegistry: How Echo Resolves Type Names
TypeNameRegistry is the static registry that translates between System.Type objects and their serialized string forms. It maintains separate caches for compact names and full names.
Predefined compact names
For the most common types, Echo uses single-character or two-character tokens to minimise output size:| .NET Type | Compact Token |
|---|---|
int | i |
string | s |
bool | b |
float | f |
double | d |
long | l |
byte | y |
sbyte | Y |
short | h |
ushort | H |
uint | I |
ulong | L |
decimal | m |
char | c |
DateTime | dt |
Guid | g |
e: followed by the type name (e.g. e:Direction). Arrays append [] notation (e.g. i[] for int[]). Generic types use angle-bracket notation (e.g. List<i> for List<int>, Dictionary<s,i> for Dictionary<string, int>).
Full type names for complex types
Complex types are stored usingType.AssemblyQualifiedName — the fully-qualified assembly string that .NET’s Type.GetType() can resolve:
TypeNameRegistry caches both directions (type → string and string → type) in ConcurrentDictionary instances, so repeated serialization of the same type incurs the lookup cost only once.
Polymorphic Deserialization in Practice
Security Validation with GetStoredType()
In networked applications whereEchoObject data arrives from an untrusted source, always validate the embedded type before deserializing:
Choosing the Right TypeMode
Use Auto when…
You have polymorphic fields, interface fields, or abstract base classes anywhere in your object graph. This is the safe default for nearly all applications.
Use Aggressive when…
You need every object to be self-describing — for example, generic message-bus payloads or plugin systems where the receiver schema is not known at compile time.
Use None when…
You are serializing simple, flat configuration or value objects where every declared type is a concrete sealed class or primitive. Produces the smallest, simplest output.