CodeGym /コース /C# SELF /ネストされたオブジェクトとコレクションの扱い

ネストされたオブジェクトとコレクションの扱い

C# SELF
レベル 45 , レッスン 1
使用可能

1. はじめに

単純なオブジェクトのシリアライズは、はがきを送るようなものです:簡単で、何も失われません。でも、多くの場合オブジェクトは「ファミリーアルバム」のようになります:ネストされたオブジェクトやコレクション、さらにはコレクションのコレクションが含まれます。

例えば、ユーザーが単なるPerson(名前と年齢)だけでなく、連絡先のセット(Contact)、複数の自宅/職場の住所、そして動物好きならList<Pet>のようなフィールドを持っていると想像してください。これらはすべてネストされたオブジェクトとコレクションです。

.NETのシリアライザはこれをどう扱うのか?複雑なオブジェクトツリーをシリアライズするときに注意すべき点は何か?コレクション内に別のコレクションがあったら壊れるのか?今日は期待できることと、複雑なケースで何をすべきかを見ていきます。

「ネスト」とは.NET用語で何を意味するか

ネストされたオブジェクトは、メインのオブジェクトのプロパティやフィールドとして含まれる別のオブジェクトにすぎません。例えば、拡張されたユーザーモデルは次のようになります:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // ネストされたオブジェクトのコレクション
    public Address? HomeAddress { get; set; }           // 単一のネストされたオブジェクト (nullable)
}
public class Contact
{
    public string Type { get; set; }                    // 例えば、Email や Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

注意してください — 人は複数の連絡先を持てますが、住所は一つだけ(そしてnullかもしれません)。これは多くのモデルでよくある構成です。

2. System.Text.Jsonでのオブジェクトとコレクションのシリアライズ

簡単な例:コレクションをシリアライズしてデシリアライズする

実際にどう動くか見てみましょう:

using System.Text.Json;

var person = new Person
{
    Name = "アンナ",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "ベルリン", Street = "アレクサンダープラッツ, 1" }
};

string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

結果はだいたいこんな感じになります:

{
  "Name": "アンナ",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "ベルリン",
    "Street": "アレクサンダープラッツ, 1"
  }
}

シリアライザはネストを正しく理解します。プロパティが別のオブジェクトなら、それをネストした形でシリアライズします。リストなら配列としてシリアライズします。

事実:このようなシリアライズは、プロパティがpublicでpublicなgetter/setter(get/set)を持っていれば、自動的に動作します。

デシリアライズ:そのままで動く

string jsonInput = /* 先ほど受け取ったJSON */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "アンナ"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

問題なく動きます — 連絡先はJSON配列からList<Contact>に戻り、HomeAddressAddressクラスのオブジェクトに戻ります。

3. コレクションのシリアライズ方法:List、配列、Dictionary

モデルの中にはリスト(List<T>)だけでなく、辞書(Dictionary<TKey, TValue>)や多次元配列が含まれることがあります。

配列の例

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "開発者たち",
    Members = new[]
    {
        new Person { Name = "アレクセイ", Age = 31 },
        new Person { Name = "エカテリーナ", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Name": "開発者たち",
  "Members": [
    {
      "Name": "アレクセイ",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "エカテリーナ",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

コレクションのシリアライズは、同じ種類のはがきを山ほど包むようなもの:各要素が個別の「はがき」です。

Dictionaryの例

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "アンドレイ", "+12998887766" },
        { "マリア", "+12882223344" }
    }
};

string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Phones": {
    "アンドレイ": "+12998887766",
    "マリア": "+12882223344"
  }
}

JSONでは辞書は動的なキーを持つオブジェクトになります。

4. ネストされたコレクション(Listの中のList、配列の配列)

.NETはこのような「マトリョーシカ」もシリアライズできます:

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "ライオン" }, new Animal { Name = "トラ" } },
        new List<Animal> { new Animal { Name = "クマ" } }
    }
};

