无法使用Json.net使用Complex键序列化Dictionary

时间:2014-07-01 07:02:39

标签: c# json serialization dictionary json.net

我有一个字典,其自定义.net类型为其键。我正在尝试使用JSON.net将此字典序列化为JSON,但是它无法在序列化期间将键转换为适当的值。

class ListBaseClass
{
    public String testA;
    public String testB;
}
-----
var details = new Dictionary<ListBaseClass, string>();
details.Add(new ListBaseClass { testA = "Hello", testB = "World" }, "Normal");
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

这给我 - &gt; &#34; {\&#34; JSonSerialization.ListBaseClass \&#34;:\&#34;正常\&#34;}&#34;

但是,如果我将自定义类型作为字典中的值,则可以正常使用

  var details = new Dictionary<string, ListBaseClass>();
  details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
  var results = Newtonsoft.Json.JsonConvert.SerializeObject(details);
  var data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, ListBaseClass>>(results);

这给我 - &gt; &#34; {\&#34;正常\&#34;:{\&#34;种皮\&#34;:\&#34;你好\&#34; \&#34; TESTB \& #34;:\&#34;世界\&#34;}}&#34;

有人建议如果我遇到Json.net的某些限制或者我做错了吗?

6 个答案:

答案 0 :(得分:25)

Serialization Guide个州(参见Dictionaries和Hashtables一节;感谢@Shashwat的链接):

  

序列化字典时,字典的键是   转换为字符串并用作JSON对象属性名称。该   为密钥编写的字符串可以通过覆盖来自定义   密钥类型的ToString()或实现TypeConverter。一个   TypeConverter还将支持再次转换自定义字符串   反序列化字典时。

我找到了一个有用的示例,说明如何在Microsoft的#34; how-to&#34;上实现这种类型的转换器。页面:

基本上,我需要扩展System.ComponentModel.TypeConverter并覆盖:

bool CanConvertFrom(ITypeDescriptorContext context, Type source);

object ConvertFrom(ITypeDescriptorContext context,
                   System.Globalization.CultureInfo culture, object value);

object ConvertTo(ITypeDescriptorContext context, 
                 System.Globalization.CultureInfo culture, 
                 object value, Type destinationType);

还需要将属性 [TypeConverter(typeof(MyClassConverter))]添加到MyClass类声明中。

有了这些,我就可以自动序列化和反序列化词典

答案 1 :(得分:3)

另一种实现此目的的方法是使用自定义的ContractResolver并设置OverrideCreator。

public class DictionaryAsArrayResolver : DefaultContractResolver
{
    public override JsonContract CreateContract(Type objectType)
    {
        if (IsDictionary(objectType))
        {
            JsonArrayContract contract = base.CreateArrayContract(objectType);
            contract.OverrideCreator = (args) => CreateInstance(objectType);
            return contract;
        }

        return base.CreateContract(objectType);
    }

    internal static bool IsDictionary(Type objectType)
    {
        if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
        {
            return true;
        }

        if (objectType.GetInterface(typeof(IDictionary<,>).Name) != null)
        {
            return true;
        }

        return false;
    }

    private object CreateInstance(Type objectType)
    {
        Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(objectType.GetGenericArguments());
        return Activator.CreateInstance(dictionaryType);
    }
}

用法:

JsonSerializer jsonSerializer = new JsonSerializer();
jsonSerializer.ContractResolver = new DictionaryAsArrayResolver();

答案 2 :(得分:0)

戈登·宾(Gordon Bean)提出的答案(在撰写本文时,标记的响应最高)是一个很好的技巧,但是您可能要谨慎使用它。该解决方案有效,但是它提供了输出的序列化字符串。如果您使用的是JSON,这将给您带来不太理想的结果,因为您确实需要对象的JSON表示形式而不是字符串表示形式。

例如,假设您有一个将唯一的网格点与字符串相关联的数据结构:

class Point
{
    public int x { get; set; }
    public int y { get; set; }
}

public Dictionary<Point,string> Locations { get; set; };

使用TypeConverter替代,在序列化此对象时,将获得该对象的字符串表示形式。 (确切的字符串格式取决于您,这只是一个非常简单的示例)

"Locations": {
  "4,3": "foo",
  "3,4": "bar"
},

当我们真正想要的是:

"Locations": {
  { "x": 4, "y": 3 }: "foo",
  { "x": 3, "y": 4 }: "bar"
},

重写TypeConverter可以很好地对类进行序列化/反序列化,但是这种方法存在一些问题。

首先,这不是JSON,您可能必须编写其他自定义逻辑来处理在其他地方对其进行序列化和反序列化。 (例如,可能是客户端层中的Javascript?)

第二,现在使用该对象的其他任何地方都会喷出该字符串,之前该字符串已正确序列化为对象:

"GridCenterPoint": "0,0",

vs

"GridCenterPoint": { "x": 0, "y": 0 },

后者更合适,因为它是正确的JSON。

是的,您可以稍微控制格式,但是您无法摆脱将其呈现为字符串而不是对象的事实。 (如果您知道解决此限制的方法,请纠正我)。

似乎这个问题不是序列化程序的问题,因为json.net可以在不丢失节奏的情况下咀嚼复杂的对象,因此字典键的处理方式存在问题。如果您尝试使用示例点对象并序列化List甚至是Hashset,则会注意到生成适当的JSON并没有问题,这为我们提供了解决此问题的简便得多的方法。

理想情况下,我们只想告诉json.net将键序列化为任何对象类型,而不是将其强制为字符串,因为我们知道这是可行的。由于这似乎不是一种选择,所以另一种方法是给json.net可以使用的东西:List<KeyValuePair<T,K>>

如果将kvps列表输入到json.net的序列化器中,则将获得您所期望的结果。例如,下面是您可以实现的简单得多的包装器:

    private Dictionary<Point, string> _Locations;
    public List<KeyValuePair<Point, string>> SerializedLocations
    {
        get { return _Locations.ToList(); }
        set { _Locations= value.ToDictionary(x => x.Key, x => x.Value); }
    }

该技巧有效,因为kvp中的键不会强制转换为字符串格式。你问为什么呢?打败我。由于Dictionary对象实现了IEnumerable<KeyValuePair<TKey, TValue>>接口,因此以与kvps列表相同的方式对其进行序列化应该没有任何问题,因为本质上就是kvps列表。

总而言之,戈登·比恩(Gordon Bean)的解决方案很好,但是我认为这要好得多,因为它可以生成实际的json对象,在技术上更简单,并且不会由于替换序列化程序而产生任何副作用。

答案 3 :(得分:0)

灵感来自 gson enableComplexMapKeySerialization 以及它的外观\工作原理:

public class DictionaryAsArrayJsonConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dictionary = (IDictionary)value;

        writer.WriteStartArray();

        var en = dictionary.GetEnumerator();
        while (en.MoveNext())
        {
            writer.WriteStartArray();
            serializer.Serialize(writer, en.Key);
            serializer.Serialize(writer, en.Value);
            writer.WriteEndArray();
        }
        
        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (!CanConvert(objectType))
            throw new Exception(string.Format("This converter is not for {0}.", objectType));

        Type keyType = null;
        Type valueType = null;
        IDictionary result;

        if (objectType.IsGenericType)
        {
            keyType = objectType.GetGenericArguments()[0];
            valueType = objectType.GetGenericArguments()[1];
            var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
            result = (IDictionary)Activator.CreateInstance(dictionaryType);
        }
        else
        {
            result = (IDictionary)Activator.CreateInstance(objectType);
        }

        if (reader.TokenType == JsonToken.Null)
            return null;

        int depth = reader.Depth;
        while (reader.Read())
        {
            if (reader.TokenType == JsonToken.StartArray)
            {
            }
            else if (reader.TokenType == JsonToken.EndArray)
            {
                if (reader.Depth == depth)
                    return result;
            }
            else
            {
                object key = serializer.Deserialize(reader, keyType);
                reader.Read();
                object value = serializer.Deserialize(reader, valueType);
                result.Add(key, value);
            }
        }

        return result;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IDictionary).IsAssignableFrom(objectType);
    }

}

