Skip to main content
skill CX Codex

serialization — Claude Code skill: picking a .NET format

Tells the agent which serialization format to use in .NET: schema-based over reflection, System.Text.Json with source generators for JSON. Useful when designing APIs, messaging or event storage.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add Aaronontheweb/dotnet-skills --skill serialization -a claude-code
Open on GitHub

What serialization does

The skill maps a task to a format. REST APIs get System.Text.Json with source generators, gRPC gets Protocol Buffers, actor messaging and caching get MessagePack, configuration and logs stay on plain JSON. A separate table lists formats to avoid: BinaryFormatter for security holes, default Newtonsoft.Json because type names travel inside the payload, DataContractSerializer and XML.

It compares schema-based serialization against reflection-based across several axes: where type info lives, how versioning works, wire compatibility, AOT support. The Protobuf section spells out the rules — add new fields with new numbers, keep removed numbers in reserved, never change a field type, never reuse a number. MessagePack is shown with [MessagePackObject] and [Key(n)] contracts plus a CompositeResolver wired to the generator.

There is a migration section for Newtonsoft.Json: attribute and API mappings, private setters, polymorphism. Note that System.Text.Json does not handle $type by default, so discriminators are on you. The skill is not user-invocable; the agent picks it up when a data format is on the table.

What it helps with

  • pick a serialization format for an API, messaging or persistence
  • migrate a project from Newtonsoft.Json to System.Text.Json
  • make serialization work with Native AOT
  • design a wire format that old readers can still parse
  • cut runtime reflection out of the serialization path

Example prompts for the agent

  • Which serialization format should I use for actor messages in .NET?
  • Help me move from Newtonsoft.Json to System.Text.Json with source generators
  • I need AOT-compatible serialization for a gRPC service — what should I pick?

How to install serialization

1Skills CLI (Vercel)

npx skills add Aaronontheweb/dotnet-skills --skill serialization -a claude-code

2Manually: put the files in place

~/.claude/skills/serialization/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/serialization/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

From the same set32

dotnet-project-structure A skill that tells your agent how to lay out a .NET solution: .slnx, Directory.Build.props, central package versions and SourceLink. CX Codex +1 658installs type-design-performance A skill that steers .NET type design toward performance: sealed classes, readonly structs, deferred enumeration, the right collection for the job. CX Codex +1 655installs ilspy-decompile Decompiles .NET assemblies using ilspycmd. It helps inspect NuGet package internals, runtime libraries, and compiled binaries directly in the agent session. CX Codex +1 602installs microsoft-extensions-configuration A skill for Claude and other AI editors that guides binding appsettings.json to strongly-typed classes and validating configuration at startup. CX Codex +1 577installs crap-analysis A skill that computes CRAP scores from coverage and cyclomatic complexity to flag .NET code that is risky to touch. CX Codex +1 556installs dotnet-slopwatch Detects LLM reward hacking in .NET code, catching disabled tests, silenced compiler warnings, and empty catch blocks. It validates changes right after an agent makes them. CX Codex +1 552installs testcontainers-integration-tests Helps AI agents write .NET integration tests using xUnit and real Docker containers instead of mocks. Useful for verifying database migrations, SQL queries, Redis cache, and message queues. CX Codex +1 544installs playwright-blazor-testing A skill that walks your agent through Playwright end-to-end tests for Blazor Server and WebAssembly: navigation, forms, auth, selectors. CX Codex +1 537installs

Similar

Nearby in Backend and APIs

Everything for Backend and APIs →

Questions about serialization

How do I install serialization in Claude?

Run npx skills add Aaronontheweb/dotnet-skills --skill serialization -a claude-code. The skill file lands at ~/.claude/skills/serialization/SKILL.md.

What does the serialization skill do?

It points the agent at the right format for the job: Protobuf for gRPC and event sourcing, MessagePack for compact messages and caching, System.Text.Json with generators for JSON. It also covers schema versioning rules.

How is schema-based serialization different from reflection-based?

Schema-based keeps type info in an external schema, so versioning is explicit and AOT works. Reflection-based embeds type names in the payload, and renaming a type breaks compatibility.

