Removing deserialization of polymorphic json classes without type information using json.net

This Imgur API call returns a list containing the Gallery Image and Gallery Album classes represented in JSON.

I cannot figure out how to deserialize them automatically using Json.NET, since there is no $ type property telling the deserializer which class should be represented. There is a property called "IsAlbum" that can be used to distinguish between them.

This question seems to show one method, but it looks like a hack.

How do I deserialize these classes? (using C #, Json.NET) .

Sample data:

Image Gallery

{ "id": "OUHDm", "title": "My most recent drawing. Spent over 100 hours.", ... "is_album": false } 

Gallery Album

 { "id": "lDRB2", "title": "Imgur Office", ... "is_album": true, "images_count": 3, "images": [ { "id": "24nLu", ... "link": "http://i.imgur.com/24nLu.jpg" }, { "id": "Ziz25", ... "link": "http://i.imgur.com/Ziz25.jpg" }, { "id": "9tzW6", ... "link": "http://i.imgur.com/9tzW6.jpg" } ] } } 
+65
c # serialization imgur
Oct 10 '13 at 23:13
source share
5 answers

You can do this quite easily by creating a custom JsonConverter to handle object instances. Assuming your classes have defined something like this:

 public abstract class GalleryItem { public string id { get; set; } public string title { get; set; } public string link { get; set; } public bool is_album { get; set; } } public class GalleryImage : GalleryItem { // ... } public class GalleryAlbum : GalleryItem { public int images_count { get; set; } public List<GalleryImage> images { get; set; } } 

You should create a converter like this:

 public class GalleryItemConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(GalleryItem).IsAssignableFrom(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JObject jo = JObject.Load(reader); // Using a nullable bool here in case "is_album" is not present on an item bool? isAlbum = (bool?)jo["is_album"]; GalleryItem item; if (isAlbum.GetValueOrDefault()) { item = new GalleryAlbum(); } else { item = new GalleryImage(); } serializer.Populate(jo.CreateReader(), item); return item; } public override bool CanWrite { get { return false; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

Here is an example of a program showing the converter in action:

 class Program { static void Main(string[] args) { string json = @" [ { ""id"": ""OUHDm"", ""title"": ""My most recent drawing. Spent over 100 hours."", ""link"": ""http://i.imgur.com/OUHDm.jpg"", ""is_album"": false }, { ""id"": ""lDRB2"", ""title"": ""Imgur Office"", ""link"": ""http://alanbox.imgur.com/a/lDRB2"", ""is_album"": true, ""images_count"": 3, ""images"": [ { ""id"": ""24nLu"", ""link"": ""http://i.imgur.com/24nLu.jpg"" }, { ""id"": ""Ziz25"", ""link"": ""http://i.imgur.com/Ziz25.jpg"" }, { ""id"": ""9tzW6"", ""link"": ""http://i.imgur.com/9tzW6.jpg"" } ] } ]"; List<GalleryItem> items = JsonConvert.DeserializeObject<List<GalleryItem>>(json, new GalleryItemConverter()); foreach (GalleryItem item in items) { Console.WriteLine("id: " + item.id); Console.WriteLine("title: " + item.title); Console.WriteLine("link: " + item.link); if (item.is_album) { GalleryAlbum album = (GalleryAlbum)item; Console.WriteLine("album images (" + album.images_count + "):"); foreach (GalleryImage image in album.images) { Console.WriteLine(" id: " + image.id); Console.WriteLine(" link: " + image.link); } } Console.WriteLine(); } } } 

And here is the output of the above program:

 id: OUHDm title: My most recent drawing. Spent over 100 hours. link: http://i.imgur.com/OUHDm.jpg id: lDRB2 title: Imgur Office link: http://alanbox.imgur.com/a/lDRB2 album images (3): id: 24nLu link: http://i.imgur.com/24nLu.jpg id: Ziz25 link: http://i.imgur.com/Ziz25.jpg id: 9tzW6 link: http://i.imgur.com/9tzW6.jpg 

Fiddle: https://dotnetfiddle.net/1kplME

+97
Oct 11 '13 at 0:38
source share

Just with JsonSubTypes attributes that work with Json.NET

  [JsonConverter(typeof(JsonSubtypes), "is_album")] [JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)] [JsonSubtypes.KnownSubType(typeof(GalleryImage), false)] public abstract class GalleryItem { public string id { get; set; } public string title { get; set; } public string link { get; set; } public bool is_album { get; set; } } public class GalleryImage : GalleryItem { // ... } public class GalleryAlbum : GalleryItem { public int images_count { get; set; } public List<GalleryImage> images { get; set; } } 
+24
Mar 10 '18 at 21:13
source share

The following execution should allow you to de-serialize without changing the way you develop your classes and using a field other than $ to decide what to de-serialize for.

 public class GalleryImageConverter : JsonConverter { public override bool CanConvert(Type objectType) { return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum)); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { try { if (!CanConvert(objectType)) throw new InvalidDataException("Invalid type of object"); JObject jo = JObject.Load(reader); // following is to avoid use of magic strings var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name; JToken jt; if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt)) { return jo.ToObject<GalleryImage>(); } var propValue = jt.Value<bool>(); if(propValue) { resultType = typeof(GalleryAlbum); } else{ resultType = typeof(GalleryImage); } var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType); var objectProperties=resultType.GetProperties(); foreach (var objectProperty in objectProperties) { var propType = objectProperty.PropertyType; var propName = objectProperty.Name; var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase); if (token != null) { objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject)); } } return resultObject; } catch (Exception ex) { throw; } } public override bool CanWrite { get { return false; } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 
+1
Jan 16 '18 at 18:31
source share

Extended answer to Brian Rogers . And about "use Serializer.Populate () instead of item.ToObject ()". If derived types have a consttructor or some of them have their own customconverter, you should use the general JSON deserialization method. Therefore, you must leave work to instantiate a new object in NewtonJson. That way you can achieve this in your CustomJsonConverter:

 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { ..... YOU Code For Determine Real Type of Json Record ....... // 1. Correct ContractResolver for you derived type var contract = serializer.ContractResolver.ResolveContract(DeterminedType); if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType()) { contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class } // Deserialize in general way var jTokenReader = new JTokenReader(jObject); var result = serializer.Deserialize(jTokenReader, DeterminedType); return (result); } 

This work if you have recursion of objects.

+1
Feb 15 '19 at 13:24
source share

I am only posting this to clarify some of the confusion. If you work with a predefined format and need to deserialize it, this is what I found to be the best and demonstrates the mechanics so that others can tweak it as needed.

 public class BaseClassConverter : JsonConverter { public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var j = JObject.Load(reader); var retval = BaseClass.From(j, serializer); return retval; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { serializer.Serialize(writer, value); } public override bool CanConvert(Type objectType) { // important - do not cause subclasses to go through this converter return objectType == typeof(BaseClass); } } // important to not use attribute otherwise you'll infinite loop public abstract class BaseClass { internal static Type[] Types = new Type[] { typeof(Subclass1), typeof(Subclass2), typeof(Subclass3) }; internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last()); // type property based off of class name [JsonProperty(PropertyName = "type", Required = Required.Always)] public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } } // convenience method to deserialize a JObject public static new BaseClass From(JObject obj, JsonSerializer serializer) { // this is our object type property var str = (string)obj["type"]; // we map using a dictionary, but you can do whatever you want var type = TypesByName[str]; // important to pass serializer (and its settings) along return obj.ToObject(type, serializer) as BaseClass; } // convenience method for deserialization public static BaseClass Deserialize(JsonReader reader) { JsonSerializer ser = new JsonSerializer(); // important to add converter here ser.Converters.Add(new BaseClassConverter()); return ser.Deserialize<BaseClass>(reader); } } 
0
Sep 18 '17 at 20:42 on
source share



All Articles