JavaRush /Курси /C# SELF /Глибоке занурення у System...

Глибоке занурення у System.Text.Json

C# SELF
Рівень 47 , Лекція 1
Відкрита

1. Вступ

На цій лекції зробимо крок від "дай мені просто зберегти список" до гнучкої, точної та продуктивної серіалізації за допомогою System.Text.Json. У сучасних .NET‑проєктах JSON — де‑факто стандарт обміну даними. Базові виклики Serialize/Deserialize прості, але реальні завдання вимагають тонкого налаштування: ігнорування/перейменування полів, керування форматами дат, захист від циклів, робота з великими обсягами даних, власні конвертери тощо.

Ми розглянемо не лише методи JsonSerializer, а й параметри через JsonSerializerOptions, атрибути, роботу зі Stream, керування памʼяттю та впровадження своїх правил серіалізації через JsonConverter.

Коротка історія та позиціонування System.Text.Json

Довгий час у .NET домінував Newtonsoft.Json (Json.NET) — гнучкий і зрілий, але не завжди найшвидший і найлегший за залежностями. З .NET Core 3.0 зʼявився вбудований System.Text.Json: висока продуктивність, мінімум залежностей (у складі платформи), щільна інтеграція з ASP.NET Core і постійний розвиток разом із релізами .NET.

2. Базові класи й методи

Базовий елемент — статичний клас JsonSerializer, який пропонує два варіанти:

  • Серіалізація: обʼєкт → JSON‑рядок (Serialize)
  • Десеріалізація: JSON‑рядок → обʼєкт потрібного типу (Deserialize)

Приклад серіалізації простого обʼєкта

using System.Text.Json;

var person = new Person { Name = "Іван", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // {"Name":"Іван","Age":30}

Приклад десеріалізації

var json = "{\"Name\":\"Анна\",\"Age\":22}";
var anna = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(anna.Name); // Анна

Примітка: тип Person уже реалізований у попередніх лекціях — використовуємо його і тут.

3. Керування серіалізацією: JsonSerializerOptions

У реальних проєктах майже завжди потрібні налаштування: імена властивостей у camelCase, формати дат, обробка циклів, значення за замовчуванням тощо. Усе це регулюється через JsonSerializerOptions.

Приклад налаштування

var options = new JsonSerializerOptions
{
    WriteIndented = true,    // Красиво форматувати JSON (додає відступи й перенесення рядків)
    PropertyNameCaseInsensitive = true, // Ігнорувати регістр імен властивостей під час десеріалізації
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase для властивостей (а не PascalCase)
};

string json = JsonSerializer.Serialize(person, options);
/*
{
  "name": "Іван",
  "age": 30
}
*/

Чому це важливо? Більшість фронтенд‑фреймворків очікують саме camelCase, а не .NET‑івський PascalCase.

4. Атрибути: System.Text.Json.Serialization

Іноді зручніше керувати серіалізацією прямо з моделі за допомогою атрибутів. Їх додають до полів/властивостей, щоб впливати на імена, включення/виключення та обробку значень.

Основні атрибути

Атрибут Що робить
[JsonIgnore]
Вилучає властивість із серіалізації/десеріалізації
[JsonPropertyName("ім'я")]
Використовує іншу назву в JSON
[JsonInclude]
Додає непублічну властивість або поле до серіалізації
[JsonNumberHandling]
Керує обробкою числових значень

Приклад: керування властивостями через атрибути

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("full_name")]
    public string Name { get; set; }

    [JsonIgnore]
    public int SecretCode { get; set; }

    public int Age { get; set; }
}
var person = new Person { Name = "Петро", Age = 45, SecretCode = 123 };
string json = JsonSerializer.Serialize(person);
// {"full_name":"Петро","Age":45}

Зверніть увагу: SecretCode не потрапив у JSON, а Name серіалізувався як "full_name".

5. Серіалізація колекцій і вкладених обʼєктів

Колекції — це просто

var numbers = new List<int> { 1, 2, 3 };
string json = JsonSerializer.Serialize(numbers); // [1,2,3]

var people = new List<Person> {
    new Person { Name = "Анна", Age = 20 },
    new Person { Name = "Максим", Age = 40 }
};
string jsonList = JsonSerializer.Serialize(people);
// [{"Name":"Анна","Age":20},{"Name":"Максим","Age":40}]

Вкладені структури

public class Group
{
    public string Name { get; set; }
    public List<Person> Members { get; set; }
}

var group = new Group
{
    Name = "Розробники",
    Members = new List<Person>
    {
        new Person { Name = "Саша", Age = 23 },
        new Person { Name = "Маша", Age = 28 }
    }
};

string jsonGroup = JsonSerializer.Serialize(group, options);
/*
{
  "name": "Розробники",
  "members": [
    { "name": "Саша", "age": 23 },
    { "name": "Маша", "age": 28 }
  ]
}
*/

6. Десеріалізація: що важливо знати?

var json = "[{\"Name\":\"Іван\",\"Age\":21}]";
var list = JsonSerializer.Deserialize<List<Person>>(json);
Console.WriteLine(list[0].Name); // Іван

