One ISerializer<T> for MemoryPack, MessagePack and System.Text.Json, source-generated per type: swap formats without touching call sites, with no allocation added over the raw library and no reflection.
- Adds no allocation over the raw library. Serializing into a reused
IBufferWriter<byte>allocates 0 B in all three formats, asserted by tests. Deserializing allocates exactly what the raw library does. - A thin layer, measured. In the same benchmark run the wrapper serializes in 0.96× to 1.44× the time of calling the library directly.
- Works under NativeAOT and trimming. Each serializer is generated for a closed type and calls the backend's own source-generated formatter. A NativeAOT smoke app is published and run in CI.
- Mistakes fail the build. A System.Text.Json type missing from your
JsonSerializerContextis compile error ZASZ004, not a runtime exception on the first message.
dotnet add package ZeroAlloc.Serialisation
dotnet add package ZeroAlloc.Serialisation.MemoryPack # or .MessagePack, or .SystemTextJsonThe core package carries the source generator. Add the backend for each format you use.
using System.Buffers;
using MemoryPack;
using ZeroAlloc.Serialisation;
ISerializer<OrderCreated> serializer = new OrderCreatedSerializer(); // generated
var buffer = new ArrayBufferWriter<byte>();
serializer.Serialize(buffer, new OrderCreated { OrderId = Guid.NewGuid(), Product = "Coffee" });
OrderCreated? copy = serializer.Deserialize(buffer.WrittenSpan);
Console.WriteLine($"{buffer.WrittenCount} bytes, {copy?.Product}");
[MemoryPackable]
[ZeroAllocSerializable(SerializationFormat.MemoryPack)]
public partial class OrderCreated
{
public Guid OrderId { get; set; }
public string Product { get; set; } = "";
}In an app with DI, services.AddOrderCreatedSerializer() registers it and you inject ISerializer<OrderCreated>. Switching to MessagePack or JSON changes the attribute, not the code that uses it.
- A typed
ISerializer<T>that writes to anyIBufferWriter<byte>, such as a pipe or a pooled buffer, and reads from aReadOnlySpan<byte>. - Three backends: MemoryPack, MessagePack and System.Text.Json.
- DI registration generated per type, with an option to make it internal for libraries.
- Runtime dispatch by
TypethroughISerializerDispatcher, for event stores and message buses. It is a generated switch, with no reflection and no dictionary lookup. - Closed generic types such as
Envelope<Order>, declared once on the assembly. - Value objects serialized as their underlying value: a ZeroAlloc.ValueObjects
CustomerIdwrites42, not{"value":42}. - ZeroAlloc.Rest integration for non-JSON HTTP payloads, see REST integration.
Serialize into a reused IBufferWriter<byte>, .NET 10, BenchmarkDotNet, raw library and wrapper in the same run.
| Backend | Raw library | ZeroAlloc wrapper | Wrapper time vs raw |
|---|---|---|---|
| MemoryPack | 0 B | 0 B | 1.21× |
| MessagePack | 0 B | 0 B | 1.25× |
| System.Text.Json | 0 B | 0 B | 1.23× |
Allocations are exact; the time is a same-run ratio from three launches. The performance page has the deserialize numbers, the generated dispatcher's allocation profile, and when to call the raw library instead.
- Documentation: serialisation.zeroalloc.net, sources in docs/. Start with Getting started, or AOT and trimming and the diagnostics reference.
- Upgrading from 1.x: Migrating 1.x to 2.x. Release history: CHANGELOG.md.
- Related packages: ZeroAlloc.EventSourcing, ZeroAlloc.Outbox, ZeroAlloc.Rest, ZeroAlloc.ValueObjects.
- Issues and pull requests are welcome on GitHub.
- MIT licensed. See LICENSE.