Using Json.NET converters to deserialize properties
I have a class definition that contains a property that returns an interface.
public class Foo
{
public int Number { get; set; }
public ISomething Thing { get; set; }
}
Attempting to serialize the Foo class using Json.NET gives me an error message l开发者_运维百科ike, "Could not create an instance of type 'ISomething'. ISomething may be an interface or abstract class."
Is there a Json.NET attribute or converter that would let me specify a concrete Something
class to use during deserialization?
One of the things you can do with Json.NET is:
var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);
The TypeNameHandling
flag will add a $type
property to the JSON, which allows Json.NET to know which concrete type it needs to deserialize the object into. This allows you to deserialize an object while still fulfilling an interface or abstract base class.
The downside, however, is that this is very Json.NET-specific. The $type
will be a fully-qualified type, so if you're serializing it with type info,, the deserializer needs to be able to understand it as well.
Documentation: Serialization Settings with Json.NET
You can achieve this through the use of the JsonConverter class. Suppose you have a class with an interface property;
public class Organisation {
public string Name { get; set; }
[JsonConverter(typeof(TycoonConverter))]
public IPerson Owner { get; set; }
}
public interface IPerson {
string Name { get; set; }
}
public class Tycoon : IPerson {
public string Name { get; set; }
}
Your JsonConverter is responsible for serializing and de-serializing the underlying property;
public class TycoonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPerson));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return serializer.Deserialize<Tycoon>(reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Left as an exercise to the reader :)
throw new NotImplementedException();
}
}
When you work with an Organisation deserialized via Json.Net the underlying IPerson for the Owner property will be of type Tycoon.
Instead of passing a customized JsonSerializerSettings object to JsonConvert.SerializeObject() with the TypeNameHandling.Objects option, as previously mentioned, you can just mark that specific interface property with an attribute so the generated JSON wouldn't be bloated with "$type" properties on EVERY object:
public class Foo
{
public int Number { get; set; }
// Add "$type" property containing type info of concrete class.
[JsonProperty( TypeNameHandling = TypeNameHandling.Objects )]
public ISomething { get; set; }
}
In the most recent version of the third party Newtonsoft Json converter you can set a constructor with a concrete type relating to the interfaced property.
public class Foo
{
public int Number { get; private set; }
public ISomething IsSomething { get; private set; }
public Foo(int number, Something concreteType)
{
Number = number;
IsSomething = concreteType;
}
}
As long as Something implements ISomething this should work. Also do not put a default empty constructor in case the JSon converter attempts to use that, you must force it to use the constructor containing the concrete type.
PS. this also allows you to make your setters private.
Had a same problem so i came up with my own Converter which uses known types argument.
public class JsonKnownTypeConverter : JsonConverter
{
public IEnumerable<Type> KnownTypes { get; set; }
public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
{
KnownTypes = knownTypes;
}
protected object Create(Type objectType, JObject jObject)
{
if (jObject["$type"] != null)
{
string typeName = jObject["$type"].ToString();
return Activator.CreateInstance(KnownTypes.First(x =>typeName.Contains("."+x.Name+",")));
}
throw new InvalidOperationException("No supported type");
}
public override bool CanConvert(Type objectType)
{
if (KnownTypes == null)
return false;
return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
}
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
var target = Create(objectType, jObject);
// Populate the object properties
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
I defined two extension methods for deserializing and serializing:
public static class AltiJsonSerializer
{
public static T DeserializeJson<T>(this string jsonString, IEnumerable<Type> knownTypes = null)
{
if (string.IsNullOrEmpty(jsonString))
return default(T);
return JsonConvert.DeserializeObject<T>(jsonString,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Converters = new List<JsonConverter>
(
new JsonConverter[]
{
new JsonKnownTypeConverter(knownTypes)
}
)
}
);
}
public static string SerializeJson(this object objectToSerialize)
{
return JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented,
new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
}
}
You can define your own way of comparing and identifying types in the convertes, i only use class name.
Normally I have always used the solution with TypeNameHandling
as suggested by DanielT, but in cases here 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.
The source code and an overview are available on this blog post.
I just wanted to complete the example that @Daniel T. showed us above:
If you are using this code to serialize your object:
var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);
The code to deserialize the json should look like this:
var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
var entity = JsonConvert.DeserializeObject<EntityType>(json, settings);
This is how a json gets conformed when using the TypeNameHandling
flag:
I've wondered this same thing, but I'm afraid it can't be done.
Let's look at it this way. You hand to JSon.net a string of data, and a type to deserialize into. What is JSON.net to do when it hit's that ISomething? It can't create a new type of ISomething because ISomething is not an object. It also can't create an object that implements ISomething, since it doesn't have a clue which of the many objects that may inherit ISomething it should use. Interfaces, are something that can be automatically serialized, but not automatically deserialized.
What I would do would be to look at replacing ISomething with a base class. Using that you might be able to get the effect you are looking for.
Here is a reference to an article written by ScottGu
Based on that, I wrote some code which I think might be helpful
public interface IEducationalInstitute
{
string Name
{
get; set;
}
}
public class School : IEducationalInstitute
{
private string name;
#region IEducationalInstitute Members
public string Name
{
get { return name; }
set { name = value; }
}
#endregion
}
public class Student
{
public IEducationalInstitute LocalSchool { get; set; }
public int ID { get; set; }
}
public static class JSONHelper
{
public static string ToJSON(this object obj)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
return serializer.Serialize(obj);
}
public static string ToJSON(this object obj, int depth)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
serializer.RecursionLimit = depth;
return serializer.Serialize(obj);
}
}
And this is how you would call it
School myFavSchool = new School() { Name = "JFK High School" };
Student sam = new Student()
{
ID = 1,
LocalSchool = myFavSchool
};
string jSONstring = sam.ToJSON();
Console.WriteLine(jSONstring);
//Result {"LocalSchool":{"Name":"JFK High School"},"ID":1}
If I understand it correctly, I do not think you need to specify a concrete class which implements the interface for JSON serialization.
精彩评论