1. Введение
Вы уже знаете, что .NET может сериализовать и десериализовать объекты в XML по умолчанию, основываясь на публичных полях и свойствах. Но когда хочется сделать красивый XML, совместимый с чужими программами/стандартами, или когда мы не хотим включать в XML какие-то внутренние детали, нужна тонкая настройка.
В C# для этого существуют атрибуты сериализации — специальные «метки», которыми мы помечаем классы, свойства или поля. Благодаря этим атрибутам мы можем управлять буквально всем: какие поля попадут в XML, какие будут игнорироваться, как элемент будет называться, становиться ли атрибутом, иметь ли сложную структуру и даже какой порядок будет у элементов!
Представьте, что сериализация по умолчанию — это как автоматическое заполнение бланка: имя-фамилия-отчество в свои ячейки, всё остальное — как попало. А с атрибутами вы как супер-канцелярский чиновник: сами решаете, какую строку куда записывать, что пропустить, что выделить красным, а что вообще спрятать в секретный раздел.
Сопоставление объекта и XML
| Класс/Свойство | Атрибут | Результат в XML |
|---|---|---|
|
Класс UserList | |
|
|
|
|
|
id="..." (атрибут) |
|
|
|
|
|
|
|
[XmlIgnore] (см. RegisteredAtString) | - |
|
|
|
|
|
|
2. Обзор основных атрибутов сериализации XML
Давайте пройдемся по самым популярным атрибутам, которыми пользователи .NET украшали свои классы на протяжении многих поколений!
Атрибут [XmlElement] — Имя элемента
Меняет имя элемента в XML или отмечает свойство/поле как XML-элемент.
public class Person
{
[XmlElement("FullName")]
public string Name { get; set; }
}
XML:
<Person>
<FullName>Василий Петров</FullName>
</Person>
Если не указать атрибут — по умолчанию имя элемента будет совпадать с именем свойства/поля.
Атрибут [XmlAttribute] — Сериализация в XML-атрибут
Позволяет сериализовать свойство/поле как XML-атрибут, а не как элемент.
public class Person
{
[XmlAttribute("id")]
public int Id { get; set; }
}
XML:
<Person id="123"></Person>
В реальных XML-API часто используют атрибуты для идентификаторов, дат, флагов и т.д.
Атрибут [XmlIgnore] — Пропускать поле/свойство
Мечта любого параноика: полностью исключить свойство из процесса сериализации и десериализации! Всё, что помечено этим атрибутом, просто не попадёт в финальный XML.
public class Person
{
[XmlIgnore]
public string InternalNote { get; set; }
}
XML: (свойства InternalNote здесь нет и не будет)
Атрибуты [XmlArray] и [XmlArrayItem] — контроль над коллекциями
Специальные атрибуты для массивов и коллекций. [XmlArray] задаёт имя внешней обёртки-массива, а [XmlArrayItem] — имя отдельного элемента.
public class Person
{
[XmlArray("Phones")]
[XmlArrayItem("Phone")]
public string[] PhoneNumbers { get; set; }
}
XML:
<Person>
<Phones>
<Phone>+12951234567</Phone>
<Phone>+12876543210</Phone>
</Phones>
</Person>
Атрибут [XmlRoot] — Название корневого элемента
Помечает сам класс, чтобы изменить имя корневого элемента в XML.
[XmlRoot("User")]
public class Person
{
public string Name { get; set; }
}
XML:
<User>
<Name>Анна</Name>
</User>
Атрибут [XmlText] — Сериализация в текстовое содержимое элемента
Иногда хочется (или требуется спецификацией), чтобы значение свойства стало просто текстом внутри элемента, а не его вложенным подэлементом или атрибутом.
public class Note
{
[XmlText]
public string Content { get; set; }
}
XML:
<Note>Позвони бабушке!</Note>
Атрибуты [XmlNamespaceDeclarations], [XmlElement(Type = ...)]
Для более продвинутых сценариев (например, сериализация с пространствами имён, поддержка наследования и др.), есть и более специфические атрибуты. Обязательно посмотрите официальную документацию по XML-сериализации для полного списка.
3. Практика: улучшаем сериализацию на реальном примере
Давайте шаг за шагом, доработаем наше учебное приложение до «XML-сериализации премиум-класса», чтобы оно радовало даже самого строгого XML-инспектора.
Базовый класс без атрибутов
Допустим, у нас есть такой класс пользователя:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Emails { get; set; }
public DateTime RegisteredAt { get; set; }
public string Password { get; set; }
}
Дефолтная сериализация даст примерно такой XML (ещё и дату запишет в своём стиле):
<User>
<Id>42</Id>
<Name>Ivan Ivanov</Name>
<Emails>
<string>user@mail.com</string>
<string>admin@site.org</string>
</Emails>
<RegisteredAt>2024-07-01T00:00:00</RegisteredAt>
<Password>qwerty</Password>
</User>
Что не так:
- Пароль лучше не сериализовать вообще.
- Хотим дату как атрибут и человеческий формат.
- Хотим Emails в красивой форме.
- Хотим корневой элемент <Person>.
Добавляем настройки через атрибуты
[XmlRoot("Person")]
public class User
{
[XmlAttribute("id")]
public int Id { get; set; }
[XmlElement("FullName")]
public string Name { get; set; }
[XmlArray("EMails")]
[XmlArrayItem("Email")]
public string[] Emails { get; set; }
[XmlAttribute("registered")]
public string RegisteredAtString
{
get => RegisteredAt.ToString("yyyy-MM-dd");
set => RegisteredAt = DateTime.Parse(value);
}
[XmlIgnore]
public string Password { get; set; }
[XmlIgnore]
public DateTime RegisteredAt { get; set; }
}
Объяснение тонкостей:
- Используем [XmlAttribute("registered")] для сериализации даты как атрибута.
- Чтобы сериализовать не сам DateTime, а его строковое представление, добавляем отдельное свойство RegisteredAtString, а реальное поле скрываем через [XmlIgnore].
- Пароль скрываем с помощью [XmlIgnore].
- Массив email-ов настроен так, что внешний тег <EMails>, а каждый email — <Email>.
Результат:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
<Email>admin@site.org</Email>
</EMails>
</Person>
4. Работа с вложенными объектами
XML лучше всего проявляет себя, если есть вложенные объекты (например, пользователь и его адреса).
public class Address
{
[XmlAttribute]
public string City { get; set; }
[XmlText]
public string Details { get; set; }
}
public class User
{
// ...другие свойства, см. выше...
public Address[] Addresses { get; set; }
}
И добавим настройку для массива адресов:
[XmlArray("UserAddresses")]
[XmlArrayItem("Address")]
public Address[] Addresses { get; set; }
Тогда XML будет выглядеть так:
<Person id="42" registered="2024-07-01">
<FullName>Ivan Ivanov</FullName>
<EMails>
<Email>user@mail.com</Email>
</EMails>
<UserAddresses>
<Address City="Neon city">ул. Замковая, д.1</Address>
<Address City="North Cave">ул. Крайняя д.12</Address>
</UserAddresses>
</Person>
Видно, что у каждого <Address> city — это атрибут, а сам адрес указан как текст внутри.
5. Частые ошибки и нюансы
Если вы сериализуете сложный объект и не видите изменений в XML, которые ожидали — скорее всего, забыли правильно указать атрибут. Иногда бывает, что свойства/поля остаются без маркировки. Тогда .NET просто выбирает дефолтное поведение.
Ещё одна ловушка — имя коллекции. Если коллекция (например, массив строк) не помечена ни [XmlArray], ни [XmlArrayItem], то элементы запишутся с тегом string (как видно в начальном примере выше). Чтобы этого не было, всегда явно задавайте имена тегов через атрибуты.
Будьте осторожны с типами свойств «getter-only» (только для чтения) — такие свойства не десериализуются по умолчанию. Старайтесь делать публичный set.
Некоторые типы (например, словари или интерфейсы) не сериализуются стандартным XmlSerializer-ом, либо требуют отдельной обработки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