I am trying to extend the JSON.net example given here http://james.newtonking.com/projects/json/help/CustomCreationConverter.html
I have another sub class deriving
The project JsonSubTypes implement a generic converter that handle this feature with the helps of attributes.
For the concrete sample provided here is how it works:
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")]
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Employee : Person
{
public string Department { get; set; }
public string JobTitle { get; set; }
}
public class Artist : Person
{
public string Skill { get; set; }
}
[TestMethod]
public void Demo()
{
string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
"{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
"{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]";
var persons = JsonConvert.DeserializeObject<IReadOnlyCollection<Person>>(json);
Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);
}
Just thought i would share a solution also based on this that works with the Knowntype attribute using reflection , had to get derived class from any base class, solution can benefit from recursion to find the best matching class though i didn't need it in my case, matching is done by the type given to the converter if it has KnownTypes it will scan them all until it matches a type that has all the properties inside the json string, first one to match will be chosen.
usage is as simple as:
string json = "{ Name:\"Something\", LastName:\"Otherthing\" }";
var ret = JsonConvert.DeserializeObject<A>(json, new KnownTypeConverter());
in the above case ret will be of type B.
JSON classes:
[KnownType(typeof(B))]
public class A
{
public string Name { get; set; }
}
public class B : A
{
public string LastName { get; set; }
}
Converter code:
/// <summary>
/// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer
/// Selected class will be the first class to match all properties in the json object.
/// </summary>
public class KnownTypeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on JObject
System.Attribute[] attrs = System.Attribute.GetCustomAttributes(objectType); // Reflection.
// Displaying output.
foreach (System.Attribute attr in attrs)
{
if (attr is KnownTypeAttribute)
{
KnownTypeAttribute k = (KnownTypeAttribute) attr;
var props = k.Type.GetProperties();
bool found = true;
foreach (var f in jObject)
{
if (!props.Any(z => z.Name == f.Key))
{
found = false;
break;
}
}
if (found)
{
var target = Activator.CreateInstance(k.Type);
serializer.Populate(jObject.CreateReader(),target);
return target;
}
}
}
throw new ObjectNotFoundException();
// Populate the object properties
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
A lot of the times the implementation will exist in the same namespace as the interface. So, I came up with this:
public class InterfaceConverter : JsonConverter
{
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.ReadFrom(reader);
var typeVariable = this.GetTypeVariable(token);
if (TypeExtensions.TryParse(typeVariable, out var implimentation))
{ }
else if (!typeof(IEnumerable).IsAssignableFrom(objectType))
{
implimentation = this.GetImplimentedType(objectType);
}
else
{
var genericArgumentTypes = objectType.GetGenericArguments();
var innerType = genericArgumentTypes.FirstOrDefault();
if (innerType == null)
{
implimentation = typeof(IEnumerable);
}
else
{
Type genericType = null;
if (token.HasAny())
{
var firstItem = token[0];
var genericTypeVariable = this.GetTypeVariable(firstItem);
TypeExtensions.TryParse(genericTypeVariable, out genericType);
}
genericType = genericType ?? this.GetImplimentedType(innerType);
implimentation = typeof(IEnumerable<>);
implimentation = implimentation.MakeGenericType(genericType);
}
}
return JsonConvert.DeserializeObject(token.ToString(), implimentation);
}
public override bool CanConvert(Type objectType)
{
return !typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.IsInterface || typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.GetGenericArguments().Any(t => t.IsInterface);
}
protected Type GetImplimentedType(Type interfaceType)
{
if (!interfaceType.IsInterface)
{
return interfaceType;
}
var implimentationQualifiedName = interfaceType.AssemblyQualifiedName?.Replace(interfaceType.Name, interfaceType.Name.Substring(1));
return implimentationQualifiedName == null ? interfaceType : Type.GetType(implimentationQualifiedName) ?? interfaceType;
}
protected string GetTypeVariable(JToken token)
{
if (!token.HasAny())
{
return null;
}
return token.Type != JTokenType.Object ? null : token.Value<string>("$type");
}
}
Therefore, you can include this globally like so:
public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new InterfaceConverter()
}
};