Десериализация json-строки в Словарь (Dictionary<TKey, TValue>)

337
16 января 2017, 18:24
{
    "rates_scores_stats": [{
            "name": 10,
            "value": 3545
        }, {
            "name": 9,
            "value": 1004
        }, {
            "name": 8,
            "value": 820
        }, {
            "name": 7,
            "value": 493
        }, {
            "name": 6,
            "value": 218
        }, {
            "name": 5,
            "value": 138
        }, {
            "name": 4,
            "value": 80
        }, {
            "name": 3,
            "value": 41
        }, {
            "name": 2,
            "value": 26
        }, {
            "name": 1,
            "value": 83
        }
    ],
    "rates_statuses_stats": [{
            "name": "Запланировано",
            "value": 2506
        }, {
            "name": "Смотрю",
            "value": 7861
        }, {
            "name": "Просмотрено",
            "value": 1947
        }, {
            "name": "Отложено",
            "value": 1443
        }, {
            "name": "Брошено",
            "value": 1358
        }
    ]
}

Есть JSON описанный выше. Для десериализации я использую Newtonsoft.Json. В частности, метод JsonConvert.DeserializeObject(json)

Я знаю, что для этого json можно описать типичный класс следующим образом

Тоже самое текстом: http://pastexen.com/code.php?file=aMvORa6As3.cs

Но мне надо превратить это не в коллекцию вспомогательных классов или же коллекцию KeyValuePair. Мне нужен именно словарь, чтобы я мог обратиться к такому объекту следующим образом:

var result = JsonConvert.DeserializeObject<MyClass>(json);
var val = result.rates_scores_stats[10]; // val = 3545
var val1 = result.rates_statuses_stats["Смотрю"]; // val1 = 7861

Можно ли как-нибудь сериализовать это в словарь? Может с помощью get/set как-то извернуться?

P.S.: Я знаю, что в шарпе есть Linq, который позволит сделать что-то типо result.FirstOrDefault(x.value => x.name == "10"), но я пишу переносимую библиотеку и хочу упростить доступ к данным =/

Answer 1

Например, можно десериализовать в JObject, и потом сконвертировать данные вручную:

var obj = JObject.Parse(json);
var rates_scores_stats =
    ((JArray)obj["rates_scores_stats"]).ToDictionary(entry => (int)entry["name"],
                                                     entry => (int)entry["value"]);
var rates_statuses_stats =
    ((JArray)obj["rates_statuses_stats"]).ToDictionary(entry => (string)entry["name"],
                                                       entry => (int)entry["value"]);
var result = new MyClass()
{
    rates_scores_stats = rates_scores_stats,
    rates_statuses_stats = rates_statuses_stats
};

Альтернативный ответ здесь. Немного модифицировав его, можно получить менее «ручное» решение.

Создадим отдельный конвертер (спёрт и модифицирован из ответа по ссылке):

public class JsonGenericDictionaryOrArrayConverter : JsonConverter
{
    // поскольку у вас в JSON'е не key/value, а name/value, нужен специальный класс
    // для десериализации одного элемента массива
    class NameValuePair<N, V>
    {
        public N name { get; set; }
        public V value { get; set; }
    }
    public override bool CanConvert(Type objectType)
    {
        return GetDictionaryKeyValueTypes(objectType).Count() == 1;
    }
    public override bool CanWrite { get { return false; } }
    object ReadJsonGeneric<TKey, TValue>(
        JsonReader reader, Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var tokenType = reader.TokenType;
        var dict = existingValue as IDictionary<TKey, TValue>;
        if (dict == null)
        {
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            dict = (IDictionary<TKey, TValue>)contract.DefaultCreator();
        }
        if (tokenType == JsonToken.StartArray)
        {
            var pairs = new JsonSerializer()
                                .Deserialize<NameValuePair<TKey, TValue>[]>(reader);
            if (pairs == null)
                return existingValue;
            foreach (var pair in pairs)
                dict.Add(pair.name, pair.value);
        }
        else if (tokenType == JsonToken.StartObject)
        {
            // Using "Populate()" avoids infinite recursion.
            // https://github.com/JamesNK/Newtonsoft.Json/blob/
            // ee170dc5510bb3ffd35fc1b0d986f34e33c51ab9/Src/Newtonsoft.Json/Converters/
            // CustomCreationConverter.cs
            serializer.Populate(reader, dict);
        }
        return dict;
    }
    public override object ReadJson(
            JsonReader reader, Type objectType, object existingValue,
            JsonSerializer serializer)
    {
        // Throws an exception if not exactly one.
        var keyValueTypes = GetDictionaryKeyValueTypes(objectType).Single();
        var method = GetType().GetMethod(
                "ReadJsonGeneric",
                BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
        var genericMethod = method.MakeGenericMethod(
                new[] { keyValueTypes.Key, keyValueTypes.Value });
        return genericMethod.Invoke(
                this,
                new object[] { reader, objectType, existingValue, serializer });
    }
    public override void WriteJson(
            JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException();
    }
    static IEnumerable<KeyValuePair<Type, Type>> GetDictionaryKeyValueTypes(Type type)
    {
        foreach (Type intType in GetInterfacesAndSelf(type))
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                var args = intType.GetGenericArguments();
                if (args.Length == 2)
                    yield return new KeyValuePair<Type, Type>(args[0], args[1]);
            }
        }
    }
    static IEnumerable<Type> GetInterfacesAndSelf(Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }
}

