Skip to main content

Source Generator

The ZeroAlloc.Serialisation.Generator package contains a Roslyn incremental source generator. For every type annotated with [ZeroAllocSerializable] it emits:

  1. A sealed {TypeName}Serializer : ISerializer<{TypeName}> class
  2. 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:

FormatSerialize callDeserialize call
MemoryPackMemoryPackSerializer.Serialize(writer, value)MemoryPackSerializer.Deserialize<T>(buffer)
MessagePackMessagePackSerializer.Serialize(writer, value, options)via ReadOnlySequence<byte> (unavoidable copy — MessagePack 3.x has no ReadOnlySpan overload)
SystemTextJsonusing 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 partialpartial is only required by the backend's own generator (e.g. MemoryPack).