JavaRush /Курсы /C# SELF /Контроль над процессом через атрибуты

Контроль над процессом через атрибуты

C# SELF
45 уровень , 2 лекция
Открыта

1. Для чего нужны атрибуты при сериализации?

Когда вы сериализуете объекты в JSON или XML, происходит что-то похожее на сканирование вашей комнаты роботом-уборщиком. Всё, что явно лежит на виду (публичные свойства/поля), попадает «в мешок» (в файл или строку), а остальное игнорируется. Но иногда вам не хочется, чтобы робот незаметно показывал содержимое вашего портфеля, или хочется аккуратно назвать вещи по-другому — например, не по-русски, а по-английски.

Тут и появляются атрибуты! Они позволяют управлять процессом сериализации: скрывать лишнее, менять имена, навешивать порядок, игнорировать отдельные части объекта или говорить сериализатору, что у вас есть особые правила. Это как стикеры на вещах: «не трогать», «особо важно», «переименовать в JSON».

Типовые задачи контроля сериализации

  • Изменить имя свойства или поля в выходном документе (например, в JSON вместо FirstName сделать "first_name")
  • Скрыть некоторые поля/свойства из сериализации или десериализации (например, пароли, внутренние счетчики)
  • Управлять обработкой значений по умолчанию или null-значений
  • Задать порядок элементов (актуально для XML)
  • Описать дополнительные параметры (атрибуты) для XML-элементов

Каждая платформа сериализации — будь то System.Text.Json, Newtonsoft.Json или XmlSerializer — использует свои атрибуты для этих целей.

2. Атрибуты для System.Text.Json: современный и модный

Классический сериализатор JSON из системы .NET поддерживает неплохой список атрибутов, которые живут в пространстве имён System.Text.Json.Serialization.

Самые полезные атрибуты:

Атрибут Для чего нужен Пример использования
JsonPropertyName("...")
Преобразует имя свойства в JSON
[JsonPropertyName("id")]
JsonIgnore
Полностью исключает свойство из сериализации
[JsonIgnore]
JsonInclude
Сериализует публичное поле (а не только свойство)
[JsonInclude]
JsonIgnore(Condition = ...)
Игнорировать свойство при определённых условиях
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

Примеры использования:

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } // Имя в JSON будет 'first_name'

    [JsonIgnore]
    public string Password { get; set; } // Не попадет в JSON

    [JsonPropertyName("born_year")]
    public int? YearOfBirth { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Nickname { get; set; } // Если null — не попадет в JSON
}

Давайте сериализуем такого человека:

var person = new Person
{
    FirstName = "Ivan",
    Password = "123456",
    YearOfBirth = 2000,
    Nickname = null
};

string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}

Заметьте: и пароль, и прозвище пропали из JSON-базы! (Пароль — потому что игнорируется всегда, а прозвище — только если оно null).

Почему это важно? На практике вы часто не хотите, чтобы клиентская сторона (или злоумышленник!) увидел критичные данные вроде паролей, токенов, счётчиков, внутренних временных меток. С помощью [JsonIgnore] и подобных атрибутов это можно сделать в одну строку.

3. Атрибуты для Newtonsoft.Json: флагман гибкости

Если вы работаете с Newtonsoft.Json (документация здесь), то возможностей у вас станет ещё больше (и немного проще, если вы привыкли к старым проектам).

Основные атрибуты:

Атрибут Назначение
JsonProperty("...")
Задает имя свойства в JSON, а также порядок, обязательность и др.
JsonIgnore
Исключает поле/свойство из сериализации
JsonRequired
Требует обязательного наличия при десериализации
JsonConverter
Позволяет указать собственный конвертер для сложных случаев
DefaultValueHandling
Управляет сериализацией значений по умолчанию

Пример на практике:

using Newtonsoft.Json;

public class UserProfile
{
    [JsonProperty("login")]
    public string Username { get; set; }

    [JsonIgnore]
    public string InternalNotes { get; set; }

    [JsonProperty(Required = Required.Always)]
    public string Email { get; set; }
}

Дополнительные возможности JsonProperty — обязательность (Required), порядок (Order) и т.д. Например:

[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }

4. Атрибуты для XML: стиль "classic"

XmlSerializer использует целый арсенал собственных атрибутов из пространства имён System.Xml.Serialization.

Чаще всего встречающиеся:

