Desserializando classs json polimórficas sem informações de tipo usando json.net

Essa chamada Imgur api retorna uma lista contendo as classs Gallery Image e Gallery Album representadas no json.

Não consigo ver como desserializá-los automaticamente usando o Json.NET, já que não existe uma propriedade $ type informando ao desserializador qual class deve ser representada. Existe uma propriedade chamada “IsAlbum” que pode ser usada para diferenciar os dois.

Esta questão parece mostrar um método, mas parece um pouco de truque.

Como faço para desserializar essas classs? (usando C #, Json.NET) .

Dados de amostra:

Imagem da galeria

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

Álbum da galeria

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

}

Você pode fazer isso com bastante facilidade, criando um JsonConverter personalizado para manipular a instanciação de objects. Supondo que você tenha suas classs definidas algo assim:

 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; } } 

Você criaria o conversor assim:

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

Aqui está um exemplo de programa mostrando o conversor em ação:

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

E aqui está a saída do programa acima:

 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 

Simplesmente com atributos JsonSubTypes que funcionam com o 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; } } 

Após a implementação, você deve remover a serialização sem alterar a maneira como projetou suas classs e usando um campo diferente de $ type para decidir o que deve ser serializado.

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

Estou apenas postando isso para esclarecer um pouco da confusão. Se você está trabalhando com um formato predefinido e precisa desserializá-lo, isso é o que eu achei que funcionou melhor e demonstra a mecânica para que outros possam ajustá-lo conforme necessário.

 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 subclasss 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); } }