Serialization

aaronontheweb/dotnet-skills/skills/serialization

by aarononthewebe426ed93a9f3No license1.2K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 3 weeks ago

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.

Instructions onlySoftware Development
AI-generated overview

Guides .NET developers in choosing serialization formats and migrating to schema-based, AOT-compatible serialization.

What it does
This skill provides guidance on selecting serialization formats for .NET applications, comparing schema-based options like Protobuf and MessagePack against reflection-based ones like Newtonsoft.Json. It covers System.Text.Json source generators, Protocol Buffers, MessagePack, migration from Newtonsoft.Json, wire compatibility patterns, and performance trade-offs. It produces recommendations, code examples, and configuration snippets rather than executable output.
When to use it
Use it when choosing a serialization format for APIs, messaging, or persistence in .NET. It also fits migrating from Newtonsoft.Json to System.Text.Json, implementing AOT-compatible serialization, designing wire formats for distributed systems, or optimizing serialization performance.
Requirements
No scripts or runtime dependencies; it is an instruction-only reference. Examples reference .NET packages such as Google.Protobuf, Grpc.Tools, MessagePack, and MessagePack.Annotations, but nothing needs to be installed to read the guidance.

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

AspectSchema-BasedReflection-Based
ExamplesProtobuf, MessagePack, System.Text.Json (source gen)Newtonsoft.Json, BinaryFormatter
Type info in payloadNo (external schema)Yes (type names embedded)
VersioningExplicit field numbers/namesImplicit (type structure)
PerformanceFast (no reflection)Slower (runtime reflection)
AOT compatibleYesNo
Wire compatibilityExcellentPoor

Recommendation: Use schema-based serialization for anything that crosses process boundaries.


Format Recommendations

Use CaseRecommended FormatWhy
REST APIsSystem.Text.Json (source gen)Standard, AOT-compatible
gRPCProtocol BuffersNative format, excellent versioning
Actor messagingMessagePack or ProtobufCompact, fast, version-safe
Event sourcingProtobuf or MessagePackMust handle old events forever
CachingMessagePackCompact, fast
ConfigurationJSON (System.Text.Json)Human-readable
LoggingJSON (System.Text.Json)Structured, parseable

Formats to Avoid

FormatProblem
BinaryFormatterSecurity vulnerabilities, deprecated, never use
Newtonsoft.Json defaultType names in payload break on rename
DataContractSerializerComplex, poor versioning
XMLVerbose, 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 contextvar json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// Deserialize with contextvar order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// Configure in ASP.NET Corebuilder.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.Protobufdotnet add package Grpc.Tools

Define Schema

protobuf
// orders.protosyntax = "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 numbersmessage 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 reservedmessage Order {    string id = 1;    // customer_id removed, but field 2 is reserved    reserved 2;}
// UNSAFE: Change field typesmessage Order {    int32 id = 1;  // Was: string - BREAKS!}
// UNSAFE: Reuse field numbersmessage 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 MessagePackdotnet 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; }}
// Serializevar bytes = MessagePackSerializer.Serialize(order);
// Deserializevar order = MessagePackSerializer.Deserialize<Order>(bytes);

AOT-Compatible Setup

csharp
// Use source generator for AOT[MessagePackObject]public partial class Order { }  // partial enables source gen
// Configure resolvervar options = MessagePackSerializerOptions.Standard    .WithResolver(CompositeResolver.Create(        GeneratedResolver.Instance,  // Generated        StandardResolver.Instance));

Migrating from Newtonsoft.Json

Common Issues

NewtonsoftSystem.Text.JsonFix
$type in JSONNot supported by defaultUse discriminators or custom converters
JsonPropertyJsonPropertyNameDifferent attribute
DefaultValueHandlingDefaultIgnoreConditionDifferent API
NullValueHandlingDefaultIgnoreConditionDifferent API
Private settersRequires [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 allowvar 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):

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


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 typespublic 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 formatsettings.TypeNameHandling = TypeNameHandling.All;  // Breaks on rename!
// Don't use reflection serialization for hot pathsJsonConvert.SerializeObject(order);  // Slow, not AOT-compatible

Resources

Source and attribution

Source:aaronontheweb/dotnet-skillsinskills/serializationat commite426ed9

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal