Перейти к основному содержимому

Сериализация и десериализация объектов

Разработчику Архитектору

Сериализация и десериализация объектов

Сериализация и парсинг

ФорматБиблиотеки в .NET
JSONSystem.Text.Json (встроена), Newtonsoft.Json (NuGet)
XMLXmlSerializer, XDocument, LINQ to XML
Бинарный / legacyBinaryFormatter (устарел, не использовать в новом коде), protobuf и др. через NuGet

В HTTP-запросах к API, сохранении настроек, обмене данными между микросервисами, кэшировании (например, в Redis) используется сериализация и десериализация.

Сериализация - это преобразование объекта C# в формат, пригодный для хранения или передачи (например, в строку JSON), а десериализация - восстановление объекта из такого формата.

public record UserDto(string Name, int Age);

var user = new UserDto("Timur", 30);

// Сериализация: объект → JSON
string json = JsonSerializer.Serialize(user);
// Результат: {"Name":"Timur","Age":30}

// Десериализация: JSON → объект
var restored = JsonSerializer.Deserialize<UserDto>(json);

Разбор:

  • record UserDto(string Name, int Age) объявляет неизменяемый DTO-тип с автоматически созданными свойствами, конструктором и сравнением по значениям.
  • new UserDto("Timur", 30) создаёт объект в памяти, который дальше становится источником данных для сериализации.
  • JsonSerializer.Serialize(user) преобразует объект в JSON-строку; имена полей берутся из свойств (Name, Age), значения — из текущего состояния объекта.
  • JsonSerializer.Deserialize<UserDto>(json) читает JSON и пытается восстановить UserDto; generic-параметр <UserDto> явно задаёт целевой тип.
  • В этом фрагменте показан полный цикл обмена данными: объект приложения -> транспортный формат -> объект приложения после получения.
  • Важно контролировать структуру JSON-контракта: несовпадение имён/типов полей между отправителем и получателем приводит к частично заполненному объекту или ошибке десериализации.

Для работы с JSON испольуется Newtonsoft.Json или System.Text.Json.

Начиная с .NET Core 3.0, Microsoft добавила в ядро мощную библиотеку — System.Text.Json, поэтому "в коробке" есть средство для работы, встроенное в .NET, с высокой производительностью. Но для сложных сценариев лучше использовать Newtonsoft.Json.

Большинство проектов до сих пор используют Newtonsoft.Json (также известный как Json.NET) — самую популярную стороннюю библиотеку для работы с JSON.

using Newtonsoft.Json;

string json = JsonConvert.SerializeObject(user);
User restored = JsonConvert.DeserializeObject<User>(json);

Разбор:

  • using Newtonsoft.Json; подключает пространство имён библиотеки Json.NET, где находятся JsonConvert и вспомогательные атрибуты сериализации.
  • JsonConvert.SerializeObject(user) сериализует объект в строку JSON с правилами Newtonsoft (они отличаются от System.Text.Json по умолчанию в ряде сценариев).
  • JsonConvert.DeserializeObject<User>(json) создаёт экземпляр User из JSON; тип задаётся через generic-аргумент.
  • Этот стиль удобен, когда проект использует экосистему Json.NET: кастомные конвертеры, гибкие настройки и совместимость со старым кодом.
  • Если JSON не соответствует модели User, результатом может быть null, частично заполненный объект или исключение в зависимости от конфигурации.

Парсинг JSON - важный навык. Можно записывать какие-то данные в этот формат обмена и передавать, затем читать на сервере, разбивая на сопутствующие элементы и обрабатывая результаты.

Иногда структура JSON неизвестна заранее — например, при интеграции с внешним API. Тогда помогает LINQ-to-JSON. Это динамический парсинг, использующий JObject и JArray (объект и массив соответственно).

string json = """
{
"name": "Timur",
"hobbies": ["reading", "coding"],
"address": { "city": "Moscow" }
}
""";

JObject obj = JObject.Parse(json);

string name = (string)obj["name"];
string city = (string)obj["address"]["city"];
JArray hobbies = (JArray)obj["hobbies"];

