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

47

我有一个以自定义 .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);

这个给我-->"{\"JSonSerialization.ListBaseClass\":\"Normal\"}"

然而如果我在字典中使用自定义类型作为值,它能够正常工作。

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

这是我的输出 --> "{\"Normal\":{\"testA\":\"Hello\",\"testB\":\"World\"}}"

有人能否建议我是否遇到了Json.net的某些限制或者我做错了什么?

7个回答

42
这是一个当人们开始使用序列化器时经常遇到的问题。虽然你发现的限制有点令人困惑,但有一个简单的解决方案。
当我第一次写这个答案的时候,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>。
如果你将一组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); }
    }

这个技巧有效是因为键值对中的键并不会被强制转换为字符串格式。你问为什么类型转换器会生成字符串格式?我也不知道。字典对象实现了IEnumerable<KeyValuePair<TKey, TValue>>接口,所以在序列化时应该没有问题,就像键值对列表一样,因为字典本质上就是这样的。在编写Newtonsoft字典序列化器时,有人(詹姆斯·牛顿?)决定复杂的键太麻烦了。可能还有一些我没有考虑到的特殊情况,使得这个问题更加棘手。
我认为这是一个更好的解决方案,因为它生成了实际的JSON对象,技术上更简单,并且不会产生任何替换序列化器所导致的副作用。

1
我对这个解决方案进行了一些微调,发现它是最快实现且在@roger-hill提到的意义上也是正确的。我的微调是将Dictionary属性设为public,添加[JsonIgnore],并将KeyValuePair列表设为private,并带有[JsonProperty]属性。我使用了一个私有的[JsonConstructor]来接受keyValuePairs列表,但我想仅使用[JsonProperty]属性也可以。 - bmw15
@bmw15 看起来仅使用JsonProperty是不起作用的。您需要显式地设置KeyValuePairs列表,或者即使使用[JsonProperty],JsonConvert也会忽略该属性。我尝试了您的方法和Roger Hill的方法。 - TheForgot3n1
我尝试了很多方法,但这是我找到的最佳解决方案。 - Clément Andraud

34

序列化指南(请参见“字典和哈希表”部分;感谢@Shashwat提供的链接)说明:

  

当序列化字典时,字典的键被转换为字符串并用作JSON对象属性名称。可以通过重写键类型的ToString()方法或实现TypeConverter来自定义键的字符串。TypeConverter还支持在反序列化字典时将自定义字符串转换回原始键。

我在Microsoft的 “如何” 页面上找到了一个有用的示例,演示了如何实现这样的类型转换:

基本上,我需要扩展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);

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

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


这项解决方案用于处理复杂字典键的问题,对我们非常有效。在类嵌套中使用了带有不同具体实现的通用类两次时,我们的自定义JsonConverter不能够使用复杂键类上的声明性属性,必须将其作为参数传递给 JsonConvert.SerializeObject,虽然可以使用但并不理想。 - mungflesh
我刚刚为一个复杂的字典键类型使其工作。对于其他人的提示 - 这与JSON.Net包中的JSONTypeConverter无关。此外,没有需要使用的JSON设置。这个答案确实包含了你需要做的一切。我发现最初在重写函数内设置断点有助于更好地理解它们何时被使用以及被要求做什么。 - StayOnTarget
非常好的答案,即使它对我没有用,因为我正在使用不支持System.ComponentModel.TypeConverter的Windows Phone 8.1。 - user3141326

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

更好的方法是在调用CreateArrayContract时将IDictionary<,>映射到Dictionary<,>,这将防止Newtonsoft.Json添加字典类型信息。这也使设置OverrideCreator变得不必要。 - Tal Aloni

2

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

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

这样,每个MyDictionary对象都会被序列化为一个键/值对的数组,并且在处理复杂键类型时也能正常工作:

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

0

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

我已经进行了一些测试,这段代码在那些测试中运行良好...但是可能会漏掉一两个边缘情况。


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

一切都更容易

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

7
请问您能否提供更多细节说明,以解释这个解决方案是如何解决 OP 的错误的? - Aditya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接