Source Generator
The ZeroAlloc.Serialisation.Generator package contains a Roslyn incremental source generator. For every type annotated with [ZeroAllocSerializable] it emits:
- A sealed
{TypeName}Serializer : ISerializer<{TypeName}>class - A per-type DI registration extension method
Additionally, it emits one SerializerDispatcher class per assembly covering all annotated types.
Per-Type Output
Given:
[MemoryPackable]
[ZeroAllocSerializable(SerializationFormat.MemoryPack)]
public partial class PersonEvent
{
public string Name { get; set; } = "";
public int Age { get; set; }
}
The generator emits:
// <auto-generated/>
#pragma warning disable IL2026, IL3050 // T is a closed, concrete type — AOT safety guaranteed by the caller's generator
internal sealed class PersonEventSerializer : ISerializer<PersonEvent>
{
public void Serialize(IBufferWriter<byte> writer, PersonEvent value)
=> global::MemoryPack.MemoryPackSerializer.Serialize(writer, value);
public PersonEvent? Deserialize(ReadOnlySpan<byte> buffer)
{
if (buffer.IsEmpty) return default;
return global::MemoryPack.MemoryPackSerializer.Deserialize<PersonEvent>(buffer);
}
}
public static partial class SerializerServiceCollectionExtensions
{
public static IServiceCollection AddPersonEventSerializer(this IServiceCollection services)
{
services.TryAddSingleton<ISerializer<PersonEvent>, PersonEventSerializer>();
return services;
}
}
Assembly-Wide Dispatcher
In addition to per-type files, the generator emits a single SerializerDispatcher.g.cs covering every [ZeroAllocSerializable] type in the assembly:
// <auto-generated/>
public sealed partial class SerializerDispatcher : ISerializerDispatcher
{
public ReadOnlyMemory<byte> Serialize(object value, Type type)
{
var __writer = new ArrayBufferWriter<byte>();
switch (value)
{
case global::MyApp.OrderCreated __e: new global::MyApp.OrderCreatedSerializer().Serialize(__writer, __e); break;
case global::MyApp.OrderShipped __e: new global::MyApp.OrderShippedSerializer().Serialize(__writer, __e); break;
default:
throw new NotSupportedException(
$"No serializer registered for {type.FullName}. " +
"Ensure the type is annotated with [ZeroAllocSerializable].");
}
return __writer.WrittenMemory;
}
public object? Deserialize(ReadOnlyMemory<byte> data, Type type)
{
if (type == typeof(global::MyApp.OrderCreated)) return new global::MyApp.OrderCreatedSerializer().Deserialize(data.Span);
if (type == typeof(global::MyApp.OrderShipped)) return new global::MyApp.OrderShippedSerializer().Deserialize(data.Span);
throw new NotSupportedException(
$"No serializer registered for {type.FullName}. " +
"Ensure the type is annotated with [ZeroAllocSerializable].");
}
}
The partial modifier allows adding hand-written cases for types that cannot be annotated.
A companion SerializerDispatcherExtensions.g.cs is also emitted:
// <auto-generated/>
public static partial class SerializerServiceCollectionExtensions
{
public static IServiceCollection AddSerializerDispatcher(this IServiceCollection services)
{
services.TryAddSingleton<ISerializerDispatcher, SerializerDispatcher>();
return services;
}
}
AOT Safety
T is closed at generation time — PersonEvent is a compile-time constant, not a generic parameter. MemoryPack's own source generator has already emitted the IMemoryPackable<PersonEvent> implementation, so no dynamic code is required. The #pragma warning disable IL2026, IL3050 suppresses the analyzer warnings that would otherwise fire on the static API calls.
The SerializerDispatcher uses compile-time typeof() comparisons and pattern matching — no reflection on the Type parameter.
Formats
The SerializationFormat enum selects which backend API the generated code calls:
| Format | Serialize call | Deserialize call |
|---|---|---|
MemoryPack | MemoryPackSerializer.Serialize(writer, value) | MemoryPackSerializer.Deserialize<T>(buffer) |
MessagePack | MessagePackSerializer.Serialize(writer, value, options) | via ReadOnlySequence<byte> (unavoidable copy — MessagePack 3.x has no ReadOnlySpan overload) |
SystemTextJson | using var jw = new Utf8JsonWriter(writer); JsonSerializer.Serialize(jw, value) | JsonSerializer.Deserialize<T>(buffer, typeInfo) |
Supported Types
Any class or struct decorated with [ZeroAllocSerializable]. The type does not need to be partial — partial is only required by the backend's own generator (e.g. MemoryPack).