1. Введение
В предыдущих лекциях мы всегда работали со строго типизированными структурами. Например, у нас есть класс Person, который мы сериализуем в JSON и обратно:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Но иногда вы не знаете заранее структуру данных. Например:
- Вы пишете парсер для внешнего сервиса, где структура ответа не фиксирована.
- Вам нужно извлечь только часть информации, не обязательно заполнять целый класс.
- Нужно отредактировать или сгенерировать JSON "на лету", основываясь на динамических условиях.
Тут и вступают в игру "динамические структуры" — объекты, которые хранят JSON-дерево как набор ключей и значений, не требуя заранее описанного C#-класса.
Почему чаще всего используют Newtonsoft.Json
В .NET есть два главных игрока на поле работы с JSON:
- System.Text.Json — встроенная библиотека Microsoft (развивается с .NET Core 3.0).
- Newtonsoft.Json (Json.NET) — популярная библиотека, давшая миру C# такие классы, как JObject и JArray.
На момент написания лекции System.Text.Json всё ещё не предлагает полного аналога JObject/JArray с тем же удобством. Поэтому, если нужно часто разбирать или модифицировать сложные/неизвестные JSON-структуры, чаще выбирают Newtonsoft.Json.
Основные типы: JObject, JArray, JValue и семейство JToken
- JToken — базовый тип для всех JSON-узлов.
- JObject — JSON-объект { ... }, набор пар "ключ-значение".
- JArray — массив [ ... ].
- JValue — отдельное значение (42, "текст", true, null).
Идея проста: парсим JSON — получаем "дерево" из этих токенов и можем по нему ходить, искать, менять, удалять и добавлять элементы.
2. Чтение неизвестного JSON
Допустим, нам пришёл вот такой JSON, и мы не хотим или не можем заранее писать под него класс:
{
"status": "ok",
"amount": 150.5,
"items": [
{
"name": "book",
"qty": 1
},
{
"name": "pen",
"qty": 3
}
]
}
С помощью Newtonsoft.Json мы можем превратить его в дерево и исследовать на лету.
Пример: чтение JSON в JObject
using Newtonsoft.Json.Linq;
string json = @"{
""status"": ""ok"",
""amount"": 150.5,
""items"": [
{ ""name"": ""book"", ""qty"": 1 },
{ ""name"": ""pen"", ""qty"": 3 }
]
}";
// Парсим строку и получаем дерево
JObject root = JObject.Parse(json);
// Берём свойства как из словаря
string status = (string)root["status"]; // "ok"
double amount = (double)root["amount"]; // 150.5
// items — это массив, а значит JArray
JArray items = (JArray)root["items"];
// Перебираем массив
foreach (JObject item in items)
{
string name = (string)item["name"];
int qty = (int)item["qty"];
Console.WriteLine($"Товар: {name}, Кол-во: {qty}");
}
Потрясающе удобно: никакого объявления классов — можно быстро выковырять нужные куски.
3. Индексаторы и динамический доступ
Индексаторы:
- Для объекта: root["status"], root["items"]
- Для массива: items[0], items[1]
Чтобы получить значение сразу в нужном типе, используйте приведение (string), (int), (bool), (double) — библиотека автоматически преобразует тип.
Если данных может не быть, осторожнее: доступ к отсутствующему ключу вернёт null, и явное приведение упадёт. Лучше использовать методы с проверкой:
if (root.TryGetValue("amount", out var token))
{
double amount = token.Value<double>();
// Такой способ удобнее: Value<T>() – сразу преобразует
}
Вложенные объекты и массивы тоже читаются просто:
// Получить второй товар
JObject secondItem = (JObject)root["items"][1];
string itemName = (string)secondItem["name"]; // "pen"
Можно и динамически:
dynamic droot = root;
Console.WriteLine(droot.status); // "ok"
Но помните: с dynamic компилятор не проверит доступ к полям — ошибки всплывут только во время выполнения.
4. Модификация JSON-дерева на лету
Добавление элементов
root["currency"] = "RUB"; // Добавили новое свойство
items.Add(new JObject
{
["name"] = "eraser",
["qty"] = 2
});
Изменение и удаление
root["status"] = "done"; // Изменили значение
items[0]["qty"] = 5; // Увеличили количество первого товара
items.RemoveAt(1); // Удалили второй товар
Итоговое сохранение в строку
string modifiedJson = root.ToString();
// Или root.ToString(Formatting.Indented) для красоты
5. Создание JSON-структуры с нуля
var person = new JObject
{
["name"] = "Alice",
["age"] = 22,
["languages"] = new JArray { "C#", "Python" },
["isStudent"] = true
};
Console.WriteLine(person.ToString(Newtonsoft.Json.Formatting.Indented));
{
"name": "Alice",
"age": 22,
"languages": [
"C#",
"Python"
],
"isStudent": true
}
Полезно, если нужно вернуть во внешний API только часть данных или собрать JSON по условиям.
6. Как собрать свой объект из JObject
Иногда нужно превратить гибкий JSON в строгий C#-объект. Варианты:
- Обычная десериализация: JsonConvert.DeserializeObject<MyClass>(...).
- Ручная сборка объекта, извлекая нужные поля из JObject.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Допустим, нам прилетел вот такой JSON:
string incoming = @"{ ""name"":""Bob"", ""age"":30, ""extraField"":true }";
JObject j = JObject.Parse(incoming);
// Собираем объект вручную — только нужные поля
var person = new Person
{
Name = (string)j["name"],
Age = (int)j["age"],
// extraField нам не нужен — и отлично!
};
7. Примеры ошибок и нюансов: типичные ловушки
Работа с динамическими JSON-структурами гибка, но есть "подводные камни":
- Обращение к несуществующему ключу/элементу даёт null; явное приведение к значимому типу вызовет исключение.
- Несоответствие ожидаемого типа (ожидаем объект, а пришло значение) — ошибка приведения.
- Для перебора полей объекта используйте Properties():
foreach (var prop in root.Properties())
{
Console.WriteLine($"Поле: {prop.Name}, Значение: {prop.Value}");
}
- У Newtonsoft.Json удобно работать с LINQ-подобными запросами (фильтрация, поиск):
var expensiveItems = items.Where(obj => (int)obj["qty"] > 2);
foreach (var item in expensiveItems)
Console.WriteLine(item);
8. Типичные ошибки при работе с JObject/JArray
Ошибка №1: отсутствие ожидаемого поля или несовпадение типа. Очень часто разработчик рассчитывает, что в объекте будет определённое поле, но его либо нет, либо оно другого типа. Если ожидается объект, а приходит число, при приведении типов возникнет исключение. Проверяйте наличие и тип перед использованием.
Ошибка №2: обращение к полям вложенных структур без проверки на null. Когда в JSON есть вложенные объекты, а ключи отличаются или отсутствуют, попытка обратиться к отсутствующему полю может привести к падению. Делайте проверку на null перед чтением значений вложенных узлов.
Ошибка №3: приведение к значимому типу при значении null. Если ключ существует, но его значение равно null, выражение вроде (int)j["age"] вызовет исключение. Используйте Value<T>() — он вернёт значение по умолчанию (для int это 0, для строк — null).
Ошибка №4: чрезмерное увлечение динамическими объектами вместо чётких моделей. Слишком сложные структуры удобнее и безопаснее описать C#-классами: это уменьшит количество ошибок и повысит читаемость кода. Динамику применяйте там, где структура действительно неизвестна или сильно варьируется.
Теперь вы знаете, как быстро и безопасно парсить, изменять и создавать JSON-структуры с помощью JObject и JArray, даже без заранее описанных моделей.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