Source text

SKILL.md · MIT
---
name: serialization
description: Choose the right serialization format for .NET applications. Prefer schema-based formats (Protobuf, MessagePack) over reflection-based (Newtonsoft.Json). Use System.Text.Json with AOT source generators for JSON scenarios.
user-invocable: false
---

# Serialization in .NET

## When to Use This Skill

Use this skill when:
- Choosing a serialization format for APIs, messaging, or persistence
- Migrating from Newtonsoft.Json to System.Text.Json
- Implementing AOT-compatible serialization
- Designing wire formats for distributed systems
- Optimizing serialization performance

---

## Schema-Based vs Reflection-Based

| 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 Recommendations

| Use Case | Recommended Format | Why |
|----------|-------------------|-----|
| **REST APIs** | System.Text.Json (source gen) | Standard, AOT-compatible |
| **gRPC** | Protocol Buffers | Native format, excellent versioning |
| **Actor messaging** | MessagePack or Protobuf | Compact, fast, version-safe |
| **Event sourcing** | Protobuf or MessagePack | Must handle old events forever |
| **Caching** | MessagePack | Compact, fast |
| **Configuration** | JSON (System.Text.Json) | Human-readable |
| **Logging** | JSON (System.Text.Json) | Structured, parseable |

### Formats to Avoid

| 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 |

---

## System.Text.Json with Source Generators

For JSON serialization, use System.Text.Json with source generators for AOT compatibility and performance.

### Setup

```csharp
// Define a JsonSerializerContext with all your types
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(OrderItem))]
[JsonSerializable(typeof(Customer))]
[JsonSerializable(typeof(List<Order>))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }
```

### Usage

```csharp
// Serialize with context
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);

// Deserialize with context
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);

// Configure in ASP.NET Core
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});
```

### Benefits

- **No reflection at runtime** - All type info generated at compile time
- **AOT compatible** - Works with Native AOT publishing
- **Faster** - No runtime type analysis
- **Trim-safe** - Linker knows exactly what's needed

---

## Protocol Buffers (Protobuf)

Best for: Actor systems, gRPC, event sourcing, any long-lived wire format.

### Setup

```bash
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
```

### Define Schema

```protobuf
// orders.proto
syntax = "proto3";

message Order {
    string id = 1;
    string customer_id = 2;
    repeated OrderItem items = 3;
    int64 created_at_ticks = 4;

    // Adding new fields is always safe
    string notes = 5;  // Added in v2 - old readers ignore it
}

message OrderItem {
    string product_id = 1;
    int32 quantity = 2;
    int64 price_cents = 3;
}
```

### Versioning Rules

```protobuf
// 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 (old readers ignore unknown, new readers use default)
// Just stop using the field, keep the number reserved
message Order {
    string id = 1;
    // customer_id removed, but field 2 is reserved
    reserved 2;
}

// 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!
}
```

---

## MessagePack

Best for: High-performance scenarios, compact payloads, actor messaging.

### Setup

```bash
dotnet add package MessagePack
dotnet add package MessagePack.Annotations
```

### Usage with Contracts

```csharp
[MessagePackObject]
public sealed class Order
{
    [Key(0)]
    public required string Id { get; init; }

    [Key(1)]
    public required string CustomerId { get; init; }

    [Key(2)]
    public required IReadOnlyList<OrderItem> Items { get; init; }

    [Key(3)]
    public required DateTimeOffset CreatedAt { get; init; }

    // New field - old readers skip unknown keys
    [Key(4)]
    public string? Notes { get; init; }
}

// Serialize
var bytes = MessagePackSerializer.Serialize(order);

// Deserialize
var order = MessagePackSerializer.Deserialize<Order>(bytes);
```

### AOT-Compatible Setup

```csharp
// Use source generator for AOT
[MessagePackObject]
public partial class Order { }  // partial enables source gen

// Configure resolver
var options = MessagePackSerializerOptions.Standard
    .WithResolver(CompositeResolver.Create(
        GeneratedResolver.Instance,  // Generated
        StandardResolver.Instance));
```

---

## Migrating from Newtonsoft.Json

### Common Issues

