【问题标题】:Deserialize collection of interface-instances?反序列化接口实例的集合?
【发布时间】:2019-02-13 05:05:58
【问题描述】:

我想通过 json.net 序列化这段代码:

public interface ITestInterface
{
    string Guid {get;set;}
}

public class TestClassThatImplementsTestInterface1
{
    public string Guid { get;set; }
}

public class TestClassThatImplementsTestInterface2
{
    public string Guid { get;set; }
}


public class ClassToSerializeViaJson
{
    public ClassToSerializeViaJson()
    {             
         this.CollectionToSerialize = new List<ITestInterface>();
         this.CollectionToSerialize.add( new TestClassThatImplementsTestInterface2() );
         this.CollectionToSerialize.add( new TestClassThatImplementsTestInterface2() );
    }
    List<ITestInterface> CollectionToSerialize { get;set; }
}

我想用 json.net 序列化/反序列化 ClassToSerializeViaJson。序列化正在工作,但反序列化给了我这个错误:

Newtonsoft.Json.JsonSerializationException:无法创建 ITestInterface 类型的实例。类型是接口或抽象类,不能实例化。

那么我该如何反序列化List&lt;ITestInterface&gt; 集合呢?

【问题讨论】:

  • 你已经尝试了什么?您甚至阅读过 JSON.NET 的文档吗?!我很确定序列化和反序列化是此类文档首先要介绍的内容之一。
  • 是的,序列化正在工作,但是当我尝试反序列化时出现错误:Newtonsoft.Json.JsonSerializationException:无法创建 ITestInterface 类型的实例。类型是接口或抽象类,不能实例化。
  • 那么也许你应该在你的问题中提出这个问题?而不是问这种开放式的“我该怎么办?” “它不起作用”的问题,您确实需要提供所有信息,这是什么错误?它发生在哪里?到目前为止,您尝试过什么解决方法?请使用这些内容编辑您的问题,以便社区可以更好地帮助您,而不是标记您的问题。
  • Nicholas Westby 在awesome article 中提供了一个很好的解决方案

标签: c# json.net


【解决方案1】:

我在自己尝试执行此操作时发现了这个问题。在我实现Piotr Stapp's(Garath's) answer 之后,我被它看起来多么简单而震惊。如果我只是实现一个方法,该方法已经传递了我想要实例化的确切类型(作为字符串),为什么库不自动绑定它?

我实际上发现我不需要任何自定义活页夹,只要我告诉它我正在做的事情,Json.Net 就能完全满足我的需求。

序列化时:

string serializedJson = JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple
});

反序列化时:

var deserializedObject = JsonConvert.DeserializeObject<ClassToSerializeViaJson>(serializedJson, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects
});

相关文档:Serialization Settings for Json.NETTypeNameHandling setting

【讨论】:

  • 这确实有效,但人们应该明白这会大大增加 json 的大小。对我来说,它将输出文件的大小增加了 10 倍以上。
  • 请注意,将具体类型名称放入生成的 JSON 中可能会被视为泄漏实现细节,如果在您自己的代码之外的任何地方使用 JSON,则绝对不干净。此外,如果您要反序列化的 JSON 来自外部源,那么期望它包含您的类型名称是不合理的。
  • 使用此解决方案,应清理传入类型以避免潜在的安全隐患。详情请见TypeNameHandling caution in Newtonsoft Json
  • Ingero's answer 下面通过使用 TypeNameHandling.Auto 而不是 TypeNameHandling.Objects 来最大限度地减少对 $types 的污染
  • 我同意@JacekGorgoń;我从 ASP.NET 核心控制器自动获取我的 JSON。
【解决方案2】:

下面是你想要做的完整工作示例:

public interface ITestInterface
{
    string Guid { get; set; }
}

public class TestClassThatImplementsTestInterface1 : ITestInterface
{
    public string Guid { get; set; }
    public string Something1 { get; set; }
}

public class TestClassThatImplementsTestInterface2 : ITestInterface
{
    public string Guid { get; set; }
    public string Something2 { get; set; }
}

public class ClassToSerializeViaJson
{
    public ClassToSerializeViaJson()
    {
        this.CollectionToSerialize = new List<ITestInterface>();
    }
    public List<ITestInterface> CollectionToSerialize { get; set; }
}

