【问题标题】:JSON serialization of array with polymorphic objects具有多态对象的数组的 JSON 序列化
【发布时间】:2011-07-08 09:39:30
【问题描述】:

.NET 标准 JavascriptSerializer/JsonDataContractSerializer 或外部解析器是否可以使用包含对象类型的包装方法序列化对象数组?

例如,从 List 生成这个 JSON:

[{ 'dog': { ...dog properties... } },
 { 'cat': { ...cat properties... } }]

而不是典型的:

[{ ...dog properties... },
 { ...cat properties... }]

这在 Java 中使用 JsonTypeInfo.As.WRAPPER_OBJECT 属性在 Jackson 中是可行的。

【问题讨论】:

  • 嘿,您找到解决方案了吗?我目前正面临类似的问题?服务器(Java,带有 Jersey 的 Glassfish)将对象序列化为 JSON,客户端(C#)需要反序列化它。使用 XML 时一切正常...

标签: .net json serialization polymorphism


【解决方案1】:

我见过的最接近的可能是使用JavaScriptSerializer 并将JavaScriptTypeResolver 传递给构造函数。它不会生成格式与您在问题中完全相同的 JSON,但它确实有一个 _type 字段来描述正在序列化的对象的类型。它可能会有点难看,但也许它会为你解决问题。

这是我的示例代码:

public abstract class ProductBase
{
    public String Name { get; set; }
    public String Color { get; set; }
}

public class Drink : ProductBase
{
}

public class Product : ProductBase
{
}

class Program
{
    static void Main(string[] args)
    {
        List<ProductBase> products = new List<ProductBase>()
        {
            new Product() { Name="blah", Color="Red"},
            new Product(){ Name="hoo", Color="Blue"},
            new Product(){Name="rah", Color="Green"},
            new Drink() {Name="Pepsi", Color="Brown"}
        };

        JavaScriptSerializer ser = new JavaScriptSerializer(new SimpleTypeResolver());

        Console.WriteLine(ser.Serialize(products));    
    }
}

结果如下所示:

[
  {"__type":"TestJSON1.Product, TestJSON1, Version=1.0.0.0, Culture=neutral, Publ
icKeyToken=null","Name":"blah","Color":"Red"},
  {"__type":"TestJSON1.Product, Test
JSON1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Name":"hoo","Colo
r":"Blue"},
  {"__type":"TestJSON1.Product, TestJSON1, Version=1.0.0.0, Culture=neu
tral, PublicKeyToken=null","Name":"rah","Color":"Green"},
  {"__type":"TestJSON1.Dr
ink, TestJSON1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Name":"P
epsi","Color":"Brown"}
]

我使用的是 SimpleTypeConverter,默认情况下它是框架的一部分。您可以创建自己的来缩短 __type 返回的内容。

编辑:如果我创建自己的JavaScriptTypeResolver 来缩短返回的类型名称,我可以生成如下内容:

[
  {"__type":"TestJSON1.Product","Name":"blah","Color":"Red"},
  {"__type":"TestJSON1.Product","Name":"hoo","Color":"Blue"},
  {"__type":"TestJSON1.Product","Name":"rah","Color":"Green"},
  {"__type":"TestJSON1.Drink","Name":"Pepsi","Color":"Brown"}
]

使用这个转换器类:

public class MyTypeResolver : JavaScriptTypeResolver
{
    public override Type ResolveType(string id)
    {
        return Type.GetType(id);
    }

    public override string ResolveTypeId(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type");
        }

        return type.FullName;
    }
}

然后将它传递给我的 JavaScriptSerializer 构造函数(而不是 SimpleTypeConverter)。

我希望这会有所帮助!

【讨论】:

  • 感谢您的详细回答(我会赞成)。我知道这个解决方案,但想与无法接收那些“__type”属性的 Java 实现的客户端进行互操作。
  • 非常感谢,这正是我想要的!
  • @PeterMorris - 不客气。实际上,我最终也写了一篇关于它的博客文章。这是一个有趣的问题。 geekswithblogs.net/DavidHoerster/archive/2012/01/06/…
【解决方案2】:

Json.NET 对此有一个巧妙的解决方案。有一个设置可以智能地添加类型信息 - 像这样声明它:

new JsonSerializer { TypeNameHandling = TypeNameHandling.Auto };

这将确定是否需要类型嵌入并在必要时添加。假设我有以下课程:

public class Message
{
    public object Body { get; set; }
}

public class Person
{
    public string Name { get; set; }
}

public class Manager : Person
{

}

public class Department
{
    private List<Person> _employees = new List<Person>();
    public List<Person> Employees { get { return _employees; } }
}

请注意,消息正文是对象类型,并且 Manager 是 Person 的子类。如果我使用具有单个经理的部门正文序列化消息,我会得到:

{
    "Body":
    {
        "$type":"Department, MyAssembly",
        "Employees":[
            {
                "$type":"Manager, MyAssembly",
                "Name":"Tim"
            }]
    }
}

注意它是如何添加 $type 属性来描述 Department 和 Manager 类型的。如果我现在将一个人添加到员工列表并将消息正文更改为部门类型,如下所示:

public class Message
{
    public Department Body { get; set; }
}

然后不再需要 Body 类型注释并且新的 Person 没有注释 - 没有注释假定元素实例是声明的数组类型。序列化格式变为:

{
    "Body":
    {
        "Employees":[
            {
                "$type":"Manager, MyAssembly",
                "Name":"Tim"
            },
            {
                "Name":"James"
            }]
    }
}

这是一种有效的方法——类型注释只在需要的地方添加。虽然这是特定于 .NET 的,但该方法非常简单,可以处理其他平台上的反序列化器/消息类型应该相当容易地扩展以处理此问题。

我不会在公共 API 中使用它,因为它是非标准的。在这种情况下,您需要避免多态性,并使版本控制和类型信息在消息中具有非常明确的属性。

【讨论】:

  • 可以创建一个 SerializationBinder 派生类,它可以为类型映射稍微友好的字符串。然后将其绑定到 SerializerSettings.Binder 属性。这仍然没有我想要的那么干净。最好有一个可以添加到类中的属性来指定类型代码应该是什么。
【解决方案3】:

1) 您可以使用 Dictionary 来完成这项工作,...

[{"Cat":{"Name":"Pinky"}},{"Cat":{"Name":"Winky"}},{"Dog":{"Name":"Max"} }]

public class Cat 
{
    public string Name { get; set; }
}

public class Dog 
{
    public string Name { get; set; }
}


    internal static void Main()
    {
        List<object> animals = new List<object>();
        animals.Add(new Cat() { Name = "Pinky" });
        animals.Add(new Cat() { Name = "Winky" });
        animals.Add(new Dog() { Name = "Max" });
        // Convert every item in the list into a dictionary
        for (int i = 0; i < animals.Count; i++)
        {
            var animal = new Dictionary<string, object>();
            animal.Add(animals[i].GetType().Name, animals[i]);
            animals[i] = animal;
        }
        var serializer = new JavaScriptSerializer();
        var json = serializer.Serialize(animals.ToArray());


        animals = (List<object>)serializer.Deserialize(json, animals.GetType());
        // convert every item in the dictionary back into a list<object> item
        for (int i = 0; i < animals.Count; i++)
        {
            var animal = (Dictionary<string, object>)animals[i];
            animal = (Dictionary<string, object>)animal.Values.First();
            animals[i] = animal.Values.First();
        }
    }

2) 或者使用 JavaScriptConverter 可以处理类型的序列化。

[{"cat":{"Omnivore":true}},{"aardvark":{"Insectivore":false}},{"aardvark":{"Insectivore":true}}]

abstract class AnimalBase { }

class Aardvark : AnimalBase
{
    public bool Insectivore { get; set; }
}

class Dog : AnimalBase
{
    public bool Omnivore { get; set; }
}

class AnimalsConverter : JavaScriptConverter
{
    private IDictionary<string, Type> map;

    public AnimalsConverter(IDictionary<string, Type> map) { this.map = map; }

    public override IEnumerable<Type> SupportedTypes
    {
        get { return new Type[]{typeof(AnimalBase)}; }
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        var result = new Dictionary<string, object>();
        var type = obj.GetType();
        var name = from x in this.map where x.Value == type select x.Key;
        if (name.Count<string>() == 0)
            return null;
        var value = new Dictionary<string, object>();
        foreach (var prop in type.GetProperties())
        {
            if(!prop.CanRead) continue;
            value.Add(prop.Name, prop.GetValue(obj, null));
        }
        result.Add(name.First<string>(), value);
        return result;
    }

    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        var keys = from x in this.map.Keys where dictionary.ContainsKey(x) select x;
        if (keys.Count<string>() <= 0) return null;
        var key = keys.First<string>();
        var poly = this.map[key];
        var animal = (AnimalBase)Activator.CreateInstance(poly);
        var values = (Dictionary<string, object>)dictionary[key];
        foreach (var prop in poly.GetProperties())
        {
            if(!prop.CanWrite) continue;
            var value = serializer.ConvertToType(values[prop.Name], prop.PropertyType);
            prop.SetValue(animal, value, null);
        }
        return animal;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var animals = new List<AnimalBase>();
        animals.Add(new Dog() { Omnivore = true });
        animals.Add(new Aardvark() { Insectivore = false });
        animals.Add(new Aardvark() { Insectivore = true });
        var convertMap = new Dictionary<string, Type>();
        convertMap.Add("cat", typeof(Dog));
        convertMap.Add("aardvark", typeof(Aardvark));
        var converter = new AnimalsConverter(convertMap);
        var serializer = new JavaScriptSerializer();
        serializer.RegisterConverters(new JavaScriptConverter[] {converter});
        var json = serializer.Serialize(animals.ToArray());
        animals.Clear();
        animals.AddRange((AnimalBase[])serializer.Deserialize(json, typeof(AnimalBase[])));
    }
}

【讨论】:

    【解决方案4】:

    我按照问题完成了这项工作。不是很简单,但这里有。在 Json.NET 中没有简单的方法可以做到这一点。如果它支持预序列化回调,您可以在其中插入自己的类型信息,那就太棒了,但那是另一回事了。

    我有一个多态类实现的接口 (IShape)。其中一个类是容器(复合模式)并包含包含对象的列表。我用接口做到了这一点,但同样的概念也适用于基类。

    public class Container : IShape
    {
        public virtual List<IShape> contents {get;set;}
        // implement interface methods
    

    根据问题,我希望将其序列化为:

      "container": {
        "contents": [
          {"box": { "TopLeft": {"X": 0.0,"Y": 0.0},"BottomRight": {"X": 1.0, "Y": 1.0} } },
          {"line": {"Start": { "X": 0.0,"Y": 0.0},"End": {"X": 1.0,"Y": 1.0 }} },
    

    等等

    为此,我编写了一个包装类。每个实现接口的对象在包装器中都有一个属性。这会在序列化程序中设置属性名称。条件序列化确保使用正确的属性。所有接口方法都委托给被包装的类,访问者的 Accept() 调用被定向到被包装的类。这意味着在使用接口的上下文中,Wrapped 或 unwrapped 类的行为将相同。

        public class SerializationWrapper : IShape
        {
            [JsonIgnore]
            public IShape Wrapped { get; set; }
            // Accept method for the visitor - redirect visitor to the wrapped class
            // so visitors will behave the same with wrapped or unwrapped.
            public void Accept(IVisitor visitor) => Wrapped.Accept(visitor);
    
            public bool ShouldSerializeline() => line != null;
            // will serialize as line : { ...
            public Line line { get =>Wrapped as Line;}
    
            public bool ShouldSerializebox() => box != null;
            public Box box { get => Wrapped as Box; }
    
            public bool ShouldSerializecontainer() => container != null;
            public Container container { get => Wrapped as Container; }
    
            // IShape methods delegated to Wrapped
            [JsonIgnore]
            public Guid Id { get => Wrapped.Id; set => Wrapped.Id = value; }
    

    我还实现了一个访问者模式来遍历对象图。由于软件设计的其余部分,我已经有了这个,但如果你只有一个简单的集合,你可以迭代集合并添加包装器。

        public class SerializationVisitor : IVisitor
        {
            public void Visit(IContainer shape)
            {
                // replace list items with wrapped list items
                var wrappedContents = new List<IShape>();
                shape.Contents.ForEach(s => { wrappedContents.Add(new SerializationWrapper(){ Wrapped = s}); s.Accept(this); });
                shape.Contents = wrappedContents;
            }
    
            public void Visit(ILine shape){}
            public void Visit(IBox shape){}
        }
    

    访问者将 Container 类的内容替换为类的包装版本。

    序列化,它会产生所需的输出。

            SerializationVisitor s = new SerializationVisitor();
            s.Visit(label);
    

    因为我已经有了访问者并且正在通过接口做所有事情,所以我自己的序列化器可能同样容易,无论如何......

    【讨论】:

      猜你喜欢
      • 2011-10-29
      • 1970-01-01
      • 1970-01-01
      • 2021-02-11
      • 1970-01-01
      • 2020-07-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多