#️⃣

【C#】.NETのシリアライザまとめ

に公開

今回の記事は.NETのシリアライザについて。何らかのアプリケーションを作る際、通信や永続化などのためにシリアライザを用いることはほぼ必須になります。そこで、今回は.NETで利用可能なシリアライザについて、各フォーマット毎にまとめていきます。

JSON

System.Text.Json

https://learn.microsoft.com/ja-jp/dotnet/api/system.text.json?view=net-8.0

現在の.NETで標準ライブラリとして組み込まれているJSONシリアライザです。.NET Standardの場合はNuGetで配布されているものが利用できます。

通常のオブジェクトからのシリアライズ/デシリアライズのほか、JsonNodeやJsonDocumentを用いてJSONを直接解析することも可能です。

using System.Text.Json;

var person = new Person()
{
    Name = "Alice",
    Age = 18
};

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

// デシリアライズ
person = JsonSerializer.Deserialize<Person>(json);

また、属性や独自のJsonConverterを用いてメンバー名の変更などの挙動を調整できるほか、UTF-8やSource Generatorにも対応しており、適切に用いることで高いパフォーマンスを得ることが可能です。

var json = JsonSerializer.Serialize(obj, PersonSerializerContext.Default.Person);

class Person
{
    [JsonPropertyName("name")] // 属性によるメンバー名の変更
    public string Name { get; init; }

    [JsonPropertyName("age")]
    public int Age { get; init; }
}

// JsonSerializerContextを継承したクラスにJsonSerializable属性を付加することでSource Generatorを有効化する
[JsonSerializable(typeof(Person))]
partial class PersonSerializerContext : JsonSerializerContext
{
}

JSON.NET (Newtonsoft.Json)

https://www.newtonsoft.com/json

System.Text.Jsonが登場する以前から使われていたJSONシリアライザです。オブジェクトからのシリアライズ/デシリアライズと、JObjectによるJSONの解析を行うことが可能です。

// シリアライズ
string json = JsonConvert.SerializeObject(person);

// デシリアライズ
person = JsonConvert.DeserializeObejct<Person>(json);

現在はSystem.Text.Jsonを使うことが一般的ですが、JSON.NETも豊富な機能と十分な速度を備えているため、今でも広く用いられています。

JsonUtility (Unityのみ)

Unityには組み込みでJsonUtilityという簡易的なJSONシリアライザが用意されています。

// シリアライズ
string json = JsonUtility.ToJson(person);

// デシリアライズ
person = JsonUtility.FromJson<Person>(json);

他のライブラリとは異なり、JsonUtilityはUnityのシリアライズの規則に沿ってシリアライズ/デシリアライズを行います。すなわち、publicまたは[SerializeField]が付いたフィールドが対象になり、またクラス自体には[Serializable]をつける必要があります。要するにInspectorに表示されるような状態で定義したクラスしかシリアライズできません。

[Serializable]
class Person
{
    [SerializeField] string name;
    [SerializeField] int age;

    ...
}

ライブラリを入れる必要がないため手軽に利用できますが、「ルートが配列の場合に対応していない」「配列やList以外のコレクション(Dictionaryなど)をシリアライズできない」「nullを表現できない(代わりに規定値が代入される)」などの厄介な制約が多々あります。また最小限のAPIしか用意されていないので、カスタマイズの余地もほぼありません。

手軽さは魅力ですが、実際に使う際にはSystem.Text.JsonやJSON.NETを利用すべきでしょう。

XML

System.Xml

https://learn.microsoft.com/ja-jp/dotnet/api/system.xml?view=net-9.0

.NETの初期から存在する、XMLを扱うための標準ライブラリです。XMLを扱うための様々な機能が用意されていますが、シリアライザとして扱う場合はSystem.Xml.Serializationを利用します。

var serializer = new XmlSerializer(typeof(Person));
var fs = new FileStream("filename");

serializer.Serialize(fs, person);
person = (Person)serializer.Deserialize(fs);

YAML

YamlDotNet

YamlDotNetはYAMLを解析するためのライブラリです。オブジェクトのシリアライズ/デシリアライズのほか、YAMLを扱うための様々な機能が提供されています。

https://github.com/aaubry/YamlDotNet

using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

var yml = @"
name: George Washington
age: 89
height_in_inches: 5.75
addresses:
  home:
    street: 400 Mockingbird Lane
    city: Louaryland
    state: Hawidaho
    zip: 99970