Разбор:

  • Многострочная строка """ ... """ хранит JSON-образец в исходнике без ручного экранирования кавычек.
  • JObject.Parse(json) строит динамическое дерево JSON-узлов, где к полям можно обращаться по ключам.
  • obj["name"] и obj["address"]["city"] показывают адресацию простого поля и вложенного объекта.
  • Явные приведения (string) и (JArray) превращают универсальные JSON-токены в ожидаемые .NET-типы для дальнейшей работы.
  • Такой подход полезен, когда контракт нестабилен и жёсткую модель DTO писать преждевременно.
  • Нужно проверять наличие узлов и null, иначе при отсутствии поля в ответе внешнего API возможны ошибки во время выполнения.

Это полезно, когда API возвращает разные поля в зависимости от контекста, или нужно извлечь только часть данных. Также можно писать таким образом универсальный клиент.

Хотя JSON стал стандартом, XML всё ещё используется — в SOAP, конфигурациях, старых API. Хотя частенько можно увидеть комбинацию XML+Java, но всё же и в C# используется из-за распространённости форматов. Для работы с ним используется сериализация XML:

using System.Xml.Serialization;
using System.IO;

var user = new User { Name = "Timur", Age = 25 };
var serializer = new XmlSerializer(typeof(User));

using var writer = new StringWriter();
serializer.Serialize(writer, user);
string xml = writer.ToString();

Разбор:

  • XmlSerializer создаётся для конкретного типа (typeof(User)), поэтому знает, как преобразовать его свойства в XML-элементы.
  • StringWriter выступает буфером вывода: сериализатор пишет XML в поток, затем строка читается через ToString().
  • serializer.Serialize(writer, user) выполняет саму сериализацию и формирует XML-документ с корневым элементом типа.
  • Этот механизм часто применяют в legacy-интеграциях, где протокол или контракт требуют именно XML.
  • В реальных системах важно согласовывать имена элементов, namespace и схему XSD, чтобы принимающая сторона корректно распознала документ.

Как результат:

<User>
<Name>Timur</Name>
<Age>25</Age>
</User>

Разбор:

  • Корневой тег <User> соответствует типу объекта, который сериализовали.
  • Дочерние теги <Name> и <Age> отображают значения его публичных свойств.
  • XML сохраняет структуру иерархически, поэтому его удобно валидировать по схеме и обрабатывать системами, ориентированными на SOAP/enterprise-интеграции.
  • Такой результат — транспортное представление: на принимающей стороне он обычно десериализуется обратно в объект.

Иногда нужно получить байтовое представление данных — например, для отправки в HTTP-запросе или сохранения в файл.

string json = JsonSerializer.Serialize(user);
byte[] bytes = Encoding.UTF8.GetBytes(json);

// Отправка в теле запроса
var content = new ByteArrayContent(bytes);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");

Разбор:

  • JsonSerializer.Serialize(user) сначала превращает объект в текстовый JSON, чтобы получить стандартизованный формат обмена.
  • Encoding.UTF8.GetBytes(json) кодирует строку в байтовый массив, пригодный для сетевой передачи и записи в поток.
  • ByteArrayContent(bytes) оборачивает байты в HTTP-контент для клиента (HttpClient) или другого транспорта.
  • ContentType = "application/json" явно сообщает получателю, как интерпретировать тело запроса.
  • Без корректной кодировки и MIME-типа API может принять запрос как неизвестный формат или неверно прочитать символы.

Приложения редко хранят данные только в памяти. Они обычно используют базы данных для долговременного хранения структурированных данных, файлы для конфигураций, логов, медиа, документов и прочего.

Чтобы работать с этими данными, приложения используют:

  • ORM для удобной работы с БД через объекты в коде;
  • Файловые операции - чтение/запись файлов напрямую.

Обмен данными - это интеграция, когда одно приложение (или сервис) хочет поделиться данными с другим. Она почти всегда происходит через HTTP-запросы, сообщения (в очередях) или файловые обмены. К примеру, веб-сервис на C# отправляет данные о заказе в систему учёта на Python.

