1. Giới thiệu
Serialize một object đơn giản giống như gửi một tấm bưu thiếp: đơn giản, không có gì bị mất. Nhưng thường object của chúng ta trở thành "album gia đình": có các đối tượng lồng nhau, collections và thậm chí collections lồng trong collections.
Tưởng tượng user của chúng ta không chỉ là một Person với tên và tuổi, mà ví dụ có một bộ contacts (Contact), vài địa chỉ nhà/cơ quan, và có thể một field kiểu List<Pet> nếu người đó thích thú cưng. Tất cả đều là đối tượng lồng nhau và collections.
Các serializer trong .NET xử lý chuyện này thế nào? Cần lưu ý gì khi serialize một cây object phức tạp? Có sập mọi thứ nếu trong collection có collection khác không? Hôm nay mình sẽ giải thích rõ những gì mong đợi — và nên làm gì trong các trường hợp khó.
Ý nghĩa của "lồng nhau" trong ngữ cảnh .NET
Đối tượng lồng nhau là đơn giản là một object khác được dùng như property hoặc field bên trong object chính của bạn. Ví dụ, đây là model user mở rộng:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public List<Contact> Contacts { get; set; } // Bộ sưu tập của các đối tượng lồng nhau
public Address? HomeAddress { get; set; } // Một đối tượng lồng nhau (nullable)
}
public class Contact
{
public string Type { get; set; } // Ví dụ: Email hoặc Phone
public string Value { get; set; }
}
public class Address
{
public string City { get; set; }
public string Street { get; set; }
}
Chú ý — một người có thể có nhiều contacts, còn address thường chỉ một (và có thể là null). Đây là kịch bản cơ bản của nhiều model.
2. Serialize objects và collections với System.Text.Json
Ví dụ đơn giản: serialize và deserialize một collection
Thử xem nó hoạt động như thế nào trong thực tế:
using System.Text.Json;
var person = new Person
{
Name = "Anna",
Age = 28,
Contacts = new List<Contact>
{
new Contact { Type = "Email", Value = "anna@example.com" },
new Contact { Type = "Phone", Value = "+1234567890" }
},
HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};
string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
Kết quả sẽ xấp xỉ như sau:
{
"Name": "Anna",
"Age": 28,
"Contacts": [
{
"Type": "Email",
"Value": "anna@example.com"
},
{
"Type": "Phone",
"Value": "+1234567890"
}
],
"HomeAddress": {
"City": "Berlin",
"Street": "Alexanderplatz, 1"
}
}
Serializer xử lý lồng nhau rất ngon. Nếu một property là object khác, nó sẽ serialize nó "lồng" vào. Nếu là list thì serialize như array.
Thực tế: Serialize tự động hoạt động cho bất kỳ collection và đối tượng lồng nhau nào, miễn là chúng public và có getter/setter public (get/set).
Deserialize: hoạt động "mặc định"
string jsonInput = /* JSON mà ta vừa có được */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"
Mọi thứ hoạt động — contacts trở lại từ JSON array thành List<Contact>, HomeAddress trở lại thành object Address.
3. Cách serialize collections: List, arrays, Dictionary
Thường trong model của bạn không chỉ có lists (List<T>), mà còn dictionaries (Dictionary<TKey, TValue>) hoặc mảng nhiều chiều.
Ví dụ với array
public class Team
{
public string Name { get; set; }
public Person[] Members { get; set; }
}
var team = new Team
{
Name = "Nhóm phát triển",
Members = new[]
{
new Person { Name = "Aleksey", Age = 31 },
new Person { Name = "Ekaterina", Age = 27 }
}
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Name": "Nhóm phát triển",
"Members": [
{
"Name": "Aleksey",
"Age": 31,
"Contacts": null,
"HomeAddress": null
},
{
"Name": "Ekaterina",
"Age": 27,
"Contacts": null,
"HomeAddress": null
}
]
}
Serialize collection giống như đóng gói một đống bưu thiếp giống nhau: mỗi phần tử là một "tấm bưu thiếp" riêng.
Ví dụ với Dictionary
public class Phonebook
{
public Dictionary<string, string> Phones { get; set; }
}
var phonebook = new Phonebook
{
Phones = new Dictionary<string, string>
{
{ "Andrey", "+12998887766" },
{ "Mariya", "+12882223344" }
}
};
string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"Phones": {
"Andrey": "+12998887766",
"Mariya": "+12882223344"
}
}
Trong JSON, dictionary trở thành object với các khóa động.
4. Collections lồng nhau (List trong List, arrays of arrays)
.NET có thể serialize cả những "matrioshka" như vậy:
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 = "Sư tử" }, new Animal { Name = "Hổ" } },
new List<Animal> { new Animal { Name = "Gấu" } }
}
};
string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
JSON:
{
"AnimalGroups": [
[ { "Name": "Sư tử" }, { "Name": "Hổ" } ],
[ { "Name": "Gấu" } ]
]
}
Cũng tương tự khi deserialize ngược lại.
5. Đặc điểm serialize đối tượng lồng nhau với XmlSerializer
Ví dụ serialize đối tượng lồng nhau
using System.Xml.Serialization;
using System.IO;
var person = new Person
{
Name = "Ivan",
Age = 35,
HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 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());
Kết quả:
<?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>Ivan</Name>
<Age>35</Age>
<Contacts>
<Contact>
<Type>Email</Type>
<Value>ivan@domain.de</Value>
</Contact>
</Contacts>
<HomeAddress>
<City>Bonn</City>
<Street>Beethoven str., 100</Street>
</HomeAddress>
</Person>
Đặc điểm của collections trong XmlSerializer
Theo mặc định XmlSerializer serialize collection như thẻ "Contacts", bên trong thẻ đó mỗi phần tử sẽ có thẻ "Contact" riêng. Loại collection phải public, và phần tử của nó phải là kiểu có thể serialize được.
Điểm quan trọng. Nếu collection rỗng, XML vẫn sẽ chứa thẻ rỗng:
<Contacts />
Collection được hỗ trợ. XmlSerializer hỗ trợ List<T>, mảng T[], các collection implement ICollection<T>. Nó không hỗ trợ ví dụ như Dictionary<TKey, TValue>! Để serialize dictionary bạn sẽ cần bọc dictionary thành list cặp key-value hoặc dùng kỹ thuật/biblioteka ngoài.
6. Hỗ trợ objects và collections trong Newtonsoft.Json
Newtonsoft.Json hỗ trợ lồng nhau linh hoạt hơn một chút. Trong 99% trường hợp mọi thứ hoạt giống như System.Text.Json, nhưng có vài điểm cộng: có thể serialize cả private fields (nếu cấu hình), dictionaries với key phức tạp hơn, dynamic types, và thậm chí circular references (với các setting đặc biệt).
Ví dụ
using Newtonsoft.Json;
var person = new Person
{
Name = "Pavel",
Age = 40,
Contacts = new List<Contact>
{
new Contact { Type = "SMS", Value = "+10000000013" }
}
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);
Khác biệt. Nếu object có ví dụ private fields, bạn có thể serialize chúng bằng cách cấu hình ContractResolver. Nhưng với các collections và objects lồng nhau cơ bản thì mọi thứ tự động hoạt động "mặc định".
7. Các lỗi điển hình, bẫy và lưu ý
Lỗi 1: Đối tượng lồng nhau là null.
Nếu trong object bạn serialize có vài property chưa được gán (null), thì trong JSON hoặc XML chúng có thể không xuất hiện, hoặc sẽ được ghi là null. Ví dụ, nếu HomeAddress của Person là null, thì trong JSON:
"HomeAddress": null
Trong XML mặc định thẻ đó sẽ không có — bạn có thể thêm nó bằng cấu hình serializer ([XmlElement(IsNullable=true)]).
Lỗi 2: Serialize private properties/fields.
Mặc định hầu hết serializers (JSON và XML) chỉ làm việc với properties public. Nếu bạn muốn serialize private hoặc protected fields, phải public chúng hoặc cấu hình serializer (ví dụ qua ContractResolver trong Newtonsoft.Json).
Lỗi 3: Lớp không có constructor mặc định.
Trong XML-serialization (và thường cả JSON), lớp nên có constructor public không tham số. Nếu không có, sẽ có exception khi deserialize.
Lỗi 4: Serialize dictionaries trong XML.
XmlSerializer không hỗ trợ trực tiếp serialize Dictionary<TKey, TValue>. Đây thường là điều bất ngờ. Giải pháp có thể là bọc dictionary vào list cặp hoặc dùng serializer khác.
Lỗi 5: Circular references.
Nếu một object tham chiếu đến object khác mà object kia lại tham chiếu ngược lại (ví dụ parent và child), serialize "vòng tròn" có thể gây StackOverflow (StackOverflowException) hoặc exception về circular reference. Ở JSON serializers có thể cấu hình để xử lý, nhưng thường đúng hơn là nên xem lại thiết kế object.
GO TO FULL VERSION