JavaRush /Курси /C# SELF /Серіалізація словників

Серіалізація словників

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

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-структура Коли використовувати
Dictionary<string, Book>
{ "key1": {...}, "key2": {...} }
Ключі — прості рядки, потрібен швидкий пошук і унікальність
List<KeyValuePair<string, Book>>
[{"Key": "key1", "Value": {...}}, ... ]
Ключ — складний тип, потрібен контроль порядку, можливі дублікати

8. Типові питання на співбесідах

1. Чи можна серіалізувати Dictionary<DateTime, string>?
Так, але ключі перетворюються на рядкове представлення (зазвичай ISO-формат на кшталт "yyyy-MM-ddTHH:mm:ss"). Іноді під час десеріалізації можуть виникнути проблеми з локалями та форматами дат.

2. Що станеться, якщо серіалізувати Dictionary<int, string>?
Ключі буде серіалізовано як рядки, навіть якщо в початковому словнику були числа. Під час десеріалізації все відновиться коректно.

3. Чому не можна серіалізувати словник з обʼєктами як ключами?
Лише рядки можуть бути іменами властивостей JSON-обʼєкта, а обʼєкти — ні.

4. Якщо все ж потрібно серіалізувати словник зі складним ключем?
Краще переглянути структуру або серіалізувати як список пар «key-value», де ключ серіалізується повністю як поле-обʼєкт, а не як імʼя властивості.

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