Поширений сценарій: якщо в JSON немає якогось поля, відповідна властивість отримає значення за замовчуванням. Зайві поля в JSON, яких немає в моделі, ігноруються. Але якщо типи не збігаються (наприклад, замість числа прийшов рядок) — під час десеріалізації буде згенеровано виняток.

7. Обробка дат, часу, форматів і числових значень

public class Meeting
{
    public string Topic { get; set; }
    public DateTime Time { get; set; }
}

var meeting = new Meeting { Topic = "Зустріч", Time = DateTime.Now };
string json = JsonSerializer.Serialize(meeting);
// {"Topic":"Зустріч","Time":"2024-06-06T20:30:00.0000000+03:00"}

За замовчуванням DateTime серіалізується у форматі ISO 8601. Потрібен інший формат (наприклад, лише дата)? Використайте окрему властивість або власний конвертер (див. нижче).

FAQ: Щоб числа серіалізувалися як рядки (наприклад, телефони чи великі ID), використовуйте атрибут [JsonNumberHandling(JsonNumberHandling.WriteAsString)].

8. Потоки та робота з файлами

Працювати можна не лише з рядками, а й зі Stream — це важливо для великих даних (файли, мережа).

Приклад запису у файл

using var fs = File.Create("person.json");
JsonSerializer.Serialize(fs, person);
// Не забудьте викликати fs.Flush() або використайте using!

Приклад читання з файлу

using var fs = File.OpenRead("person.json");
var restored = JsonSerializer.Deserialize<Person>(fs);

З потоками доступні асинхронні методи SerializeAsync/DeserializeAsync — стануть у пригоді для високонавантажених сервісів.

9. Власні конвертери

Якщо стандартні правила не підходять (нестандартні формати дат/чисел, складні значення, власні структури) — створіть JsonConverter.

Приклад: дата лише у форматі «dd.MM.yyyy»

public class CustomDateConverter : JsonConverter<DateTime>
{
    public override DateTime Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString(), "dd.MM.yyyy", null);
    }

    public override void Write(
        Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("dd.MM.yyyy"));
    }
}

var options = new JsonSerializerOptions();
options.Converters.Add(new CustomDateConverter());

var dt = new DateTime(2024, 6, 1);
string json = JsonSerializer.Serialize(dt, options); // "01.06.2024"

Власні конвертери корисні для серіалізації координат, векторів, кольорів, нестандартних дат і валют, різних ID‑форматів тощо.

10. Корисні нюанси

Обробка циклічних посилань і глибоких ієрархій

var options = new JsonSerializerOptions
{
    ReferenceHandler = ReferenceHandler.Preserve, // Зберігає всі обʼєкти з $id/$ref
    WriteIndented = true
};

Важливо: у JSON зʼявляться службові властивості $id і $ref. Для обміну із зовнішніми системами, які їх не розуміють, це може не підійти.

Відмінності між System.Text.Json і Newtonsoft.Json

System.Text.Json уже дуже потужний, але поки не закриває абсолютно всі сценарії Newtonsoft.Json (приватні конструктори, складні динамічні обʼєкти тощо). Для більшості стандартних завдань радимо вбудований серіалізатор — він швидший і без зайвих залежностей.

Інтерактивна робота з JSON: DOM API

Коли потрібно працювати з JSON без повної моделі, використовуйте JsonDocument і JsonElement.

using var doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;

if (root.TryGetProperty("Name", out var nameProperty))
{
    Console.WriteLine(nameProperty.GetString());
}

11. Корисні опції та їхні ефекти

Властивість Значення/Призначення
WriteIndented
true — форматування з відступами
PropertyNameCaseInsensitive
true — ігнорувати регістр імен властивостей під час десеріалізації
PropertyNamingPolicy
JsonNamingPolicy.CamelCase
DefaultIgnoreCondition
Правила ігнорування null і значень за замовчуванням
ReferenceHandler
Preserve
,
IgnoreCycles
AllowTrailingCommas
true — дозволити кому в кінці масиву
NumberHandling
Перетворення чисел у рядки й назад (та ін.)
Converters
Список власних конвертерів

12. Поширені помилки та практичні поради

Помилка № 1: неправильний тип під час десеріалізації. Якщо серіалізували список, десеріалізуйте саме в список: List<T>, а не одиночний обʼєкт.

Помилка № 2: некоректний регістр імен властивостей. Без налаштування врахування регістру властивості можуть «не знаходитися». Використовуйте PropertyNameCaseInsensitive або налаштуйте PropertyNamingPolicy.

Помилка № 3: некоректна обробка дат. За замовчуванням формат — ISO 8601. Потрібен інший — створіть і застосуйте конвертер (JsonConverter<DateTime>).

Помилка № 4: очікування серіалізації приватних/статичних полів. За замовчуванням беруться публічні властивості. Для нестандартних випадків використовуйте відповідні атрибути (наприклад, [JsonInclude]).

Помилка № 5: нерозуміння значень за замовчуванням. Відсутнє поле в JSON → значення за замовчуванням для властивості. Враховуйте це в логіці.

Помилка № 6: неправильне керування потоками. Закривайте ресурси з using або await using, щоб уникнути витоків і блокувань.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