Deserialisieren polymorpher JSON-Klassen ohne Typinformationen mithilfe von json.net

77

Dieser Imgur-API- Aufruf gibt eine Liste zurück, die sowohl die in JSON dargestellten Klassen " Gallery Image" als auch " Gallery Album" enthält .

Ich kann nicht sehen, wie diese mit Json.NET automatisch deserialisiert werden, da es keine Eigenschaft vom Typ $ gibt, die dem Deserializer mitteilt, welche Klasse dargestellt werden soll. Es gibt eine Eigenschaft namens "IsAlbum", mit der zwischen beiden unterschieden werden kann.

Diese Frage scheint eine Methode zu zeigen, sieht aber wie ein Hack aus.

Wie deserialisiere ich diese Klassen? (mit C #, Json.NET) .

Beispieldaten:

Galerie Bild

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

Galerie 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"
        }
    ]
}
}
Peter Kneale
quelle
Sie möchten den Json-String nehmen und in Klassen einteilen? Und ich bin verwirrt von dem, was du meinst there is no $type property.
gunr2171
1
Ja, ich habe den JSON-String und möchte in C # -Klassen deserialisieren. Json.NET verwendet anscheinend eine Eigenschaft namens $ type, um zwischen verschiedenen Typen in einem Array zu unterscheiden. Diese Daten haben diese Eigenschaft nicht und verwenden nur die Eigenschaft 'IsAlbum'.
Peter Kneale

Antworten:

110

Sie können dies ziemlich einfach tun, indem Sie eine benutzerdefinierte JsonConverterDatei erstellen , um die Objektinstanziierung zu handhaben. Angenommen, Sie haben Ihre Klassen wie folgt definiert:

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

Sie würden den Konverter folgendermaßen erstellen:

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

Hier ist ein Beispielprogramm, das den Konverter in Aktion zeigt:

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

Und hier ist die Ausgabe des obigen Programms:

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

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

Brian Rogers
quelle
20
Dies funktioniert nicht, wenn polymorphe Objekte rekursiv sind, dh wenn ein Album andere Alben enthalten kann. Im Konverter sollte Serializer.Populate () anstelle von item.ToObject () verwendet werden. Siehe stackoverflow.com/questions/29124126/…
Ivan Krivyakov
5
Wenn Sie diesen Ansatz ausprobieren und ihn finden, führt dies zu einer Endlosschleife (und schließlich zu einem Stapelüberlauf). Möglicherweise möchten Sie den PopulateAnsatz anstelle von verwenden ToObject. Siehe die Antworten zu stackoverflow.com/questions/25404202/… und stackoverflow.com/questions/29124126/… . Ich habe hier ein Beispiel für die beiden Ansätze in einem Gist: gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95 .
Chris Oldwood
Ich war in verschiedenen Antworten ungefähr 8 Stunden verloren, sie waren über CustomCreationConverter. Endlich hat diese Antwort funktioniert und ich fühle mich erleuchtet. Mein inneres Objekt enthält seinen Typ als Zeichenfolge und ich verwende ihn für eine solche Konvertierung. JObject item = JObject.Load (Leser); Typ type = Type.GetType (Element ["Typ"]. Wert <string> ()); return item.ToObject (Typ);
Furkan Ekinci
1
Dies funktioniert gut für mich, vorausgesetzt, Sie setzen das Konverterattribut nicht auf die Basisklasse. Der Konverter muss (über Einstellungen usw.) in den Serializer eingespeist werden und in CanConvert nur nach dem Basistyp suchen. Ich diskutiere mit Populate (), aber ich mag keine der beiden Methoden.
Xtravar
Ich habe den Konverter so repariert, dass er JsonSerializer.Populate()nicht JObject.ToObject()wie von Ivan und Chris vorgeschlagen verwendet wird. Dadurch werden Probleme mit rekursiven Schleifen vermieden und der Konverter kann erfolgreich mit Attributen verwendet werden.
Brian Rogers
35

