Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using a custom type discriminator to tell JSON.net which type of a class hierarchy to deserialize

Suppose I have the following class hierarchy:

public abstract class Organization {     /* properties related to all organizations */ }  public sealed class Company : Organization {      /* properties related to companies */ }   public sealed class NonProfitOrganization : Organization {      /* properties related to non profit organizations */ } 

Is it possible to have json.net use property (say "type" or "discriminator") to determine which type the object when it deserializes the organization? For example, the following should deserialize an instance of Company.

{    "type": "company"    /* other properties related to companies */ } 

And the following should deserialize an instance of NonProfitOrganization.

{    "type": "non-profit"    /* other properties related to non profit */ } 

When I call the following:

Organization organization = JsonConvert.DeserializeObject<Organization>(payload); 

where payload is the above JSON snippets. I had a look at setting the "TypeNameHandling" on properties or classes but it serializes the whole .NET type, which isn't "portable" between the client and server when the classes are defined in different namespaces and assemblies.

I'd rather define the type is a neutral manner which clients written in any language can use to determine the actual type of the object type being serialized.

like image 971
bloudraak Avatar asked Jun 19 '12 10:06

bloudraak


2 Answers

In case you are still looking, here is an example: http://james.newtonking.com/archive/2011/11/19/json-net-4-0-release-4-bug-fixes.aspx

This will allow you to create a table based mapping:

public class TypeNameSerializationBinder : SerializationBinder {     public TypeNameSerializationBinder(Dictionary<Type, string> typeNames = null)     {         if (typeNames != null)         {             foreach (var typeName in typeNames)             {                 Map(typeName.Key, typeName.Value);             }         }     }      readonly Dictionary<Type, string> typeToName = new Dictionary<Type, string>();     readonly Dictionary<string, Type> nameToType = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);      public void Map(Type type, string name)     {         this.typeToName.Add(type, name);         this.nameToType.Add(name, type);     }      public override void BindToName(Type serializedType, out string assemblyName, out string typeName)     {         var name = typeToName.Get(serializedType);         if (name != null)         {             assemblyName = null;             typeName = name;         }         else         {             assemblyName = serializedType.Assembly.FullName;             typeName = serializedType.FullName;                         }     }      public override Type BindToType(string assemblyName, string typeName)     {         if (assemblyName == null)         {             var type = this.nameToType.Get(typeName);             if (type != null)             {                 return type;             }         }         return Type.GetType(string.Format("{0}, {1}", typeName, assemblyName), true);     } } 

The code has a slight defect in that if a type name mapping is attempted where the type is unique but the name is already used, the Map method will throw an exception after the type-to-name mapping is already added leaving the table in an inconsistent state.

like image 152
eulerfx Avatar answered Sep 22 '22 16:09

eulerfx


To take eulerfx's answer further; I wanted to apply DisplayName attribute to a class and have that automatically become the type name used; to that end:

public class DisplayNameSerializationBinder : DefaultSerializationBinder {     private Dictionary<string, Type> _nameToType;     private Dictionary<Type, string> _typeToName;      public DisplayNameSerializationBinder()     {         var customDisplayNameTypes =             this.GetType()                 .Assembly                 //concat with references if desired                 .GetTypes()                 .Where(x => x                     .GetCustomAttributes(false)                     .Any(y => y is DisplayNameAttribute));          _nameToType = customDisplayNameTypes.ToDictionary(             t => t.GetCustomAttributes(false).OfType<DisplayNameAttribute>().First().DisplayName,             t => t);          _typeToName = _nameToType.ToDictionary(             t => t.Value,             t => t.Key);      }      public override void BindToName(Type serializedType, out string assemblyName, out string typeName)     {         if (false == _typeToName.ContainsKey(serializedType))         {             base.BindToName(serializedType, out assemblyName, out typeName);             return;         }          var name = _typeToName[serializedType];          assemblyName = null;         typeName = name;     }      public override Type BindToType(string assemblyName, string typeName)     {         if (_nameToType.ContainsKey(typeName))             return _nameToType[typeName];          return base.BindToType(assemblyName, typeName);     } } 

and usage example:

public class Parameter {     public string Name { get; set; } };  [DisplayName("bool")] public class BooleanParameter : Parameter { }  [DisplayName("string")] public class StringParameter : Parameter {     public int MinLength { get; set; }     public int MaxLength { get; set; } }  [DisplayName("number")] public class NumberParameter : Parameter {     public double Min { get; set; }     public double Max { get; set; }     public string Unit { get; set; } }  [DisplayName("enum")] public class EnumParameter : Parameter {     public string[] Values { get; set; } }  internal class Program {     private static void Main(string[] args)     {         var parameters = new Parameter[]         {             new BooleanParameter() {Name = "alive"},             new StringParameter() {Name = "name", MinLength = 0, MaxLength = 10},             new NumberParameter() {Name = "age", Min = 0, Max = 120},             new EnumParameter() {Name = "status", Values = new[] {"Single", "Married"}}         };          JsonConvert.DefaultSettings = () => new JsonSerializerSettings         {             Binder = new DisplayNameSerializationBinder(),             TypeNameHandling = TypeNameHandling.Auto,             NullValueHandling = NullValueHandling.Ignore,             DefaultValueHandling = DefaultValueHandling.Ignore,             Formatting = Formatting.Indented,             ContractResolver = new CamelCasePropertyNamesContractResolver()         };          var json = JsonConvert.SerializeObject(parameters);         var loadedParams = JsonConvert.DeserializeObject<Parameter[]>(json);         Console.WriteLine(JsonConvert.SerializeObject(loadedParams));       } } 

output:

[   {     "$type": "bool",     "name": "alive"   },   {     "$type": "string",     "maxLength": 10,     "name": "name"   },   {     "$type": "number",     "max": 120.0,     "name": "age"   },   {     "$type": "enum",     "values": [       "Single",       "Married"     ],     "name": "status"   } ] 
like image 20
Meirion Hughes Avatar answered Sep 20 '22 16:09

Meirion Hughes