110 votes

Désérialiser des classes json polymorphes sans information de type en utilisant json.net

Cet appel Imgur api renvoie une liste contenant à la fois les classes Gallery Image et Gallery Album représentées en JSON.

Je ne vois pas comment les désérialiser automatiquement en utilisant Json.NET étant donné qu'il n'y a pas de propriété $type indiquant au désérialiseur quelle classe doit être représentée. Il y a une propriété appelée "IsAlbum" qui peut être utilisée pour différencier les deux.

Cette question semble montrer une méthode mais cela semble être un peu bidouillage.

Comment puis-je désérialiser ces classes? (en utilisant C#, Json.NET).

Données d'exemple:

Gallery Image

{
    "id": "OUHDm",
    "title": "Mon dessin le plus récent. 100 heures passées dessus.",
        ...
    "is_album": false
}

Gallery Album

{
    "id": "lDRB2",
    "title": "Bureau Imgur",
    ...
    "is_album": true,
    "nombre_images": 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"
        }
    ]
}
}

146voto

Brian Rogers Points 12160

Vous pouvez le faire assez facilement en créant un JsonConverter personnalisé pour gérer l'instanciation de l'objet. En supposant que vous ayez vos classes définies quelque chose comme ceci:

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

Vous créerait le convertisseur comme ceci:

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

        // Utilisation d'un booléen nullable ici au cas où "is_album" n'est pas présent sur un élément
        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();
    }
}

Voici un programme d'exemple montrant le convertisseur en action:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
            {
                ""id"": ""OUHDm"",
                ""title"": ""Mon dessin le plus récent. Plus de 100 heures passées."",
                ""link"": ""http://i.imgur.com/OUHDm.jpg"",
                ""is_album"": false
            },
            {
                ""id"": ""lDRB2"",
                ""title"": ""Bureau Imgur"",
                ""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 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("images d'album (" + album.images_count + "):");
                foreach (GalleryImage image in album.images)
                {
                    Console.WriteLine("    id: " + image.id);
                    Console.WriteLine("    link: " + image.link);
                }
            }
            Console.WriteLine();
        }
    }
}

Et voici la sortie du programme ci-dessus:

id: OUHDm
title: Mon dessin le plus récent. Plus de 100 heures passées.
link: http://i.imgur.com/OUHDm.jpg

id: lDRB2
title: Bureau Imgur
link: http://alanbox.imgur.com/a/lDRB2
images d'album (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

72voto

manuc66 Points 94

Tout simplement avec des attributs JsonSubTypes qui fonctionnent avec 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; }
    }

4voto

Avancé à la réponse de Brian Rogers. Et à propos de "utilisez Serializer.Populate() au lieu de item.ToObject()". Si des types dérivés ont des constructeurs ou certains d'entre eux possèdent leur propre convertisseur personnalisé, vous devez utiliser la méthode générale pour désérialiser JSON. Vous devez donc laisser le travail d'instanciation d'un nouvel objet à NewtonJson. Vous pouvez y parvenir avec votre CustomJsonConverter :

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    ..... Votre code pour déterminer le vrai type de l'enregistrement JSON .......

    // 1. Bon ContractResolver pour votre type dérivé
    var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
    if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
    {
        contract.Converter = null; // Nettoie le mauvais convertisseur saisi par DefaultContractResolver de votre classe de base pour la classe dérivée
    }

    // Désérialiser de manière générale
    var jTokenReader = new JTokenReader(jObject);
    var result = serializer.Deserialize(jTokenReader, DeterminedType);

    return (result);
}

Cela fonctionne si vous avez une récursion d'objets.

1voto

xtravar Points 415

Je ne publie ceci que pour dissiper une partie de la confusion. Si vous travaillez avec un format prédéfini et que vous avez besoin de le désérialiser, voici ce qui a fonctionné le mieux pour moi et démontre le fonctionnement pour que d'autres puissent l'ajuster si nécessaire.

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

1voto

Suivant la mise en œuvre devrait vous permettre de désérialiser sans changer la façon dont vous avez conçu vos classes et en utilisant un champ autre que $type pour décider comment le désérialiser.

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("Type d'objet invalide");
            JObject jo = JObject.Load(reader);
            // ce qui suit vise à éviter l'utilisation de chaînes magiques
            var nomProprietéEstAlbum = ((MemberExpression)((Expression>)(s => s.is_album)).Body).Member.Name;
            JToken jt;
            if (!jo.TryGetValue(nomProprietéEstAlbum, StringComparison.InvariantCultureIgnoreCase, out jt))
            {
                return jo.ToObject();
            }
            var valProp = jt.Value();
            Type resultType;
            if(valProp) {
                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 typeProp = objectProperty.PropertyType;
                var nomProp = objectProperty.Name;
                var token = jo.GetValue(nomProp, StringComparison.InvariantCultureIgnoreCase);
                if (token != null)
                {
                    objectProperty.SetValue(resultObject,token.ToObject(typeProp)?? 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();
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X