";

var deserializer = new DeserializerBuilder()
    .WithNamingConvention(UnderscoredNamingConvention.Instance) 
    .Build();

var p = deserializer.Deserialize<Person>(yml);
var h = p.Addresses["home"];
System.Console.WriteLine($"{p.Name} is {p.Age} years old and lives at {h.Street} in {h.City}, {h.State}.");

VYaml

VYamlは新しく登場したYAML 1.2対応のシリアライザで、作者はVContainerで有名なハダシAさんです。

https://github.com/hadashiA/VYaml

最大の特徴はパフォーマンスで、前述のYamlDotNetよりも6倍ほど高速に動作します。

VYamlはSource Generatorを用いた最適化を行うため、対象の型にはpartialキーワードと属性を付与しておく必要があります。

using VYaml.Annotations;

[YamlObject]
public partial class Sample
{
    public string A;
    public string B { get; set; }
    public string C { get; private set; }
    public string D { get; init; }

    [YamlIgnore]
    public int PublicProperty2 => PublicProperty + PublicField;
}

シリアライズ/デシリアライズはYamlSerializerを通して行うことができます。

// シリアライズ
var utf8Yaml = YamlSerializer.Serialize(new Sample
{
    A = "hello",
    B = "foo",
    C = "bar",
    D = "hoge",
});

// デシリアライズ
var sample = YamlSerializer.Deserialize<Sample>(utf8Yaml);

CSV

CsvHelper

CsvHelperは.NETでCSVを扱うためのライブラリです。シリアライザとはやや異なりますが、便宜上ここで紹介しています。

https://joshclose.github.io/CsvHelper/

using CsvHelper;
using System.Globalization;
using System.IO;
using System.Linq;

using var reader = new StreamReader("data.csv");
using var csvReader = new CsvReader(reader, CultureInfo.InvariantCulture);

var records = csvReader.GetRecords<Person>().ToArray();

using var writer = new StreamWriter("people.csv");
using var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture);

csvWriter.WriteRecords(records);

単純なCSVの読み書きに加え、独自のマッピングやバリデーションなどの機能が提供されています。

Csv-CSharp

Csv-CSharpはCSVのシリアライズ/デシリアライズに特化したライブラリです。(宣伝になってしまいますが)このライブラリは私が開発・メンテナンスを行なっているものになります。

https://github.com/nuskey8/Csv-CSharp

最大の特徴はシリアライザとして扱えるようになっていることで、CSVから配列へのデシリアライズを直感的に行えるようになっています。

var array = new Person[]
{
    new() { Name = "Alice", Age = 18 }, 
    new() { Name = "Bob", Age = 23 }, 
    new() { Name = "Carol", Age = 31 },
};

// シリアライズ
string csv = CsvSerializer.SerializeToString(array); 

// デシリアライズ
array = CsvSerializer.Deserialize<Person>(csv);

[CsvObject]
public partial class Person
{ 
    [Column(0)]
    public string Name { get; set; }
    
    [Column(1)]
    public int Age { get; set; }
}

型を定義せずに直接CSVを解析したい場合はCsvDocumentを利用します。

var document = CsvSerializer.ConvertToDocument(csv);

foreach (var row in document.Rows)
{
    var name = row["Name"].GetValue<string>();
    var age = row["Age"].GetValue<int>();
}

また、Source Generatorの活用により高いパフォーマンスを達成しており、CsvHelperの70〜80倍ほど高速に動作するようになっています。

MessagePack

MessagePack for C#

MessagePack for C#は非常に高速なMessagePackシリアライザです。開発はUniTaskやUniRx/R3等でお馴染みのCysharpのneueccさんと、MicrosoftのAArnottさんです。

https://github.com/MessagePack-CSharp/MessagePack-CSharp

v2まではAOT対応のためにmpcと呼ばれるコードジェネレータが必要でしたが、v3ではSource Generator対応が完了しているため、面倒なコード生成を行わずに手軽に扱うことが可能です。

using MessagePack;

var mc = new MyClass
{
    Age = 99,
    FirstName = "hoge",
    LastName = "huga",
};

// シリアライズ
byte[] bytes = MessagePackSerializer.Serialize(mc);

// デシリアライズ
MyClass mc2 = MessagePackSerializer.Deserialize<MyClass>(bytes);

[MessagePackObject]
public class MyClass
{
    [Key(0)]
    public int Age { get; set; }

