AOT-friendly serialization patterns for .NET applications. Covers System.Text.Json source generators for compile-time
serialization, Protocol Buffers (Protobuf) for efficient binary serialization, and MessagePack for high-performance
compact binary format. Includes performance tradeoff guidance for choosing the right serializer and warnings about
reflection-based serialization in AOT scenarios.
Scope
System.Text.Json source generators for compile-time serialization
Protocol Buffers (Protobuf) for binary serialization
MessagePack for high-performance compact format
Performance tradeoff guidance for serializer selection
AOT-safe serialization patterns and anti-patterns
Out of scope
Source generator authoring patterns -- see [skill:dotnet-csharp-source-generators]
HTTP client factory and resilience pipelines -- see [skill:dotnet-http-client] and [skill:dotnet-resilience]
Native AOT architecture and trimming -- see [skill:dotnet-native-aot] and [skill:dotnet-trimming]
Cross-references: [skill:dotnet-csharp-source-generators] for understanding how STJ source generators work under the
hood. See [skill:dotnet-integration-testing] for testing serialization round-trip correctness.
Serialization Format Comparison
Format
Library
AOT-Safe
Human-Readable
Relative Size
Relative Speed
Best For
JSON
System.Text.Json (source gen)
Yes
Yes
Largest
Good
APIs, config, web clients
Protobuf
Google.Protobuf
Yes
No
Smallest
Fastest
Service-to-service, gRPC wire format
MessagePack
MessagePack-CSharp
Yes (with AOT resolver)
No
Small
Fast
High-throughput caching, real-time
JSON
Newtonsoft.Json
No (reflection)
Yes
Largest
Slower
Legacy only -- do not use for AOT
When to Choose What
System.Text.Json with source generators: Default choice for APIs, configuration, and any scenario where
human-readable output or web client consumption matters. AOT-safe when using source generators.
Protobuf: Default wire format for gRPC. Best throughput and smallest payload size for service-to-service
communication. Schema-first development with .proto files.
MessagePack: When you need binary compactness without .proto schema management. Good for caching layers,
real-time messaging, and high-throughput scenarios where schema evolution is managed via attributes.
System.Text.Json Source Generators
System.Text.Json source generators produce compile-time serialization code, eliminating runtime reflection. This is
required for Native AOT and strongly recommended for all new projects. See [skill:dotnet-csharp-source-generators]
for the underlying incremental generator mechanics.
Basic Setup
Define a JsonSerializerContext with [JsonSerializable] attributes for each type you serialize:
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
[JsonSerializable(typeof(OrderStatus))]
publicpartialclassAppJsonContext : JsonSerializerContext
{
}
```json
### Using the Generated Context
```csharp
// Serializestring json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Deserialize
Order? result = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// With options (created once, reused)var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = AppJsonContext.Default
};
string json = JsonSerializer.Serialize(order, options);
```json
### ASP.NET Core Integration
Register the source-generated context so Minimal APIs use it automatically. Note that `ConfigureHttpJsonOptions` applies to Minimal APIs only -- MVC controllers require separate configuration via `AddJsonOptions`:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Minimal APIs: ConfigureHttpJsonOptions
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// MVC Controllers: AddJsonOptions (if using controllers)
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
var app = builder.Build();
// Minimal API endpoints automatically use the registered context
app.MapGet("/orders/{id}", async (int id, OrderService service) =>
{
var order = await service.GetAsync(id);
return order ? Results.Ok(order) : Results.NotFound();
});
app.MapPost(, (Order order, OrderService service) =>
{
service.CreateAsync(order);
Results.Created(, order);
});
```text
;
});
```json
```csharp
[]
[]
[]
:
{
}
```json
```csharp
[]
[]
[]
{
Amount { ; ; }
Currency { ; ; } = ;
}
:
{
Last4Digits { ; ; } = ;
}
[]
:
{
}
```json
---
Protocol Buffers provide schema-first binary serialization. Protobuf the wire format gRPC AOT-safe.
```xml
<PackageReference Include= Version= />
<PackageReference Include= Version= PrivateAssets= />
```xml
```protobuf
syntax = ;
import ;
option csharp_namespace = ;
message OrderMessage {
int32 id = ;
customer_id = ;
repeated OrderItemMessage items = ;
google.protobuf.Timestamp created_at = ;
}
message OrderItemMessage {
product_id = ;
int32 quantity = ;
unit_price = ;
}
```text
Use Protobuf binary serialization without gRPC you need compact payloads caching, messaging, storage:
```csharp
Google.Protobuf;
[] bytes = order.ToByteArray();
restored = OrderMessage.Parser.ParseFrom(bytes);
stream = File.OpenWrite();
order.WriteTo(stream);
```text
```xml
<ItemGroup>
<Protobuf Include= GrpcServices= />
</ItemGroup>
```xml
---
MessagePack-CSharp provides high-performance binary serialization smaller payloads than JSON good .NET integration.
```xml
<PackageReference Include= Version= />
<!-- For AOT support -->
<PackageReference Include= Version= />
```xml
```csharp
MessagePack;
[]
{
[]
Id { ; ; }
[]
CustomerId { ; ; } = ;
[]
List<OrderItem> Items { ; ; } = [];
[]
DateTimeOffset CreatedAt { ; ; }
}
```text
```csharp
[] bytes = MessagePackSerializer.Serialize(order);
restored = MessagePackSerializer.Deserialize<Order>(bytes);
lz4Options = MessagePackSerializerOptions.Standard.WithCompression(
MessagePackCompression.Lz4BlockArray);
[] compressed = MessagePackSerializer.Serialize(order, lz4Options);
```text
For Native AOT compatibility, use the MessagePack source generator to produce a resolver:
```csharp
MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard
.WithResolver(GeneratedResolver.Instance);
```text
---
**Do use reflection-based serializers Native AOT trimming scenarios.** Reflection-based serialization fails at runtime the linker removes type metadata.
Newtonsoft.Json (`JsonConvert.SerializeObject` / `JsonConvert.DeserializeObject`) relies heavily runtime reflection. It **incompatible** Native AOT trimming:
```csharp
json = JsonConvert.SerializeObject(order);
order = JsonConvert.DeserializeObject<Order>(json);
json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
```json
Even System.Text.Json falls back to reflection without a source-generated context:
```csharp
json = JsonSerializer.Serialize(order);
json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
```json
Replace `JsonConvert.SerializeObject` / `DeserializeObject` `JsonSerializer.Serialize` / `Deserialize`
Replace `[JsonProperty]` `[JsonPropertyName]`
Replace `JsonConverter` `<>`
4. `` `[]`
5. `` / `` `` / `` -
6. - --
---
##
### ()
| | (/) | (/) | |
|--------|--------------------|-----------------------|-------------|
| | | | |
| | | | |
| | | | () |
| | | | () |
| | | | () |
###
- ** ``** -- ; create once reuse
- **Use `JsonSerializerContext`** -- eliminates warm-up cost reduces allocation
- **Use `Utf8JsonWriter` / `Utf8JsonReader`** streaming scenarios you process JSON without full materialization
- **Use Protobuf `ByteString`** binary data instead of base64-encoded strings JSON
- **Enable MessagePack LZ4 compression** large payloads over the wire
---
- **Default to System.Text.Json source generators** all JSON serialization -- it AOT-safe, fast, built the framework
- **Use Protobuf service-to-service binary serialization** -- especially the wire format gRPC
- **Use MessagePack high-throughput caching real-time** -- binary compactness matters but `.proto` schema management unwanted
- **Never use Newtonsoft.Json AOT-targeted projects** -- it reflection-based incompatible trimming
- **Always register `JsonSerializerContext` ASP.NET Core** -- use `ConfigureHttpJsonOptions` Minimal APIs `AddJsonOptions` ; MessagePack requires `[MessagePackObject]`
See [skill:dotnet-native-aot] comprehensive AOT compilation pipeline, [skill:dotnet-aot-architecture] AOT-first design patterns, [skill:dotnet-trimming] trimming strategies ILLink descriptor configuration.
---
**Do use `JsonSerializer.Serialize(obj)` without a context AOT projects** -- it falls back to reflection fails at runtime. Always pass the source-generated `TypeInfo`.
**Do forget to list collection types `[JsonSerializable]`** -- `[JsonSerializable((Order))]` does cover `List<Order>`. Add `[JsonSerializable((List<Order>))]` separately.
**Do use Newtonsoft.Json `[JsonProperty]` attributes System.Text.Json** -- they are silently ignored. Use `[JsonPropertyName]` instead.
**Do mix MessagePack `[Key]` integer keys `[Key]` keys** the same type hierarchy -- pick one strategy stay consistent.
**Do omit `GrpcServices` attribute `<Protobuf>` items** -- without it, both client server stubs are generated, which may cause build errors you only need one.
---
- [System.Text.Json source generation](https:
- [Migrate Newtonsoft.Json to System.Text.Json](https:
- [Protocol Buffers .NET](https:
- [MessagePack-CSharp](https:
- [Native AOT deployment](https:
is
not
null
"/orders"
async
await
return
$"/orders/{order.Id}"
### Combining Multiple Contexts
When your application has multiple serialization contexts (e.g., different bounded contexts or libraries):
```csharp
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(
AppJsonContext.Default,
CatalogJsonContext.Default,
InventoryJsonContext.Default
)
for MVC controllers (they are separate registrations)
- **Annotate all serialized types** -- STJ source generators only generate code for types listed in `[JsonSerializable]`