不能使用Json.net序列化具有复杂键的字典

本文关键字:复杂 字典 序列化 Json net 不能 | 更新日期: 2023-09-27 18:11:07

我有一个自定义。net类型作为其关键的字典。我试图序列化这个字典JSON使用JSON.net,但是它不能在序列化过程中将键转换为适当的值。

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);

This Give me -> "{'"JSonSerialization。ListBaseClass '",'"正常'"}"

然而,如果我在字典中使用自定义类型作为值,它会很好地工作

  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);

这给我——>"{'"正常'":{'"甲壳'",'"你好'",'"的'":'"'"}}"

有人可以建议如果我击中一些限制的Json.net或我做错了什么?

不能使用Json.net序列化具有复杂键的字典

这是人们在开始使用序列化器时遇到的一个非常常见的问题。虽然您发现的限制存在的原因有点令人费解,但有一个简单的解决方案。

几年前我第一次写这个答案的时候,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": { "x": 0, "y": 0 },

现在序列化为:

"GridCenterPoint": "0,0",

您可以稍微控制一下TypeConverter的格式,但是您无法摆脱它被呈现为字符串而不是对象的事实。

这个问题不是序列化器的问题,因为Json。. NET咀嚼复杂的对象而不丢失一个节拍,这是处理字典键的方式的问题。如果您尝试使用示例对象,并序列化List甚至Hashset,您会注意到生成正确的JSON没有问题。这给了我们一个更简单的方法来解决这个问题。

理想情况下,我们只想告诉Json。NET将键序列化为任何对象类型,而不是强制它是字符串。由于这似乎不是一个选项,另一种方法是提供Json。. NET的一些东西,它可以工作:一个List<KeyValuePair<T,K>> .

如果您将KeyValuePairs列表提供给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); }
    }

来自@bmw15评论的更新:
你可以把Dictionary道具设为public,给它添加一个[JsonIgnore],然后KeyValuePairs私有列表,带有[JsonProperty]属性

这个技巧有效,因为kvp中的键不会被强制转换为字符串格式。您可能会问,为什么类型转换器生成字符串格式?我都快崩溃了。Dictionary对象实现了IEnumerable<KeyValuePair<TKey, TValue>>接口,因此以与kvps列表相同的方式序列化它应该没有任何问题,因为这本质上就是字典。有人(James Newton?)在编写Newtonsoft字典序列化器时做出了一个决定,即复杂的键太混乱而无法处理。可能有一些我没有考虑到的极端情况,使这个问题变得更加棘手。

我认为这是一个更好的解决方案,因为它生成实际的JSON对象,技术上更简单,并且不会产生替换序列化器所产生的任何副作用。

序列化指南说明(参见:字典和哈希表;谢谢@Shashwat的链接):

序列化字典时,字典的键为转换为字符串并用作JSON对象属性名。的为键写入的字符串可以通过重写自定义ToString()用于键类型或通过实现TypeConverter。一个TypeConverter还将支持重新转换自定义字符串当反序列化字典时。

我在微软的"how-to"页面上找到了一个关于如何实现这种类型转换器的有用示例:

    实现类型转换器(参见值转换的类型转换器一节)。
实际上,我需要扩展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类声明中。

有了这些,我就能够自动地序列化和反序列化字典

另一种方法是使用自定义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();

在@roger-hill的回答之后,我想出了一个轻量级的解决方案来达到同样的结果:

    [JsonArray]
    public class MyDictionary<K, V> : Dictionary<K, V>
    {
    }

通过这种方式,每个MyDictionary对象都被序列化为键/值对数组,在复杂键类型下也能正常工作:

[{
    "Key": ...,
    "Value": ...
}, ...]

灵感来自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(…)

根据@roger-hill的深刻回应,我创建了以下JsonConverter,将IDictionary对象转换为KeyValuePair对象的List

github联系

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));
    }
}

我已经做了一些测试,这段代码在这些测试中工作得很好…但我可能遗漏了一两个边缘情况。

一切都更容易了

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; }
}