2013-10-10 12 views
46

Questo Imgur api chiamata restituisce un elenco contenente sia Galleria immagini e Gallery Album classi rappresentate in JSON.deserializzazione JSON classi polimorfiche senza informazioni sul tipo di utilizzo json.net

Non riesco a vedere come deserializzare questi automaticamente utilizzando Json.NET dato che non esiste alcuna proprietà di tipo $ che dice al deserializzatore quale classe deve essere rappresentata. Esiste una proprietà chiamata "IsAlbum" che può essere utilizzata per differenziare i due.

This domanda sembra mostrare un metodo ma sembra un po 'un hack.

Come faccio per deserializzare queste classi? (utilizzando C#, Json.NET).

dati di esempio:

Galleria immagini

{ 
    "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" 
     } 
    ] 
} 

}

+0

Si vuole prendere la stringa JSON e metterlo in classi? E sono confuso da cosa intendi per "non esiste una proprietà di tipo $". – gunr2171

+0

Sì, ho la stringa json e voglio deserializzare in classi C#. Sembra che Json.NET usi una proprietà chiamata $ type per tracciare una distinzione tra diversi tipi tenuti in un array. Questi dati non hanno quella proprietà e usano solo la proprietà 'IsAlbum'. –

risposta

77

È possibile farlo abbastanza facilmente con la creazione di un c ustom JsonConverter per gestire l'istanza dell'oggetto. Supponendo di aver vostre classi definite qualcosa di simile:

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

Si potrebbe creare il convertitore in questo modo:

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<bool>()) 
     { 
      return item.ToObject<GalleryAlbum>(); 
     } 
     else 
     { 
      return item.ToObject<GalleryImage>(); 
     } 
    } 

    public override void WriteJson(JsonWriter writer, 
     object value, JsonSerializer serializer) 
    { 
     throw new NotImplementedException(); 
    } 
} 

Ecco un esempio di programma che mostra il convertitore in azione:

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

E ecco l'output del programma di cui sopra:

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 
+11

Questo non funziona se gli oggetti polimorfici sono ricorsivi, cioè se un album può contenere altri album. Nel convertitore si dovrebbe usare Serializer.Populate() invece di item.ToObject(). Vedi http://stackoverflow.com/questions/29124126/polymorphic-json-deserialization-failing-using-json-net –

+0

Perfetto, questo ha funzionato meravigliosamente! –

+0

Chiunque provi questo approccio e lo trovi risultato in un ciclo infinito (e infine un overflow dello stack), potresti voler usare l'approccio 'Populate' invece di' ToObject'. Vedere le risposte a https://stackoverflow.com/questions/25404202/custom-inheritance-jsonconverter-fails-when-jsonconverterattribute-is-used e https://stackoverflow.com/questions/29124126/polymorphic-json-deserialization- failing-using-json-net. Ho un esempio dei due approcci in un Gist qui: https://gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95. –

0

Sto solo postando questo per chiarire un po 'di confusione. Se stai lavorando con un formato predefinito e hai bisogno di deserializzare, questo è quello che ho trovato più utile e dimostra i meccanismi in modo che altri possano modificarlo secondo necessità.

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

Come si usa questo quando si utilizza la conversione implicita senza utilizzare l'attributo [JsonConverter()] (che viene commentato come "importante")? Es: deserializzazione tramite l'attributo '[FromBody]'? –

+1

Suppongo che tu possa semplicemente modificare le impostazioni globali di JsonFormatter per includere questo convertitore. Vedi https://stackoverflow.com/questions/41629523/using-a-custom-json-formatter-for-web-api-2 – xtravar

+0

Oh geniale - grazie. –

1

Dopo attuazione dovrebbe farvi de-serializzare senza cambiare il modo in cui avete progettato le vostre classi e utilizzando un campo diverso tipo $ per decidere cosa de-serializzare in.

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

Basta con JsonSubTypes attributi che funzionano 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<GalleryImage> images { get; set; } 
    } 
Problemi correlati