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
.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))]
public partial class AppJsonContext : JsonSerializerContext
{
}
```json
### Using the Generated Context
```csharp
// 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);
```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 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);
});
```text
### 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
);
});
```json
### Common Configuration
```csharp
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
public partial class AppJsonContext : JsonSerializerContext
{
}
```json
### Handling Polymorphism
```csharp
[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
{
}
```json
---
## Protobuf Serialization
Protocol Buffers provide schema-first binary serialization. Protobuf is the default wire format for gRPC and is AOT-safe.
### Package
```xml
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
```xml
### Proto File
```protobuf
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;
}
```text
### Standalone Protobuf (Without gRPC)
Use Protobuf for binary serialization without gRPC when you need compact payloads for caching, messaging, or file storage:
```csharp
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);
```text
### Proto File Registration in .csproj
```xml
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
```xml
---
## MessagePack Serialization
MessagePack-CSharp provides high-performance binary serialization with smaller payloads than JSON and good .NET integration.
### Package
```xml
<PackageReference Include="MessagePack" Version="3.*" />
<!-- For AOT support -->
<PackageReference Include="MessagePack.SourceGenerator" Version="3.*" />
```xml
### Basic Usage with Source Generator (AOT-Safe)
```csharp
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; }
}
```text
### Serialization
```csharp
// 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);
```text
### AOT Resolver Setup
For Native AOT compatibility, use the MessagePack source generator to produce a resolver:
```csharp
// 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);
```text
---
## 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:
```csharp
// 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);
```json
### System.Text.Json Without Source Generators
Even System.Text.Json falls back to reflection without a source-generated context:
```csharp
// 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);
```json
### Migration Path from Newtonsoft.Json
1. Replace `JsonConvert.SerializeObject` / `DeserializeObject` with `JsonSerializer.Serialize` / `Deserialize`
2. Replace `[JsonProperty]` with `[JsonPropertyName]`
3. Replace `JsonConverter` base class with `JsonConverter<T>` from System.Text.Json
4. Create a `JsonSerializerContext` with `[JsonSerializable]` for all serialized types
5. Replace `JObject` / `JToken` dynamic access with `JsonDocument` / `JsonElement` or strongly-typed models
6. 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` / `Utf8JsonReader`** for streaming scenarios where you process JSON without full materialization
- **Use Protobuf `ByteString`** for 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 `.proto` schema management is unwanted
- **Never use Newtonsoft.Json for new AOT-targeted projects** -- it is reflection-based and incompatible with trimming
- **Always register `JsonSerializerContext` in ASP.NET Core** -- use `ConfigureHttpJsonOptions` for Minimal APIs and `AddJsonOptions` for 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
1. **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-generated `TypeInfo`.
2. **Do not forget to list collection types in `[JsonSerializable]`** -- `[JsonSerializable(typeof(Order))]` does not cover `List<Order>`. Add `[JsonSerializable(typeof(List<Order>))]` separately.
3. **Do not use Newtonsoft.Json `[JsonProperty]` attributes with System.Text.Json** -- they are silently ignored. Use `[JsonPropertyName]` instead.
4. **Do not mix MessagePack `[Key]` integer keys with `[Key]` string keys** in the same type hierarchy -- pick one strategy and stay consistent.
5. **Do not omit `GrpcServices` attribute on `<Protobuf>` items** -- without it, both client and server stubs are generated, which may cause build errors if you only need one.
---
## References
- [System.Text.Json source generation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation)
- [Migrate from Newtonsoft.Json to System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft)
- [Protocol Buffers for .NET](https://learn.microsoft.com/en-us/dotnet/architecture/grpc-for-wcf-developers/protobuf-data-types)
- [MessagePack-CSharp](https://github.com/MessagePack-CSharp/MessagePack-CSharp)
- [Native AOT deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- Use Serena: Navigation, refactoring, dependency analysis, precise edits
- Use Read/Grep: Reading full files, pattern matching, simple text operations
- Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
1---2name: dotnet-serialization3category: data4subcategory: serialization5description: Serializes data. System.Text.Json source generators, Protobuf, MessagePack, AOT-safe patterns.6license: MIT7targets: ['*']8tags: [csharp, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for csharp tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-serialization2425AOT-friendly serialization patterns for .NET applications. Covers System.Text.Json source generators for compile-time26serialization, Protocol Buffers (Protobuf) for efficient binary serialization, and MessagePack for high-performance27compact binary format. Includes performance tradeoff guidance for choosing the right serializer and warnings about28reflection-based serialization in AOT scenarios.2930## Scope3132- System.Text.Json source generators for compile-time serialization33- Protocol Buffers (Protobuf) for binary serialization34- MessagePack for high-performance compact format35- Performance tradeoff guidance for serializer selection36- AOT-safe serialization patterns and anti-patterns3738## Out of scope3940- Source generator authoring patterns -- see [skill:dotnet-csharp-source-generators]41- HTTP client factory and resilience pipelines -- see [skill:dotnet-http-client] and [skill:dotnet-resilience]42- Native AOT architecture and trimming -- see [skill:dotnet-native-aot] and [skill:dotnet-trimming]4344Cross-references: [skill:dotnet-csharp-source-generators] for understanding how STJ source generators work under the45hood. See [skill:dotnet-integration-testing] for testing serialization round-trip correctness.4647---4849## Serialization Format Comparison5051| Format | Library | AOT-Safe | Human-Readable | Relative Size | Relative Speed | Best For |52| ----------- | ----------------------------- | ----------------------- | -------------- | ------------- | -------------- | ------------------------------------- |53| JSON | System.Text.Json (source gen) | Yes | Yes | Largest | Good | APIs, config, web clients |54| Protobuf | Google.Protobuf | Yes | No | Smallest | Fastest | Service-to-service, gRPC wire format |55| MessagePack | MessagePack-CSharp | Yes (with AOT resolver) | No | Small | Fast | High-throughput caching, real-time |56| JSON | Newtonsoft.Json | **No** (reflection) | Yes | Largest | Slower | **Legacy only -- do not use for AOT** |5758### When to Choose What5960- **System.Text.Json with source generators**: Default choice for APIs, configuration, and any scenario where61 human-readable output or web client consumption matters. AOT-safe when using source generators.62- **Protobuf**: Default wire format for gRPC. Best throughput and smallest payload size for service-to-service63 communication. Schema-first development with `.proto` files.64- **MessagePack**: When you need binary compactness without `.proto` schema management. Good for caching layers,65 real-time messaging, and high-throughput scenarios where schema evolution is managed via attributes.6667---6869## System.Text.Json Source Generators7071System.Text.Json source generators produce compile-time serialization code, eliminating runtime reflection. This is72**required** for Native AOT and strongly recommended for all new projects. See [skill:dotnet-csharp-source-generators]73for the underlying incremental generator mechanics.7475### Basic Setup7677Define a `JsonSerializerContext` with `[JsonSerializable]` attributes for each type you serialize:7879````csharp8081using System.Text.Json.Serialization;8283[JsonSerializable(typeof(Order))]84[JsonSerializable(typeof(List<Order>))]85[JsonSerializable(typeof(OrderStatus))]86public partial class AppJsonContext : JsonSerializerContext87{88}8990```json9192### Using the Generated Context9394```csharp9596// Serialize97string json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);9899// Deserialize100Order? result = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);101102// With options (created once, reused)103var options = new JsonSerializerOptions104{105 PropertyNamingPolicy = JsonNamingPolicy.CamelCase,106 TypeInfoResolver = AppJsonContext.Default107};108109string json = JsonSerializer.Serialize(order, options);110111```json112113### ASP.NET Core Integration114115Register 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`:116117```csharp118119var builder = WebApplication.CreateBuilder(args);120121// Minimal APIs: ConfigureHttpJsonOptions122builder.Services.ConfigureHttpJsonOptions(options =>123{124 options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);125});126127// MVC Controllers: AddJsonOptions (if using controllers)128builder.Services.AddControllers()129 .AddJsonOptions(options =>130 {131 options.JsonSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);132 });133134var app = builder.Build();135136// Minimal API endpoints automatically use the registered context137app.MapGet("/orders/{id}", async (int id, OrderService service) =>138{139 var order = await service.GetAsync(id);140 return order is not null ? Results.Ok(order) : Results.NotFound();141});142143app.MapPost("/orders", async (Order order, OrderService service) =>144{145 await service.CreateAsync(order);146 return Results.Created($"/orders/{order.Id}", order);147});148149```text150151### Combining Multiple Contexts152153When your application has multiple serialization contexts (e.g., different bounded contexts or libraries):154155```csharp156157builder.Services.ConfigureHttpJsonOptions(options =>158{159 options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(160 AppJsonContext.Default,161 CatalogJsonContext.Default,162 InventoryJsonContext.Default163 );164});165166```json167168### Common Configuration169170```csharp171172[JsonSourceGenerationOptions(173 PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,174 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,175 WriteIndented = false)]176[JsonSerializable(typeof(Order))]177[JsonSerializable(typeof(List<Order>))]178public partial class AppJsonContext : JsonSerializerContext179{180}181182```json183184### Handling Polymorphism185186```csharp187188[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]189[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]190[JsonDerivedType(typeof(WalletPayment), "wallet")]191public abstract class Payment192{193 public decimal Amount { get; init; }194 public string Currency { get; init; } = "USD";195}196197public class CreditCardPayment : Payment198{199 public string Last4Digits { get; init; } = "";200}201202// Register the base type -- derived types are discovered via attributes203[JsonSerializable(typeof(Payment))]204public partial class AppJsonContext : JsonSerializerContext205{206}207208```json209210---211212## Protobuf Serialization213214Protocol Buffers provide schema-first binary serialization. Protobuf is the default wire format for gRPC and is AOT-safe.215216### Package217218```xml219220<PackageReference Include="Google.Protobuf" Version="3.*" />221<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />222223```xml224225### Proto File226227```protobuf228229syntax = "proto3";230231import "google/protobuf/timestamp.proto";232233option csharp_namespace = "MyApp.Contracts";234235message OrderMessage {236 int32 id = 1;237 string customer_id = 2;238 repeated OrderItemMessage items = 3;239 google.protobuf.Timestamp created_at = 4;240}241242message OrderItemMessage {243 string product_id = 1;244 int32 quantity = 2;245 double unit_price = 3;246}247248```text249250### Standalone Protobuf (Without gRPC)251252Use Protobuf for binary serialization without gRPC when you need compact payloads for caching, messaging, or file storage:253254```csharp255256using Google.Protobuf;257258// Serialize to bytes259byte[] bytes = order.ToByteArray();260261// Deserialize from bytes262var restored = OrderMessage.Parser.ParseFrom(bytes);263264// Serialize to stream265using var stream = File.OpenWrite("order.bin");266order.WriteTo(stream);267268```text269270### Proto File Registration in .csproj271272```xml273274<ItemGroup>275 <Protobuf Include="Protos\*.proto" GrpcServices="Both" />276</ItemGroup>277278```xml279280---281282## MessagePack Serialization283284MessagePack-CSharp provides high-performance binary serialization with smaller payloads than JSON and good .NET integration.285286### Package287288```xml289290<PackageReference Include="MessagePack" Version="3.*" />291<!-- For AOT support -->292<PackageReference Include="MessagePack.SourceGenerator" Version="3.*" />293294```xml295296### Basic Usage with Source Generator (AOT-Safe)297298```csharp299300using MessagePack;301302[MessagePackObject]303public partial class Order304{305 [Key(0)]306 public int Id { get; init; }307308 [Key(1)]309 public string CustomerId { get; init; } = "";310311 [Key(2)]312 public List<OrderItem> Items { get; init; } = [];313314 [Key(3)]315 public DateTimeOffset CreatedAt { get; init; }316}317318```text319320### Serialization321322```csharp323324// Serialize325byte[] bytes = MessagePackSerializer.Serialize(order);326327// Deserialize328var restored = MessagePackSerializer.Deserialize<Order>(bytes);329330// With compression (LZ4)331var lz4Options = MessagePackSerializerOptions.Standard.WithCompression(332 MessagePackCompression.Lz4BlockArray);333byte[] compressed = MessagePackSerializer.Serialize(order, lz4Options);334335```text336337### AOT Resolver Setup338339For Native AOT compatibility, use the MessagePack source generator to produce a resolver:340341```csharp342343// In your project, the source generator automatically produces a resolver344// from types annotated with [MessagePackObject].345// Register the generated resolver at startup:346MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard347 .WithResolver(GeneratedResolver.Instance);348349```text350351---352353## Anti-Patterns: Reflection-Based Serialization354355**Do not use reflection-based serializers in Native AOT or trimming scenarios.** Reflection-based serialization fails at runtime when the linker removes type metadata.356357### Newtonsoft.Json (JsonConvert)358359Newtonsoft.Json (`JsonConvert.SerializeObject` / `JsonConvert.DeserializeObject`) relies heavily on runtime reflection. It is **incompatible** with Native AOT and trimming:360361```csharp362363// BAD: Reflection-based -- fails under AOT/trimming364var json = JsonConvert.SerializeObject(order);365var order = JsonConvert.DeserializeObject<Order>(json);366367// GOOD: Source-generated -- AOT-safe368var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);369var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);370371```json372373### System.Text.Json Without Source Generators374375Even System.Text.Json falls back to reflection without a source-generated context:376377```csharp378379// BAD: No context -- uses runtime reflection380var json = JsonSerializer.Serialize(order);381382// GOOD: Explicit context -- uses source-generated code383var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);384385```json386387### Migration Path from Newtonsoft.Json3883891. Replace `JsonConvert.SerializeObject` / `DeserializeObject` with `JsonSerializer.Serialize` / `Deserialize`3902. Replace `[JsonProperty]` with `[JsonPropertyName]`3913. Replace `JsonConverter` base class with `JsonConverter<T>` from System.Text.Json3924. Create a `JsonSerializerContext` with `[JsonSerializable]` for all serialized types3935. Replace `JObject` / `JToken` dynamic access with `JsonDocument` / `JsonElement` or strongly-typed models3946. Test serialization round-trips -- attribute semantics differ between libraries395396---397398## Performance Guidance399400### Throughput Benchmarks (Approximate)401402| Format | Serialize (ops/sec) | Deserialize (ops/sec) | Payload Size |403|--------|--------------------|-----------------------|-------------|404| Protobuf | Highest | Highest | Smallest |405| MessagePack | High | High | Small |406| STJ Source Gen | Good | Good | Larger (text) |407| STJ Reflection | Moderate | Moderate | Larger (text) |408| Newtonsoft.Json | Lower | Lower | Larger (text) |409410### Optimization Tips411412- **Reuse `JsonSerializerOptions`** -- creating options is expensive; create once and reuse413- **Use `JsonSerializerContext`** -- eliminates warm-up cost and reduces allocation414- **Use `Utf8JsonWriter` / `Utf8JsonReader`** for streaming scenarios where you process JSON without full materialization415- **Use Protobuf `ByteString`** for binary data instead of base64-encoded strings in JSON416- **Enable MessagePack LZ4 compression** for large payloads over the wire417418---419420## Key Principles421422- **Default to System.Text.Json with source generators** for all JSON serialization -- it is AOT-safe, fast, and built into the framework423- **Use Protobuf for service-to-service binary serialization** -- especially as the wire format for gRPC424- **Use MessagePack for high-throughput caching and real-time** -- when binary compactness matters but `.proto` schema management is unwanted425- **Never use Newtonsoft.Json for new AOT-targeted projects** -- it is reflection-based and incompatible with trimming426- **Always register `JsonSerializerContext` in ASP.NET Core** -- use `ConfigureHttpJsonOptions` for Minimal APIs and `AddJsonOptions` for MVC controllers (they are separate registrations)427- **Annotate all serialized types** -- STJ source generators only generate code for types listed in `[JsonSerializable]`; MessagePack requires `[MessagePackObject]`428429See [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.430431---432433## Agent Gotchas4344351. **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-generated `TypeInfo`.4362. **Do not forget to list collection types in `[JsonSerializable]`** -- `[JsonSerializable(typeof(Order))]` does not cover `List<Order>`. Add `[JsonSerializable(typeof(List<Order>))]` separately.4373. **Do not use Newtonsoft.Json `[JsonProperty]` attributes with System.Text.Json** -- they are silently ignored. Use `[JsonPropertyName]` instead.4384. **Do not mix MessagePack `[Key]` integer keys with `[Key]` string keys** in the same type hierarchy -- pick one strategy and stay consistent.4395. **Do not omit `GrpcServices` attribute on `<Protobuf>` items** -- without it, both client and server stubs are generated, which may cause build errors if you only need one.440441---442443## References444445- [System.Text.Json source generation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation)446- [Migrate from Newtonsoft.Json to System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft)447- [Protocol Buffers for .NET](https://learn.microsoft.com/en-us/dotnet/architecture/grpc-for-wcf-developers/protobuf-data-types)448- [MessagePack-CSharp](https://github.com/MessagePack-CSharp/MessagePack-CSharp)449- [Native AOT deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)450````451452## Code Navigation (Serena MCP)453454**Primary approach:** Use Serena symbol operations for efficient code navigation:4554561. **Find definitions**: `serena_find_symbol` instead of text search4572. **Understand structure**: `serena_get_symbols_overview` for file organization4583. **Track references**: `serena_find_referencing_symbols` for impact analysis4594. **Precise edits**: `serena_replace_symbol_body` for clean modifications460461**When to use Serena vs traditional tools:**462463- **Use Serena**: Navigation, refactoring, dependency analysis, precise edits464- **Use Read/Grep**: Reading full files, pattern matching, simple text operations465- **Fallback**: If Serena unavailable, traditional tools work fine466467**Example workflow:**468469```text470# Instead of:471Read: src/Services/OrderService.cs472Grep: "public void ProcessOrder"473474# Use:475serena_find_symbol: "OrderService/ProcessOrder"476serena_get_symbols_overview: "src/Services/OrderService.cs"477```