Сериализация и десериализация объектов
Сериализация и десериализация объектов
Сериализация и парсинг
| Формат | Библиотеки в .NET |
|---|---|
| JSON | System.Text.Json (встроена), Newtonsoft.Json (NuGet) |
| XML | XmlSerializer, XDocument, LINQ to XML |
| Бинарный / legacy | BinaryFormatter (устарел, не использовать в новом коде), 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, поэтому существует два подхода:
-
отдельный запрос на загрузку файла - сначала передаётся метаинформация (как раз JSON), а затем файл отправляется отдельно (например, через
multipart/form-data). -
кодирование в строку (Base64), это конечно неэффективно по объёму, но удобно для встраивания, будет выглядеть так:
{
"filename": "report.pdf",
"data": "JVBERi0xLjQKJeLjz9MKMyAwIG9iago8PC9MZW5ndGggND..."
}
Разбор:
filenameхранит метаданные о файле, чтобы получатель знал имя/тип и мог корректно сохранить или показать объект.dataсодержит Base64-строку — текстовое представление бинарных байтов файла внутри JSON.- Такой формат удобен для единичного payload, где метаданные и содержимое нужно передать одним сообщением.
- Цена удобства — рост объёма данных и дополнительная нагрузка на CPU при кодировании/декодировании.
- Для больших файлов практичнее
multipart/form-dataили отдельный канал загрузки без Base64.
И всё это в итоге является интеграционным потоком - конвейером, где происходит работа по цепочке:
[Объект в памяти] - [Сериализация] - [Передача] - [Десериализация] - [Обработка]
Разбор:
- Схема фиксирует жизненный цикл данных в интеграции между системами.
- На шаге "Сериализация" объект приводится к формату, который понимает сеть и внешние сервисы.
- На шаге "Передача" данные движутся по HTTP, очереди сообщений или файлам.
- На шаге "Десериализация" принимающая сторона восстанавливает структуру в объектную модель.
- Финальный этап "Обработка" применяет бизнес-логику уже к валидированным и распакованным данным.
- Такой конвейер удобно использовать как чеклист для диагностики интеграционных ошибок.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
Что важно проверить перед интеграцией
Мини-чеклист для API/очередей:
- имена полей и регистр (
camelCasevsPascalCase); - nullable/обязательные поля;
- формат даты и времени (желательно ISO 8601);
- версия контракта и стратегия совместимости;
- обработка неизвестных полей и значений enum.
Типичные ошибки в продакшене
- Жёсткая привязка к внутренней доменной модели вместо отдельного DTO-контракта.
- Тихое игнорирование ошибок десериализации без логирования причины.
- Смешивание транспортного формата и бизнес-валидации в одном слое.
- Передача больших бинарных данных в JSON без необходимости (Base64 раздувает размер).
Практическая рекомендация по структуре кода
Разделяйте ответственность по слоям:
- transport DTO (контракт);
mapper DTO <-> domain;- валидация входа;
- сериализация/десериализация как инфраструктурная задача.
Так проще развивать API, добавлять версии и не ломать текущих клиентов.
См. также: LINQ, Служебные классы и утилиты .NET, Операционная система.