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
Іноді зручніше керувати серіалізацією прямо з моделі за допомогою атрибутів. Їх додають до полів/властивостей, щоб впливати на імена, включення/виключення та обробку значень.
Основні атрибути
| Атрибут | Що робить |
|---|---|
|
Вилучає властивість із серіалізації/десеріалізації |
|
Використовує іншу назву в JSON |
|
Додає непублічну властивість або поле до серіалізації |
|
Керує обробкою числових значень |
Приклад: керування властивостями через атрибути
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. Корисні опції та їхні ефекти
| Властивість | Значення/Призначення |
|---|---|
|
true — форматування з відступами |
|
true — ігнорувати регістр імен властивостей під час десеріалізації |
|
|
|
Правила ігнорування null і значень за замовчуванням |
|
, |
|
true — дозволити кому в кінці масиву |
|
Перетворення чисел у рядки й назад (та ін.) |
|
Список власних конвертерів |
12. Поширені помилки та практичні поради
Помилка № 1: неправильний тип під час десеріалізації. Якщо серіалізували список, десеріалізуйте саме в список: List<T>, а не одиночний обʼєкт.
Помилка № 2: некоректний регістр імен властивостей. Без налаштування врахування регістру властивості можуть «не знаходитися». Використовуйте PropertyNameCaseInsensitive або налаштуйте PropertyNamingPolicy.
Помилка № 3: некоректна обробка дат. За замовчуванням формат — ISO 8601. Потрібен інший — створіть і застосуйте конвертер (JsonConverter<DateTime>).
Помилка № 4: очікування серіалізації приватних/статичних полів. За замовчуванням беруться публічні властивості. Для нестандартних випадків використовуйте відповідні атрибути (наприклад, [JsonInclude]).
Помилка № 5: нерозуміння значень за замовчуванням. Відсутнє поле в JSON → значення за замовчуванням для властивості. Враховуйте це в логіці.
Помилка № 6: неправильне керування потоками. Закривайте ресурси з using або await using, щоб уникнути витоків і блокувань.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