首页 > 解决方案 > 将带有键的嵌套 JObject 反序列化为 List

问题描述

我需要使用 Newtonsoft.Json 将 json 反序列化回对象实例。

但是,它是一个列表类型的对象,条目的键对我很有用。

我不知道如何在不手动映射字段的情况下自动反序列化。

这是回应:

{
    coins: {
        365Coin: {
            id: 74,
            tag: "365",
            algorithm: "Keccak",
            lagging: true,
            listed: false,
            status: "No available stats",
            testing: false
        },
        Aiden: {
            id: 65,
            tag: "ADN",
            algorithm: "Scrypt-OG",
            lagging: true,
            listed: false,
            status: "No available stats",
            testing: false
        },
        Adzcoin: {
            id: 157,
            tag: "ADZ",
            algorithm: "X11",
            lagging: false,
            listed: false,
            status: "Active",
            testing: false
        }
        ... [With various key representing the name of coins]
    }
}

完整回复:https ://whattomine.com/calculators.json

我对这门课的最佳猜测是:

internal class WhatToMineCalculatorsResponse
{
    // Should be Dictionary???
    [JsonProperty("coins")]
    public IList<WhatToMineCalculatorResponse> Coins { get; set; }
}

internal class WhatToMineCalculatorResponse
{
    // I want the key set in this field
    public string Name { get; set; }

    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("tag")]
    public string Symbol { get; set; }

    [JsonProperty("status")]
    public string Status { get; set; }

    [JsonProperty("algorithm")]
    public string Algo { get; set; }

    [JsonProperty("listed")]
    public bool IsListed { get; set; }
}

请注意,我希望将密钥包含在我的班级中,而不是作为字典的密钥。以后很难找回密钥。

标签: c#json.net

解决方案


您不能完全通过属性指定IList<T>for someT应序列化为 JSON 对象。正如其序列化指南中所解释的,Newtonsoft 将字典和哈希表映射到 JSON 对象,但将所有其他枚举、列表和数组映射到 JSON 数组。相反,您将不得不使用自定义JsonConverter.

首先,定义以下转换器:

internal class WhatToMineCalculatorResponseListConverter : KeyedListToJsonObjectConverterBase<WhatToMineCalculatorResponse>
{
    protected override string KeyPropertyUnderlyingName => nameof(WhatToMineCalculatorResponse.Name);
}

public abstract class KeyedListToJsonObjectConverterBase<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsArray)
            return false;
        return typeof(IList<T>).IsAssignableFrom(objectType);
    }

    protected abstract string KeyPropertyUnderlyingName { get; }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Get the key property name from the underlying name
        var itemContract = serializer.ContractResolver.ResolveContract(typeof(T)) as JsonObjectContract;
        if (itemContract == null)
            throw new JsonSerializationException(string.Format("type {0} is not serialized as a JSON object"));
        var keyProperty = itemContract.Properties.Where(p => p.UnderlyingName == KeyPropertyUnderlyingName).SingleOrDefault();
        if (keyProperty == null)
            throw new JsonSerializationException(string.Format("Key property {0} not found", KeyPropertyUnderlyingName));

        // Validate initial token.
        if (reader.SkipComments().TokenType == JsonToken.Null)
            return null;
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException(string.Format("Unexpected token {0} at {1}", reader.TokenType, reader.Path));

        // Allocate the List<T>.  (It might be some subclass of List<T>, so use the default creator.
        var list = existingValue as ICollection<T> ?? (ICollection<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();

        // Process each key/value pair.
        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.Comment:
                    break;
                case JsonToken.EndObject:
                    return list;
                case JsonToken.PropertyName:
                    {
                        // Get the name.
                        var name = (string)reader.Value;
                        reader.ReadAndAssert();
                        // Load the object
                        var jItem = JObject.Load(reader);
                        // Add the name property
                        jItem.Add(keyProperty.PropertyName, name);
                        // Deserialize the item and add it to the list.
                        list.Add(jItem.ToObject<T>(serializer));
                    }
                    break;
                default:
                    {
                        throw new JsonSerializationException(string.Format("Unexpected token {0} at {1}", reader.TokenType, reader.Path));
                    }
            }
        }
        // Should not come here.
        throw new JsonSerializationException("Unclosed object at path: " + reader.Path);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // Get the key property name from the underlying name
        var itemContract = serializer.ContractResolver.ResolveContract(typeof(T)) as JsonObjectContract;
        if (itemContract == null)
            throw new JsonSerializationException(string.Format("type {0} is not serialized as a JSON object"));
        var keyProperty = itemContract.Properties.Where(p => p.UnderlyingName == KeyPropertyUnderlyingName).SingleOrDefault();
        if (keyProperty == null)
            throw new JsonSerializationException(string.Format("Key property {0} not found", KeyPropertyUnderlyingName));

        var converters = serializer.Converters.ToArray();
        var list = (IEnumerable<T>)value;
        writer.WriteStartObject();
        foreach (var item in list)
        {
            var jItem = JObject.FromObject(item, serializer);
            var name = (string)jItem[keyProperty.PropertyName];
            jItem.Remove(keyProperty.PropertyName);
            writer.WritePropertyName(name);
            jItem.WriteTo(writer, converters);
        }
        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static JsonReader SkipComments(this JsonReader reader)
    {
        while (reader.TokenType == JsonToken.Comment && reader.Read())
            ;
        return reader;
    }

    public static void ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
        {
            new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
        }
    }
}

然后,您可以按如下方式反序列化:

var settings = new JsonSerializerSettings
{
    Converters = { new WhatToMineCalculatorResponseListConverter() },
};
var root = JsonConvert.DeserializeObject<WhatToMineCalculatorsResponse>(responseString, settings);

笔记:

  • 在序列化 a并且类型具有要用作 JSON 对象属性名称的特定属性KeyedListToJsonObjectConverterBase<T>的任何情况下,都可以重用基类转换器。只需覆盖并返回实际的 .Net 属性名称(不是序列化名称)。List<T>TKeyPropertyUnderlyingName

  • 代码看起来有点复杂,因为我做了KeyedListToJsonObjectConverterBase<T>足够通用的处理来处理 key 属性为只读的情况,例如:

    internal class WhatToMineCalculatorResponse
    {
        readonly string _name;
    
        public WhatToMineCalculatorResponse(string name)
        {
            this._name = name;
        }
    
        // I want the key set in this field
        public string Name { get { return _name; } }
    
        // Remainder of class unchanged
    }
    

在这里工作.Net 小提琴。


推荐阅读