    [Key(1)]
    public string FirstName { get; set; }

    [Key(2)]
    public string LastName { get; set; }

    [IgnoreMember]
    public string FullName { get { return FirstName + LastName; } }
}

MessagePack for C#の完成度は本当に素晴らしく、後発の多くのシリアライザの実装がこのライブラリを参考にしています。(VYamlやOdin SerializerのAPI設計にも影響が感じられる)

また、Visual Studioの内部でもMessagePackのシリアライザとして採用されており、.NETではほとんど標準的なバイナリシリアライザになっています。C#でMessagePackを扱いたい場合には、このライブラリを選んでおけば間違い無いでしょう。

MsgPack-Cli

MsgPack-CliはMessagePack for C#以前から存在するMessagePackシリアライザです。

https://github.com/msgpack/msgpack-cli

var serializer = MessagePackSerializer.Get<T>();
serializer.Pack(stream, obj);
var unpackedObject = serializer.Unpack(stream);

こちらは現在積極的にメンテナンスされていないため、今ならMessagePack for C#を使った方が良いでしょう。

Protocol Buffers

Google.Protobuf

Google.ProtobufはGoogle公式が提供するC#向けのProtobufシリアライザです。

https://github.com/protocolbuffers/protobuf/tree/main/csharp

Google.Protobufを用いるには、.protoファイルからprotocを用いてコードを生成する必要があります。

syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
}

上のような.protoファイルを作成し、protocコマンドを実行することで、必要なコードが生成されます。

$ protoc "./sample.proto" --csharp_out="./"

あとは生成されたコードを用いて以下のようにシリアライズ・デシリアライズしてあげればOKです。

var person = new Person
{
    Name = "Alice",
    Age = 30
};

// シリアライズ
using var stream = new MemoryStream();
person.WriteTo(stream);
byte[] bytes = stream.ToArray();

// デシリアライズ
var parsed = Person.Parser.ParseFrom(bytes);
Console.WriteLine(parsed.Name); // Alice

protobuf-net

protobuf-netはサードパーティ実装のProtobufシリアライザです。Google.Protobufよりも高速で、よりC#らしいAPIになるように設計されています。

https://github.com/protobuf-net/protobuf-net

基本的にはクラスに専用の属性を付けることでシリアライズ対象の設定を行いますが、.protoファイルからコードを生成することも可能です。

[ProtoContract]
public class Person
{
    [ProtoMember(1)]
    public string Name { get; set; }

    [ProtoMember(2)]
    public int Age { get; set; }
}

// シリアライズ
byte[] data = ProtoBuf.Serializer.Serialize(person);

// デシリアライズ
person = ProtoBuf.Serializer.Deserialize<Person>(new MemoryStream(data));

その他

MemoryPack

MemoryPackはMessagePack for C#でお馴染みのneueccさんによる独自フォーマットのシリアライザです。MemoryPackはC#に最適化されたバイナリフォーマットを用いることで、他のシリアライザの追従を許さない圧倒的な速度を発揮します。また、機能面でもMessagePack for C#に匹敵する豊富な機能が提供されています。

https://github.com/Cysharp/MemoryPack

MemoryPackはSource Generatorを全面的に採用しており、必要なコードをコンパイル時に生成します。そのため、対象のクラスには[MemoryPackable]属性とpartialキーワードを付ける必要があります。

using MemoryPack;

[MemoryPackable]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

あとは一般的なシリアライザのように利用することが可能です。

var person = new Person { Age = 40, Name = "John" };

var bin = MemoryPackSerializer.Serialize(person);
person = MemoryPackSerializer.Deserialize<Person>(bin);

独自フォーマットであるため言語間運用は難しいですが、通信の双方がC#である場合などには最適なシリアライザになるでしょう。

[廃止] BinaryFormatter

BinaryFormatterは.NETの初期から存在するシリアライザです。現在ではセキュリティ上の脆弱性から非推奨であり、MessagePack for C#等への移行が推奨されています。

https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter?view=net-8.0

また、.NET 9以降ではBinaryFormatterは完全に削除されています。

https://devblogs.microsoft.com/dotnet/binaryformatter-removed-from-dotnet-9/

新しいプロジェクトで使うことはないと思われますが、既に利用している場合は別のシリアライザへ移行する必要があります。

まとめ

というわけで、.NET向けのシリアライザまとめでした。新しいプロジェクトでシリアライザを選択する際の参考となれば幸いです。

Discussion