| Newtonsoft | System.Text.Json | Fix |
|------------|------------------|-----|
| `$type` in JSON | Not supported by default | Use discriminators or custom converters |
| `JsonProperty` | `JsonPropertyName` | Different attribute |
| `DefaultValueHandling` | `DefaultIgnoreCondition` | Different API |
| `NullValueHandling` | `DefaultIgnoreCondition` | Different API |
| Private setters | Requires `[JsonInclude]` | Explicit opt-in |
| Polymorphism | `[JsonDerivedType]` (.NET 7+) | Explicit discriminators |

### Migration Pattern

```csharp
// Newtonsoft (reflection-based)
public class Order
{
    [JsonProperty("order_id")]
    public string Id { get; set; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public string? Notes { get; set; }
}

// System.Text.Json (source-gen compatible)
public sealed record Order(
    [property: JsonPropertyName("order_id")]
    string Id,

    string? Notes  // Null handling via JsonSerializerOptions
);

[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }
```

### Polymorphism with Discriminators

```csharp
// .NET 7+ polymorphism
[JsonDerivedType(typeof(CreditCardPayment), "credit_card")]
[JsonDerivedType(typeof(BankTransferPayment), "bank_transfer")]
public abstract record Payment(decimal Amount);

public sealed record CreditCardPayment(decimal Amount, string Last4) : Payment(Amount);
public sealed record BankTransferPayment(decimal Amount, string AccountNumber) : Payment(Amount);

// Serializes as:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }
```

---

## Wire Compatibility Patterns

### Tolerant Reader

Old code must safely ignore unknown fields:

```csharp
// Protobuf/MessagePack: Automatic - unknown fields skipped
// System.Text.Json: Configure to allow
var options = new JsonSerializerOptions
{
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
```

### Introduce Read Before Write

Deploy deserializers before serializers for new formats:

```csharp
// 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 (next release, after V1 deployed everywhere)
public (byte[] data, string manifest) Serialize(Order order) =>
    _useV2Format
        ? (SerializeV2(order), "Order.V2")
        : (SerializeV1(order), "Order.V1");
```

### Never Embed Type Names

```csharp
// BAD: Type name in payload - renaming class breaks wire format
{
    "$type": "MyApp.Order, MyApp.Core",
    "id": "123"
}

// GOOD: Explicit discriminator - refactoring safe
{
    "type": "order",
    "id": "123"
}
```

---

## Performance Comparison

Approximate throughput (higher is better):

| Format | Serialize | Deserialize | Size |
|--------|-----------|-------------|------|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json (source gen) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json (reflection) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |

For hot paths, prefer MessagePack or Protobuf.

---

## Akka.NET Serialization

For Akka.NET actor systems, use schema-based serialization:

```hocon
akka {
  actor {
    serializers {
      messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
    }
    serialization-bindings {
      "MyApp.Messages.IMessage, MyApp" = messagepack
    }
  }
}
```

See [Akka.NET Serialization Docs](https://getakka.net/articles/networking/serialization.html).

---

## Best Practices

### DO

```csharp
// Use source generators for System.Text.Json
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }

// Use explicit field numbers/keys
[MessagePackObject]
public class Order
{
    [Key(0)] public string Id { get; init; }
}

// Use records for immutable message types
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
```

### DON'T

```csharp
// Don't use BinaryFormatter (ever)
var formatter = new BinaryFormatter();  // Security risk!

// Don't embed type names in wire format
settings.TypeNameHandling = TypeNameHandling.All;  // Breaks on rename!

// Don't use reflection serialization for hot paths
JsonConvert.SerializeObject(order);  // Slow, not AOT-compatible
```

---

## Resources

- **System.Text.Json Source Generation**: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation
- **Protocol Buffers**: https://protobuf.dev/
- **MessagePack-CSharp**: https://github.com/MessagePack-CSharp/MessagePack-CSharp
- **Akka.NET Serialization**: https://getakka.net/articles/networking/serialization.html
- **Wire Compatibility**: https://getakka.net/community/contributing/wire-compatibility.html

© Aaronontheweb · MIT · Aaronontheweb/dotnet-skills

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor