Is it possible with .NET standard JavascriptSerializer/JsonDataContractSerializer or external parsers, to serialize objects array using a wrapper approach including the object type?
For example, to generate this JSON from a List:
[{ 'dog': { ...dog properties... } },
{ 'cat': { ...cat properties... } }]
instead of typica开发者_JS百科l:
[{ ...dog properties... },
{ ...cat properties... }]
This is doable in Java with Jackson using JsonTypeInfo.As.WRAPPER_OBJECT attribute.
Json.NET has a neat solution for this. There is a setting that intelligently adds type information - declare it like this:
new JsonSerializer { TypeNameHandling = TypeNameHandling.Auto };
This will determine whether type embedding is required and add it where necessary. Lets say I had the following classes:
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; } }
}
Notice the Message Body is of type object, and that Manager subclasses Person. If I serialize a Message with a Department Body that has a single Manager I get this:
{
"Body":
{
"$type":"Department, MyAssembly",
"Employees":[
{
"$type":"Manager, MyAssembly",
"Name":"Tim"
}]
}
}
Notice how it's added the $type property to describe the Department and Manager types. If I now add a Person to the Employees list and change the Message Body to be of type Department like this:
public class Message
{
public Department Body { get; set; }
}
then the Body type annotation is no longer needed and the new Person is not annotated - absence of annotation assumes the element instance is of the declared array type. The serialized format becomes:
{
"Body":
{
"Employees":[
{
"$type":"Manager, MyAssembly",
"Name":"Tim"
},
{
"Name":"James"
}]
}
}
This is an efficient approach - type annotation is only added where required. While this is .NET specific, the approach is simple enough to handle that deserializers/message types on other platforms should be fairly easily extended to handle this.
I'd be reticent about using this in a public API though, as it is non-standard. In that case you'd want to avoid polymorphism, and make versioning and type information very explicit properties in the message.
Probably the closest that I've seen is to use the JavaScriptSerializer
and pass in a JavaScriptTypeResolver
to the constructor. It doesn't produce JSON formatted exactly as you have it in your question, but it does have a _type
field that describes the type of the object that's being serialized. It can get a little ugly, but maybe it will do the trick for you.
Here's my sample code:
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));
}
}
And the result looks like this:
[
{"__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"}
]
I'm using the SimpleTypeConverter, which is part of the framework by default. You can create your own to shorten what's returned by __type
.
EDIT: If I create my own JavaScriptTypeResolver
to shorten the type name returned, I can produce something like this:
[
{"__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"}
]
Using this converter class:
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;
}
}
And just passing it into my JavaScriptSerializer
constructor (instead of the SimpleTypeConverter
).
I hope this helps!
1) You can use a Dictionary<string,object> to do the job,...
[{"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) Or using the JavaScriptConverter it is possible to handle the serialization for a type.
[{"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[])));
}
}
I got this done as per the question. Wasn't exactly straightforward, but here goes. There is not an easy way to do this in Json.NET. Would be awesome if it supported a pre-serialization callback where you could insert your own type information, but that is another story.
I have an interface (IShape) that the polymorphic classes implement. One of the classes is a container (composite pattern) and contains a list of contained objects. I did this with interfaces but the same concept applies to base classes.
public class Container : IShape
{
public virtual List<IShape> contents {get;set;}
// implement interface methods
As per the question, I want this to serialize as:
"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 }} },
etc.
To do this I wrote a wrapper class. Each of the objects that implement the interface has a property in the wrapper. This sets the property name in the serializer. Conditional serialization ensures the correct property is used. All interface methods are delegated to the wrapped class, and visitor Accept() calls are directed to the wrapped class. This means in contexts using the interface, the Wrapped or unwrapped classes will behave the same.
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; }
I also have a visitor pattern implemented to traverse the object graph. I had this already due to the rest of the software design, but if you just have a simple collection you could just iterate the collection and add the wrapper.
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){}
}
The visitor replaces the content of the Container class with wrapped versions of the classes.
Serialize, and it produces the required output.
SerializationVisitor s = new SerializationVisitor();
s.Visit(label);
Since I already have the Visitor and am doing everything through interfaces it is probably just as easy to do my own serializer, anyway.......
精彩评论