Using the Web API to deserialize into a class with an abstract property

I am trying to write a set of classes to represent a particularly complex object, and in one of these classes I have a property that is set as a base (abstract) class of three possible derived classes. I am setting up the ASP.NET Web API to handle serialization and deserialization, which means that by default Json.NET uses JSON. How can I get the Web API to properly deserialize JSON sent via POST or PUT to the corresponding derived class?

A class with an abstract element looks like this (I want the clarity of Xml decorators to be clear, because they work just fine for deserializing xml with XmlSerializer)

[Serializable] public class FormulaStructure { [XmlElement("column", typeof(ColumnStructure))] [XmlElement("function", typeof(FunctionStructure))] [XmlElement("operand", typeof(OperandStructure))] public AFormulaItemStructure FormulaItem; } 

The abstract class is pretty simple:

 [Serializable] public abstract class AFormulaItemStructure { } 

And there are three derivatives of the abstract class:

 [Serializable] public class ColumnStructure: AFormulaItemStructure { [XmlAttribute("type")] public string Type; [XmlAttribute("field")] public string Field; [XmlAttribute("display")] public string Display; } [Serializable] public class FunctionStructure: AFormulaItemStructure { [XmlAttribute("type")] public string Type; [XmlAttribute("name")] public string Name; [XmlElement("parameters")] public string Parameters; } [Serializable] public class OperandStructure: AFormulaItemStructure { [XmlAttribute("type")] public string Type; [XmlElement("left")] public string Left; [XmlElement("right")] public string Right; } 

Currently, using the [DataContract] attributes, Json.NET formatting does not populate the derived class, leaving the null property.


Questions

Is it possible to mix XmlSerializer attributes with DataContractSerializer attributes in one class? I use XmlSerializer because I use xml in the xml that I created, but this can be changed if necessary, since I am developing the xml schema myself.

What is equivalent in Json.NET for [KnownType()] ? Json.NET does not seem to support the version of DataContractSerializer KnownType . Do I need a roll of my JsonConverter to determine the correct type?

How to decorate classes so that DataContractSerializer or DataContractJsonSerializer properly deserialize objects in both Xml and Json? My goal is to put this in an ASP.NET web API, so I want to flexibly generate Xml or Json, depending on the type requested. Is there an alternative formatter that I need to use to work with this complex class if Json.NET does not work?

I need the ability to generate an object on the client side, not necessarily including .NET class names in it.


Testing and refinement

In my testing of the web API, serialization is sent to the client by default:

 {"FormulaItem":{"type":"int","field":"my_field","display":"My Field"}} 

which is perfect for my purposes. Returning to the API and deserializing to the corresponding derived types, however, does not work (it generates null for the property).

Testing Tommy Grovnes below, the DataContractSerializer that he used for testing generates:

 {"FormulaItem":{"__type":"column:#ExpressionStructureExperimentation.Models","display":"My Field","field":"my_field","type":"int"}} 

which doesnโ€™t work for me or for the convenience of maintaining the code (refactoring becomes PITA if I hard-code the entire namespace in JavaScript to create these objects).

+4
source share
3 answers

After we ran into some problems much further compared to the previous answer, I discovered a SerializationBinder class that JSON can use to serialize / deserialize namespaces.

First code

I generated a class to inherit from SerializationBinder :

 public class KnownTypesBinder : System.Runtime.Serialization.SerializationBinder { public KnownTypesBinder() { KnownTypes = new List<Type>(); AliasedTypes = new Dictionary<string, Type>(); } public IList<Type> KnownTypes { get; set; } public IDictionary<string, Type> AliasedTypes { get; set; } public override Type BindToType(string assemblyName, string typeName) { if (AliasedTypes.ContainsKey(typeName)) { return AliasedTypes[typeName]; } var type = KnownTypes.SingleOrDefault(t => t.Name == typeName); if (type == null) { type = Type.GetType(Assembly.CreateQualifiedName(assemblyName, typeName)); if (type == null) { throw new InvalidCastException("Unknown type encountered while deserializing JSON. This can happen if class names have changed but the database or the JavaScript references the old class name."); } } return type; } public override void BindToName(Type serializedType, out string assemblyName, out string typeName) { assemblyName = null; typeName = serializedType.Name; } } 

How it works

Let's say I have a set of classes defined this way:

 public class Class1 { public string Text { get; set; } } public class Class2 { public int Value { get; set; } } public class MyClass { public Class1 Text { get; set; } public Class2 Value { get; set; } } 

Alias โ€‹โ€‹types

This allows me to generate proper names for classes to be serialized / deserialized. In my global.asax file, I use a binder as such:

 KnownTypesBinder binder = new KnownTypesBinder() binder.AliasedTypes["Class1"] = typeof(Project1.Class1); binder.AliasedTypes["WhateverStringIWant"] = typeof(Project1.Class2); var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter; json.SerializerSettings.Binder = binder; 

Now when I serialize, say, MyClass as JSON, I get the following:

 { item: { $type: "Project1.MyClass", Text: { $type: "Class1", Text: "some value" }, Value: { $type: "WhateverStringIWant", Value: 88 } } } 

Known Types

I can also disable assembly information and strictly use the class name by adding information to KnownTypesBinder :

 KnownTypesBinder binder = new KnownTypesBinder() binder.KnownTypes.Add(typeof(Project1.Class1)); binder.KnownTypes.Add(typeof(Project1.Class1)); 

In the two examples given, Class1 referenced the same way. However, if I reorganize Class1 , say, NewClass1 , then this second example will start sending a different name. This may or may not be a big deal, depending on whether you use types or not.

Final thoughts

The advantage of AliasedTypes is that I can give it whatever line I want, and it doesn't matter how much I reorganize the code, the connection between .NET and JavaScript (or any other user there) is not interrupted.

Be careful not to mix AliasedType and KnownType , which have the same class name, because the code is written that AliasType will defeat KnownType . When the binder does not recognize the type (with an alias or known), it will provide the fully qualified name of the type assembly.

+2
source

You can mix, as already mentioned, but I donโ€™t think you need to use the WEB api yourself, but WCF Rest creates xml and json from DataContracts (without Xml tags ..), mark your classes as follows:

 [DataContract] public class FormulaStructure { [DataMember] public AFormulaItemStructure FormulaItem; } [DataContract] [KnownType(typeof(ColumnStructure))] [KnownType(typeof(FunctionStructure))] [KnownType(typeof(OperandStructure))] public abstract class AFormulaItemStructure { } [DataContract(Name="column")] public class ColumnStructure : AFormulaItemStructure { [DataMember(Name="type")] public string Type; [DataMember(Name = "field")] public string Field; [DataMember(Name = "display")] public string Display; } [DataContract(Name="function")] public class FunctionStructure : AFormulaItemStructure { [DataMember(Name = "type")] public string Type; [DataMember(Name = "name")] public string Name; [DataMember(Name = "parameters")] public string Parameters; } [DataContract(Name = "operand")] public class OperandStructure : AFormulaItemStructure { [DataMember(Name = "type")] public string Type; [DataMember(Name = "left")] public string Left; [DataMember(Name = "right")] public string Right; } 

If you need more control over the generated XML / JSON, you may need to configure it further. I used this code to check:

  public static string Serialize(FormulaStructure structure) { using (MemoryStream memoryStream = new MemoryStream()) using (StreamReader reader = new StreamReader(memoryStream)) { var serializer = new DataContractSerializer(typeof(FormulaStructure)); serializer.WriteObject(memoryStream, structure); memoryStream.Position = 0; return reader.ReadToEnd(); } } public static FormulaStructure Deserialize(string xml) { using (Stream stream = new MemoryStream()) { byte[] data = System.Text.Encoding.UTF8.GetBytes(xml); stream.Write(data, 0, data.Length); stream.Position = 0; var deserializer = new DataContractSerializer(typeof(FormulaStructure)); return (FormulaStructure)deserializer.ReadObject(stream); } } 
+2
source

In the end, I broke down and added the .NET class information to the module in string variables to simplify refactoring.

 module.net = {}; module.net.classes = {}; module.net.classes['column'] = "ColumnStructure"; module.net.classes['function'] = "FunctionStructure"; module.net.classes['operand'] = "OperandStructure"; module.net.getAssembly = function (className) { return "MyNamespace.Models." + module.net.classes[className] + ", MyAssembly"; } 

and generated JSON as

 { "FormulaItem": { "$type": module.net.getAssembly('column'), "type": "int", "field": "my_field", "display": "My Field" } } 
0
source