Deserializar clases json polimórficas sin información de tipo usando json.net

Esta llamada de Imgur api devuelve una lista que contiene las clases de galería de imágenes y de galería representadas en json.

No puedo ver cómo deserializar estos automáticamente usando Json.NET dado que no hay una propiedad $ type que indique al deserializador qué clase debe representarse. Hay una propiedad llamada “IsAlbum” que se puede usar para diferenciar entre los dos.

Esta pregunta parece mostrar un método, pero parece un truco.

¿Cómo hago para deserializar estas clases? (usando C #, Json.NET) .

Data de muestra:

Imagen de la galería

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

Album de la galería

 { "id": "lDRB2", "title": "Imgur Office", ... "is_album": true, "images_count": 3, "images": [ { "id": "24nLu", ... "link": "http://sofes.miximages.com/serialization/24nLu.jpg" }, { "id": "Ziz25", ... "link": "http://sofes.miximages.com/serialization/Ziz25.jpg" }, { "id": "9tzW6", ... "link": "http://sofes.miximages.com/serialization/9tzW6.jpg" } ] } 

}

Puede hacerlo con bastante facilidad creando un JsonConverter personalizado para manejar la instanciación de objetos. Suponiendo que tiene sus clases definidas algo como esto:

 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 images { get; set; } } 

Deberías crear el convertidor así:

 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 item = JObject.Load(reader); if (item["is_album"].Value()) { return item.ToObject(); } else { return item.ToObject(); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

Aquí hay un progtwig de ejemplo que muestra el convertidor en acción:

 class Program { static void Main(string[] args) { string json = @" [ { ""id"": ""OUHDm"", ""title"": ""My most recent drawing. Spent over 100 hours."", ""link"": ""http://sofes.miximages.com/serialization/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://sofes.miximages.com/serialization/24nLu.jpg"" }, { ""id"": ""Ziz25"", ""link"": ""http://sofes.miximages.com/serialization/Ziz25.jpg"" }, { ""id"": ""9tzW6"", ""link"": ""http://sofes.miximages.com/serialization/9tzW6.jpg"" } ] } ]"; List items = JsonConvert.DeserializeObject>(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(); } } } 

Y aquí está el resultado del progtwig anterior:

 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 

Simplemente con los atributos JsonSubTypes que funcionan con 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 images { get; set; } } 

La implementación siguiente debería permitirle deserializar sin cambiar la forma en que ha diseñado sus clases y utilizando un campo que no sea $ type para decidir en qué serializarlo.

 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>)(s => s.is_album)).Body).Member.Name; JToken jt; if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt)) { return jo.ToObject(); } var propValue = jt.Value(); 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(); } } 

Solo estoy publicando esto para aclarar algo de la confusión. Si está trabajando con un formato predefinido y necesita deserializarlo, esto es lo que encontré que funcionó mejor y demuestra la mecánica para que otros puedan modificarlo según sea necesario.

 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 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(reader); } }