Einfach mit JsonSubTypes- Attributen, die mit Json.NET funktionieren

    [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; }
    }
manuc66
quelle
7
Dies sollte die beste Antwort sein. Ich habe den größten Teil eines Tages damit verbracht, an einer Lösung für dieses Problem zu arbeiten und benutzerdefinierte JsonConverter-Klassen von Dutzenden von Autoren zu untersuchen. Ihr Nuget-Paket hat all diesen Aufwand durch drei Codezeilen ersetzt. Gut gemacht, Sir. Gut gemacht.
Kenneth Cochran
In der Java-Welt bietet die Jackson-Bibliothek eine ähnliche Unterstützung über @JsonSubTypesAttribute. Ein weiteres Anwendungsfallbeispiel finden Sie unter stackoverflow.com/a/45447923/863980 ( siehe auch Cage / Animal-Beispiel von @KonstantinPelepelin in Kommentaren).
Vulkanischer Rabe
Dies ist zwar die einfachste Antwort, aber leider mit Leistungskosten verbunden. Ich fand heraus, dass die Deserialisierung mit einem handgeschriebenen Konverter 2-3-mal schneller ist (wie in der Antwort von @BrianRogers gezeigt)
Sven Vranckx
Hallo @SvenVranckx, zögern Sie nicht, ein Problem auf github.com/manuc66/JsonSubTypes
manuc66
2

Fortgeschrittene Antwort von Brian Rogers . Und über "Verwenden Sie Serializer.Populate () anstelle von item.ToObject ()". Wenn abgeleitete Typen über Konstruktoren verfügen oder einige von ihnen über einen eigenen benutzerdefinierten Konverter verfügen, müssen Sie die allgemeine Methode zum Deserialisieren von JSON verwenden. Sie müssen also die Arbeit verlassen, um NewtonJson ein neues Objekt zu instanziieren. Auf diese Weise können Sie es in Ihrem CustomJsonConverter erreichen:

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

Dies funktioniert, wenn Sie eine Rekursion von Objekten haben.

Игорь Орлов
quelle
Dieser Code ist in mutithreaded-Kontexten nicht sicher, da der Standard-Vertragsauflöser Verträge zwischenspeichert. Durch das Festlegen von Optionen für den Vertrag (wie seinen Konverter) verhält er sich bei gleichzeitigen und sogar nachfolgenden Anrufen anders.
Dan Davies Brackett
serializer.ContractResolver.ResolveContract (DeterminedType) - Gibt den bereits zwischengespeicherten Vertrag zurück. Also contract.Converter = null; zwischengespeichertes Objekt ändern. Es ändert nur die Referenz für zwischengespeicherte Objekte und ist threadsicher.
16горь Орлов
1

Nach der Implementierung sollten Sie die De-Serialisierung durchführen können, ohne die Art und Weise zu ändern, in der Sie Ihre Klassen entworfen haben, und indem Sie ein anderes Feld als $ type verwenden, um zu entscheiden, in was die De-Serialisierung erfolgen soll.

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();
    }
}
Abdul Rehman Gill
quelle
0

Ich poste dies nur, um die Verwirrung zu beseitigen. Wenn Sie mit einem vordefinierten Format arbeiten und es deserialisieren müssen, hat dies meiner Meinung nach am besten funktioniert und die Mechanik demonstriert, damit andere es nach Bedarf anpassen können.

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);
        }
    }
xtravar
quelle
Wie verwenden Sie dies, wenn Sie die implizite Konvertierung verwenden, ohne das Attribut [JsonConverter ()] zu verwenden (das als "wichtig" kommentiert wird)? ZB: Deserialisieren über [FromBody]Attribut?
Alex McMillan
1
Ich gehe davon aus, dass Sie die Einstellungen des globalen JsonFormatter einfach bearbeiten können, um diesen Konverter einzuschließen. Siehe stackoverflow.com/questions/41629523/…
xtravar