Работа с JSON в C# 7 с использованием Newtonsoft.Json

Введение

JSON (JavaScript Object Notation) — это текстовый формат обмена данными, основанный на подмножестве языка JavaScript. В C# для работы с JSON чаще всего используется библиотека Newtonsoft.Json (также известная как Json.NET), которая предоставляет мощные и гибкие инструменты для сериализации и десериализации данных.

Основные подходы к парсингу JSON

1. Использование dynamic (как в вашем примере с ZennoPoster)

using Newtonsoft.Json;
using System.Dynamic;
 
// Ваш JSON в виде строки
string jsonString = @"{...ваш JSON...}";
 
// Десериализация в dynamic объект
dynamic data = JsonConvert.DeserializeObject<ExpandoObject>(jsonString);
 
// Доступ к свойствам через точечную нотацию
string doneMissions = data.payload?.doneMissions?.ToString() ?? "";
string dailyMintStreak = data.payload?.dailyMintStreak?.ToString() ?? "";

Преимущества:

  • Не требует создания классов
  • Быстрая реализация для простых задач
  • Гибкость при работе с неизвестной структурой

Недостатки:

  • Отсутствие IntelliSense
  • Ошибки только во время выполнения
  • Снижение производительности
  • Сложность отладки

2. Использование JObject/JToken (рекомендуемый подход для гибкости)

using Newtonsoft.Json.Linq;
 
string jsonString = @"{
    ""errors"":[],
    ""payload"":{
        ""doneMissions"":""xion-daily-mint"",
        ""dailyMintStreak"":10,
        ""balance"":0.35167,
        ""account"":{
            ""userId"":""ph3Wb4NK"",
            ""username"":""blockbarronbe""
        }
    }
}";
 
// Парсинг в JObject
JObject root = JObject.Parse(jsonString);
 
// Извлечение значений разными способами
string doneMissions = root["payload"]?["doneMissions"]?.ToString() ?? "";
int dailyMintStreak = root["payload"]?["dailyMintStreak"]?.Value<int>() ?? 0;
decimal balance = root["payload"]?["balance"]?.Value<decimal>() ?? 0m;
 
// Альтернативный способ через SelectToken (путь в стиле JSONPath)
string userId = root.SelectToken("payload.account.userId")?.ToString() ?? "";
string username = root.SelectToken("payload.account.username")?.ToString() ?? "";
 
// Проверка существования свойства
bool hasPayload = root["payload"] != null;
bool hasErrors = root["errors"] != null && root["errors"].HasValues;

3. Использование строго типизированных классов (рекомендуемый подход для производства)

// Определяем структуру классов согласно JSON
public class RootObject
{
    public List<object> Errors { get; set; } = new List<object>();
    public Payload Payload { get; set; }
    public bool Success { get; set; }
    public DateTime Now { get; set; }
}
 
public class Payload
{
    public string DoneMissions { get; set; }
    public Dictionary<string, bool> EligibleToMint { get; set; }
    public Dictionary<string, int> MintedCount { get; set; }
    public int DailyMintStreak { get; set; }
    public decimal Balance { get; set; }
    public int TotalApprovedReferrals { get; set; }
    public double EarnByHour { get; set; }
    public Session Session { get; set; }
    public decimal Staked { get; set; }
    public int UserXp { get; set; }
    public bool PartnerNftXpClaimed { get; set; }
    public Account Account { get; set; }
}
 
public class Session
{
    public DateTime ExpiresOn { get; set; }
    public string Token { get; set; }
}
 
