将反序列化为接口的对象转换回其原始类型

Cast object, de-serialized as an interface, back to its original type

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 classes,它们都实现了 IAnimal 接口,其中包含我需要的所有属性和方法。反序列化时,我需要让他们再次拥有 IAnmial 接口。

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

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

我认为如果你想要动态方式那么你必须在序列化和反序列化过程中做以下事情。

var jsonAnimal = JsonConvert.SerializeObject(dog1, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All
            });

IAnimal animal = JsonConvert.DeserializeObject(jsonAnimal, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All
            }) as IAnimal;

报告的错误是因为无法实例化接口并且默认情况下,Json.NET没有rules/knowledge如何从任意映射合适的具体类型接口。然而,仅仅使用一个 non-abstract 基础 class 是不够的,与接口不同,它可以被实例化。如果这样做,将在反序列化时创建基类型的实例:这是不正确的,因为原始 class 类型应该被实例化。

要使 round-trip 进程处理多态基类型,例如 IAnimal(或 Animal 基 class),关于 具体类型的足够信息需要保存在JSON中。在反序列化时,此信息用于创建 原始具体类型 .

的实例

鉴于上面确定的 issue/approach 和规定的限制,可以使用 IAnimal 接口上的 custom JsonConverter with a JsonConverterAttribute 解决此任务+.

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

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

  • 属性允许在不明确将其添加到序列化或反序列化的情况下使用转换器call-sites.

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


当引用 Json.NET 版本 12 并添加 Newtonsoft.Json 作为附加命名空间时,此代码作为程序在 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}] 并使用 reflection/attributes 将“dog”映射到 Dog(一种已知的IAnimal 的亚型)。但是,“更好”的域编码不属于原始问题,因此可以根据需要在改进的 follow-up 中进行探索。

最后一点,当库不能保证使用 Json.NET 并且它“必须”用于 serialize/deserialize 时,仍然可以手动 map-to/from Object-Graph 表示(例如字典、数组和基元)可以通过库 round-tripped。这仍然涉及在 map-to 上对具体类型进行编码,以便它可以用于在 map-from.

上创建正确的实例