serialization について
API、メッセージング、永続化に適した形式を選び、互換性と効率を考慮したシリアライズを実装する .NET スキルです。
主な機能
- 01スキーマ方式とリフレクション方式の比較
- 02System.Text.Json のソース生成
- 03Protobuf と MessagePack の利用
- 04ワイヤ形式のバージョニング設計
- 05Newtonsoft.Json からの移行
活用シーン
- 01REST または gRPC の形式選定
- 02Native AOT 向けシリアライズの準備
- 03分散サービス間の互換性維持
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill serializationインストール後、Claude Code で/serializationを入力して読み込まれたか確認します。
詳細
.NET でのシリアル化
このスキルを使用する場面
次の場合にこのスキルを使用します:
- API、メッセージング、永続化に使用するシリアル化形式を選ぶとき
- Newtonsoft.Json から System.Text.Json に移行するとき
- AOT 互換のシリアル化を実装するとき
- 分散システムのワイヤ形式を設計するとき
- シリアル化のパフォーマンスを最適化するとき
スキーマベースとリフレクションベース
| 観点 | スキーマベース | リフレクションベース |
|---|---|---|
| 例 | Protobuf、MessagePack、System.Text.Json(ソース生成) | Newtonsoft.Json、BinaryFormatter |
| ペイロード内の型情報 | なし(外部スキーマ) | あり(型名が埋め込まれる) |
| バージョン管理 | 明示的なフィールド番号/名前 | 暗黙的(型の構造) |
| パフォーマンス | 高速(リフレクションなし) | 低速(実行時リフレクション) |
| AOT 互換性 | あり | なし |
| ワイヤ互換性 | 優れている | 低い |
推奨事項:プロセス境界を越えるデータには、スキーマベースのシリアル化を使用してください。
形式の推奨事項
| ユースケース | 推奨形式 | 理由 |
|---|---|---|
| REST API | System.Text.Json(ソース生成) | 標準的で、AOT 互換 |
| gRPC | Protocol Buffers | ネイティブ形式で、バージョン管理に優れている |
| アクターメッセージング | MessagePack または Protobuf | コンパクトで高速、バージョン互換性が高い |
| イベントソーシング | Protobuf または MessagePack | 古いイベントを将来にわたって処理できる必要がある |
| キャッシュ | MessagePack | コンパクトで高速 |
| 構成 | JSON(System.Text.Json) | 人間が読みやすい |
| ログ記録 | JSON(System.Text.Json) | 構造化されており、解析可能 |
避けるべき形式
| 形式 | 問題 |
|---|---|
| BinaryFormatter | セキュリティ上の脆弱性があり、非推奨です。絶対に使用しないでください |
| Newtonsoft.Json の既定設定 | ペイロード内の型名は、名前変更時に問題を引き起こします |
| DataContractSerializer | 複雑で、バージョン管理に適していません |
| XML | 冗長で低速、複雑 |
ソースジェネレーターを使用した System.Text.Json
JSON のシリアル化には、AOT 互換性とパフォーマンスのために、ソースジェネレーターを使用した System.Text.Json を使います。
セットアップ
// 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 { }
使用方法
// 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);
});
利点
- 実行時のリフレクションなし - すべての型情報がコンパイル時に生成されます
- AOT 互換 - Native AOT による発行で動作します
- 高速 - 実行時の型解析がありません
- トリミングセーフ - リンカーが必要な要素を正確に把握できます
Protocol Buffers(Protobuf)
最適な用途:アクターシステム、gRPC、イベントソーシング、長期間使用するあらゆるワイヤ形式。
セットアップ
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
スキーマを定義する
// 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;
}
バージョン管理のルール
// 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
最適な用途:高パフォーマンスが求められるシナリオ、コンパクトなペイロード、アクターメッセージング。
セットアップ
dotnet add package MessagePack
dotnet add package MessagePack.Annotations
コントラクトを使用した操作
[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 互換のセットアップ
// 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));
Newtonsoft.Json からの移行
よくある問題
| Newtonsoft | System.Text.Json | 対処方法 |
|---|---|---|
JSON 内の $type | 既定ではサポートされません | 判別子またはカスタムコンバーターを使用します |
JsonProperty | JsonPropertyName | 属性が異なります |
DefaultValueHandling | DefaultIgnoreCondition | API が異なります |
NullValueHandling | DefaultIgnoreCondition | API が異なります |
| プライベートセッター | [JsonInclude] が必要です | 明示的に有効化します |
| ポリモーフィズム | [JsonDerivedType](.NET 7 以降) | 判別子を明示します |
移行パターン
// 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 { }
判別子を使用したポリモーフィズム
// .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" }
ワイヤ互換性のパターン
寛容なリーダー
古いコードでは、未知のフィールドを安全に無視できる必要があります:
// Protobuf/MessagePack: Automatic - unknown fields skipped
// System.Text.Json: Configure to allow
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
書き込み前に読み取りを導入する
新しい形式のシリアライザーより先に、デシリアライザーをデプロイします:
// 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");
型名を埋め込まない
// 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"
}
パフォーマンス比較
スループットの目安(高いほど優れています):
| 形式 | シリアライズ | デシリアライズ | サイズ |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json(ソース生成) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json(リフレクション) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
頻繁に実行される処理では、MessagePack または Protobuf を優先してください。
Akka.NET のシリアライズ
Akka.NET のアクターシステムでは、スキーマベースのシリアライズを使用してください:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
詳しくは、Akka.NET Serialization Docs を参照してください。
ベストプラクティス
推奨事項
// 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 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
リソース
- System.Text.Json のソース生成: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 のシリアライズ:https://getakka.net/articles/networking/serialization.html
- ワイヤー互換性:https://getakka.net/community/contributing/wire-compatibility.html
コメント
Markdown 対応