Имея этот вспомогательный класс, можно десериализовать просто так:

var settings = new JsonSerializerSettings
{
    Converters = { new JsonGenericDictionaryOrArrayConverter() }
};
var result = JsonConvert.DeserializeObject<MyClass>(jsonString, settings);

Обновление: Для portable library (.NET 4.5.1 + Windows Universal 8.1 + Windows Phone 8.1) у меня скомпилировалось такое:

public class JsonGenericDictionaryOrArrayConverter : JsonConverter
{
    // поскольку у вас в JSON'е не key/value, а name/value, нужен специальный класс
    // для десериализации одного элемента массива
    class NameValuePair<N, V>
    {
        public N name { get; set; }
        public V value { get; set; }
    }
    public override bool CanConvert(Type objectType)
    {
        return GetDictionaryKeyValueTypes(objectType).Count() == 1;
    }
    public override bool CanWrite { get { return false; } }
    public object ReadJsonGeneric<TKey, TValue>(
        JsonReader reader, Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var tokenType = reader.TokenType;
        var dict = existingValue as IDictionary<TKey, TValue>;
        if (dict == null)
        {
            var contract = serializer.ContractResolver.ResolveContract(objectType);
            dict = (IDictionary<TKey, TValue>)contract.DefaultCreator();
        }
        if (tokenType == JsonToken.StartArray)
        {
            var pairs = new JsonSerializer()
                            .Deserialize<NameValuePair<TKey, TValue>[]>(reader);
            if (pairs == null)
                return existingValue;
            foreach (var pair in pairs)
                dict.Add(pair.name, pair.value);
        }
        else if (tokenType == JsonToken.StartObject)
        {
            // Using "Populate()" avoids infinite recursion.
            // https://github.com/JamesNK/Newtonsoft.Json/blob/
            // ee170dc5510bb3ffd35fc1b0d986f34e33c51ab9/Src/Newtonsoft.Json/Converters/
            // CustomCreationConverter.cs
            serializer.Populate(reader, dict);
        }
        return dict;
    }
    public override object ReadJson(
            JsonReader reader, Type objectType, object existingValue,
            JsonSerializer serializer)
    {
        // Throws an exception if not exactly one.
        var keyValueTypes = GetDictionaryKeyValueTypes(objectType).Single();
        var method = GetType().GetTypeInfo().GetDeclaredMethod("ReadJsonGeneric");
        var genericMethod = method.MakeGenericMethod(
            new[] { keyValueTypes.Key, keyValueTypes.Value });
        return genericMethod.Invoke(
                this,
                new object[] { reader, objectType, existingValue, serializer });
    }
    public override void WriteJson(
            JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException();
    }
    static IEnumerable<KeyValuePair<Type, Type>> GetDictionaryKeyValueTypes(Type type)
    {
        foreach (Type intType in GetInterfacesAndSelf(type))
        {
            if (intType.GetTypeInfo().IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                var args = intType.GetTypeInfo().GenericTypeArguments;
                // вроде бы именно GenericTypeArguments, а не Parameters
                if (args.Length == 2)
                    yield return new KeyValuePair<Type, Type>(args[0], args[1]);
            }
        }
    }
    static IEnumerable<Type> GetInterfacesAndSelf(Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.GetTypeInfo().IsInterface)
            return new[] { type }.Concat(type.GetTypeInfo().ImplementedInterfaces);
        else
            return type.GetTypeInfo().ImplementedInterfaces;
    }
}
READ ALSO
C# Сброс таймера

C# Сброс таймера

ЗдравствуйтеПишу приложение "угадай слово"

654
Получить значение ListView

Получить значение ListView

Добрый вечер, подскажите пожалуйста, есть ListView:

362
Как получить название переменной в виде строки?

Как получить название переменной в виде строки?

Заранее прошу прощения если вопрос кому-то покажется глупым и бессмысленным, но для меня он имеет смыслНеобходима функция которая на входе...

308
Несколько баз данных Symfony 3

Несколько баз данных Symfony 3

Symfony 3Проект использует основную базу данных

407