【问题标题】:Not ableTo Serialize Dictionary with Complex key using Json.net无法使用 Json.net 序列化具有复杂键的字典
【发布时间】:2014-08-21 15:47:37
【问题描述】:

我有一个自定义 .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\"}"

但是,如果我将自定义类型作为 Dictionary 中的值,则效果很好

  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 的一些限制或者我做错了什么,有人可以建议吗?

【问题讨论】:

    标签: c# json serialization dictionary json.net


    【解决方案1】:

    您可能不想使用 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&lt;KeyValuePair&lt;T,K&gt;&gt;

    如果您将 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); }
        }
    

    这个技巧很有效,因为 kvp 中的键不会被强制转换为字符串格式。你问为什么是字符串格式?它打败了我。 Dictionary 对象实现了IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt; 接口,因此以与kvps 列表相同的方式对其进行序列化应该没有任何问题,因为这本质上就是字典。有人(詹姆斯牛顿?)在编写 Newtonsoft 字典序列化程序时做出了一个决定,即复杂的键太乱而无法处理。可能有一些我没有考虑过的极端情况使这个问题变得更加棘手。

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

    【讨论】:

    • 我对这个解决方案做了一些调整,发现它是最快实现的,并且在@roger-hill 提到的意义上也是正确的。我的调整是公开 Dictionary 属性,向其中添加 [JsonIgnore],并使用 [JsonProperty] 属性将 KeyValuePairs 列表设为私有。我使用了一个私有的 [JsonConstructor] 来接受 keyValuePairs 列表,但我想它也可以只使用 [JsonProperty] 属性。
    • @bmw15 看起来它不适用于 JsonProperty。您需要让构造函数显式设置 KeyValuePairs 列表,否则 JsonConvert 会简单地忽略该属性,即使使用 [JsonProperty] 也是如此。我尝试了您的方法和 Roger Hill 的方法。
    【解决方案2】:

    Serialization Guide 状态(参见:字典和哈希表部分;感谢@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 类声明中。

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

    【讨论】:

    • 这对我们有用,可以解决复杂字典键的问题。如果在类嵌套中两次使用泛型类和不同的具体实现,我们的自定义 JsonConverter 无法使用复杂键类上的声明性属性 - 我们必须将其作为参数传递给 @ 987654327@,它有效但不太理想。
    • 我也刚刚为复杂的字典键类型工作。其他人请注意 - 这与 JSON.Net 包中的 JSONTypeConverter 内容无关。此外,没有需要为此使用的 JSON 设置。这个答案确实包含您需要做的事情。我发现最初将断点放在被覆盖的函数中很有用,可以更好地了解它们的使用时间和要求它们做什么。
    • 很好的答案,即使它对我不起作用,因为我使用的是 Windows Phone 8.1,其中 System.ComponentModel.TypeConverter 不受支持。
    【解决方案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();
    

    【讨论】:

    • 更好的方法是将 IDictionary 映射到 Dictionary (调用 CreateArrayContract 时),这将阻止 Newtonsoft.Json 添加 Dictionary 类型信息。这也将不需要设置 OverrideCreator。
    【解决方案4】:

    灵感来自 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(...) 用于所有内容

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

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

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

        通过这种方式,每个MyDictionary 对象都被序列化为一个键/值对的数组,对于复杂的键类型也能正常表现:

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

        【讨论】:

          【解决方案7】:

          一切都变得简单

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

          【讨论】:

          • 能否请您添加一些有关如何解决 OPs 错误的详细信息?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-07-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多