.NETにおけるシリアライゼーション
このスキルを使用するタイミング
このスキルは、以下の場合に使用します:
- API、メッセージング、永続化のためのシリアライゼーションフォーマットを選択する
- Newtonsoft.JsonからSystem.Text.Jsonに移行する
- AOT互換のシリアライゼーションを実装する
- 分散システム向けのワイヤフォーマットを設計する
- シリアライゼーションのパフォーマンスを最適化する
スキーマベース vs リフレクションベース
| アスペクト | スキーマベース | リフレクションベース |
|---|---|---|
| 例 | 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を使用します。
セットアップ
// すべての型を使用してJsonSerializerContextを定義します
[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 { }
使用法
// コンテキストを使用してシリアライズ
var json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
// コンテキストを使用してデシリアライズ
var order = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);
// 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;
// 新しいフィールドの追加は常に安全です
string notes = 5; // v2で追加 - 古いリーダーは無視します
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
int64 price_cents = 3;
}
バージョニングルール
// 安全: 新しい番号で新しいフィールドを追加
message Order {
string id = 1;
string customer_id = 2;
string shipping_address = 5; // 新規 - 安全
}
// 安全: フィールドを削除(古いリーダーは不明なフィールドを無視、新しいリーダーはデフォルト値を使用)
// フィールドの使用を停止し、番号を予約しておきます
message Order {
string id = 1;
// customer_id は削除されましたが、フィールド2は予約されています
reserved 2;
}
// 安全でない: フィールドタイプを変更
message Order {
int32 id = 1; // 以前: string - 壊れます!
}
// 安全でない: フィールド番号を再利用
message Order {
reserved 2;
string new_field = 2; // 2を再利用 - 壊れます!
}
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; }
// 新しいフィールド - 古いリーダーは不明なキーをスキップします
[Key(4)]
public string? Notes { get; init; }
}
// シリアライズ
var bytes = MessagePackSerializer.Serialize(order);
// デシリアライズ
var order = MessagePackSerializer.Deserialize<Order>(bytes);
AOT互換セットアップ
// AOT用にソースジェネレーターを使用
[MessagePackObject]
public partial class Order { } // partialによりソース生成が有効になります
// リゾルバーを設定
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
GeneratedResolver.Instance, // 生成
StandardResolver.Instance));
Newtonsoft.Jsonからの移行
一般的な問題
| Newtonsoft | System.Text.Json | 修正方法 |
|---|---|---|
JSONの $type | デフォルトではサポートされていません | 判別子またはカスタムコンバーターを使用 |
JsonProperty | JsonPropertyName | 異なる属性 |
DefaultValueHandling | DefaultIgnoreCondition | 異なるAPI |
NullValueHandling | DefaultIgnoreCondition | 異なるAPI |
| プライベートセッター | [JsonInclude] が必要 | 明示的なオプトイン |
| ポリモーフィズム | [JsonDerivedType] (.NET 7+) | 明示的な判別子 |
移行パターン
// Newtonsoft(リフレクションベース)
public class Order
{
[JsonProperty("order_id")]
public string Id { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string? Notes { get; set; }
}
// System.Text.Json(ソース生成互換)
public sealed record Order(
[property: JsonPropertyName("order_id")]
string Id,
string? Notes // JsonSerializerOptionsを介したnull処理
);
[JsonSerializable(typeof(Order))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class OrderJsonContext : JsonSerializerContext { }
判別子を使用したポリモーフィズム
// .NET 7+ ポリモーフィズム
[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);
// 次のようにシリアライズされます:
// { "$type": "credit_card", "amount": 100, "last4": "1234" }
ワイヤ互換性パターン
耐障害性のあるリーダー
古いコードは、未知のフィールドを安全に無視する必要があります:
// Protobuf/MessagePack: 自動 - 未知のフィールドはスキップされます
// System.Text.Json: 許可するように設定
var options = new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip
};
読み取りを先に導入、書き込みは後で
新しいフォーマットのシリアライザーより先にデシリアライザーを展開します:
// フェーズ1: デシリアライザーを追加(すべての場所に展開)
public Order Deserialize(byte[] data, string manifest) => manifest switch
{
"Order.V1" => DeserializeV1(data),
"Order.V2" => DeserializeV2(data), // 新規 - V2を読み取り可能
_ => throw new NotSupportedException()
};
// フェーズ2: シリアライザーを有効化(次のリリース、V1がすべての場所に展開された後)
public (byte[] data, string manifest) Serialize(Order order) =>
_useV2Format
? (SerializeV2(order), "Order.V2")
: (SerializeV1(order), "Order.V1");
型名を埋め込まない
// 悪い例: ペイロード内の型名 - クラスの名前変更でワイヤフォーマットが壊れます
{
"$type": "MyApp.Order, MyApp.Core",
"id": "123"
}
// 良い例: 明示的な判別子 - リファクタリングセーフ
{
"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シリアライゼーションドキュメントを参照してください。
ベストプラクティス
実行すべきこと
// System.Text.Jsonにソースジェネレーターを使用
[JsonSerializable(typeof(Order))]
public partial class AppJsonContext : JsonSerializerContext { }
// 明示的なフィールド番号/キーを使用
[MessagePackObject]
public class Order
{
[Key(0)] public string Id { get; init; }
}
// 不変メッセージタイプにレコードを使用
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
実行すべきでないこと
// BinaryFormatterを使用しないでください(絶対に)
var formatter = new BinaryFormatter(); // セキュリティリスク!
// ワイヤフォーマットに型名を埋め込まないでください
settings.TypeNameHandling = TypeNameHandling.All; // 名前変更時に壊れます!
// ホットパスにリフレクションシリアライゼーションを使用しないでください
JsonConvert.SerializeObject(order); // 低速、AOT非互換
リソース
- System.Text.Jsonソース生成: https://learn.microsoft.com/ja-jp/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