Атрибут Для чего нужен
[XmlElement("...")]
Элемент в XML, с другим именем
[XmlAttribute("...")]
Преобразует свойство в атрибут XML-элемента
[XmlIgnore]
Исключает свойство или поле из XML-сериализации
[XmlArray("...")]
Для коллекций — задаёт имя XML-массива
[XmlArrayItem("...")]
Для коллекций — задаёт имя элемента массива
[XmlRoot("...")]
Изменяет имя корневого XML-тега

Пример на практике:

using System.Xml.Serialization;

[XmlRoot("human")]
public class Person
{
    [XmlElement("firstname")]
    public string Name { get; set; }

    [XmlAttribute("years")]
    public int Age { get; set; }

    [XmlIgnore]
    public string Secret { get; set; }
}

var person = new Person { Name = "Anna", Age = 32, Secret = "42" };

После сериализации XML будет примерно таким:

<human years="32"><firstname>Anna</firstname></human>

Обратите внимание, что поле Secret не попало в XML, а Age сериализовано как атрибут, а не как вложенный тег.

5. Полезные нюансы

Под капотом: как атрибуты работают

Когда сериализатор встречает ваш класс, он буквально «читается» рефлексией (магия .NET, подробнее см. System.Reflection). Сериализатор вчитывает метаданные: например, есть ли у свойства атрибут JsonIgnore или XmlElement. В зависимости от этого он добавляет данные в итоговый документ (или наоборот, пропускает).

Это удобный способ отделить «схему данных» от бизнес-логики. Ваш класс — это ваша бизнес-логика, а атрибуты — это, по сути, паспорт для сериализации.

Таблица соответствий ключевых атрибутов

Функция System.Text.Json Newtonsoft.Json XmlSerializer
Изменить имя
[JsonPropertyName]
[JsonProperty]
[XmlElement]
[XmlAttribute]
Игнорировать
[JsonIgnore]
[JsonIgnore]
[XmlIgnore]
Кастомный формат
[JsonConverter]
[JsonConverter]
— (через IXmlSerializable, больно)
Корень объекта
[XmlRoot]
Коллекция
[XmlArray]
[XmlArrayItem]

6. Продвинутые сценарии

Иногда вам нужно сериализовать объект не так, как умеет стандартный сериализатор. Например, вы хотите хранить дату в формате UNIX timestamp, а не в стандартном ISO формате. Для такого случая есть атрибуты, которые позволяют подключить собственные конвертеры.

В System.Text.Json:

public class UnixDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        => writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}

public class LogEntry
{
    [JsonConverter(typeof(UnixDateTimeConverter))]
    public DateTime EventTime { get; set; }
}

Теперь при сериализации поле EventTime превратится в число, а не строку с датой.

В Newtonsoft.Json:

public class BoolToYesNoConverter : JsonConverter<bool>
{
    public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
        => writer.WriteValue(value ? "yes" : "no");

    public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
        => (string)reader.Value == "yes";
}

public class Answer
{
    [JsonConverter(typeof(BoolToYesNoConverter))]
    public bool IsCorrect { get; set; }
}

Теперь булево значение сериализуется в "yes" или "no", а обратно превращается в true или false.

7. Особенности и подводные камни

Когда вы добавляете атрибуты, помните: разные сериализаторы используют разные атрибуты. Если вы работаете и с JSON, и с XML (или, в крайнем случае, одновременно с Newtonsoft.Json и System.Text.Json), не забывайте ставить оба нужных атрибута, иначе будет сюрприз.

Стандартное имя свойства будет взято сериализатором, если только вы не переопределите его через атрибут.

Будьте внимательны при наследовании: дочерние классы наследуют публичные поля/свойства и, если на родителе был атрибут, он будет применяться и в дочернем классе. Это часто вызывает удивления, но так задумано.

Типичная ошибка: Очень часто встречается ситуация, когда разработчики случайно помечают поле, которое обязательно должно попасть в сериализацию, атрибутом JsonIgnore (или наоборот — забывают игнорировать чувствительные данные). Или, например, ставят XmlElement к приватному полю — и удивляются, почему сериализатор его не видит (XmlSerializer обрабатывает только public члены!).

2
Задача
C# SELF, 45 уровень, 2 лекция
Недоступна
Игнорирование null-значений
Игнорирование null-значений
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