1. Вступ
Словник (або Dictionary<TKey, TValue> у C#) — це колекція пар «ключ-значення». Такий тип даних незамінний, коли потрібно швидко знаходити значення за унікальним ідентифікатором (наприклад, шукати телефон за іменем у телефонній книзі).
На відміну від списків (List<T>), де порядок елементів має значення, словник зосереджується на швидкому доступі до даних за ключем. Але якщо під час серіалізації списку все просто (JSON-масив), то під час серіалізації словника виникає низка нюансів:
- Ключ має бути типом, який можна серіалізувати (найчастіше — рядок, але іноді це може бути число або навіть інший обʼєкт).
- У JSON немає окремого типу «словник» — лише обʼєкти або масиви.
Давайте детальніше розберімося, як .NET серіалізує словники, з якими труднощами можна зіткнутися, і як правильно «навчити» наш код працювати з такими структурами.
2. Серіалізація словника з ключами-рядками
Почнемо з класики — словник, де і ключ, і значення — рядкові.
// Приклад словника: книга та її автор
var books = new Dictionary<string, string>
{
["Майстер і Маргарита"] = "Михайло Булгаков",
["Гаррі Поттер"] = "Джоан Роулінг",
["Володар мух"] = "Вільям Голдінг"
};
// Серіалізація у рядок JSON
string json = JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("Словник серіалізовано в JSON:\n" + json);
// Збережемо у файл (синхронно)
File.WriteAllText("books.json", json);
Console.WriteLine("JSON записано у файл books.json.");
// Прочитаємо назад із файлу (синхронно)
string jsonFromFile = File.ReadAllText("books.json");
// Десеріалізація назад у словник
var restoredBooks = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonFromFile);
Console.WriteLine("Результат десеріалізації:");
foreach (var pair in restoredBooks)
Console.WriteLine($"{pair.Key} -> {pair.Value}");
Що вийде у файлі books.json?
{
"Майстер і Маргарита": "Михайло Булгаков",
"Гаррі Поттер": "Джоан Роулінг",
"Володар мух": "Вільям Голдінг"
}
Як це працює?
Серіалізатор перетворює наш Dictionary<string, string> на JSON-обʼєкт, у якому кожен ключ стає іменем властивості, а значення — значенням цієї властивості. Це зручно, коли ключі — рядки й вони унікальні.
3. Словник з нестандартним типом ключа
Усе просто, поки ключ — рядок. А якщо це, наприклад, число?
var bookIds = new Dictionary<int, string>
{
[1001] = "Майстер і Маргарита",
[1002] = "Гаррі Поттер",
[1003] = "Володар мух"
};
string jsonIntKeys = JsonSerializer.Serialize(bookIds, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonIntKeys);
Результат:
{
"1001": "Майстер і Маргарита",
"1002": "Гаррі Поттер",
"1003": "Володар мух"
}
Що сталося?
- Під час серіалізації числові ключі перетворюються на рядки, тому що в JSON імена властивостей можуть бути лише рядками.
- Під час десеріалізації назад у Dictionary<int, string> серіалізатор спробує перетворити рядок на число.
Приклад десеріалізації:
var restoredBookIds = JsonSerializer.Deserialize<Dictionary<int, string>>(jsonIntKeys);
// Усе працює! Ключі знову стали числами.
А що, якщо ключ — складний тип, наприклад, обʼєкт?
var dict = new Dictionary<Author, string>
{
[new Author { Name = "Голдінг", BirthYear = 1911 }] = "Володар мух"
};
Спроба серіалізувати такий словник призведе до винятку:
System.NotSupportedException: Serialization and deserialization of 'Dictionary<Author, string>' instances are not supported.
Чому так?
JSON-обʼєкт не може використовувати щось інше, окрім рядка, як імʼя властивості. Тому ключами в словнику під час серіалізації мають бути прості типи, які однозначно перетворюються на рядок (найчастіше рядок або число). Складні обʼєкти не можна використовувати як ключі під час серіалізації у JSON стандартними засобами.
4. Словник із вкладеними обʼєктами як значення
З ключами розібралися — тепер подивімося, що відбувається, якщо значення — складний обʼєкт (наприклад, Book або Author).
Приклад
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
}
var authorDirectory = new Dictionary<string, Author>
{
["bulgakov"] = new Author { Name = "Михайло Булгаков", BirthYear = 1891 },
["golding"] = new Author { Name = "Вільям Голдінг", BirthYear = 1911 }
};
string jsonAuthors = JsonSerializer.Serialize(authorDirectory, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonAuthors);
Результат:
{
"bulgakov": {
"Name": "Михайло Булгаков",
"BirthYear": 1891
},
"golding": {
"Name": "Вільям Голдінг",
"BirthYear": 1911
}
}
- Усе вкладене серіалізується згідно зі структурою обʼєктів.
- Десеріалізація назад у Dictionary<string, Author> також працює без проблем.
5. Словник у складі іншого обʼєкта
Дуже часто словники використовують як поле всередині складнішого обʼєкта. Наприклад, у бібліотеці є каталог книг, де кожен ключ — назва жанру, а значення — список книг цього жанру.
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
}
public class Library
{
public Dictionary<string, List<Book>> CatalogByGenre { get; set; }
}
var library = new Library
{
CatalogByGenre = new Dictionary<string, List<Book>>
{
["Фантастика"] = new List<Book>
{
new Book { Title = "Соляріс", Author = "Станіслав Лем" }
},
["Класика"] = new List<Book>
{
new Book { Title = "Володар мух", Author = "Вільям Голдінг" },
new Book { Title = "Зрима пітьма", Author = "Вільям Голдінг" }
}
}
};
string jsonLibrary = JsonSerializer.Serialize(library, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonLibrary);
Фрагмент вихідного JSON:
{
"CatalogByGenre": {
"Фантастика": [
{
"Title": "Соляріс",
"Author": "Станіслав Лем"
}
],
"Класика": [
{
"Title": "Володар мух",
"Author": "Вільям Голдінг"
},
{
"Title": "Зрима пітьма",
"Author": "Вільям Голдінг"
}
]
}
}
Усе працює — вкладені словники та колекції серіалізуються і десеріалізуються рекурсивно.
6. Особливості та «підводні камені» серіалізації словників
Повторювані ключі
У словнику ключі завжди унікальні. А от якщо вручну підсунути JSON із повторюваними ключами:
{
"foo": "first",
"foo": "second"
}
Результат: останнє значення ("second") перезапише перше, помилки не буде. Так працює більшість парсерів JSON.
Порядок елементів
Словник — неупорядкована колекція. Під час серіалізації порядок ключів у JSON може відрізнятися від початкового. Якщо порядок критичний — використовуйте список пар (List<KeyValuePair<string, T>>), але зазвичай для словника порядок не має значення.
JSON і вкладені словники
Рівні вкладеності не обмежені, але для коректної роботи кожен рівень має вкладатися в обмеження JSON (ключі — рядки, значення — валідні JSON-обʼєкти/масиви).
Використання JsonSerializerOptions
Іноді потрібно, щоб імена властивостей були не в PascalCase, а в camelCase. Це особливо важливо, якщо ви інтегруєтеся з фронтендом на JavaScript, де camelCase вважається стандартом для назв полів.
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
string camelJson = JsonSerializer.Serialize(authorDirectory, options);
Важливо: у словниках цей параметр впливає лише на серіалізацію вкладених обʼєктів (їхні властивості), а не на ключі словника. Ключі в словнику завжди серіалізуються у тому вигляді, у якому їх задано в C#.
7. Проблеми зі складними та нерядковими ключами
Із серіалізацією словників зі ключами-рядками та числовими ключами (наприклад, int, long, Guid) усе працює «з коробки». А от якщо ви спробуєте використовувати як ключ користувацький клас або структуру — отримаєте помилку NotSupportedException.
Для серіалізації таких випадків існують обхідні підходи:
- Використати інший формат зберігання, наприклад, серіалізувати словник як масив обʼєктів із полями "Key" і "Value".
- Написати конвертер (JsonConverter), який перетворює складний ключ у рядок і назад.
- Якщо структура справді складна — інколи варто переглянути архітектуру і не використовувати складні обʼєкти як ключ словника.
Приклад обходу через серіалізацію як списку пар
public class AuthorInfo
{
public Author Author { get; set; }
public string Book { get; set; }
}
// замість Dictionary<Author, string>
var list = new List<AuthorInfo>
{
new AuthorInfo { Author = new Author { Name = "Вільям Голдінг", BirthYear = 1911 }, Book = "Володар мух" }
};
// такий список серіалізується без проблем
Порівняння: словник vs. список пар під час серіалізації
| Тип колекції | JSON-структура | Коли використовувати |
|---|---|---|
|
|
Ключі — прості рядки, потрібен швидкий пошук і унікальність |
|
|
Ключ — складний тип, потрібен контроль порядку, можливі дублікати |
8. Типові питання на співбесідах
1. Чи можна серіалізувати Dictionary<DateTime, string>?
Так, але ключі перетворюються на рядкове представлення (зазвичай ISO-формат на кшталт "yyyy-MM-ddTHH:mm:ss"). Іноді під час десеріалізації можуть виникнути проблеми з локалями та форматами дат.
2. Що станеться, якщо серіалізувати Dictionary<int, string>?
Ключі буде серіалізовано як рядки, навіть якщо в початковому словнику були числа. Під час десеріалізації все відновиться коректно.
3. Чому не можна серіалізувати словник з обʼєктами як ключами?
Лише рядки можуть бути іменами властивостей JSON-обʼєкта, а обʼєкти — ні.
4. Якщо все ж потрібно серіалізувати словник зі складним ключем?
Краще переглянути структуру або серіалізувати як список пар «key-value», де ключ серіалізується повністю як поле-обʼєкт, а не як імʼя властивості.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