Данные нельзя передать "как есть" - их нужно упаковать в понятный формат:

  • Простые объекты (пользователь, заказ) передается в JSON или XML;
  • Текст - как строка (plain text);
  • Файлы (PDF, изображения) - как поток байтов (бинарные данные).

Это и есть сериализация - преобразование объекта в формат, пригодный для передачи.

Отправка происходит так - объект сериализуется в JSON/байты, затем передаётся по сети.

Получение - данные десериализуются и становятся объектом в принимающем приложении.

Поэтому нужно, чтобы отправитель и получатель были готовы к такой интеграции, подготовив соответствующие системы хранения и обработки, и конечно продумав и реализовав модели, и вся структура должна быть согласована.

Это работает по-разному, к примеру, может быть некий сервис, который просто реализует возможность предоставлять данные из своих баз. Для этого он просто реализует методы со своей внутренней логикой, и публикуется по адресу - эндпоинту. Затем формируется документация, где описывается, какие методы можно вызывать, как обращаться, как должны выглядеть запросы, и как будут выглядеть ответы. В такой ситуации, запрашивающим системам (которые будут интегрироваться) нужно будет просто изучить документацию и обеспечить, чтобы запросы были такими же, как ожидается, а ответы могли обрабатываться и использоваться. И зачастую неважно, как системы это делают внутри - главное чтобы исходящие/входящие данные были корректными.

Бинарные данные - особый случай. PDF, фото, видео - это байты. Их нельзя просто впихнуть в JSON, поэтому существует два подхода:

  1. отдельный запрос на загрузку файла - сначала передаётся метаинформация (как раз JSON), а затем файл отправляется отдельно (например, через multipart/form-data).

  2. кодирование в строку (Base64), это конечно неэффективно по объёму, но удобно для встраивания, будет выглядеть так:

{
"filename": "report.pdf",
"data": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9MZW5ndGggND..."
}

Разбор:

  • filename хранит метаданные о файле, чтобы получатель знал имя/тип и мог корректно сохранить или показать объект.
  • data содержит Base64-строку — текстовое представление бинарных байтов файла внутри JSON.
  • Такой формат удобен для единичного payload, где метаданные и содержимое нужно передать одним сообщением.
  • Цена удобства — рост объёма данных и дополнительная нагрузка на CPU при кодировании/декодировании.
  • Для больших файлов практичнее multipart/form-data или отдельный канал загрузки без Base64.

И всё это в итоге является интеграционным потоком - конвейером, где происходит работа по цепочке:

[Объект в памяти] - [Сериализация] - [Передача] - [Десериализация] - [Обработка]

Разбор:

  • Схема фиксирует жизненный цикл данных в интеграции между системами.
  • На шаге "Сериализация" объект приводится к формату, который понимает сеть и внешние сервисы.
  • На шаге "Передача" данные движутся по HTTP, очереди сообщений или файлам.
  • На шаге "Десериализация" принимающая сторона восстанавливает структуру в объектную модель.
  • Финальный этап "Обработка" применяет бизнес-логику уже к валидированным и распакованным данным.
  • Такой конвейер удобно использовать как чеклист для диагностики интеграционных ошибок.

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.


Что важно проверить перед интеграцией

Мини-чеклист для API/очередей:

  • имена полей и регистр (camelCase vs PascalCase);
  • nullable/обязательные поля;
  • формат даты и времени (желательно ISO 8601);
  • версия контракта и стратегия совместимости;
  • обработка неизвестных полей и значений enum.

Типичные ошибки в продакшене

  • Жёсткая привязка к внутренней доменной модели вместо отдельного DTO-контракта.
  • Тихое игнорирование ошибок десериализации без логирования причины.
  • Смешивание транспортного формата и бизнес-валидации в одном слое.
  • Передача больших бинарных данных в JSON без необходимости (Base64 раздувает размер).

Практическая рекомендация по структуре кода

Разделяйте ответственность по слоям:

  • transport DTO (контракт);
  • mapper DTO <-> domain;
  • валидация входа;
  • сериализация/десериализация как инфраструктурная задача.

Так проще развивать API, добавлять версии и не ломать текущих клиентов.

См. также: LINQ, Служебные классы и утилиты .NET, Операционная система.