Сериализация JSON массива с полиморфными объектами
Возможно ли это с помощью стандартного Java JavascriptSerializer/JsonDataContractSerializer или внешних парсеров, для сериализации массива объектов с использованием метода обертки, включая тип объекта?
Например, чтобы сгенерировать этот JSON из списка:
[{ 'dog': { ...dog properties... } },
{ 'cat': { ...cat properties... } }]
вместо типичного:
[{ ...dog properties... },
{ ...cat properties... }]
Это выполнимо в Java с Jackson, используя атрибут JsonTypeInfo.As.WRAPPER_OBJECT.
Ответы
Ответ 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);
Так как у меня уже есть посетитель и я все делаю через интерфейсы, то, вероятно, так же легко сделать свой собственный сериализатор, так или иначе.......