dotnet-serialization
npx skills add https://github.com/novotnyllc/dotnet-artisan --skill dotnet-serialization
Agent 安装分布
Skill 文档
dotnet-serialization
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
.protofiles. - MessagePack: When you need binary compactness without
.protoschema 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))]
public partial class AppJsonContext : JsonSerializerContext
{
}
Using the Generated Context
// Serialize
string 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);
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:
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 is not null ? Results.Ok(order) : Results.NotFound();
});
app.MapPost("/orders", async (Order order, OrderService service) =>
{
await service.CreateAsync(order);
return Results.Created($"/orders/{order.Id}", order);
});
Combining Multiple Contexts
When your application has multiple serialization contexts (e.g., different bounded contexts or libraries):
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(
AppJsonContext.Default,
CatalogJsonContext.Default,
InventoryJsonContext.Default
);
});
Common Configuration
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
public partial class AppJsonContext : JsonSerializerContext
{
}
Handling Polymorphism
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
[JsonDerivedType(typeof(WalletPayment), "wallet")]
public abstract class Payment
{
public decimal Amount { get; init; }
public string Currency { get; init; } = "USD";
}
public class CreditCardPayment : Payment
{
public string Last4Digits { get; init; } = "";
}
// Register the base type -- derived types are discovered via attributes
[JsonSerializable(typeof(Payment))]
public partial class AppJsonContext : JsonSerializerContext
{
}
Protobuf Serialization
Protocol Buffers provide schema-first binary serialization. Protobuf is the default wire format for gRPC and is AOT-safe.
Package
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
Proto File
syntax = "proto3";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "MyApp.Contracts";
message OrderMessage {
int32 id = 1;
string customer_id = 2;
repeated OrderItemMessage items = 3;
google.protobuf.Timestamp created_at = 4;
}
message OrderItemMessage {
string product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
Standalone Protobuf (Without gRPC)
Use Protobuf for binary serialization without gRPC when you need compact payloads for caching, messaging, or file storage:
using Google.Protobuf;
// Serialize to bytes
byte[] bytes = order.ToByteArray();
// Deserialize from bytes
var restored = OrderMessage.Parser.ParseFrom(bytes);
// Serialize to stream
using var stream = File.OpenWrite("order.bin");
order.WriteTo(stream);
Proto File Registration in .csproj
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
MessagePack Serialization
MessagePack-CSharp provides high-performance binary serialization with smaller payloads than JSON and good .NET integration.
Package
<PackageReference Include="MessagePack" Version="3.*" />
<!-- For AOT support -->
<PackageReference Include="MessagePack.SourceGenerator" Version="3.*" />
Basic Usage with Source Generator (AOT-Safe)
using MessagePack;
[MessagePackObject]
public partial class Order
{
[Key(0)]
public int Id { get; init; }
[Key(1)]
public string CustomerId { get; init; } = "";
[Key(2)]
public List<OrderItem> Items { get; init; } = [];
[Key(3)]
public DateTimeOffset CreatedAt { get; init; }
}
Serialization
// Serialize
byte[] bytes = MessagePackSerializer.Serialize(order);
// Deserialize
var restored = MessagePackSerializer.Deserialize<Order>(bytes);
// With compression (LZ4)
var lz4Options = MessagePackSerializerOptions.Standard.WithCompression(
MessagePackCompression.Lz4BlockArray);
byte[] compressed = MessagePackSerializer.Serialize(order, lz4Options);
AOT Resolver Setup
For Native AOT compatibility, use the MessagePack source generator to produce a resolver:
// In your project, the source generator automatically produces a resolver
// from types annotated with [MessagePackObject].
// Register the generated resolver at startup:
MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard
.WithResolver(GeneratedResolver.Instance);
Anti-Patterns: Reflection-Based Serialization
Do not use reflection-based serializers in Native AOT or trimming scenarios. Reflection-based serialization fails at runtime when the linker removes type metadata.
Newtonsoft.Json (JsonConvert)
Newtonsoft.Json (JsonConvert.SerializeObject / JsonConvert.DeserializeObject) relies heavily on runtime reflection. It is incompatible with Native AOT and trimming:
// BAD: Reflection-based -- fails under AOT/trimming
var json = JsonConvert.SerializeObject(order);
var order = JsonConvert.DeserializeObject<Order>(json);
// GOOD: Source-generated -- AOT-safe
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
System.Text.Json Without Source Generators
Even System.Text.Json falls back to reflection without a source-generated context:
// BAD: No context -- uses runtime reflection
var json = JsonSerializer.Serialize(order);
// GOOD: Explicit context -- uses source-generated code
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
Migration Path from Newtonsoft.Json
- Replace
JsonConvert.SerializeObject/DeserializeObjectwithJsonSerializer.Serialize/Deserialize - Replace
[JsonProperty]with[JsonPropertyName] - Replace
JsonConverterbase class withJsonConverter<T>from System.Text.Json - Create a
JsonSerializerContextwith[JsonSerializable]for all serialized types - Replace
JObject/JTokendynamic access withJsonDocument/JsonElementor strongly-typed models - Test serialization round-trips — attribute semantics differ between libraries
Performance Guidance
Throughput Benchmarks (Approximate)
| Format | Serialize (ops/sec) | Deserialize (ops/sec) | Payload Size |
|---|---|---|---|
| Protobuf | Highest | Highest | Smallest |
| MessagePack | High | High | Small |
| STJ Source Gen | Good | Good | Larger (text) |
| STJ Reflection | Moderate | Moderate | Larger (text) |
| Newtonsoft.Json | Lower | Lower | Larger (text) |
Optimization Tips
- Reuse
JsonSerializerOptions— creating options is expensive; create once and reuse - Use
JsonSerializerContext— eliminates warm-up cost and reduces allocation - Use
Utf8JsonWriter/Utf8JsonReaderfor streaming scenarios where you process JSON without full materialization - Use Protobuf
ByteStringfor binary data instead of base64-encoded strings in JSON - Enable MessagePack LZ4 compression for large payloads over the wire
Key Principles
- Default to System.Text.Json with source generators for all JSON serialization — it is AOT-safe, fast, and built into the framework
- Use Protobuf for service-to-service binary serialization — especially as the wire format for gRPC
- Use MessagePack for high-throughput caching and real-time — when binary compactness matters but
.protoschema management is unwanted - Never use Newtonsoft.Json for new AOT-targeted projects — it is reflection-based and incompatible with trimming
- Always register
JsonSerializerContextin ASP.NET Core — useConfigureHttpJsonOptionsfor Minimal APIs andAddJsonOptionsfor MVC controllers (they are separate registrations) - Annotate all serialized types — STJ source generators only generate code for types listed in
[JsonSerializable]; MessagePack requires[MessagePackObject]
See [skill:dotnet-native-aot] for comprehensive AOT compilation pipeline, [skill:dotnet-aot-architecture] for AOT-first design patterns, and [skill:dotnet-trimming] for trimming strategies and ILLink descriptor configuration.
Agent Gotchas
- Do not use
JsonSerializer.Serialize(obj)without a context in AOT projects — it falls back to reflection and fails at runtime. Always pass the source-generatedTypeInfo. - Do not forget to list collection types in
[JsonSerializable]—[JsonSerializable(typeof(Order))]does not coverList<Order>. Add[JsonSerializable(typeof(List<Order>))]separately. - Do not use Newtonsoft.Json
[JsonProperty]attributes with System.Text.Json — they are silently ignored. Use[JsonPropertyName]instead. - Do not mix MessagePack
[Key]integer keys with[Key]string keys in the same type hierarchy — pick one strategy and stay consistent. - Do not omit
GrpcServicesattribute on<Protobuf>items — without it, both client and server stubs are generated, which may cause build errors if you only need one.