public class TypeNameSerializationBinder : SerializationBinder
{
    public string TypeFormat { get; private set; }

    public TypeNameSerializationBinder(string typeFormat)
    {
        TypeFormat = typeFormat;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        var resolvedTypeName = string.Format(TypeFormat, typeName);
        return Type.GetType(resolvedTypeName, true);
    }
}

class Program
{
    static void Main()
    {
        var binder = new TypeNameSerializationBinder("ConsoleApplication.{0}, ConsoleApplication");
        var toserialize = new ClassToSerializeViaJson();

        toserialize.CollectionToSerialize.Add(
            new TestClassThatImplementsTestInterface1()
            {
                Guid = Guid.NewGuid().ToString(), Something1 = "Some1"
            });
        toserialize.CollectionToSerialize.Add(
            new TestClassThatImplementsTestInterface2()
            {
                Guid = Guid.NewGuid().ToString(), Something2 = "Some2"
            });

        string json = JsonConvert.SerializeObject(toserialize, Formatting.Indented, 
            new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                Binder = binder
            });
        var obj = JsonConvert.DeserializeObject<ClassToSerializeViaJson>(json, 
            new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                Binder = binder 
            });

        Console.ReadLine();
    }
}

【讨论】:

  • 您可以在活页夹中使用 FullyQualifiedName 而不是名称,您不必传递 TypeFormat
  • 这对我来说效果很好。自从不久前回答了这个问题以来的一些更新。看来他们现在正在使用接口ISerializationBinder,而不是覆盖SerializationBinder。同样在BindToName 中,我使用serializedType.AssemblyQualifiedName 而不是Name,并且将assemblyName 和完全限定的typeName 都传递给BindToType,所以现在不需要构造函数。然后用var resolvedTypeName = string.Format("{0}, {1}", typeName,assemblyName); and everything should work without providing the namespace &amp; assembly in the constructor更新BindToType
  • 谢谢你,真的拯救了一天!
【解决方案3】:

我也对 Garath 的简单性感到惊讶,并且得出的结论是 Json 库可以自动完成。但我也认为它比 Ben Jenkinson 的答案更简单(尽管我可以看到它已被 json 库的开发人员自己修改)。根据我的测试,您只需将 TypeNameHandling 设置为 Auto,如下所示:

var objectToSerialize = new List<IFoo>();
// TODO: Add objects to list
var jsonString = JsonConvert.SerializeObject(objectToSerialize,
       new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });
var deserializedObject = JsonConvert.DeserializeObject<List<IFoo>>(jsonString, 
       new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

来自TypeNameHandling Enumeration documentation

Auto:当对象的类型为 serialized 与其声明的类型不同。请注意,这 默认不包含根序列化对象。

【讨论】:

  • 比原帖晚了几年,但这是一种最新的工作方式。您付出的代价是类型名称作为 JSON 中每个对象的“$type”属性输出,但在许多情况下都可以。
  • 这是正确的解决方案,也适用于更复杂的数据结构。我花了几天的时间才找到它...您可以添加到设置TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple 以尽可能缩短类型输出
  • 同样使用此解决方案,应该清理传入类型以避免潜在的安全隐患。详情请见TypeNameHandling caution in Newtonsoft Json
  • 需要注意的是,这仅适用于您在创建 json 时可以控制它的情况。如果它是从其他任何地方创建的,则此反序列化将不起作用。
【解决方案4】:

使用默认设置,您不能。 JSON.NET 无法知道如何反序列化数组。但是,您可以指定要为您的接口类型使用的类型转换器。要了解如何执行此操作,请参阅此页面:http://blog.greatrexpectations.com/2012/08/30/deserializing-interface-properties-using-json-net/

您还可以在这个 SO 问题中找到有关此问题的信息:Casting interfaces for deserialization in JSON.NET

【讨论】:

    【解决方案5】:

    这是一个老问题,但我想我会添加一个更深入的答案(以我写的文章的形式):http://skrift.io/articles/archive/bulletproof-interface-deserialization-in-jsonnet/

    TLDR:您可以使用 JSON 转换器来确定要反序列化哪个类以使用您喜欢的任何自定义逻辑,而不是配置 Json.NET 以将类型名称嵌入序列化 JSON。 p>

    这样做的好处是您可以重构类型而不必担心反序列化中断。

    【讨论】:

      【解决方案6】:

      可以使用 JSON.NET 和 JsonSubTypes 属性来完成:

      [JsonConverter(typeof(JsonSubtypes))]
      [JsonSubtypes.KnownSubTypeWithProperty(typeof(Test1), "Something1")]
      [JsonSubtypes.KnownSubTypeWithProperty(typeof(Test2), "Something2")]
      public interface ITestInterface
      {
          string Guid { get; set; }
      }
      
      public class Test1 : ITestInterface
      {
          public string Guid { get; set; }
          public string Something1 { get; set; }
      }
      
      public class Test2 : ITestInterface
      {
          public string Guid { get; set; }
          public string Something2 { get; set; }
      }
      

      简单地说:

      var fromCode = new List<ITestInterface>();
      // TODO: Add objects to list
      var json = JsonConvert.SerializeObject(fromCode);
      var fromJson = JsonConvert.DeserializeObject<List<ITestInterface>>(json);
      

      【讨论】:

        【解决方案7】:

        我想反序列化未由我的应用程序序列化的 JSON,因此我需要手动指定具体实现。我已经扩展了 Nicholas 的答案。

        假设我们有

        public class Person
        {
            public ILocation Location { get;set; }
        }
        

        以及

        的具体实例
        public class Location: ILocation
        {
            public string Address1 { get; set; }
            // etc
        }
        

        在这个类中添加

        public class ConfigConverter<I, T> : JsonConverter
        {
            public override bool CanWrite => false;
            public override bool CanRead => true;
            public override bool CanConvert(Type objectType)
            {
                return objectType == typeof(I);
            }
            public override void WriteJson(JsonWriter writer,
                object value, JsonSerializer serializer)
            {
                throw new InvalidOperationException("Use default serialization.");
            }
        
            public override object ReadJson(JsonReader reader,
                Type objectType, object existingValue,
                JsonSerializer serializer)
            {
                var jsonObject = JObject.Load(reader);
                var deserialized = (T)Activator.CreateInstance(typeof(T));
                serializer.Populate(jsonObject.CreateReader(), deserialized);
                return deserialized;
            }
        }
        

        然后用 JsonConverter 属性定义你的接口

        public class Person
        {
            [JsonConverter(typeof(ConfigConverter<ILocation, Location>))]
            public ILocation Location { get;set; }
        }
        

        【讨论】:

        • 一些实现使用serializer.Populate 其他使用jsonObject.ToObject。有区别吗?
        【解决方案8】:

        与 Inrego 的回答几乎重复,但值得进一步解释:

        如果您使用TypeNameHandling.Auto,则它仅在需要时包含类型/程序集名称(即接口和基类/派生类)。所以你的 JSON 更干净、更小、更具体。

        与 XML/SOAP 相比,这不是它的主要卖点之一吗?

        【讨论】:

          【解决方案9】:

          尽可能避免使用 TypeNameHandling.Auto,尤其是用户可控制的值。

          您需要为集合类型编写自己的反序列化器。

          我没有重复其他已经发布样板转换器代码的人(特别是Nicholas Westby,他的博客文章非常有用并链接了above),我已经包含了反序列化接口集合的相关更改(我有一个枚举接口属性以区分实现者):

              public override object ReadJson(JsonReader reader,
                  Type objectType, object existingValue,
                  JsonSerializer serializer)
              {
                  Collection<T> result = new Collection<T>();
                  var array = JArray.Load(reader);
                  foreach (JObject jsonObject in array)
                  { 
                      var rule = default(T);
                      var value = jsonObject.Value<string>("MyDistinguisher");
                      MyEnum distinguisher;
                      Enum.TryParse(value, out distinguisher);
                      switch (distinguisher)
                      {
                          case MyEnum.Value1:
                              rule = serializer.Deserialize<Type1>(jsonObject.CreateReader());
                              break;
                          case MyEnum.Value2:
                              rule = serializer.Deserialize<Type2>(jsonObject.CreateReader());
                              break;
                          default:
                              rule = serializer.Deserialize<Type3>(jsonObject.CreateReader());
                              break;
                      }
                      result.Add(rule);
                  }
                  return result;
              }
          

          我希望这对下一个寻找接口集合反序列化器的人有所帮助。

          【讨论】:

          • 如何使用 system.text.json 实现?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多