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; }
}
注意 — 一個人可能有多個 Contacts,但地址只有一個(而且它可能是 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"
一切都會運作——Contacts 會從 JSON 陣列變回 List<Contact>,HomeAddress 會變回 Address 類別的物件。
3. 集合如何被序列化: List, 陣列, Dictionary
在你的模型裡常見的集合不只 List(List<T>),也可能有 Dictionary(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 中,Dictionary 會變成一個具有動態鍵的物件。
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 />
支援的集合。 XmlSerializer 支援 List<T>、陣列 T[]、實作 ICollection<T> 的集合。但不支援像 Dictionary<TKey, TValue> 這種型別!要序列化字典通常要把字典包成鍵值對列表,或使用其他序列化工具或第三方函式庫。
6. Newtonsoft.Json 對物件與集合的支援
Newtonsoft.Json 在巢狀支援上更靈活一點。99% 的情況下它和 System.Text.Json 的行為一樣,但有些優勢:可以序列化私有欄位(如果你明確設定)、支援帶複雜鍵的字典、支援動態型別,甚至可以透過設定處理循環參考。
範例
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。例如,如果 HomeAddress 在 Person 是 null,在 JSON 裡會看到:
"HomeAddress": null
在 XML 裡預設情況下可能根本沒有那個標籤——你可以用序列化器的設定(例如 [XmlElement(IsNullable=true)])來改變這個行為。
錯誤 2:序列化私有屬性/欄位。
預設多數序列化器(不論 JSON 或 XML)只處理 public 屬性。如果你想序列化私有或受保護的欄位,要把它們改成 public,或透過序列化器的設定(例如在 Newtonsoft.Json 裡用 ContractResolver)來包含它們。
錯誤 3:類別沒有預設建構子。
在 XML 序列化(以及很多 JSON 情況)中,類別通常需要一個 public 的無參數建構子。沒有的話,反序列化會拋出例外。
錯誤 4:在 XML 中序列化字典。
XmlSerializer 不直接支援序列化 Dictionary<TKey, TValue>。這常讓人措手不及。解法通常是把字典包成鍵值對的列表,或改用支援字典的序列化器。
錯誤 5:循環參考。
如果物件 A 透過參考包含物件 B,而 B 又回頭參考 A(像父母/子女的雙向參考),序列化會「繞圈」,可能導致 StackOverflowException(堆疊溢位)或拋出循環參考的例外。某些 JSON 序列化器可以透過設定來處理,但更好的方式通常是重新檢視物件設計。
GO TO FULL VERSION