1. はじめに
辞書(C# の Dictionary<TKey, TValue>)は「キーと値」のペアのコレクションだよ。ユニークな識別子で素早く値を探したいときに便利(例:電話帳で名前から電話番号を探すとか)。
リスト(List<T>)と違って、要素の順序は重要じゃなくて、キーによる高速アクセスに特化している。リストをシリアライズするのは簡単(JSON の配列)だけど、辞書のシリアライズにはいくつか注意点があるんだ:
- キーはシリアライズ可能な型である必要がある(普通は文字列、時々数値、稀に別のオブジェクト)。
- JSON には辞書という独自の型はなく、オブジェクトか配列しかない。
これから .NET が辞書をどうシリアライズするか、どんな問題に遭遇するか、そういう構造に対応する正しい書き方を見ていこう。
2. 文字列キーの辞書のシリアライズ
まずは定番 — キーも値も文字列の辞書。
// 辞書の例:本とその著者
var books = new Dictionary<string, string>
{
["マスターとマルガリータ"] = "ミハイル・ブルガーコフ",
["ハリー・ポッター"] = "ジョアン・ローリング",
["蠅の王"] = "ウィリアム・ゴールディング"
};
// JSON 文字列へシリアライズ
string json = JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("辞書をJSONにシリアライズしたよ:\n" + json);
// ファイルに保存(同期)
File.WriteAllText("books.json", json);
Console.WriteLine("JSONをbooks.jsonに書き込んだよ。");
// ファイルから読み戻す(同期)
string jsonFromFile = File.ReadAllText("books.json");
// 辞書へデシリアライズ
var restoredBooks = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonFromFile);
Console.WriteLine("デシリアライズの結果:");
foreach (var pair in restoredBooks)
Console.WriteLine($"{pair.Key} -> {pair.Value}");
books.json に何が入る?
{
"マスターとマルガリータ": "ミハイル・ブルガーコフ",
"ハリー・ポッター": "ジョアン・ローリング",
"戦争と平和": "ウィリアム・ゴールディング"
}
仕組みは?
シリアライザは Dictionary<string, string> を JSON オブジェクトに変換して、各キーがプロパティ名になり、値がそのプロパティの値になるんだ。キーが文字列で一意ならこれが便利。
3. 非標準キー型の辞書
キーが文字列なら簡単だけど、例えば数値だったら?
var bookIds = new Dictionary<int, string>
{
[1001] = "マスターとマルガリータ",
[1002] = "ハリー・ポッター",
[1003] = "蠅の王"
};
string jsonIntKeys = JsonSerializer.Serialize(bookIds, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonIntKeys);
結果:
{
"1001": "マスターとマルガリータ",
"1002": "ハリー・ポッター",
"1003": "蠅の王"
}
何が起きた?
- C# は数値のキーを文字列に変換したよ。JSON のプロパティ名は文字列だけだからね。
- デシリアライズするときはシリアライザがその文字列を数値に戻そうとするよ。
デシリアライズの例:
var restoredBookIds = JsonSerializer.Deserialize<Dictionary<int, string>>(jsonIntKeys);
// 問題なし!キーはまた数値になったよ。
キーが複雑な型(オブジェクト)だったら?
var dict = new Dictionary<Author, string>
{
[new Author { Name = "ゴーリング", BirthYear = 1911 }] = "蠅の王"
};
こういう辞書をシリアライズしようとすると 例外 が投げられるよ:
System.NotSupportedException: Serialization and deserialization of 'Dictionary<Author, string>' instances are not supported.
なぜ?
JSON のオブジェクトのプロパティ名には文字列しか使えないからだよ。だからシリアライズ時のキーは文字列へ一意に変換できる単純な型(普通は string や数値)である必要がある。複雑なオブジェクトは標準的な方法ではキーにできないんだ。
4. 値がネストしたオブジェクトの辞書
キーは解決したから、次は値が複雑なオブジェクト(例えば Book や Author)の場合を見てみよう。
例
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
}
var authorDirectory = new Dictionary<string, Author>
{
["bulgakov"] = new Author { Name = "ミハイル・ブルガーコフ", BirthYear = 1891 },
["golding"] = new Author { Name = "ウィリアム・ゴールディング", BirthYear = 1911 }
};
string jsonAuthors = JsonSerializer.Serialize(authorDirectory, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonAuthors);
結果:
{
"bulgakov": {
"Name": "ミハイル・ブルガーコフ",
"BirthYear": 1891
},
"golding": {
"Name": "ウィリアム・ゴールディング",
"BirthYear": 1911
}
}
- ネストされたものはオブジェクトの構造に従ってシリアライズされるよ。
- Dictionary<string, Author> へのデシリアライズも問題なく動くよ。
5. 辞書が他のオブジェクトに含まれる場合
辞書はもっと大きなオブジェクトのフィールドとしてよく使われる。例えば図書館のカタログで、キーがジャンル名、値がそのジャンルの本のリスト、みたいなケース。
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
}
public class Library
{
public Dictionary<string, List<Book>> CatalogByGenre { get; set; }
}
var library = new Library
{
CatalogByGenre = new Dictionary<string, List<Book>>
{
["サイエンスフィクション"] = new List<Book>
{
new Book { Title = "ソラリス", Author = "スタニスワフ・レム" }
},
["古典"] = new List<Book>
{
new Book { Title = "蠅の王", Author = "ウィリアム・ゴールディング" },
new Book { Title = "ズリマヤ・トゥマ", Author = "ウィリアム・ゴールディング" }
}
}
};
string jsonLibrary = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonLibrary);
出力 JSON のフラグメント:
{
"CatalogByGenre": {
"サイエンスフィクション": [
{
"Title": "ソラリス",
"Author": "スタニスワフ・レム"
}
],
"古典": [
{
"Title": "蠅の王",
"Author": "ウィリアム・ゴールディング"
},
{
"Title": "ズリマヤ・トゥマ",
"Author": "ウィリアム・ゴールディング"
}
]
}
}
問題なし — ネストした辞書やコレクションは再帰的にシリアライズ/デシリアライズされるよ。
6. 辞書のシリアライズに関する注意点と落とし穴
重複するキー
辞書ではキーは常にユニーク。でも手作業で重複キーを含む JSON を与えるとどうなるか:
{
"foo": "first",
"foo": "second"
}
結果:最後の値("second")が上書きされるだけで、エラーにはならない。ほとんどの JSON パーサはそう動くよ。
要素の順序
辞書は順序を保証しないコレクションだ。シリアライズ時に JSON のプロパティの順序が元と違うことがある。順序が重要ならペアのリスト(List<KeyValuePair<string, T>>)を使うといいけど、普通は辞書で順序は気にしないよね。
JSON とネストした辞書
ネストの深さに制限はないけど、各レベルで JSON の制約に従う必要がある(キーは文字列、値は有効な JSON オブジェクト/配列)。
JsonSerializerOptions の使い方
プロパティ名を PascalCase から camelCase にしたいことがあるよね。フロントエンド(JavaScript)と連携するなら camelCase の方が一般的。
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
string camelJson = JsonSerializer.Serialize(authorDirectory, options);
重要:辞書の場合、このオプションはネストされたオブジェクト(そのプロパティ)にのみ影響して、辞書のキー自体には影響しないよ。辞書のキーは C# 側で指定したあの文字列のままシリアライズされる。
7. 複雑・非文字列キーに関する問題
文字列キーや数値キー(例:int, long, Guid)の辞書は普通に動く。でもユーザー定義クラスや構造体をキーにすると NotSupportedException が出るよ。
そういうケースの回避方法はいくつかある:
- 辞書をオブジェクトの配列として保存する(各要素に "Key" と "Value" フィールドを持たせる)。
- カスタムコンバータ(JsonConverter)を書いて、複雑なキーを文字列に変換して戻す。
- 本当にキーが複雑なら、そもそも辞書のキーに複雑なオブジェクトを使う設計を見直す。
ペアのリストとしてシリアライズする回避例
public class AuthorInfo
{
public Author Author { get; set; }
public string Book { get; set; }
}
// Dictionary<Author, string> の代わりに
var list = new List<AuthorInfo>
{
new AuthorInfo { Author = new Author { Name = "ウィリアム・ゴールディング", BirthYear = 1911 }, Book = "蠅の王" }
};
// このようなリストは問題なくシリアライズされる
比較:シリアライズでの辞書 vs. ペアのリスト
| コレクションの型 | JSON 構造 | いつ使うか |
|---|---|---|
|
|
キーが単純な文字列で、高速検索と一意性が必要なとき |
|
|
キーが複雑な型、順序制御が必要、重複を許したいとき |
8. 面接でよくある質問
1. Dictionary<DateTime, string> をシリアライズできる?
できるよ。ただしキーは文字列表現に変換される(通常は ISO 形式の "yyyy-MM-ddTHH:mm:ss" のようなもの)。デシリアライズ時にロケールや日付フォーマットの違いで問題が出ることがあるから注意してね。
2. Dictionary<int, string> をシリアライズしたらどうなる?
キーは文字列としてシリアライズされる(元が数値でも)。デシリアライズは普通に元の int キーに戻るよ。
3. なぜオブジェクトをキーにした辞書はシリアライズできないの?
JSON オブジェクトのプロパティ名に使えるのは文字列だけで、オブジェクトはプロパティ名にならないからだよ。
4. 複雑なキーの辞書をシリアライズしたい場合は?
構造を見直すか、辞書を "key-value" のペアのリストとしてシリアライズするか、あるいはカスタムの JsonConverter を書いてキーを文字列に変換して戻す、という選択肢があるよ。
GO TO FULL VERSION