C# の Serializable 属性とシリアライズ

オブジェクトをファイルに保存したり、ネットワーク越しに送信したりするには、メモリ上のデータをバイト列や文字列に変換する必要がある。この変換をシリアライズ、逆の変換をデシリアライズと呼ぶ。C# では Serializable 属性を付けることで、クラスがシリアライズ可能であることを明示できる。

Serializable 属性の基本

Serializable 属性をクラスに付けると、そのクラスのインスタンスがシリアライズ対象として認識される。

using System;

[Serializable]
public class GameSaveData
{
    public string PlayerName { get; set; } = "";
    public int Level { get; set; }
    public double PlayTime { get; set; }
}

この属性はメタデータとしてアセンブリに記録され、シリアライザがオブジェクトを変換してよいかどうかの判断材料になる。属性がないクラスを BinaryFormatter でシリアライズしようとすると SerializationException が発生する。

BinaryFormatter によるシリアライズ

.NET Framework 時代に広く使われたのが BinaryFormatter だ。オブジェクトをバイナリ形式でストリームに書き出す。

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
public class Config
{
    public string Theme { get; set; } = "dark";
    public int FontSize { get; set; } = 14;
}

// シリアライズ
var config = new Config { Theme = "light", FontSize = 16 };
using (var stream = new FileStream("config.dat", FileMode.Create))
{
    var formatter = new BinaryFormatter();
    formatter.Serialize(stream, config);
}

// デシリアライズ
using (var stream = new FileStream("config.dat", FileMode.Open))
{
    var formatter = new BinaryFormatter();
    var loaded = (Config)formatter.Deserialize(stream);
    Console.WriteLine($"{loaded.Theme}, {loaded.FontSize}");
}

ただし BinaryFormatter はセキュリティ上の問題から、.NET 5 以降では非推奨となっている。任意の型をデシリアライズできるため、悪意あるデータを読み込むとリモートコード実行の脆弱性につながるおそれがあるためだ。

NonSerialized でフィールドを除外する

シリアライズしたくないフィールドには NonSerialized 属性を付ける。パスワードや一時的な計算結果など、永続化すべきでないデータに使う。

[Serializable]
public class UserSession
{
    public string UserName { get; set; } = "";
    public DateTime LoginTime { get; set; }

    [NonSerialized]
    private string _tempToken = "";

    public void SetToken(string token) => _tempToken = token;
    public string GetToken() => _tempToken;
}

_tempToken フィールドはシリアライズ対象から除外される。デシリアライズ後はデフォルト値(null)に戻る点に注意が必要だ。

Serializable(デフォルト)

クラスのすべてのフィールドがシリアライズ対象になる。特別な指定がなければ全フィールドが保存される。

NonSerialized

特定のフィールドだけを除外する。一時データや機密情報に適用する。デシリアライズ後はデフォルト値になる。

現代の C# におけるシリアライズ

現在の .NET では、BinaryFormatter に代わって System.Text.JsonNewtonsoft.Json が主流となっている。これらは Serializable 属性を必要としない。

using System.Text.Json;

public class Product
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

var product = new Product { Name = "キーボード", Price = 9800 };

// シリアライズ
string json = JsonSerializer.Serialize(product);
Console.WriteLine(json);

// デシリアライズ
var restored = JsonSerializer.Deserialize<Product>(json);

System.Text.Json では属性によるシリアライズ制御も用意されている。

JsonPropertyName

JSON 上のプロパティ名を指定する。C# の命名規則と JSON の命名規則が異なるときに使う。

JsonIgnore

特定のプロパティをシリアライズ対象から除外する。NonSerialized と似た役割を果たす。

JsonConverter

独自の変換ロジックを適用する。日付フォーマットの変更や列挙型の文字列変換などに使う。

using System.Text.Json.Serialization;

public class ApiResponse
{
    [JsonPropertyName("status_code")]
    public int StatusCode { get; set; }

    [JsonPropertyName("message")]
    public string Message { get; set; } = "";

    [JsonIgnore]
    public DateTime ProcessedAt { get; set; }
}

Serializable 属性を使う場面

Serializable 属性が今でも必要になるのは、レガシーコードとの互換性を保つ場合や、AppDomain 境界を越えてオブジェクトを渡す場合、一部の .NET Framework API を利用する場合などに限られる。新規プロジェクトでは System.Text.Json を使い、JSON ベースでシリアライズするのが標準的な選択だ。

Serializable + BinaryFormatter(レガシー)

セキュリティ上の問題で非推奨に

System.Text.Json が標準に(.NET 5 以降)

シリアライズの方式は変わっても、「属性でシリアライズの挙動を制御する」という設計思想は一貫している。SerializableNonSerialized の関係を理解しておけば、JsonIgnoreJsonPropertyName といった現代の属性もすんなり使いこなせるはずだ。