Заметки по сериализации + System.Text.Json

от автора

В своем подходе к обучению я применяю довольно простую схему — у меня есть репозиторий, и каждый раз, когда я сталкиваюсь с новой для себя технологией — я иду туда, создаю папку-подпапку, в ней .cs файл и решаю какую-то проблему, применяя изученное. Если теоретическая составляющая темы слишком обширна, чтобы запихнуть ее в голову и комментарии к коду, я пишу конспекты в .md формат.
Недавно я актуализировал конспект по сериализации, и мне пришла идея — почему бы не вытесать из этого статью.

В этой статье мы пройдемся по тому как сериализация работает, основным форматам и языкам разметки, а также объединим знания об использовании .NET System.Text.Json в формат напоминалки. Сразу же отмечу два дисклеймера:

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

  2. Мы довольно подробно поговорим об XML и SOAP, однако работы с классом XmlSerializer вы здесь не увидите. Причины можно найти в данном видеофрагменте. А если серьезно — мне просто не довелось применить данный вид сериализации на практике, но знание основ XML не навредит. О BinaryFormatter и ISerializable речи идти вовсе не будет, так как грядущий .NET 9 захлопнет крышку этого гроба окончательно.

Примечание: Хабр не очень дружит с переносом строки внутри блоков кода, для удобства чтения раздутых XML и комментариев — используйте Shift+Колесико мыши. Возможно, отоспавшись, я оформлю переносы вручную.

Оглавление:

  1. Форматы и языки разметки

  2. System.Text.Json

Сериализация

Сериализация — процесс преобразования объекта среды выполнения в форму, пригодную для дальнейшей транспортировки. Выражаясь простым языком, это процесс записи данных объекта в памяти, то есть класса или структуры, в такую форму, которую можно передать по сети, сохранить на диске, использовать между процессами и так далее. Десериализация, соотвественно, обратный процесс восстановления состояния объекта из формата транспортировки.

Обычно это текстовые форматы вроде 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.

    Экранируемые символы
    &lt; — представляет символ < (открывающая угловая скобка). &gt; — представляет символ > (закрывающая угловая скобка). &amp; — представляет символ & (амперсанд). &quot; — представляет символ " (двойная кавычка). &apos; — представляет символ ' (одинарная кавычка). 
  • Отличие 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *