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; } // 一个嵌套对象(可为空)
}
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>,HomeAddress 会变回 Address 类型的对象。
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 />
支持的集合。 XmlSerializer 支持 List<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。例如,如果 Person 的 HomeAddress 为 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:循环引用。
如果一个对象通过引用包含另一个对象,而另一个对象又引用回第一个(比如父和子之间),循环序列化会导致栈溢出(StackOverflowException)或关于循环引用的异常。JSON 序列化器可以通过配置绕开,但通常更好的做法是重新考虑对象设计。
GO TO FULL VERSION