可能创建与 Tal Aloni 代码相同的 json,但作为 JsonConverter 而不是合约。更灵活,因为它可以用于带有 JsonConverterAttribute 的选定属性或带有 JsonSerializerSettings.Converters.Add(...) 的所有内容

答案 4 :(得分:0)

基于@roger-hill 富有洞察力的 response,我创建了以下 JsonConverter,它将 IDictionary 对象转换为 ListKeyValuePair对象。

github link

public class ListDictionaryConverter : JsonConverter
{
    private static (Type kvp, Type list, Type enumerable, Type[] args) GetTypes(Type objectType)
    {
        var args = objectType.GenericTypeArguments;
        var kvpType = typeof(KeyValuePair<,>).MakeGenericType(args);
        var listType = typeof(List<>).MakeGenericType(kvpType);
        var enumerableType = typeof(IEnumerable<>).MakeGenericType(kvpType);

        return (kvpType, listType, enumerableType, args);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var (kvpType, listType, _, args) = GetTypes(value.GetType());
        
        var keys = ((IDictionary)value).Keys.GetEnumerator();
        var values = ((IDictionary)value).Values.GetEnumerator();
        var cl = listType.GetConstructor(Array.Empty<Type>());
        var ckvp = kvpType.GetConstructor(args);
        
        var list = (IList)cl!.Invoke(Array.Empty<object>());
        while (keys.MoveNext() && values.MoveNext())
        {
            list.Add(ckvp!.Invoke(new []{keys.Current, values.Current}));
        }
        
        serializer.Serialize(writer, list);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var (_, listType, enumerableType, args) = GetTypes(objectType);
        
        var list = ((IList)(serializer.Deserialize(reader, listType)));

        var ci = objectType.GetConstructor(new[] {enumerableType});
        if (ci == null)
        {
            ci = typeof(Dictionary<,>).MakeGenericType(args).GetConstructor(new[] {enumerableType});
        }
        
        var dict = (IDictionary) ci!.Invoke(new object[]{ list });

        return dict;
    }

    public override bool CanConvert(Type objectType)
    {
        if (!objectType.IsGenericType) return objectType.IsAssignableTo(typeof(IDictionary));
        
        var args = objectType.GenericTypeArguments;
        return args.Length == 2 && objectType.IsAssignableTo(typeof(IDictionary<,>).MakeGenericType(args));
    }
}

我做了一些测试,这段代码在这些测试中运行良好……但我可能遗漏了一两个极端情况。

答案 5 :(得分:-4)

一切都比较容易

var details = new Dictionary<string, ListBaseClass>();
details.Add("Normal", new ListBaseClass { testA = "Hello", testB = "World" });
var results = Newtonsoft.Json.JsonConvert.SerializeObject(details.ToList());
var data = 
Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<ListBaseClass, string>> results);

样品

 class Program
{
    static void Main(string[] args)
    {
        var  testDictionary = new Dictionary<TestKey,TestValue>()
        {
            {
                new TestKey()
                {
                    TestKey1 = "1",
                    TestKey2 = "2",
                    TestKey5 = 5
                },
                new TestValue()
                {
                    TestValue1 = "Value",
                    TestValue5 = 96
                }
            }
        };

        var json = JsonConvert.SerializeObject(testDictionary);
        Console.WriteLine("=== Dictionary<TestKey,TestValue> ==");
        Console.WriteLine(json);
        // result: {"ConsoleApp2.TestKey":{"TestValue1":"Value","TestValue5":96}}


        json = JsonConvert.SerializeObject(testDictionary.ToList());
        Console.WriteLine("=== List<KeyValuePair<TestKey, TestValue>> ==");
        Console.WriteLine(json);
        // result: [{"Key":{"TestKey1":"1","TestKey2":"2","TestKey5":5},"Value":{"TestValue1":"Value","TestValue5":96}}]


        Console.ReadLine();

    }
}

class TestKey
{
    public string TestKey1 { get; set; }

    public string TestKey2 { get; set; }

    public int TestKey5 { get; set; }
}

class TestValue 
{
    public string TestValue1 { get; set; }

    public int TestValue5 { get; set; }
}