首页 > 解决方案 > 将反序列化为接口的对象转换回其原始类型

问题描述

namespace Animals
{
    interface IAnimal
    {
        string MakeNoise();
    }

    class Dog : IAnimal
    {
        public string MakeNoise()
        {
            return "Woof";
        }
    }

    class Cat : IAnimal
    {
        public string MakeNoise()
        {
            return "Miauw";
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Dog dog1 = new Dog();
            dog1.Name = "Fikkie";

            var jsonAnimal = JsonConvert.SerializeObject(dog1);
            IAnimal animal = JsonConvert.DeserializeObject<IAnimal>(jsonAnimal) as IAnimal;
            animal.MakeNoise();
        }
    }
}

我正在序列化和存储几个 Dog 和 Cat 类,它们都实现了 IAnimal 接口,其中包含我需要的所有属性和方法。反序列化时,我需要让它们再次拥有 IAnmial 接口。

DeserializeObject 现在抛出错误:'无法创建 Animals.IAnimal 类型的实例。类型是接口或抽象类,不能实例化。

我无法更改 Newtonsoft 执行,因为这发生在一个不可访问的类中,所以不幸的是 TypeNameHandling = TypeNameHandling.All 是不可能的。该方法确实需要一个类型,所以它似乎是我在这个例子中的确切实现。

标签: c#interfacecastingjson.net

解决方案


报告的错误是因为无法实例化接口,并且默认情况下,Json.NET 没有关于如何从任意接口映射合适的具体类型的规则/知识。但是,仅使用与接口不同,可以实例化的非抽象基类是不够的。如果这样做了,将在反序列化时创建一个基类型的实例:这是不正确的,因为原始类类型应该被实例化。

对于通过多态基类型(例如 IAnimal(或 Animal 基类))工作的往返过程,需要将有关具体类型的足够信息保存在 JSON 中。在反序列化时,此信息用于创建原始具体类型的实例。

鉴于上述问题/方法以及规定的限制,可以使用IAnimal 接口+上带有JsonConverterAttribute的自定义 JsonConverter来解决此任务

  • 转换器负责存储元数据并创建原始具体类型的实例

    在提供的实现中,它通过TypeNameHandling使用 Json.NET 的内置支持, 其中“在序列化 JSON 时包含 [s] 类型信息并读取 [s] 类型信息,以便在反序列化 JSON 时创建 [原始] 类型。”< /p>

  • 该属性允许使用转换器而无需将其显式添加到序列化或反序列化调用站点

+如果IAnimal添加了属性,或者可序列化的包装器类型将转换器属性添加到 IAnimal 属性,则此方法将起作用。作为一个潜在的缺点,这种方法需要执行合同保证——即。使用 Json.NET。


Newtonsoft.Json当引用 Json.NET 版本 12 并添加为附加命名空间时,此代码作为程序在 LINQPad 中运行。

// using Newtonsoft.Json

public void Main() {
    var dog = new Dog();
    var json = JsonConvert.SerializeObject(dog);
    // json -> {"$type":"UserQuery+Dog, query_mtutnt"}
    var anAnimal = JsonConvert.DeserializeObject<IAnimal>(json);
    Console.WriteLine($"{anAnimal.GetType().Name} says {anAnimal.Noise}!");
    // -> Dog says Woof!
}

[JsonConverterAttribute(typeof(AnimalConverter))]
public interface IAnimal
{
    [JsonIgnore] // don’t save value in JSON
    string Noise { get; }
}

public class Dog : IAnimal
{
    public string Noise => "Woof";
}

public class Cat : IAnimal
{
    public string Noise => "Meow";
}

internal sealed class AnimalConverter : JsonConverter
{
    // Have to prevent the inner serialization from infinite recursion as
    // this code is leveraging the built-in TypeNameHandling support.
    // This approach will need updates if there are nested IAnimal usages.
    [ThreadStatic]
    private static bool TS_Converting;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var mySerializer = JsonSerializer.Create(new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });

        try
        {
            TS_Converting = true;
            mySerializer.Serialize(writer, value);
        }
        finally
        {
            TS_Converting = false;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var mySerializer = JsonSerializer.Create(new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });

        return mySerializer.Deserialize(reader);
    }

    public override bool CanRead
    {
        get { return true; }
    }

    public override bool CanWrite
    {
        get { return !TS_Converting; }
    }

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

从生成的 JSON 中{"$type":"UserQuery+Dog, query_mtutnt"}可以看出,如果类型的命名空间或程序集发生更改,对完整类型名称进行编码的通用方法可能会出现问题。(通常,由于随机程序集名称的变化,JSON 不能在 LINQPad 运行之间传输。)

如果这是我的项目,我可能会编写转换器以使用 JSON 结构,例如["dog", {"age": 2}]并使用反射/属性将“狗”映射到 Dog(IAnimal 的已知子类型)。但是,“更好”的域编码不在最初的问题范围内,可以根据需要在精细的后续行动中进行探索。

最后一点,当库不能保证使用 Json.NET 并且它“必须”用于序列化/反序列化时,仍然可以手动映射到/从 Object-Graph 表示(例如字典,数组和原语),可以通过库往返。这仍然涉及对 map-to 上的具体类型进行编码,以便它可以用于在 map-from 上创建正确的实例。


推荐阅读