Serialization Basics¶
Nalix.Codec.Serialization provides the serializer entry point and formatter resolution layer used by the framework and packet system.
Use this page when you want the lower-level model behind LiteSerializer and FormatterProvider.
Source mapping¶
src/Nalix.Codec/Serialization/IFormatter.cssrc/Nalix.Codec/Serialization/FormatterProvider.cssrc/Nalix.Codec/Serialization/LiteSerializer.cssrc/Nalix.Abstractions/Primitivessrc/Nalix.Codec/Serialization/Formatters/Collectionssrc/Nalix.Codec/Serialization/Formatters/Automatic
Main types¶
IFormatter<T>IFillableFormatter<T>FormatterProviderLiteSerializer- built-in primitive, enum, collection, memory, tuple, object, and struct formatters
What this layer does¶
The framework serialization layer is responsible for:
- resolving the right formatter for a type
- serializing and deserializing values to
byte[],Span<byte>,ReadOnlySpan<byte>, andReadOnlyMemory<byte>surfaces - supporting built-in primitive and collection shapes
- generating formatters for supported object and struct types
- rehydrating existing instances when a formatter implements
IFillableFormatter<T>
LiteSerializer¶
LiteSerializer is the main convenience API. Use it when you want to serialize or deserialize a supported model without working directly with formatter instances.
Public overload groups¶
| Operation | Overload shape | Notes |
|---|---|---|
| Serialize to owned array | Serialize<T>(in T value) |
Allocates a right-sized byte[]; unmanaged values use direct unaligned writes. |
| Serialize to caller array | Serialize<T>(in T value, byte[] buffer) |
Writes into the provided array and returns bytes written. |
| Serialize to caller span | Serialize<T>(in T value, Span<byte> buffer) |
Zero-copy caller-owned path. Span-backed writers cannot grow. |
| Deserialize into existing value | Deserialize<T>(byte[]/ReadOnlyMemory<byte>/ReadOnlySpan<byte>, ref T value) |
Uses IFillableFormatter<T>.Fill when available and value is non-null. |
| Deserialize new value | Deserialize<T>(byte[]/ReadOnlyMemory<byte>, out int bytesRead) |
Creates or returns a value and reports consumed bytes. |
| Register custom formatter | Register<T>(IFormatter<T> formatter) |
Delegates to FormatterProvider.Register. |
Example¶
byte[] bytes = LiteSerializer.Serialize(model);
MyModel clone = LiteSerializer.Deserialize<MyModel>(bytes, out int bytesRead);
Span<byte> scratch = stackalloc byte[256];
int written = LiteSerializer.Serialize(model, scratch);
FormatterProvider¶
FormatterProvider resolves a formatter for a specific type.
Use it when you want lower-level control than LiteSerializer gives you.
Register custom formatters before relying on the automatic fallback path:
That lets the serializer pick your formatter before falling back to the built-in resolution path.
Formatter contracts¶
IFormatter<T> is the formatter contract behind the serializer system. It defines the serialize and deserialize behavior for a specific type.
public interface IFormatter<T>
{
void Serialize(ref DataWriter writer, T value);
T Deserialize(ref DataReader reader);
}
IFillableFormatter<T> extends the contract with in-place rehydration:
public interface IFillableFormatter<T> : IFormatter<T>
{
void Fill(ref DataReader reader, T value);
}
When LiteSerializer.Deserialize(..., ref value) receives a non-null value and the root formatter is fillable, the serializer calls Fill instead of replacing the instance. This is important for pooled packet objects and other long-lived containers.
Supported shapes¶
The current source supports these groups directly:
- unmanaged primitives and value types
stringandstring[]- nullable value types such as
int?,Guid?,DateTime? - unmanaged arrays such as
int[],Guid[],DateTime[] - nullable arrays such as
int?[],Guid?[] - enum values, enum arrays, and enum lists
List<T>Dictionary<TKey, TValue>Queue<T>Stack<T>HashSet<T>Memory<T>andReadOnlyMemory<T>for unmanaged element typesValueTuplearity 2 through 5- automatic class and struct serialization through generated formatters
Encoding and bounds rules¶
| Shape | Encoding behavior | Guardrail |
|---|---|---|
| Unmanaged value | Raw unaligned bytes. | Reader requires at least sizeof(T) bytes. |
| Unmanaged array | 4-byte length prefix followed by raw element bytes. | Length must be within SerializerBounds.MaxArray; total byte size must not overflow. |
| Null unmanaged array | 4-byte null marker. | Marker is read before element allocation. |
| Empty unmanaged array | 4-byte zero marker. | Round-trips as an empty array. |
string |
4-byte UTF-8 byte-count prefix followed by UTF-8 payload. | Byte count must not exceed SerializerBounds.MaxString. |
Null string |
Dedicated null sentinel. | Null and empty strings remain distinct. |
| Enum | Backing integral type. | Supported backing types are byte, sbyte, short, ushort, int, uint, long, and ulong. |
Span-backed serialization wraps the caller buffer directly. If the formatter writes past the span capacity, the writer cannot expand and the operation fails instead of allocating a replacement buffer.
Configuring bounds
SerializerBounds.MaxArray and SerializerBounds.MaxString are populated at startup from
SerializationOptions (loaded via ConfigurationManager). The defaults are 1_048_576 (1 M) for both.
SerializationOptions.MaxWriterCapacity (default 128 MB) caps individual DataWriter buffer expansions.
Adjust these values in the active configuration file managed by ConfigurationManager to harden against malformed or oversized deserialization payloads.
Built-in formatter details¶
StringFormatter¶
StringFormatter writes a 32-bit UTF-8 byte length, then encodes directly into the writer's free buffer. It handles three distinct cases:
nullwrites the serializer null sentinelstring.Emptywrites length0with no payload- non-empty strings write a positive byte count plus UTF-8 bytes
During reads, negative values other than the null sentinel and lengths greater than SerializerBounds.MaxString are rejected.
EnumFormatter<T>¶
EnumFormatter<T> resolves the enum backing type once in its static constructor and caches specialized serialize/deserialize delegates. The hot path reinterprets enum bits with Unsafe.As and then delegates to the matching primitive formatter, avoiding boxing.
Supported backing types are:
byte/sbyteshort/ushortint/uintlong/ulong
Automatic member model¶
Generated object and struct formatters are field-based.
- instance fields are serialized directly
- public and non-public instance fields participate in discovery
- static members do not participate
- properties are not invoked directly as serialization accessors
- auto-properties typically work through their compiler-generated backing fields
- custom or computed properties should be treated as non-serializable unless you provide a custom formatter
This means a shape like public int Count { get; set; } is a good fit for automatic serialization, while a shape like public int Count => _items.Count; is metadata only and should usually be ignored.
Readonly and constructor-only models need extra care. If a type depends on strict immutability semantics, the recommended approach is to register a custom IFormatter<T> that reconstructs the object intentionally.
Relationship to packet serialization¶
This layer sits underneath packet model serialization and frame handling.
It is the machinery that PacketBase<TSelf> and related frame helpers build on.