string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "AnimalGroups": [
    [ { "Name": "ライオン" }, { "Name": "トラ" } ],
    [ { "Name": "クマ" } ]
  ]
}

デシリアライズ時も同様に戻ります。

5. XmlSerializerでのネストされたオブジェクトのシリアライズの特徴

ネストされたオブジェクトのシリアライズ例

using System.Xml.Serialization;
using System.IO;

var person = new Person
{
    Name = "イワン",
    Age = 35,
    HomeAddress = new Address { City = "ボン", Street = "ベートーベン通り, 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

結果:

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>イワン</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>ボン</City>
    <Street>ベートーベン通り, 100</Street>
  </HomeAddress>
</Person>

XmlSerializerでのコレクションの特徴

既定では、XmlSerializerはコレクションを"Contacts"というタグとしてシリアライズし、その中に各要素ごとに"Contact"タグを作ります。このため、コレクションの型はpublicで、その要素がシリアライズ可能な型である必要があります。

重要な点。コレクションが空の場合でも、XMLは空のタグを含みます:

<Contacts />

サポートされるコレクション。 XmlSerializerList<T>、配列 T[]ICollection<T>を実装するコレクションをサポートします。例えば、Dictionary<TKey, TValue>はサポートしていません!辞書をシリアライズするには、辞書をペアのリストでラップするか、別のシリアライザや追加のテクニックを使う必要があります。

6. Newtonsoft.Jsonでのオブジェクトとコレクションのサポート

Newtonsoft.Jsonはネストの扱いが少し柔軟です。99%の場合はSystem.Text.Jsonと同じように動きますが、利点があります:プライベートフィールドを明示的にシリアライズできる、より複雑なキーを持つ辞書、dynamicな型、そして特殊設定で循環参照も扱えます。

using Newtonsoft.Json;

var person = new Person
{
    Name = "パーヴェル",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

違い。例えばオブジェクトにプライベートフィールドがある場合、それらはContractResolverの設定でシリアライズできます。しかし基本的なネストされたコレクションやオブジェクトについては、ほとんどの場合自動的に「箱から出してすぐ使える」ように動作します。

7. 典型的なエラー、落とし穴、注意点

エラー1: ネストされたオブジェクトがnullである。
シリアライズするオブジェクトのプロパティが未設定(null)の場合、JSONやXMLではそれらがまったく存在しないか、またはnullとして記録されます。例えば、PersonHomeAddressnullなら、JSONでは:

"HomeAddress": null

XMLでは既定でそのタグは存在しません—シリアライザの設定([XmlElement(IsNullable=true)]など)で追加できます。

エラー2: プライベートプロパティ/フィールドのシリアライズ。
既定では多くのシリアライザ(JSONもXMLも)はpublicプロパティのみを扱います。プライベートやprotectedなフィールドをシリアライズしたい場合は、それらをpublicにするか、シリアライザの設定を変更する必要があります(例:Newtonsoft.JsonContractResolver)。

エラー3: デフォルトコンストラクタのないクラス。
XMLシリアライズ(そして多くの場合JSONでも)では、クラスにパラメータなしのpublicコンストラクタが必要です。ない場合、デシリアライズ時に例外が発生します。

エラー4: XMLでの辞書のシリアライズ。
XmlSerializerは標準ではDictionary<TKey, TValue>をシリアライズできません。これはよく驚きになります。解決策としては辞書をキー/値ペアのリストでラップするか、別のシリアライザを使うことです。

エラー5: 循環参照。
一つのオブジェクトが別のオブジェクトを参照し、さらにそのオブジェクトが最初のオブジェクトを参照する(例:親と子)のような場合、シリアライズがループしてStackOverflowException(StackOverflowException)や循環参照に関する例外を引き起こすことがあります。JSONシリアライザでは設定で回避できますが、しばしばオブジェクト設計を見直す方が適切です。

2
タスク
C# SELF, レベル 45, レッスン 1
ロック未解除
複雑な構造体のシリアライズとデシリアライズ
複雑な構造体のシリアライズとデシリアライズ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION