В своем подходе к обучению я применяю довольно простую схему — у меня есть репозиторий, и каждый раз, когда я сталкиваюсь с новой для себя технологией — я иду туда, создаю папку-подпапку, в ней .cs файл и решаю какую-то проблему, применяя изученное. Если теоретическая составляющая темы слишком обширна, чтобы запихнуть ее в голову и комментарии к коду, я пишу конспекты в .md формат.
Недавно я актуализировал конспект по сериализации, и мне пришла идея — почему бы не вытесать из этого статью.
В этой статье мы пройдемся по тому как сериализация работает, основным форматам и языкам разметки, а также объединим знания об использовании .NET System.Text.Json в формат напоминалки. Сразу же отмечу два дисклеймера:
-
Это личная база знаний, над которой была проведена большая редакторская работа, я старался оформить данные в формат напоминалки, к которой можно вернуться в любое время, поэтому местами могут встретиться грубые упрощения и не-академическая терминология.
-
Мы довольно подробно поговорим об XML и SOAP, однако работы с классом
XmlSerializerвы здесь не увидите. Причины можно найти в данном видеофрагменте. А если серьезно — мне просто не довелось применить данный вид сериализации на практике, но знание основ XML не навредит. ОBinaryFormatterиISerializableречи идти вовсе не будет, так как грядущий .NET 9 захлопнет крышку этого гроба окончательно.
Примечание: Хабр не очень дружит с переносом строки внутри блоков кода, для удобства чтения раздутых XML и комментариев — используйте Shift+Колесико мыши. Возможно, отоспавшись, я оформлю переносы вручную.
Оглавление:
Сериализация
Сериализация — процесс преобразования объекта среды выполнения в форму, пригодную для дальнейшей транспортировки. Выражаясь простым языком, это процесс записи данных объекта в памяти, то есть класса или структуры, в такую форму, которую можно передать по сети, сохранить на диске, использовать между процессами и так далее. Десериализация, соотвественно, обратный процесс восстановления состояния объекта из формата транспортировки.
Обычно это текстовые форматы вроде JSON или языки разметки YAML, XML, SOAP и прочие. Например, бинарные сериализаторы пишут непосредственно байты в стрим назначения, хотя, если углубиться в тему — всё перечисленное также проходит процесс кодировки (обычно используя UTF-8), и уже парсеры, разрабатываемые авторами форматов, восстанавливают состояние благодаря строгим протоколам оформления данных.
К слову, язык разметки отличается от формата текста
тем, что язык разметки это более комплексный свод правил для хранения и представления данных, использующий теги, синтаксические правила, определения новых сущностей и т.д.. Текстовые форматы же более легковесны, просты в написании/чтении и ограничиваются малым количеством контрольных сущностей.
Форматы и языки разметки
JSON
JSON (JavaScript Object Notation) — это легковесный текстовый формат для представления структурированных данных в виде пар ключ-значение, композитных объектов, состоящих из этих пар, а также JSON-массивов. JSON изначально был основан на синтаксисе JavaScript, но является языконезависимым и поддерживается большинством современных языков программирования.
Представление данных в формате JSON:
-
Простые классы и структуры состоят из JSON-свойств (или JSON Value). Свойство выглядит как пара
"name":value, или, если вам так привычнее,"key":value.public class SomeData { public float pi { get; set; } = 3.14f; public bool b { get; set; } = false; public char c { get; set; } = 'c'; public object? o { get; set; } = null; public string s { get; set; } = "Some Text"; }{ "pi": 3.14, "b": false, "c": "c", "o": null, "s": "Some Text" } -
Композитные объекты (JSON Object) заключаются в фигурные скобки {}, внутри которых содержатся свойства, которые также могут представлять собой объекты. Размер такого дерева вложенных объектов называется глубиной сериализации.
К слову, когда мы сериализуем сам корневой объект, он также начинается с фигурных скобок:public class SomeData { public float pi { get; set; } = 3.14f; public bool b { get; set; } = false; public char c { get; set; } = 'c'; public Vector2 ComplexStruct { get; set; } = new(1.5f, 2.7f); }{ // Начало SomeData "pi": 3.14, "b": false, "c": "c", "ComplexStruct": { // Начало Vector2 "X": 1.5, "Y": 2.7 } // Конец Vector2 } // Конец SomeData -
Коллекции. Стоит отметить, что разные реализации фреймворков сериализуют коллекции, соответственно, по разному. Массивы, листы, очереди и прочие сериализуются в виде обычного JsonArray.
А вот сериализация, например, словаря, будет выглядеть как объект, свойства которого являются парами"key":value.
Синтаксис Json массивов, в данном случае сериализуется массив строк:"someValues": [ "first", "second", "third" ] -
Логические единицы, то есть простые свойства, объекты, массивы и элементы внутри них разделяются запятыми. Когда мы видим упоминание «trailing commas» где-либо, это означает обработку хвостовых запятых, за которыми не следует еще один элемент, иногда это может вызывать ошибки.
-
Для сериализации специфических типов, например, словарей, ключом которых выступает композитный объект, может потребоваться предоставить свою реализацию алгоритмов конвертации и разрешения типов.
XML
XML (расширяемый язык разметки) — это язык разметки, предназначенный для хранения и передачи данных в формате, удобном для обработки компьютером. Синтаксис XML основывается на тегах, определяемых пользователем. XML также допускает атрибуты, экранирование (escape characters), комментарии, пространства имен и валидацию при помощи схем. Правильно составленный XML документ называют well-formed.
Общий синтаксис XML для C# объекта будет выглядеть так:
-
Простые свойства(элементы), имя указывается в открывающемся и закрывающемся теге, значение указывается между ними. Также,
тег может быть самозакрывающимся, используя синтаксис<tag/>.public class SomeData { public float pi { get; set; } = 3.14f; public bool b { get; set; } = false; public char c { get; set; } = 'c'; public string s { get; set; } = "Some Text"; }<?xml version="1.0" encoding="UTF-8"?> <!--Этого мы коснемся позже--> <SomeData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <pi>3.14</pi> <b>false</b> <c>c</c> <s>Some Text</s> </SomeData> -
Вы могли обратить внимание на атрибуты вроде
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance". Значение некоторых мы рассмотрим позже. Говоря о самих атрибутах — они являются частью XML тегов, синтаксически выглядят какattribute="value", не разделяются запятыми, если атрибутов несколько. Атрибуты это такие же данные, записываемые пользователем или программой, общепринятой практикой считается размещение в них метаданных, то есть «информации об информации», вторая «информация» в данном случае это значение XML-элемента, например:<Order time="8/18/2010 4:32:00"> <!--Метаданные о времени заказа--> <ID>114</ID> </Order> -
Композитные объекты работают по тем же правилам.
<?xml version="1.0" encoding="UTF-8"?> <SomeData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <pi>3.14</pi> <b>false</b> <c>с</c> <ComplexStruct> <!--Композитный объект. Кстати, это комментарий. --> <X>1.5</X> <Y>2.7</Y> </ComplexStruct> </SomeData> -
Коллекции не имеют какого-либо специфического синтаксиса. Однако, можем заметить, что теги элементов внутри массивов используют тип элемента в качестве имени, хотя это поведение можно переопределить разными конфигурациями.
<?xml version="1.0" encoding="UTF-8"?> <SomeData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <values> <string>first</string> <string>second</string> <string>third</string> </values> </SomeData> -
XML поддерживает escape sequences (экранирование) для зарезервированных символов
< > & ' ". К слову, единственные места, где можно использовать эти символы напрямую это комментарии и блоки CDATA.Экранируемые символы
< — представляет символ < (открывающая угловая скобка). > — представляет символ > (закрывающая угловая скобка). & — представляет символ & (амперсанд). " — представляет символ " (двойная кавычка). ' — представляет символ ' (одинарная кавычка).
-
Отличие CDATA от комментария заключается в том, что комментарий не является частью XML документа и игнорируется парсерами, тогда как CDATA (character data) является частью данных XML, внутри которой текст располагается «сырым» (это можно сравнить с raw string в C#).
<?xml version="1.0" encoding="UTF-8"?> <SomeData> <SomeElement> <!--Внутри этого блока я могу использовать <>&"", но комментарии не являются "полезной" частью XML документа.--> <![CDATA[ CDATA - character data, т.е. данные, которы не требуют экранирования. Я могу использовать <>&", а также это часть XML документа.]]> </SomeElement> </SomeData>
Мы уже могли обратить внимание, что в начале XML документа содержится пролог <?xml version="1.0" encoding="UTF-8"?>, описывающий версию xml и кодировку. Когда вы видите тег со знаками ?, это означает, что данный тег является не представлением данных, а инструкцией обработки (Processing Instruction - PI), подобные инструкции зачастую требуются принимающей XML документ стороне, например веб серверу.
Далее идет открывающий тег объекта, в котором указаны пространства имен через атрибут xmlns:namespace_name. Пространства имен нужны чтобы не допустить конфликта имён, а также использовать предопределенные элементы и атрибуты. Сами по себе пространства имен обычно являются URI, хотя могут быть и обычной строкой вроде foo. Большинство xml-парсеров знают о тех определениях, что предоставляет какой либо из стандартных (не-пользовательских) неймспейсов, поэтому никакого процесса запроса к веб серверу не происходит, под стандартным имеется ввиду, например, http://www.w3.org/2001/XMLSchema-instance, содержащий атрибуты вроде nil(для обозначения может ли значение быть нулевым) или type(для ограничения по типу) или schemaLocation для указания пути к XSD для текущего XML документа. При этом пройдя по самому URL мы получим просто информационную страницу с ссылками. И опять же, повторяясь снова, все пространства имен, атрибуты и элементы — всего лишь текстовые данные, с которыми уже оперируют парсеры лексеры и валидаторы, что возможно благодаря детерменированной структуре XML.
XML Схемы
XSD (XML Schema Definition) — это язык для описания структуры, содержимого и семантики XML-документов. XSD определяет правила и ограничения, которым должны соответствовать XML-документы, чтобы считаться допустимыми (валидными) по отношению к данной схеме и использует синтаксис XML. Основной задачей схемы является валидация XML документов. Например схема может передаваться какому-то сервису, который затем по ней будет валидировать приходящие XML, или же при ручном заполнении XML файла при наличии схемы можно избежать ошибок. Обращаю внимание, что ссылка на схему и ее наличие не является обязательной частью XML.
Хочу отметить, что данная тема довольно сложна и на ее полный разбор потребовалась бы отдельная статья. Пройдемся по
Основам XSD:
-
XSD начинается с импорта xsd неймспейса(
xmlns:xsв примере ниже), указания корневого тегаschemaиз этого неймспейса, а также необходимых пользователю атрибутов<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">Сделаем посложнее
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.alexfitbie.com" xmlns:ftb="http://www.alexfitbie.com" elementFormDefault="unqualified">здесь, например, мы указали, что создаем свой неймспейс, который будет использован XML документом позже, затем подключили этот неймспейс в схему(
xmlns:ftb), а атрибутelementFormDefaultуказывает на то, что целевой XML не обязан указывать неймспейс для объявления атрибутов и элементов из схемы (приqualifiedв XML мы должны были бы указывать наш неймспейс перед всеми атрибутами и элементами). -
Далее мы формируем структуру будущего XML используя элементы и атрибуты из неймспейса
xs. Мы можем указать требуемый элемент, его имя, типы которые в него можно поместить, комплексный это объект или простой и так далее.<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Person"> <!--Корневой элемент--> <xs:complexType> <!--Комплексный тип, может содержать другие элементы, атрибуты--> <xs:sequence> <!--Элементы в XML будут идти в указанном порядке--> <xs:element name="Name" type="xs:string"></xs:element> <!--Имя xml элемента, тип значения--> <xs:element name="ID" type="xs:int"></xs:element> <xs:element name = "BirthDate" type="xs:date"></xs:element> </xs:sequence> <xs:attribute name="ID" type="xs:int" use="required"/> <!--атрибуты, которые должны(здесь required) быть у элемента размещаются после sequence--> </xs:complexType> </xs:element> </xs:schema> -
Мы можем предопределить типы для повторного использования внутри нашей схемы, объявив
complexTypeна уровне схемы (то есть под корневым<xs:schema>). Здесь мы определяемStudent, который затем будет использован в массивеStudents. К слову, для создания массива элементов свободного размера мы используем атрибутыminOccurs="0" maxOccurs="unbounded".
А внутри студента мы также используем коллекцию из пользовательских типовGrade
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="School"> <!--Корневой элемент будущего XML--> <xs:complexType> <xs:all> <!--All - элементы в любом порядке(в противовес sequence). Но в данном случае у нас только массив студентов--> <xs:element name="Students"> <!--Массив--> <xs:complexType> <xs:sequence> <xs:element name="Student" type="Student" minOccurs="0" maxOccurs="unbounded"/> <!--Здесь мы ограничиваем элементы массива по типу Student(он ниже), а также указываем что этот элемент может встречаться от 0 до неограниченного кол-ва раз, тем самым создавая коллекцию--> </xs:sequence> </xs:complexType> </xs:element> </xs:all> </xs:complexType> </xs:element> <xs:complexType name="Student"> <!--Объявление типа Student, complexType под корнем схемы--> <xs:sequence> <xs:element name="Name" type="xs:string"/> <xs:element name="Grades"> <xs:complexType> <xs:sequence> <xs:element name="Grade" type="Grade" minOccurs="1" maxOccurs="30"/> <!--Массив элементов типа Grade(он ниже), с размером от 1 до 30 элементов--> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> <xs:complexType name="Grade"> <!--Также объявление типа Grade--> <xs:sequence> <xs:element name="Subject" type="xs:string"/> <xs:element name="Value" type="xs:float"/> </xs:sequence> </xs:complexType> </xs:schema>
Итоговый XML, составленный по нашей схеме выглядел бы так:
<School xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="School.xsd"> <Students> <Student ID="11"> <Name>Jimmy McGill</Name> <Grades> <Grade> <Subject>Math</Subject> <Value>5.6</Value> </Grade> </Grades> </Student> <Student ID="01"> <Name>Pit Ritt</Name> <Grades> <Grade> <Subject>History</Subject> <Value>8.7</Value> </Grade> <Grade> <Subject>Math</Subject> <Value>7.8</Value> </Grade> </Grades> </Student> </Students> </School>
Помимо XSD существует DTD (Document Type Definition), служащий также для определения структуры XML. Но он считается устаревшим, не использует синтаксис XML и обладает куда меньшими возможностями. Пример синтаксиса:
<!DOCTYPE note [ <!ELEMENT note (to,from,heading,body)> <!ELEMENT to (#PCDATA)> <!ELEMENT from (#PCDATA)> <!ELEMENT heading (#PCDATA)> <!ELEMENT body (#PCDATA)> ]>
SOAP
SOAP (Simple Object Access Protocol) — это протокол обмена сообщениями, используемый для передачи данных между компьютерами. SOAP основывается на XML и определяет строгие правила обмена данными между веб-сервисами и клиентами. Протокол включает описание сообщений, как и каким образом они должны быть переданы, а также стандартные вызовы и ответы.
То есть, SOAP сообщения представляют данные, передаваемые, например, в теле HTTP запроса/ответа. По правде говоря, с SOAP я не работал, протокол можно отнести к категории устаревших и используемых в основном в гигантских легаси проектах, вроде банковских систем и прочих Госуслуг, современные сервисы чаще полагаются на обмен данными через форматы вроде JSON или YAML.
Пример SOAP-сообщения выглядит следующим образом:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ex="http://example.com/"> <soapenv:Header/> <soapenv:Body> <ex:GetStudentDetailsRequest> <ex:StudentID>12345</ex:StudentID> </ex:GetStudentDetailsRequest> </soapenv:Body> </soapenv:Envelope>
Объяснение элементов:
-
<soapenv:Envelope>— это корневой элемент SOAP-сообщения. Он определяет начало и конец сообщения.xmlns:soapenvзадает пространство имен, связанное с протоколом SOAP. -
<soapenv:Header>— необязательный элемент, используемый для передачи метаинформации, например, аутентификации. В данном примере он пуст. -
<soapenv:Body>— обязательный элемент, содержащий основное содержимое сообщения. Внутри Body находятся данные, которые клиент передает на сервер или получает от него. -
<ex:GetStudentDetailsRequest>— это пользовательский элемент, который является частью основного содержимого сообщения. Он включает в себя информацию, которая нужна для выполнения конкретного запроса (в данном случае, запрос деталей студента по его ID). -
<ex:StudentID>— это элемент, содержащий конкретные данные запроса (в данном случае ID студента).
YAML
YAML (Yet Another Markup Language YAML Ain’t Markup Language) — это текстовый формат(«бывший» язык разметки) для сериализации данных, предназначенный для удобочитаемого представления структурированных данных. YAML используется для хранения конфигурационных файлов, передачи данных между программами и различных других задач. YAML не является форматом «из коробки» для .NET, хотя существует сторонняя библиотека YAML.NET.
Общий синтаксис YAML:
-
Yaml начинается с первой строки
---и заканчивается строкой...(опционально, например несколько yaml в одном файле). -
Простые свойства хранятся в паре
key:value--- pi: 3.14 b: false c: c o: null s: Some Text -
Для образования композитных типов и обозначения вложенности используются пробелы (не табуляция!):
--- pi: 3.14 b: false c: c ComplexStruct: # Вложенные члены X и Y X: 1.5 Y: 2.7 -
Коллекции используют или inline синтаксис
Values: {a, b, c}, или синтаксис новой строки с отступом, пробелом и тире, особенно полезным для обозначения композитных элементов массива.Students: # Array - Name: Pam Beasley # Element 1 Age: 10 AverageGrade: 3.9 - Name: Ryan Howard # Element 2 Age: 11 AverageGrade: 4.2 -
Некоторые структуры данных могут потребовать своих алгоритмов сериализации. Например словари, также как и в JSON(в/из которого yaml легко конвертируется), также могут выглядить как композитный объект
Teachers: MichaelScott: {Pam Beasley, Ryan Howard} # string key, string[] value, "" опционально JanGofrey: {"Alex Swanson"}
-
Кроме этого, присутствует явный синтаксис пары ключ-значение (Complex Mapping Key):
TeachersComplexMappingKey: ? Name: MichaelScott # Явный ключ начинается с ? Age: 41 Subject: Math : # Явное значение ключа начинается с :, в данном случае значение это массив - Name: Pam Beasley Age: 10 AverageGrade: 3.9 - Name: Ryan Howard Age: 11 AverageGrade: 4.4
Технологии сериализации
Мы переходим к практическим заметкам. Отмечу, что практически вся информация была взята и переварена из документации Microsoft, где довольно подробно описан каждый раздел и приведены примеры кода.
System.Text.Json
На сегодняшний день самой удобной библиотекой для сериализации в/из JSON является System.Text.Json. Большая часть работы с ним заключаются в использовании статических методов JsonSerializer, конфигурации при помощи экземпляра JsonSerializerOptions, передаваемого в методы JsonSerializer, использовании атрибутов, реализации кастомных обработчиков и конвертеров объектов.
Основы System.Text.Json
Для сериализации/десериализации используется JsonSerializer.Serialize<T>(), JsonSerializer.Deserialize<T>() и их async собратья. Сериализация возможна как в/из Stream, так и в обычную строку или Utf8Json Reader/Writer. Методы обладают большим количеством перегрузок для разных нужд.
Также важнейшим (но при этом необязательным) этапом сериализации является создание экземпляра JsonSerializerOptions, передаваемого в вышеупомянутые методы для конфигурации всего процесса, поэтому каждый раз, когда вы видите упоминание JsonSerializerOptions в данном материале, я имею ввиду конструкцию вроде:
JsonSerializerOptions options = new() { // В 99% случаев мы конфигурируем опции через синтаксис инициализатора WriteIndented = true, IncludeFields = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // И прочие свойства JsonSerializerOptions, что я указываю после точки }; string serialized = JsonSerializer.Serialize<MyData>(myobj, options);
Кастомизация политики имен и самих имен в выходном JSON происходит при помощи JsonSerializerOptions.PropertyNamingPolicy и атрибута [JsonPropertyName("")] перед свойством/полем.
По умолчанию JSONSerializer сериализует только все публичные свойства (если их тип поддерживает сериализацию). Это поведение можно конфигурировать, игнорируя свойства при помощи [JsonIgnore](с возможностью указания условия игнорирования) или при конфигурировании JsonSerializerOptions.DefaultIgnoreCondition (а также JsonSerializerOptions.IgnoreReadOnlyProperties). Также возможно включение в Json непубличных свойств и полей через атрибут JsonInclude / JsonSerializerOptions.IncludeFields.
Если в JSON присутствуют элементы, которые нельзя смаппить ни с одним свойством в целевом типе, мы можем создать Dictionary<string, JsonElement> и пометить его атрибутом [JsonExtensionData], куда будут писаться «потерявшиеся» JSON-свойства, для прочтения которых мы пользуемся типом из DOM модели JsonElement, его мы коснемся позже.
Поддерживаемые типы коллекций перечислены здесь. Говоря в общих чертах, для сериализации поддерживается все, что реализует IEnumerable (где оно просто перебирается и пишется в JSON). Для десериализации тип должен реализовывать один из перечисленных в статье интерфейсов, это общие интерфейсы коллекций от ICollection<> до IQueue<>, предоставляющие интерфейс для добавления элементов. Нюансы есть у Dictionary<K,V> и Stack<T>, в случае стека из-за его семантики при десериализации значения будут идти задом наперед (то есть сериализованный стек 3 2 1 0 будет десериализован в 0 1 2 3). В случае же словаря, список ключей ограничен простыми сериализуемыми типами (иначе говоря теми, которые в качестве полей/свойств сериализуются как "name":value), поскольку словарь сериализуется как JSON Object, а не Array, т.е. если мы взглянем на представление словаря в JSON:
{ // Dictionary<string, int> выглядит так "First": 1, "Second": 2, "Third": 3 }
то увидим, что string ключ пишется как имя Json свойства, а int значение — соответственно, как значение.
А для сериализации ключей более сложного типа, может потребоваться реализовать свой JsonConverter, на который мы взглянем позже.
Иммутабельные типы и заполнение
С десериализацией в изменяемые типы с публичными get set свойстами все просто, если очень грубо упростить — JSON находит член объекта, имя которого соответствует имени JSON-свойства, и пишет в него данные (на самом деле при первом обращении генерируются метаданные в режиме рефлексии, но это сейчас не важно).
Однако возможна и десериализация в иммутабельные типы, вернее сказать в их get-only/readonly данные, путем использования параметризованного конструктора. Если конструктор не один, или присутствует конструктор без параметров, конструктор для десериализации должен быть помечен [JsonConstructor]. Все имена параметров конструктора должны соответствовать именам сериализуемых полей/свойств, без учета регистра. Например, имя в конструкторе должно быть simpleNumber, а имя поля/свойства в объекте должно быть SimpleNumber/SIMPLENUMBER/simpleNumber, при этом [JsonPropertyName] ни на что в этой реализации не влияет. Также учтите, что тип передаваемых в конструктор аргументов должен совпадать с типом соответствующих свойств объекта.
public class ReadonlyData { [JsonInclude] public readonly int ID; public string Name { get; } public DateOnly BirthDate { get; init; } public IReadOnlyCollection<int> Numbers { get; } // IReadOnlyCollection чтобы одурачить вас. Это не относится к теме. [JsonConstructor] public ReadonlyData(int id, string name, DateOnly birthDate, IReadOnlyCollection<int> numbers) { ID = id; Name = name; BirthDate = birthDate; Numbers = numbers; } }
Помимо десериализации в иммутабельные типы, JsonSerializer также может заполнять инициализированные значения. В обычной ситуации сериализатор для каждого json-свойства создает новый объект, затем присваивая ссылку на него c#-свойству. Однако, если какое либо c#-свойство/поле уже инициализированно, например
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] // На уровне класса class A { [JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] // или тут public List<int> Numbers { get; } = [1, 2, 3]; }
то, для того, чтобы «заполнить» данный лист, а не пересоздавать его, мы можем использовать атрибут JsonObjectCreationHandling(JsonObjectCreationHandling.Populate). Сериализатор через get возьмет ссылку на лист, и заполнит его десериализуемыми значениями через Add(). К слову, данный атрибут помогает при работе с иммутабельными get-only/readonly свойствами ссылочного типа, однако для value-типов(struct) обязательно должен быть указан set (поскольку вместо ссылки на объект в хипе сериализатор получит копию структуры через get, заполнит ее и должен будет записать обратно через set).
Обработка ссылок. $id и $ref
Сериализуемые объекты могут оказаться сложными по своей структуре и указывать друг на друга. Более того, один и тот же объект может встречаться несколько раз в сериализуемых данных. Например, предположим у нас есть класс сотрудника, имеющего свойство, указывающего на его начальника:
public class Employee { public string Name { get; set; } public Employee? Boss { get; set; } }
и предположим у нас есть класс Corporate, содежащий ссылку на директора и полный лист сотрудников (куда директор также входит). При создании компании мы создаем граф сотрудников, где у CEO Boss = null, а у двух менеджеров Boss = CEO и так далее.
public class Corporate { public Employee CEO { get; set; } public List<Employee> Employees { get; set; } // Сюда мы пишем сотрудников, указывающих на свое начальство, и само начальство }
Если у нас будет 1 директор и 2 сотрудника, указывающих на директора, то при обычной десериализации мы получим 4 объекта директора: 1 в свойство Company.CEO, 1 в листе Company.Employees и еще по одному у каждого сотрудника в свойстве Employee.Boss:
{ "CEO": { // CEO 1 "Name": "Jim Root", "Boss": null }, "Employees": [ { "Name": "Jim Root", // CEO 2. Директор также находится в листе сотрудников. "Boss": null }, { "Name": "Jason Tward", "Boss": { "Name": "Jim Root", // CEO 3 "Boss": null } }, { "Name": "Alex Stein", "Boss": { "Name": "Jim Root", // CEO 4. Итого, десериализуя, мы получим 4! одинаковых объекта "Boss": null } } ] }
Однако, мы можем передать в конфигурацию JsonSerializerOptions определенный в .NET обработчик ссылок ReferenceHandler.Preserve:
JsonSerializerOptions options = new() { // Preserve НЕ работает с readonly полями и свойствами и НЕ работает с иммутабельными типами. ReferenceHandler = ReferenceHandler.Preserve, };
После этого каждому объекту в выходном JSON будет добавлено мета-свойство $id, являющееся целым числом. Это будет что-то вроде ключа для каждой записи в Json, в свою очередь объекты (в нашем случае сотрудники), ссылающиеся на директора, вместо полной(повторной) сериализации директора разместят в "Boss" мета-свойство $ref с ключом $id:
{ "CEO": { "$id": "1", // ключ для указания на нашего директора "Name": "Jim Root", "Boss": null }, "Employees": { "$id": "2", // У каждой сериализуемой записи будет $id, в том числе у листа. Но эти ключи нас не интересуют. "$values": [ { "$ref": "1" // Ссылка на CEO, поскольку он также в листе сотрудников }, { "$id": "3", "Name": "Jason Tward", "Boss": { "$ref": "1" // Ссылка на CEO } }, { "$id": "4", "Name": "Alex Stein", "Boss": { "$ref": "1" // Еще одна } } ] } }
Работает это довольно простым образом, внутри JsonReferenceHandler находится ссылка на класс-стратегию JsonReferenceResolver. Внутри же него находится словарь, сериализатор читает JSON директора, находит мета-свойство $id, десериализует объект, помещает в упомянутый словарь директора под ключом, равным его $id, и затем, натыкаясь на $ref в сотрудниках, он уже получает ИЗ словаря объект директора по указанному в $ref ключу и присваивает ссылку на этот объект в десериализуемое поле сотрудника Boss.
К слову, именно из-за порядка чтения, крайне важно чтобы объекты НА которые ссылалаются шли раньше чем объекты которые ссылаются, однако мы можем применить атрибут [JsonPropertyOrder] для детерменирования порядка. Совет: при отстуствии сторонних конфигураций, поля, включенные в сериализацию через, например, [JsonInclude], всегда идут позже свойств.
Полиморфизм
Полиморфизм — одна из проблем сериализации. Если имеется ссылка базового типа, указывающая на самом деле на объект дочернего, то десериализовать исходное состояние объекта без дополнительных инструментов будет невозможно, поскольку мы знаем лишь о базовом типе, какой тип объекта был фактически сериализован, остается неизвестным. Для таких ситуаций существует атрибут [JsonDerivedType(typeof(DerivedTypeName))]. Он указывается перед базовым классом, в его аргументах мы перечисляем дочерние типы. То есть, мы как бы говорим сериализатору «Если ты десериализуешь объект в ссылку этого типа, то, возможно, ты работаешь с объектом одного из перечисленных в его атрибуте дочерних типов». Атрибут может быть использован несколько раз, если унаследованных типов, которые требуют полиморфической десериализации, несколько.
На всякий случай я уточню, при сериализации сериализатор опирается на объект в памяти. Любой ваш объект, сериализуемый через ссылку типа object, будет сериализован как ваш объект, поскольку сериализатор внутри опирается на рефлексию.С десериализацией все интереснее, поскольку ему этот самый объект нужно создать, а значит опираться можно только на аргумент типа JsonSerializer.Deserialize<T>(). Для поддержки полиморфизма в JSON-представление сериализуемого объекта добавляется мета-свойство $type. Оно содержит дискриминатор(ключ) типа, экземпляр которого нужно создать. К слову атрибут [JsonDerivedType(typeof(DerivedTypeName), typeDiscriminator)] принимает аргумент typeDiscriminator, в который мы можем передать int или string, для ручной конфигурации, иначе будет использован счетчик int 0..n для каждого дочернего типа в атрибутах.
[JsonDerivedType(typeof(Developer), "developer")] // "developer" - наш дискриминатор [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)] // Об этом позже public class Employee { // ... }
Таким образом, выходной JSON получит новую запись:
{ "$type": "developer", // Другие данные }
Сопоставив значение из мета-свойства $type и один из дискриминаторов в атрибуте JsonDerivedType — сериализатор создаст экземпляр указанного типа и десериализует данные в него.
К слову, при полиморфической десериализации и сериализации в качестве аргумента типа JsonSerializer.Serialize<T>()/JsonSerializer.Deserialize<T> мы должны указывать базовый тип, то есть тип, помеченный атрибутами [JsonDerivedType].
Также выше в примере C# кода был упомянут атрибут [JsonPolymorphic]. Он служит для дополнительной конфигурации полиморфической десериализации, например в примере используется [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)], говорящий о том, что если будет обнаружен неизвестный тип, не указанный в [JsonDerivedType], то JSON десериализует объект в экземпляр ближайшего известного(указанного) предка.
JSON Document Object Model
JSON DOM (Document Object Model) — это модель представления структуры JSON-документа в памяти в виде объекта или дерева объектов, с которыми можно работать программно. Мы можем десериализовать любой объект в такую структуру, с которой можно работать не имея конкретного типа.
Основными типами для работы с DOM являются JsonNode и JsonDocument. Первый допускает работу с данными и их изменение, второй — иммутабелен. JsonNode основывается на синтаксисе индексатора
{ // Исходник "ID": 1, "Name": "Jim Carry", "Sales": [ 1,2,3,4,5 ] }
JsonNode node = JsonNode.Parse(stream)!; JsonNode idJsonValue = node["ID"]!; int ID = idJsonValue.Deserialize<int>(); Console.WriteLine(ID);
и представляет данные в виде 3 сущностей: JsonValue, т.е. примитивное ключ-значение свойство вроде "id":1, JsonObject, являющийся композитным объектом (заключен в фигурные скобки), и JsonArray, представляющий массивы. Все три типа наследуются от JsonNode, что позволяет также использовать его интерферфейс и в частности индексатор и идти по дереву объектов вглубь.
JsonDocument — иммутабельная реализация для работы с DOM. Также по сути является графом объектов из спарсенного Json и позволяет обращаться к вложенным свойствам, перебирать их и так далее. Работа с JsonDocument происходит через тип JsonElement и начинается с обращения к JsonDocument.RootElement:
{ // Исходник "ID": 1, "Name": "Jim Carry", "Sales": [ 1,2,3,4,5 ] }
using JsonDocument document = JsonDocument.Parse(stream); // IDisposable, поэтому using JsonElement root = document.RootElement; // Корневой элемент foreach (var obj in root.EnumerateObject()) // Перебираем все записи в корневом объекте { Console.WriteLine(obj); } JsonElement sales = root.GetProperty("Sales"); // Получаем массив в корне, перебираем элементы массива. foreach (JsonElement sale in sales.EnumerateArray()) { Console.Write(sale.GetInt32() + ", "); }
UTF8 Json Writer/Reader
Utf8JsonWriter / Utf8JsonReader — типы, обеспечивающие запись/чтение JSON на самом низком уровне. Вся сериализация и десериализация сводится к использованию их. Вкратце примеры их работы:
Utf8JsonWriter предоставляет API для записи шаг-за-шагом объектов, свойств, массивов, довольно прост в использовании, издалека похож на обычный StreamWriter. Если вы возьмете лист бумаги, ручку, сядете десериализовывать объект вручную и будете проговаривать каждое ваше действие — вы превратитесь в Utf8JsonWriter:
using MemoryStream ms = new(); // Создадим стрим, куда будем писать using Utf8JsonWriter writer = new(ms, options); // Writer тоже IDisposable writer.WriteStartObject(); // Начинаем писать сам сериализуемый объект writer.WriteStartObject("Employee"); // Начинаем писать Json-объект. Employee здесь имя свойства в выходном Json, т.е. "Employee":{value} // Совет: кодируем UTF-16 .NET строку в UTF-8 через JsonEncodedText для производительности своими руками. JsonEncodedText name = JsonEncodedText.Encode("Jim Carry"); writer.WriteString("Name", name); // На Json выходе получаем: "Name": "Jim Carry" writer.WriteNull("Null"); // "Null": null writer.WriteNumber("ID", 10); // "ID": 10 writer.WriteStartArray("Values"); // Начинаем писать массив внутри Json объекта writer.WriteNumberValue(10); writer.WriteNumberValue(20); writer.WriteEndArray(); // Закрываем массив writer.WriteEndObject(); // Закрываем Json объект Employee writer.WriteEndObject(); // Закрываем сам сериализуемый объект writer.Flush(); // Записываем оставшееся в буфере JsonWriter'a в целевой поток ms
Utf8JsonReader — куда более оптимизирован, является ref struct, читает Json payload(содержимое) по токенам. Токеном может быть начало объекта, имя свойства, значение свойства, конец объекта, начало массива и т.д.
JsonReaderOptions options = new() // Опции reader'a для обработки комментариев и "лишних" замыкающих запятых { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; ReadOnlySpan<byte> jsonBytes = Encoding.UTF8.GetBytes(json); // Быстренько (и неоптимизированно) получим байты Utf8JsonReader reader = new(jsonBytes, options); while (reader.Read()) // Пока буфер не кончился { switch (reader.TokenType) // Reader читает все байты одного токена, после чего мы проверяем что это за токен { case JsonTokenType.StartArray: // Если это начало массива, следующий токен будет элементом массива while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) // Пока не дошли до конца массива { Console.Write(reader.GetInt32() + ", "); // Читаем данные элемента массива и пишем их в консоль } } }
Реализация JsonConverter
Если тип по каким то причинам требует кастомных правил сериализации, то решением может быть реализация своего JsonConverter<T>, передаваемого в коллекцию JsonSerializerOptions.Converters. Конвертеры делятся на 2 типа: Basic (JsonConverter<T>) и Factory (JsonConverterFactory, создающий экземпляры basic JsonConverter<T>).
С обычным все просто, он проверяет, может ли обработать переданный тип (сериализатор передает конвертерам все сериализуемые свойства начиная с корневого объекта, где каждый проверяет через CanConvert(Type typeToConvert), может ли он конвертировать объекты данного типа). Если он может конвертировать данный тип, вызывается переопределенные вами void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
или T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);. Как мы можем видеть, конвертеру передаются Utf8Json Writer/Reader, через которые вы пишете/читаете объекты, как оговаривалось ранее.
Реализация Basic Converter
public class Employee // Тип для сериализации { public string Name { get; set; } public int ID { get; } public readonly DateTime dateOfBirth; public Employee(string name, int iD, DateTime dateOfBirth) { Name = name; ID = iD; this.dateOfBirth = dateOfBirth; } } public class EmployeeJsonConverter : JsonConverter<Employee> { public override void Write(Utf8JsonWriter writer, Employee value, JsonSerializerOptions options) { // Алгоритм записи супер примитивный, давайте просто запишем все свойства и поля объекта через дефис string data = $"{value.Name} - {value.ID} - {value.dateOfBirth.ToString(CultureInfo.InvariantCulture)}"; writer.WriteStringValue(data); } public override Employee? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Создаем переменные для будущей передачи в конструктор string Name = string.Empty; int ID = 0; DateTime dateOfBirth = DateTime.MinValue; // В нашем алгоритме записи мы просто пишем 3 поля объекта через дефис. Поэтому читаем 1 строку. string? data = reader.GetString(); if (data != null) { // Делим данные в JSON var splitted = data.Split('-', StringSplitOptions.TrimEntries); // И парсим их Name = splitted[0]; ID = int.Parse(splitted[1]); dateOfBirth = DateTime.Parse(splitted[2], CultureInfo.InvariantCulture); } return new Employee(Name, ID, dateOfBirth); } }
С фабричными конвертерами сложнее. Основная их цель — создавать basic converter’ы, упомянутые ранее, а значит фабричные конвертеры мы используем тогда, когда конвертации требует какой-то неопределенный тип, который мы не можем реализовать вручную. Самый распространенный случай — незакрытые generic типы. Например у нас есть некоторый класс DictionaryKey, который используется в качестве ключа в словаре. Поскольку это композитный тип с полями и свойствами, использовать его «из коробки» в качестве ключа не выйдет. А значит нам нужно создать конвертер для Dictionary<DictionaryKey,>, второй generic аргумент после запятой не указан, что означает открытый generic тип, благодаря чему нам не придется писать отдельный basic конвертер для каждого типа значения, что мы планируем использовать в словаре.
Убедившись, что fabrick converter имеет дело с нужным типом, он создает экземпляр уже basic converter(также написанного нами), обычно используя System.Reflection.Activator(что является обычной техникой создания экземпляров в рантайме). Итого, для работы с фабричным конвертерам нам также потребуется определить обычный конвертер, который знает как читать/писать наш DictionaryKey.
public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsGenericType && // Если тип generic typeToConvert.GetGenericTypeDefinition() == typeof(Dictionary<,>) && // И словарь typeToConvert.GetGenericArguments()[0] == typeof(DictionaryKey); // И первый (известный) аргумент это DictionaryKey, значит я могу создать для вас экземпляр обычного конвертера. } public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { var genArgs = typeToConvert.GetGenericArguments(); // Получаем все gen аргументы словаря Type valueType = genArgs[1]; // Второй аргумент, в данном случае тип значения в словаре, о котором мы ничего не знаем // Создаем экземпляр заранее определенного Basic конвертера, в данном случае JsonConverter<TValue>, типизируя его вторым generic аргументом, то есть valueType. Почему типизируем его вторым аргументом - потому что об аргументе значения словаря, т.е. DictionaryKey, он и так знает, в этом есть цель его существования. Описанный конвертер вы можете найти ниже. return (JsonConverter)Activator.CreateInstance(typeof(DictionaryKeyJsonConverter<>).MakeGenericType(valueType), // Типизируем конвертер типом значения словаря BindingFlags.Instance | BindingFlags.Public, binder: null, args: [options], // Конструктор конвертера ожидает JsonSerializerOptions culture: null)!; }
Реализация порождаемого фабрикой Basic конвертера для DictionaryKey
Представим, что у нас есть незамысловатый класс DictionaryKey. Его конвертацию для сериализации в ключ словаря мы сделаем крайне дилетантской: просто разделим дефисами значение полей. Разумеется, в полевых условиях к ToString() никто не прибегает.
public class DictionaryKey { // Поля вместо свойств просто потому что. Не играет роли. [JsonInclude] private int first; [JsonInclude] private float second; [JsonInclude] private string name; [JsonConstructor] public DictionaryKey(int first, float second, string name) { this.first = first; this.second = second; this.name = name; } public override string ToString() { return $"{first}-{second}-{name}"; } }
Теперь реализуем конвертер, который порождает вышеописанная фабрика через Activator.
public class DictionaryKeyJsonConverter<TValue> : JsonConverter<Dictionary<DictionaryKey, TValue>> { private JsonConverter<TValue> valueConverter; // Конвертер для TValue, тип значения нашего словаря. public DictionaryKeyJsonConverter(JsonSerializerOptions options) { valueConverter = (JsonConverter<TValue>)options.GetConverter(typeof(TValue)); // Который мы получаем из конфигурации сериализатора. Если в конфигурацию не был передан конвертер для данного типа, используется конвертация по умолчанию, с которой мы имели дело все это время. } public override void Write(Utf8JsonWriter writer, Dictionary<DictionaryKey, TValue> dict, JsonSerializerOptions options) { writer.WriteStartObject(); // Начинаем писать объект, который на самом деле является словарем. foreach ((DictionaryKey key, TValue value) in dict) { string propertyName = key.ToString()!; // Наш ключ для наблюдателя будет выглядеть как имя json-свойства. То есть "key": value, где key это DictionaryKey, умело уместивший свои данные в строку через ToString(), для простоты примера. // Соблюдая политику наименования, которая возможно была передана в JsonSerializerOptions, записываем наш DictionaryKey как имя свойства. writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); valueConverter.Write(writer, value, options); // А значение записываем через конвертер нашего TValue типа, полученный в конструкторе. } writer.WriteEndObject(); // Словарь записан как объект. } public override Dictionary<DictionaryKey, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Наш словарь - JSON объект, а значит начинается с этого токена. if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException(); } Dictionary<DictionaryKey, TValue> result = new(); while(reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) // Закончили читать словарь { return result; } // Запись в словаре всегда начинается с PropertyName, в которое мы умело запихали DictionaryKey if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException(); } string propertyName = reader.GetString()!; // Читаем имя свойства // И, предположим, мы ToString() реализовали путем склеивания всех свойств и полей DictionaryKey через дефисы. Расклеиваем обратно. string[] data = propertyName.Split('-', StringSplitOptions.RemoveEmptyEntries); // Предположим DictionaryKey состоит из 3 свойств int,float,string. DictionaryKey key = new(int.Parse(data[0]), float.Parse(data[1]), data[2]); // Reader читает токенами, а значит вызывав Read мы переместим его указатель с токена имени свойства(DictionaryKey здесь) на его значение, которое также является TValue для нашей пары <DictionaryKey, TValue> reader.Read(); // Делегируем чтение конвертеру TValue TValue value = valueConverter.Read(ref reader, typeof(TValue), options)!; result.Add(key, value); // Добавляем пару в словарь } throw new JsonException(); // Вас здесь не должно быть, вы пропустили JsonTokenType.EndObject, делающий return. } }
Контракты
Для каждого сериализуемого типа .NET необходимо то, что называется контрактом. Контракт определяет стоит ли включать поля, как записывать имена свойств в JSON, какие свойства игнорировать, какой конвертер использовать для какого свойства и т.д.. Обычно мы кастомизируем контракты используя JSON атрибуты, передавая сериализатору экземпляр JsonSerializerOptions, создавая свои конвертеры и т.д.. Однако, существует также опция кастомизации контрактов на более высоком уровне.
Для кастомизации конракта мы можем обратиться к JsonSerializerOptions.TypeInfoResolver и инициализировать либо своей реализацией, либо экземпляром предопределенного типа DefaultJsonTypeInfoResolver. В чем удобство второго подхода — нам не нужно наследоваться и реализовывать сложную логику, как это было с конвертерами, поскольку работа с JsonTypeInfoResolver заключается в передаче делегатов в коллекцию Modifiers.
Эта коллекция принимает любого делегата с сигнатурой Action<JsonTypeInfo>, то есть мы можем передавать туда методы (хуки в своей сути, перехватывающие процесс сериализации), которые ничего не возвращают и принимают объект JsonTypeInfo. Под этим объектом скрывается каждая сущность, которую сериализатор планирует включать в JSON, от корневого объекта до вложенных.
Если вкратце, JsonTypeInfo немного похож на MemberInfo из рефлексии, через него мы можем получить список его собственных свойств и полей, через него мы можем определить, будет ли сериализовываться тип как Json-объект/простое свойство/массив, обратиться к его C# типу, получить доступ к конвертерам, если такие определены, и так далее.
Простые примеры кастомизации контрактов:
public static void IgnorePasswords(JsonTypeInfo typeInfo) { for (int i = 0; i < typeInfo.Properties.Count; i++) // Перебираем все свойства объекта { if (typeInfo.Properties[i].PropertyType == typeof(Password)) // Если любое свойство является паролем ( какой-то наш класс) { typeInfo.Properties.RemoveAt(i); // То удаляем его из списка свойств. То есть объекты типа Password не будут включены в выходной JSON. } } }
Или, например, мы можем использовать рефлексию внутри модификатора контракта, чтобы включить все поля в сериализацию(но, конечно, переданный сериализатору JsonSerializerOptions.IncludeFields был бы куда эффективнее)
public static void IncludeFieldsModifier(JsonTypeInfo typeInfo) { if (typeInfo.Kind is not JsonTypeInfoKind.Object) // Если это не композитный JSON-объект, игнорируем его, массивы и примитивные пары "key":value не обладают полями { return; } // Вытаскиваем все поля, обращаясь к уже .NET типу сериализуемого typeInfo через свойство Type foreach (var fieldInfo in typeInfo.Type.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) { // Создаем новый JsonPropertyInfo. Это информация о JSON-свойстве, используя которую сериализатор запишет "key":value пару. JsonPropertyInfo jsonPropertyInfo = typeInfo.CreateJsonPropertyInfo(fieldInfo.FieldType, fieldInfo.Name); // Нам нужно указать на логику чтения и записи значения новоиспеченного JSON-свойства, в данном случае мы передаем методы рефлексии для чтения/записи FieldInfo, поскольку по сигнатуре они подходят под делегаты Get/Set jsonPropertyInfo.Get = fieldInfo.GetValue; // Передаем в делегат Get метод для получения значения jsonPropertyInfo.Set = fieldInfo.SetValue; // Передаем в делегат Set метод для установки значения typeInfo.Properties.Add(jsonPropertyInfo); // Добавляем JSON-свойство к JSON-свойствам нашего Json-объекта } }
После создания наших контрактов, мы передаем их в конфигурацию:
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() { Modifiers = { IncludeFieldsModifier } // Передаем наш метод в лист делегатов, которые будут вызываться. } };
Режимы сериализации
Обычно JsonSerializer работает в режиме Reflection. При первой сериализации типа создаются метаданные (контракты), описывающие как этот тип должен сериализовываться, какие свойства и поля, их атрибуты и тд.. Этот подход удовлетворяет большинство потребностей, однако в некоторых случаях, когда особенно важна оптимизация, мы можем захотеть избежать сбора метаданных в рантайме.
Поэтому у сериализатора есть второй режим работы: SourceGeneration.
Он также делится на 2 подрежима: Matadata-based и Serialization Optimization.
-
Metadata-based позволяет сгенерировать файлы исходного кода, содержащие метаданные, описывающие сериализуемые типы, которые затем будут интегрированы и скомпилированы вместе с вашим кодом.
-
Serialization-optimization — довольно свежий режим, позволяет заменить использование JsonSerializer на Utf8JsonWriter/Reader, что очень повышает производительность кода.
Однако у этих режимов есть недостатки: большинство фич сериализатора перестают работать, например разрешение ссылок, десериализации иммутабельных типов, атрибутов для заполнения инициализированных объектов и пр..
В остальном, данная тема невероятно огромная и погоня за оптимизацией зачастую не стоит свеч, поэтому я просто оставлю ссылку на документацию.
Заключение
Надеюсь, данная статья останется в ваших закладках и принесет пользу. Пиши конспекты, пей чай, пеки булки.
ссылка на оригинал статьи https://habr.com/ru/articles/840024/
Добавить комментарий