在C#开发中,XML仍然是一种重要的数据交换格式,许多配置文件、Web服务接口、第三方系统对接都要求以XML形式传递数据。当对象结构简单时序列化并不困难,但一旦遇到多层嵌套、集合、字典等复杂结构,很多开发者就会遇到各种奇怪的问题,比如某些属性莫名消失、生成的XML结构不符合预期等。本文将系统地讲解如何在C#中把复杂的嵌套对象正确地序列化为XML。

一、使用XmlSerializer序列化嵌套对象的基本方法
XmlSerializer是.NET中最常用的XML序列化工具,位于System.Xml.Serialization命名空间下。它的工作原理是基于对象的公共属性和公共字段生成XML,不需要额外标注特性也能工作。对于嵌套对象,XmlSerializer会自动递归处理,将内层对象的属性展开为子节点。
下面通过一个典型的三层嵌套结构来演示。假设我们需要描述一个订单系统,订单包含客户信息、多个商品条目,每个商品又有自己的分类信息,这种多层结构在业务系统中非常常见。
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrderItem
{
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public Category Category { get; set; } // 嵌套对象
}
public class Customer
{
public string Name { get; set; }
public string Phone { get; set; }
}
public class Order
{
public string OrderNo { get; set; }
public DateTime CreateTime { get; set; }
public Customer Customer { get; set; } // 嵌套对象
public List<OrderItem> Items { get; set; } // 嵌套集合
}
class Program
{
static void Main()
{
var order = new Order
{
OrderNo = "SO-2024-001",
CreateTime = DateTime.Now,
Customer = new Customer { Name = "张三", Phone = "13800138000" },
Items = new List<OrderItem>
{
new OrderItem
{
ProductName = "机械键盘", Price = 399.00m, Quantity = 1,
Category = new Category { Id = 5, Name = "电脑外设" }
},
new OrderItem
{
ProductName = "无线鼠标", Price = 129.50m, Quantity = 2,
Category = new Category { Id = 5, Name = "电脑外设" }
}
}
};
var serializer = new XmlSerializer(typeof(Order));
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // 去掉默认命名空间,输出更干净
using (var stream = new MemoryStream())
{
serializer.Serialize(stream, order, ns);
string xml = System.Text.Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(xml);
}
}
}
执行后可以看到,Order的属性成为根节点的直接子节点,Customer对象被展开为一个子节点,其内部属性继续作为孙节点,List集合中的每个OrderItem都会生成一个同名子节点,嵌套的Category同样被递归展开。整个层级结构清晰,完全自动化,不需要手动干预。
需要注意的是,XmlSerializer有几个隐含规则:只序列化公共的读写属性和公共字段;只处理无参构造函数能创建的类型;被static、readonly修饰的字段会被忽略。如果某个属性没有被序列化,首先应该检查是否违反了这些规则。
二、用特性精确控制XML结构
默认生成的XML节点名与类名、属性名一致,但实际对接第三方系统时,往往要求特定的节点名称或属性格式。这时就需要借助System.Xml.Serialization命名空间下的各种特性来精细化控制。
常用的控制特性包括:XmlRoot控制根节点名称,XmlElement控制子节点名称,XmlAttribute把属性输出为XML特性而非子节点,XmlIgnore完全忽略某个属性,XmlArray和XmlArrayItem控制集合节点的外层包装。通过组合这些特性,几乎可以生成任意想要的XML结构。
[XmlRoot("order")]
public class Order
{
[XmlAttribute("no")]
public string OrderNo { get; set; }
[XmlElement("created")]
public DateTime CreateTime { get; set; }
[XmlIgnore]
public string InternalRemark { get; set; } // 不参与序列化
[XmlElement("customer")]
public Customer Customer { get; set; }
[XmlArray("items")]
[XmlArrayItem("item")]
public List<OrderItem> Items { get; set; }
}
上面的配置会让订单号以特性形式输出在根节点上,创建时间节点改名为created,集合外层多一层items包装,每个元素叫item。如果希望集合没有外层包装、元素直接平铺在父节点下,把XmlArray和XmlArrayItem换成XmlElement即可,这是处理列表时一个非常实用的小技巧。
还有一种进阶用法:当同一个属性可能存放不同类型的对象时,可以使用XmlInclude配合继承体系实现多态序列化。例如定义一个基类Payment和子类AliPay、WechatPay,在序列化时XmlSerializer会自动在节点上附加xsi:type特性来记录具体类型,反序列化时能正确还原。使用多态时必须在XmlSerializer的构造函数或XmlInclude中提前声明所有可能的子类型,否则运行时会抛出异常。
三、处理字典和特殊类型的难点
XmlSerializer有一个广为人知的限制:不支持Dictionary类型。这是因为它无法在XML中表示键值对的语义。解决办法是自定义一个可序列化的键值对结构,把字典转换后再序列化。
public class SerializableKeyValue
{
public string Key { get; set; }
public string Value { get; set; }
}
public class Config
{
[XmlArray("settings")]
[XmlArrayItem("setting")]
public List<SerializableKeyValue> Settings { get; set; }
}
// 序列化前转换
var config = new Config
{
Settings = dict.Select(kv => new SerializableKeyValue
{
Key = kv.Key,
Value = kv.Value
}).ToList()
};
除了字典,还有一些类型需要特别留意。枚举默认输出名称字符串,可以通过XmlEnum特性自定义;Nullable<int>这类可空类型,值为null时对应节点会整个消失,而不是输出空节点;DateTime默认输出ISO 8601格式,如果对接方要求yyyy-MM-dd这样的自定义格式,最简单的办法是在实体类中增加一个string类型的辅助属性,用XmlElement标注真实输出属性,把原属性标记为XmlIgnore。
此外,如果对象中存在循环引用,比如父对象引用子对象、子对象又反过来引用父对象,XmlSerializer在序列化时会直接抛出异常。这种结构在ORM实体中很常见,需要在序列化前先把数据映射到专门的DTO对象上,断开循环引用,这也是推荐的工程实践——让传输模型与业务模型分离。
四、反序列化与方案选型建议
序列化的逆过程同样简单,调用Deserialize方法即可把XML还原为对象。对于嵌套结构,XmlSerializer会按照类型定义自动重建整棵对象树,集合、嵌套对象都能正确恢复。要注意反序列化时构造的XmlSerializer实例的typeof类型必须与XML根节点对应,否则会报错。
using (var reader = new StringReader(xmlContent))
{
var serializer = new XmlSerializer(typeof(Order));
var order = (Order)serializer.Deserialize(reader);
Console.WriteLine(order.Customer.Name);
}
关于性能有一个容易被忽视的要点:XmlSerializer在首次构造时会动态生成序列化程序集,这个过程比较耗时。在循环中反复new XmlSerializer会造成明显的性能浪费,正确做法是缓存静态实例复用。对于同一个类型,XmlSerializer内部有缓存机制,只要传入的参数一致,重复构造的开销其实可控,但在高频调用场景下仍然建议显式缓存。
如果项目对互操作性要求更高,还可以考虑DataContractSerializer,它基于DataContract和DataMember特性工作,支持字典类型,通过KnownType处理多态,生成的XML结构可以通过Order参数控制是按成员顺序还是按字母排列。总的来说,简单直观、需要精细控制节点结构时选XmlSerializer;需要支持字典、关注性能和数据契约时选DataContractSerializer。掌握这些技巧后,再复杂的嵌套对象都能轻松转换成符合要求的XML。
C# XML序列化嵌套对象序列化XmlSerializer修改时间:2026-09-01 13:30:35