From dotnet-skills
JSON and binary serialization patterns for .NET applications, including System.Text.Json source generators, Protocol Buffers, MessagePack, and AOT-compatible best practices. Use when configuring JSON serialization, choosing between formats, implementing Protocol Buffers for high-performance scenarios, or working with Native AOT.
npx claudepluginhub wshaddix/dotnet-skillsThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Use this skill when:
| 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 |
.proto files..proto schema management. Good for caching layers, real-time messaging, and high-throughput scenarios.| Aspect | Schema-Based | Reflection-Based |
|---|---|---|
| Examples | Protobuf, MessagePack, System.Text.Json (source gen) | Newtonsoft.Json, BinaryFormatter |
| Type info in payload | No (external schema) | Yes (type names embedded) |
| Versioning | Explicit field numbers/names | Implicit (type structure) |
| Performance | Fast (no reflection) | Slower (runtime reflection) |
| AOT compatible | Yes | No |
| Wire compatibility | Excellent | Poor |
Recommendation: Use schema-based serialization for anything that crosses process boundaries.
| Format | Problem |
|---|---|
| BinaryFormatter | Security vulnerabilities, deprecated, never use |
| Newtonsoft.Json default | Type names in payload break on rename |
| DataContractSerializer | Complex, poor versioning |
| XML | Verbose, slow, complex |
For JSON serialization, use System.Text.Json with source generators for AOT compatibility and performance.
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
[JsonSerializable(typeof(OrderStatus))]
public partial class AppJsonContext : JsonSerializerContext
{
}
// Serialize
string json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Deserialize
Order? result = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// With options
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
TypeInfoResolver = AppJsonContext.Default
};
string json = JsonSerializer.Serialize(order, options);
var builder = WebApplication.CreateBuilder(args);
// Minimal APIs
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
// MVC Controllers
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolver = JsonTypeInfoResolver.Combine(
AppJsonContext.Default,
CatalogJsonContext.Default,
InventoryJsonContext.Default
);
});
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
public partial class AppJsonContext : JsonSerializerContext
{
}
[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; } = "";
}
[JsonSerializable(typeof(Payment))]
public partial class AppJsonContext : JsonSerializerContext
{
}
Best for: Actor systems, gRPC, event sourcing, any long-lived wire format.
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
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;
}
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);
<ItemGroup>
<Protobuf Include="Protos\*.proto" GrpcServices="Both" />
</ItemGroup>
// SAFE: Add new fields with new numbers
message Order {
string id = 1;
string customer_id = 2;
string shipping_address = 5; // NEW - safe
}
// SAFE: Remove fields (keep the number reserved)
message Order {
string id = 1;
reserved 2; // customer_id removed
}
// UNSAFE: Change field types
message Order {
int32 id = 1; // Was: string - BREAKS!
}
// UNSAFE: Reuse field numbers
message Order {
reserved 2;
string new_field = 2; // Reusing 2 - BREAKS!
}
Best for: High-performance scenarios, compact payloads, actor messaging.
<PackageReference Include="MessagePack" Version="3.*" />
<PackageReference Include="MessagePack.SourceGenerator" Version="3.*" />
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; }
[Key(4)]
public string? Notes { get; init; }
}
// 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);
MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard
.WithResolver(GeneratedResolver.Instance);
Old code must safely ignore unknown fields:
// Protobuf/MessagePack: Automatic - unknown fields skipped
// System.Text.Json: Configure to allow
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
Deploy deserializers before serializers for new formats:
// Phase 1: Add deserializer (deployed everywhere)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
"Order.V1" => DeserializeV1(data),
"Order.V2" => DeserializeV2(data), // NEW - can read V2
_ => throw new NotSupportedException()
};
// Phase 2: Enable serializer (after V1 deployed everywhere)
public (byte[] data, string manifest) Serialize(Order order) =>
_useV2Format
? (SerializeV2(order), "Order.V2")
: (SerializeV1(order), "Order.V1");
// BAD: Type name in payload - renaming class breaks wire format
{
"$type": "MyApp.Order, MyApp",
"id": 123
}
// GOOD: Explicit discriminator - refactoring safe
{
"type": "order",
"id": 123
}
Approximate throughput (higher is better):
| Format | Serialize | Deserialize | Size |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json (source gen) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json (reflection) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
JsonSerializerOptions -- creating options is expensiveJsonSerializerContext -- eliminates warm-up costUtf8JsonWriter / Utf8JsonReader for streaming scenariosByteString for binary data instead of base64-encoded stringsDo not use reflection-based serializers in Native AOT or trimming scenarios.
// 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);
// 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);
JsonConvert.SerializeObject / DeserializeObject with JsonSerializer.Serialize / Deserialize[JsonProperty] with [JsonPropertyName]JsonConverter base class with JsonConverter<T> from System.Text.JsonJsonSerializerContext with [JsonSerializable] for all serialized typesJObject / JToken dynamic access with JsonDocument / JsonElement or strongly-typed modelsFor Akka.NET actor systems, use schema-based serialization:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
JsonSerializerContext in ASP.NET CoreJsonSerializer.Serialize(obj) without a context in AOT projects -- it falls back to reflection.[JsonSerializable] -- [JsonSerializable(typeof(Order))] does not cover List<Order>.[JsonProperty] attributes with System.Text.Json -- they are silently ignored.[Key] integer keys with [Key] string keys in the same type hierarchy.GrpcServices attribute on <Protobuf> items -- without it, both client and server stubs are generated.