public class Account
{
    public string UserId { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime ModifiedOn { get; set; }
    public int InvitedCount { get; set; }
    public string Twitter { get; set; }
    public string Discord { get; set; }
    public string Github { get; set; }
    public string Telegram { get; set; }
    public string Reddit { get; set; }
    public string Email { get; set; }
    public string Username { get; set; }
    public bool HasAvatar { get; set; }
    public decimal XpMultiplier { get; set; }
    public Dictionary<string, object> Metadata { get; set; }
    public List<Wallet> Wallets { get; set; }
}
 
public class Wallet
{
    public string Network { get; set; }
    public string Address { get; set; }
    public DateTime CreatedOn { get; set; }
    public DateTime ModifiedOn { get; set; }
}
 
// Использование
string jsonString = @"{...ваш JSON...}";
RootObject data = JsonConvert.DeserializeObject<RootObject>(jsonString);
 
// Теперь у нас есть строгая типизация и IntelliSense
string doneMissions = data.Payload.DoneMissions;
int dailyMintStreak = data.Payload.DailyMintStreak;
decimal balance = data.Payload.Balance;
string userId = data.Payload.Account.UserId;

Примеры парсинга в различные структуры данных

Парсинг в словарь (Dictionary)

// Пример 1: Парсинг объекта eligibleToMint в словарь
JObject root = JObject.Parse(jsonString);
JObject eligibleToMintObj = root["payload"]["eligibleToMint"] as JObject;
 
// Способ с LINQ
Dictionary<string, bool> eligibleDict = eligibleToMintObj
    .Properties()
    .ToDictionary(
        prop => prop.Name,
        prop => prop.Value.Value<bool>()
    );
 
// Способ без LINQ (для понимания логики)
Dictionary<string, bool> eligibleDictNoLinq = new Dictionary<string, bool>();
foreach (JProperty property in eligibleToMintObj.Properties())
{
    string key = property.Name;
    bool value = property.Value.Value<bool>();
    eligibleDictNoLinq.Add(key, value);
}
 
// Вывод результата
foreach (var kvp in eligibleDict)
{
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Результат:
// xion-early: false
// xion-bridge-2: false
// xion-faithful: false
// и т.д.
// Пример 2: Парсинг всего payload в словарь для гибкой работы
Dictionary<string, object> payloadDict = root["payload"]
    .ToObject<Dictionary<string, object>>();
 
// Доступ к значениям
string doneMissions = payloadDict["doneMissions"]?.ToString();
int dailyMintStreak = Convert.ToInt32(payloadDict["dailyMintStreak"]);

Парсинг в список (List)

// Пример 1: Парсинг массива wallets в список
JArray walletsArray = root["payload"]["account"]["wallets"] as JArray;
 
// Способ с LINQ - получение списка адресов
List<string> addresses = walletsArray
    .Select(w => w["address"].ToString())
    .ToList();
 
// Способ без LINQ
List<string> addressesNoLinq = new List<string>();
foreach (JToken wallet in walletsArray)
{
    string address = wallet["address"].ToString();
    addressesNoLinq.Add(address);
}
 
// Пример 2: Парсинг в список объектов
List<Wallet> wallets = walletsArray.ToObject<List<Wallet>>();
 
// Теперь можно работать с типизированными объектами
foreach (Wallet wallet in wallets)
{
    Console.WriteLine($"Network: {wallet.Network}");
    Console.WriteLine($"Address: {wallet.Address}");
    Console.WriteLine($"Created: {wallet.CreatedOn}");
}
// Пример 3: Извлечение всех булевых значений из eligibleToMint в список
JObject eligibleObj = root["payload"]["eligibleToMint"] as JObject;
 
// С LINQ
List<KeyValuePair<string, bool>> eligibleList = eligibleObj
    .Properties()
    .Select(p => new KeyValuePair<string, bool>(p.Name, p.Value.Value<bool>()))
    .ToList();
 
// Без LINQ
List<KeyValuePair<string, bool>> eligibleListNoLinq = 
    new List<KeyValuePair<string, bool>>();
foreach (JProperty prop in eligibleObj.Properties())
{
    var kvp = new KeyValuePair<string, bool>(
        prop.Name, 
        prop.Value.Value<bool>()
    );
    eligibleListNoLinq.Add(kvp);
}
 
// Фильтрация - получить только доступные для минта
List<string> availableForMint = eligibleList
    .Where(kvp => kvp.Value == true)
    .Select(kvp => kvp.Key)
    .ToList();
 
// Без LINQ
List<string> availableForMintNoLinq = new List<string>();
foreach (var kvp in eligibleList)
{
    if (kvp.Value == true)
    {
        availableForMintNoLinq.Add(kvp.Key);
    }
}

Парсинг в строку с форматированием

// Пример 1: Извлечение и форматирование данных аккаунта
JObject root = JObject.Parse(jsonString);
JObject account = root["payload"]["account"] as JObject;
 
// Создание форматированной строки с информацией об аккаунте
string accountInfo = $@"
=== Информация об аккаунте ===
User ID: {account["userId"]}
Username: {account["username"]}
Создан: {account["createdOn"]}
Изменен: {account["modifiedOn"]}
Приглашено: {account["invitedCount"]}
Аватар: {(account["hasAvatar"].Value<bool>() ? "Да" : "Нет")}
Множитель XP: {account["xpMultiplier"]}
";
 
Console.WriteLine(accountInfo);
 
// Пример 2: Создание CSV строки из данных
StringBuilder csvBuilder = new StringBuilder();
csvBuilder.AppendLine("Параметр,Значение");
csvBuilder.AppendLine($"User ID,{root.SelectToken("payload.account.userId")}");
csvBuilder.AppendLine($"Username,{root.SelectToken("payload.account.username")}");
csvBuilder.AppendLine($"Balance,{root.SelectToken("payload.balance")}");
csvBuilder.AppendLine($"Daily Streak,{root.SelectToken("payload.dailyMintStreak")}");
csvBuilder.AppendLine($"User XP,{root.SelectToken("payload.userXp")}");
 
string csvData = csvBuilder.ToString();

Работа с разными уровнями вложенности

// Пример навигации по разным уровням JSON
JObject root = JObject.Parse(jsonString);
 
// Уровень 1 - корневые свойства
bool success = root["success"].Value<bool>();
DateTime now = root["now"].Value<DateTime>();
 
// Уровень 2 - payload
decimal balance = root["payload"]["balance"].Value<decimal>();
int userXp = root["payload"]["userXp"].Value<int>();
 
// Уровень 3 - вложенные объекты
string userId = root["payload"]["account"]["userId"].ToString();
string sessionToken = root["payload"]["session"]["token"].ToString();
 
// Уровень 4 - массивы внутри вложенных объектов
string firstWalletAddress = root["payload"]["account"]["wallets"][0]["address"].ToString();
 
// Безопасная навигация с проверкой null
string safeWalletAddress = root["payload"]?["account"]?["wallets"]?[0]?["address"]?.ToString() 
    ?? "Адрес не найден";
 
// Использование SelectToken для глубокой навигации
string deepValue = root.SelectToken("payload.account.wallets[0].network")?.ToString() 
    ?? "Значение не найдено";

Сравнение производительности разных подходов

public class PerformanceComparison
{
    public static void CompareApproaches(string jsonString)
    {
        var sw = new Stopwatch();
        
        // 1. Dynamic подход (как в ZennoPoster)
        sw.Start();
        for (int i = 0; i < 10000; i++)
        {
            dynamic data = JsonConvert.DeserializeObject<ExpandoObject>(jsonString);
            string value = data.payload?.balance?.ToString() ?? "";
        }
        sw.Stop();
        Console.WriteLine($"Dynamic: {sw.ElapsedMilliseconds} ms");
        
        // 2. JObject подход
        sw.Restart();
        for (int i = 0; i < 10000; i++)
        {
            JObject data = JObject.Parse(jsonString);
            string value = data["payload"]?["balance"]?.ToString() ?? "";
        }
        sw.Stop();
        Console.WriteLine($"JObject: {sw.ElapsedMilliseconds} ms");
        
        // 3. Типизированный подход
        sw.Restart();
        for (int i = 0; i < 10000; i++)
        {
            RootObject data = JsonConvert.DeserializeObject<RootObject>(jsonString);
            string value = data.Payload?.Balance.ToString() ?? "";
        }
        sw.Stop();
        Console.WriteLine($"Typed: {sw.ElapsedMilliseconds} ms");
    }
}

Обработка ошибок и особых случаев

public class SafeJsonParser
{
    // Безопасный парсинг с обработкой ошибок
    public static T SafeDeserialize<T>(string json) where T : class, new()
    {
        try
        {
            // Настройки для обработки ошибок
            var settings = new JsonSerializerSettings
            {
                NullValueHandling = NullValueHandling.Ignore,
                MissingMemberHandling = MissingMemberHandling.Ignore,
                Error = (sender, args) =>
                {
                    // Логирование ошибки, но продолжение парсинга
                    Console.WriteLine($"Ошибка парсинга: {args.ErrorContext.Error.Message}");
                    args.ErrorContext.Handled = true;
                }
            };
            
            return JsonConvert.DeserializeObject<T>(json, settings) ?? new T();
        }
        catch (JsonException ex)
        {
            Console.WriteLine($"Критическая ошибка JSON: {ex.Message}");
            return new T();
        }
    }
    
    // Безопасное извлечение значения
    public static string SafeGetValue(JObject obj, string path, string defaultValue = "")
    {
        try
        {
            var token = obj.SelectToken(path);
            return token?.ToString() ?? defaultValue;
        }
        catch
        {
            return defaultValue;
        }
    }
    
    // Проверка структуры JSON перед парсингом
    public static bool ValidateJsonStructure(string json)
    {
        try
        {
            JObject.Parse(json);
            return true;
        }
        catch
        {
            return false;
        }
    }
}
 
// Использование
string jsonString = GetJsonFromApi();
 
if (SafeJsonParser.ValidateJsonStructure(jsonString))
{
    var data = SafeJsonParser.SafeDeserialize<RootObject>(jsonString);
    
    // Или с JObject
    JObject jObj = JObject.Parse(jsonString);
    string balance = SafeJsonParser.SafeGetValue(jObj, "payload.balance", "0");
}

Оптимизированная альтернатива коду ZennoPoster

// Вместо медленного dynamic подхода
public class OptimizedXionParser
{
    // Метод 1: Использование JObject (быстрее dynamic, но гибкий)
    public static XionData ParseWithJObject(string jsonString)
    {
        JObject root = JObject.Parse(jsonString);
        JObject payload = root["payload"] as JObject;
        JObject account = payload["account"] as JObject;
        
        return new XionData
        {
            DoneMissions = payload["doneMissions"]?.ToString() ?? "",
            TotalApprovedReferrals = payload["totalApprovedReferrals"]?.ToString() ?? "",
            DailyMintStreak = payload["dailyMintStreak"]?.ToString() ?? "",
            PartnerNftXpClaimed = payload["partnerNftXpClaimed"]?.ToString() ?? "",
            RefCode = account["userId"]?.ToString() ?? "",
            Staked = payload["staked"]?.ToString() ?? "",
            UserXp = payload["userXp"]?.ToString() ?? "",
            Balance = payload["balance"]?.ToString() ?? "",
            EarnByHour = payload["earnByHour"]?.ToString() ?? "",
            UserId = account["userId"]?.ToString() ?? "",
            CreatedOn = account["createdOn"]?.ToString() ?? "",
            ModifiedOn = account["modifiedOn"]?.ToString() ?? "",
            Username = account["username"]?.ToString() ?? "",
            HasAvatar = account["hasAvatar"]?.ToString() ?? ""
        };
    }
    
    // Метод 2: Прямая десериализация (самый быстрый)
    public static XionData ParseWithTypes(string jsonString)
    {
        var root = JsonConvert.DeserializeObject<RootObject>(jsonString);
        
        return new XionData
        {
            DoneMissions = root.Payload.DoneMissions ?? "",
            TotalApprovedReferrals = root.Payload.TotalApprovedReferrals.ToString(),
            DailyMintStreak = root.Payload.DailyMintStreak.ToString(),
            PartnerNftXpClaimed = root.Payload.PartnerNftXpClaimed.ToString(),
            RefCode = root.Payload.Account.UserId ?? "",
            Staked = root.Payload.Staked.ToString(),
            UserXp = root.Payload.UserXp.ToString(),
            Balance = root.Payload.Balance.ToString(),
            EarnByHour = root.Payload.EarnByHour.ToString(),
            UserId = root.Payload.Account.UserId ?? "",
            CreatedOn = root.Payload.Account.CreatedOn.ToString(),
            ModifiedOn = root.Payload.Account.ModifiedOn.ToString(),
            Username = root.Payload.Account.Username ?? "",
            HasAvatar = root.Payload.Account.HasAvatar.ToString()
        };
    }
}
 
public class XionData
{
    public string DoneMissions { get; set; }
    public string TotalApprovedReferrals { get; set; }
    public string DailyMintStreak { get; set; }
    public string PartnerNftXpClaimed { get; set; }
    public string RefCode { get; set; }
    public string Staked { get; set; }
    public string UserXp { get; set; }
    public string Balance { get; set; }
    public string EarnByHour { get; set; }
    public string UserId { get; set; }
    public string CreatedOn { get; set; }
    public string ModifiedOn { get; set; }
    public string Username { get; set; }
    public string HasAvatar { get; set; }
}
 
// Использование
var x = new Xion(project, instance);
var stats = x.GetWithHeaders("https://api-xion2.bonusblock.io/api/xion/get-status");
 
// Вместо медленного dynamic
var data = OptimizedXionParser.ParseWithJObject(stats);
// или еще быстрее
var data2 = OptimizedXionParser.ParseWithTypes(stats);
 
// Теперь все данные доступны через свойства
Console.WriteLine($"Balance: {data.Balance}");
Console.WriteLine($"Username: {data.Username}");

Полезные утилиты для работы с JSON

public static class JsonHelpers
{
    // Красивое форматирование JSON
    public static string PrettyPrint(string json)
    {
        var parsedJson = JObject.Parse(json);
        return parsedJson.ToString(Formatting.Indented);
    }
    
    // Сравнение двух JSON объектов
    public static bool AreJsonEqual(string json1, string json2)
    {
        var obj1 = JObject.Parse(json1);
        var obj2 = JObject.Parse(json2);
        return JToken.DeepEquals(obj1, obj2);
    }
    
    // Слияние двух JSON объектов
    public static string MergeJson(string json1, string json2)
    {
        var obj1 = JObject.Parse(json1);
        var obj2 = JObject.Parse(json2);
        
        obj1.Merge(obj2, new JsonMergeSettings
        {
            MergeArrayHandling = MergeArrayHandling.Union
        });
        
        return obj1.ToString();
    }
    
    // Извлечение всех значений определенного типа
    public static List<T> ExtractAllValues<T>(string json, string propertyName)
    {
        var root = JObject.Parse(json);
        return root.Descendants()
            .OfType<JProperty>()
            .Where(p => p.Name == propertyName)
            .Select(p => p.Value.Value<T>())
            .ToList();
    }
}

Рекомендации по выбору подхода

Используйте dynamic когда:

  • Структура JSON неизвестна или часто меняется
  • Нужен быстрый прототип
  • Производительность не критична

Используйте JObject/JToken когда:

  • Нужна гибкость без потери производительности
  • Требуется манипулировать JSON (добавлять/удалять свойства)
  • Структура JSON частично известна

Используйте типизированные классы когда:

  • Структура JSON стабильна и известна
  • Важна производительность
  • Нужна поддержка IntelliSense и проверка типов
  • Проект долгосрочный и требует поддержки

Заключение

Выбор метода парсинга JSON зависит от конкретной задачи. Для производственного кода рекомендуется использовать типизированные классы или JObject, избегая dynamic из-за проблем с производительностью. Помните, что правильная обработка ошибок и валидация данных так же важны, как и сам парсинг.