Casting interfaces for deserialization in JSON.NET
I am trying to set up a reader that will take in JSON objects from various websites (think information scraping) and translate them into C# objects. I am currently using JSON.NET for the deserialization process. The problem I am running into is that it does not know how to handle interface-level properties in a class. So something of the nature:
public IThingy Thing
Will produce the error:
Could not create an instance of type IThingy. Type is an interface or abstract class and cannot be instantiated.
It is relatively important to have it be an IThingy as opposed to a Thingy since the code I am working on is considered sensitive and unit testing is highly important. Mocking of objects for atomic test scripts is not possible with fully-fledged objects like Thingy. They must be an interface.
I've been poring over JSON.NET's documentation for a while now, and the questions I could find on this site related to this are all from over a year ago. Any help?
Also, if it matters, my app is written in .NET 4.0.
Solution 1:
@SamualDavis provided a great solution in a related question, which I'll summarize here.
If you have to deserialize a JSON stream into a concrete class that has interface properties, you can include the concrete classes as parameters to a constructor for the class! The NewtonSoft deserializer is smart enough to figure out that it needs to use those concrete classes to deserialize the properties.
Here is an example:
public class Visit : IVisit
{
/// <summary>
/// This constructor is required for the JSON deserializer to be able
/// to identify concrete classes to use when deserializing the interface properties.
/// </summary>
public Visit(MyLocation location, Guest guest)
{
Location = location;
Guest = guest;
}
public long VisitId { get; set; }
public ILocation Location { get; set; }
public DateTime VisitDate { get; set; }
public IGuest Guest { get; set; }
}
Solution 2:
Why use a converter? There is a native functionality in Newtonsoft.Json
to solve this exact problem:
Set TypeNameHandling
in the JsonSerializerSettings
to TypeNameHandling.Auto
JsonConvert.SerializeObject(
toSerialize,
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
});
This will put every type into the json, that is not held as a concrete instance of a type but as an interface or an abstract class.
Make sure that you are using the same settings for serialization and deserialization.
I tested it, and it works like a charm, even with lists.
Search Results Web result with site links
⚠️ WARNING:
Only use this for json from a known and trusted source. User snipsnipsnip correctly mentioned that this is indeed a vunerability.
See CA2328 and SCS0028 for more information.
Source and an alternative manual implementation: Code Inside Blog
Solution 3:
(Copied from this question)
In cases where I have not had control over the incoming JSON (and so cannot ensure that it includes a $type property) I have written a custom converter that just allows you to explicitly specify the concrete type:
public class Model
{
[JsonConverter(typeof(ConcreteTypeConverter<Something>))]
public ISomething TheThing { get; set; }
}
This just uses the default serializer implementation from Json.Net whilst explicitly specifying the concrete type.
An overview are available on this blog post. Source code is below:
public class ConcreteTypeConverter<TConcrete> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
//assume we can convert to anything for now
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//explicitly specify the concrete type we want to create
return serializer.Deserialize<TConcrete>(reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//use the default serialization - it works fine
serializer.Serialize(writer, value);
}
}
Solution 4:
Use this class, for mapping abstract type to real type:
public class AbstractConverter<TReal, TAbstract>
: JsonConverter where TReal : TAbstract
{
public override Boolean CanConvert(Type objectType)
=> objectType == typeof(TAbstract);
public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser)
=> jser.Deserialize<TReal>(reader);
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser)
=> jser.Serialize(writer, value);
}
...and when deserialize:
var settings = new JsonSerializerSettings
{
Converters = {
new AbstractConverter<Thing, IThingy>(),
new AbstractConverter<Thing2, IThingy2>()
},
};
JsonConvert.DeserializeObject(json, type, settings);
Solution 5:
To enable deserialization of multiple implementations of interfaces, you can use JsonConverter, but not through an attribute:
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);
DTOJsonConverter maps each interface with a concrete implementation:
class DTOJsonConverter : Newtonsoft.Json.JsonConverter
{
private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName;
private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName;
public override bool CanConvert(Type objectType)
{
if (objectType.FullName == ISCALAR_FULLNAME
|| objectType.FullName == IENTITY_FULLNAME)
{
return true;
}
return false;
}
public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (objectType.FullName == ISCALAR_FULLNAME)
return serializer.Deserialize(reader, typeof(DTO.ClientScalar));
else if (objectType.FullName == IENTITY_FULLNAME)
return serializer.Deserialize(reader, typeof(DTO.ClientEntity));
throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
DTOJsonConverter is required only for the deserializer. The serialization process is unchanged. The Json object do not need to embed concrete types names.
This SO post offers the same solution one step further with a generic JsonConverter.