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": "Повелитель мух"
}
Что произошло?
- C# преобразовал числовые ключи в строки, потому что в 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", где ключ сериализуется целиком как объект-поле, а не как имя свойства.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