serialization について
.NET 開発者が API、メッセージング、永続化向けの形式を選ぶためのシリアライゼーションガイドです。System.Text.Json、Protobuf、MessagePack などを比較し、AOT 対応、移行、ワイヤ形式の進化も扱います。
主な機能
- 01JSON、Protobuf、MessagePack を比較
- 02System.Text.Json のソース生成を解説
- 03Protobuf と MessagePack の使い方を提示
- 04ワイヤ互換性とバージョン管理を解説
- 05Newtonsoft.Json からの移行を案内
- 06リフレクションベースの注意点を提示
活用シーン
- 01API やメッセージ向けの形式選定
- 02JSON シリアライゼーションの AOT 対応
- 03プロセス間形式の互換性を保った変更
- 04Newtonsoft.Json から System.Text.Json への移行
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill serializationインストール後、Claude Code で/serializationを入力して読み込まれたか確認します。
詳細
.NET でのシリアル化
このスキルを使用する場面
次のような場合に、このスキルを使用してください。
- API、メッセージング、永続化向けのシリアル化形式を選択する
- Newtonsoft.Json から System.Text.Json に移行する
- AOT 対応のシリアル化を実装する
- 分散システム向けのワイヤ形式を設計する
- シリアル化のパフォーマンスを最適化する
シリアル化形式の比較
| 形式 | ライブラリ | AOT 対応 | 人間が読み取れる | 相対サイズ | 相対速度 | 最適な用途 |
|---|---|---|---|---|---|---|
| JSON | System.Text.Json(ソース生成) | はい | はい | 最大 | 良好 | API、設定、Web クライアント |
| Protobuf | Google.Protobuf | はい | いいえ | 最小 | 最速 | サービス間通信、gRPC ワイヤ形式 |
| MessagePack | MessagePack-CSharp | はい(AOT リゾルバー使用時) | いいえ | 小 | 高速 | 高スループットのキャッシュ、リアルタイム処理 |
| JSON | Newtonsoft.Json | いいえ(リフレクション) | はい | 最大 | 低速 | レガシーのみ -- AOT では使用しないでください |
選び方
- ソースジェネレーターを使った System.Text.Json:API、設定、人間が読み取れる出力や Web クライアントでの利用が重要なあらゆるシナリオにおける既定の選択肢です。AOT に対応しています。
- Protobuf:gRPC の既定のワイヤ形式です。サービス間通信において、最高のスループットと最小のペイロードサイズを実現します。
.protoファイルを使ったスキーマファーストの開発が可能です。 - MessagePack:
.protoスキーマの管理なしでバイナリのコンパクトさが必要な場合に適しています。キャッシュレイヤー、リアルタイムメッセージング、高スループットのシナリオに適しています。
スキーマベースとリフレクションベース
| 項目 | スキーマベース | リフレクションベース |
|---|---|---|
| 例 | Protobuf、MessagePack、System.Text.Json(ソース生成) | Newtonsoft.Json、BinaryFormatter |
| ペイロード内の型情報 | なし(外部スキーマ) | あり(型名が埋め込まれる) |
| バージョン管理 | 明示的なフィールド番号/名前 | 暗黙的(型の構造) |
| パフォーマンス | 高速(リフレクションなし) | 低速(実行時リフレクション) |
| AOT 対応 | はい | いいえ |
| ワイヤ互換性 | 優れている | 低い |
推奨:プロセス境界を越えるデータには、スキーマベースのシリアル化を使用してください。
避けるべき形式
| 形式 | 問題点 |
|---|---|
| BinaryFormatter | セキュリティ上の脆弱性があり、非推奨です。決して使用しないでください |
| Newtonsoft.Json の既定設定 | ペイロード内の型名は、名前を変更すると問題を引き起こします |
| DataContractSerializer | 複雑で、バージョン管理に適していません |
| XML | 冗長で低速、複雑です |
ソースジェネレーターを使った System.Text.Json
JSON のシリアル化には、AOT 互換性とパフォーマンスのために、ソースジェネレーターを使った System.Text.Json を使用してください。
基本設定
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);
ASP.NET Core との統合
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
{
}
Protocol Buffers(Protobuf)
最適な用途:アクターシステム、gRPC、イベントソーシング、長期間使用するワイヤ形式全般。
パッケージ
<PackageReference Include="Google.Protobuf" Version="3.*" />
<PackageReference Include="Grpc.Tools" Version="2.*" PrivateAssets="All" />
Proto ファイル
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;
}
単独での Protobuf の使用(gRPC なし)
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);
.csproj での Proto ファイルの登録
<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!
}
MessagePack
最適な用途:高パフォーマンスが求められるシナリオ、コンパクトなペイロード、アクターメッセージング。
パッケージ
<PackageReference Include="MessagePack" Version="3.*" />
<PackageReference Include="MessagePack.SourceGenerator" Version="3.*" />
ソースジェネレーターを使った基本的な使用方法(AOT 対応)
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);
AOT リゾルバーの設定
MessagePackSerializer.DefaultOptions = MessagePackSerializerOptions.Standard
.WithResolver(GeneratedResolver.Instance);
ワイヤ互換性のパターン
寛容なリーダー
古いコードは、未知のフィールドを安全に無視する必要があります。
// 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 (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
}
パフォーマンス比較
おおよそのスループット(高いほど優れています):
| 形式 | シリアル化 | デシリアル化 | サイズ |
|---|---|---|---|
| MessagePack | ★★★★★ | ★★★★★ | ★★★★★ |
| Protobuf | ★★★★★ | ★★★★★ | ★★★★★ |
| System.Text.Json(ソース生成) | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| System.Text.Json(リフレクション) | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ |
| Newtonsoft.Json | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
最適化のヒント
JsonSerializerOptionsを再利用する -- オプションの作成にはコストがかかりますJsonSerializerContextを使う -- ウォームアップコストをなくせます- ストリーミングのシナリオでは
Utf8JsonWriter/Utf8JsonReaderを使う - base64 エンコードされた文字列の代わりに、バイナリデータには Protobuf の
ByteStringを使う - 大きなペイロードでは MessagePack LZ4 圧縮を有効にする
アンチパターン:リフレクションベースのシリアル化
Native AOT またはトリミングのシナリオでは、リフレクションベースのシリアライザーを使わないでください。
Newtonsoft.Json(JsonConvert)
// 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);
ソースジェネレーターを使用しない System.Text.Json
// 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);
Newtonsoft.Json からの移行手順
JsonConvert.SerializeObject/DeserializeObjectをJsonSerializer.Serialize/Deserializeに置き換えます[JsonProperty]を[JsonPropertyName]に置き換えますJsonConverter基底クラスを、System.Text.Json のJsonConverter<T>に置き換えます- シリアル化するすべての型に対して、
[JsonSerializable]を使ったJsonSerializerContextを作成します JObject/JTokenによる動的なアクセスを、JsonDocument/JsonElementまたは厳密に型指定されたモデルに置き換えます- シリアル化の往復処理をテストします -- 属性の意味は異なります
Akka.NET のシリアル化
Akka.NET アクターシステムでは、スキーマベースのシリアル化を使用してください:
akka {
actor {
serializers {
messagepack = "Akka.Serialization.MessagePackSerializer, Akka.Serialization.MessagePack"
}
serialization-bindings {
"MyApp.Messages.IMessage, MyApp" = messagepack
}
}
}
主な原則
- すべての JSON シリアル化では、既定でソースジェネレーターを使う System.Text.Json を使用する
- サービス間のバイナリシリアル化には Protobuf を使用する
- 高スループットのキャッシュやリアルタイム処理には MessagePack を使用する
- 新しい AOT 対象プロジェクトでは Newtonsoft.Json を決して使用しない
- ASP.NET Core では常に
JsonSerializerContextを登録する - シリアル化するすべての型に注釈を付ける -- ソースジェネレーターは一覧に記載された型のコードのみを生成します
エージェントが注意すべき点
- AOT プロジェクトでは、コンテキストなしで
JsonSerializer.Serialize(obj)を使わないでください -- リフレクションにフォールバックします。 - コレクション型を
[JsonSerializable]に記載し忘れないでください --[JsonSerializable(typeof(Order))]はList<Order>を対象にしません。 - System.Text.Json で Newtonsoft.Json の
[JsonProperty]属性を使わないでください -- 属性は無視されます。 - 同じ型階層内で MessagePack の
[Key]の整数キーと文字列キーを混在させないでください。 <Protobuf>項目のGrpcServices属性を省略しないでください -- 省略すると、クライアントとサーバーの両方のスタブが生成されます。
リソース
- System.Text.Json のソース生成: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation
- Newtonsoft.Json から System.Text.Json への移行: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft
- 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
- Native AOT のデプロイ: https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/
コメント
Markdown 対応