Подтвердить что ты не робот

Сериализация JSON массива с полиморфными объектами

Возможно ли это с помощью стандартного Java JavascriptSerializer/JsonDataContractSerializer или внешних парсеров, для сериализации массива объектов с использованием метода обертки, включая тип объекта?

Например, чтобы сгенерировать этот JSON из списка:

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

вместо типичного:

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

Это выполнимо в Java с Jackson, используя атрибут JsonTypeInfo.As.WRAPPER_OBJECT.

4b9b3361

Ответ 1

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

Обратите внимание, что Тело сообщения имеет объект типа, и этот менеджер подклассифицирует Person. Если я сериализую сообщение с органом отдела, у которого есть один менеджер, я получаю следующее:

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

Обратите внимание, как он добавил свойство $type для описания типов Департамента и Менеджера. Если теперь я добавлю список Person to the Employees и изменим Body Message на тип Department следующим образом:

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

тогда аннотации типа Body больше не нужны, и новый Человек не аннотируется - отсутствие аннотации предполагает, что экземпляр элемента имеет тип объявленного массива. Сериализованный формат:

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

Это эффективный подход. Аннотации типа добавляются только там, где это необходимо. Хотя это специфично для .NET, этот подход достаточно прост, чтобы обрабатывать десериализаторы/типы сообщений на других платформах, которые должны быть довольно легко расширены, чтобы справиться с этим.

Я бы сдержанно говорил об использовании этого в публичном API, хотя он нестандартен. В этом случае вы хотите избежать полиморфизма и сделать информацию о версии и типе очень явными свойствами в сообщении.

Ответ 2

Вероятно, ближайший, который я видел, - это использовать 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.

EDIT. Если я создаю свой собственный 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).

Надеюсь, это поможет!

Ответ 3

1) Вы можете использовать словарь < string, object > чтобы выполнить эту работу,...

[{ "Cat": { "Имя" : "Pinky" }}, { "Cat": { "Имя" : "Винки" }}, { "Собака": { "Имя" : "Макс" } }]

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 можно обрабатывать сериализацию для типа.

[{ "кошки": { "Всеядные": истинно}}, { "муравьед": { "насекомоядных" ложь}}, { "муравьед": { "насекомоядных": истинно}}]

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() посетителей направляются в упакованный класс. Это означает, что в контексте, использующем интерфейс, обернутые или развернутые классы будут вести себя одинаково.

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

Так как у меня уже есть посетитель и я все делаю через интерфейсы, то, вероятно, так же легко сделать свой собственный сериализатор, так или иначе.......