Sammlung von Schnittstelleninstanzen deserialisieren?

70

Ich möchte diesen Code über json.net serialisieren:

public interface ITestInterface
{
    string Guid {get;set;}
}

public class TestClassThatImplementsTestInterface1
{
    public string Guid { get;set; }
}

public class TestClassThatImplementsTestInterface2
{
    public string Guid { get;set; }
}


public class ClassToSerializeViaJson
{
    public ClassToSerializeViaJson()
    {             
         this.CollectionToSerialize = new List<ITestInterface>();
         this.CollectionToSerialize.add( new TestClassThatImplementsTestInterface2() );
         this.CollectionToSerialize.add( new TestClassThatImplementsTestInterface2() );
    }
    List<ITestInterface> CollectionToSerialize { get;set; }
}

Ich möchte ClassToSerializeViaJson mit json.net serialisieren / deserialisieren. Die Serialisierung funktioniert, aber die Deserialisierung gibt mir den folgenden Fehler:

Newtonsoft.Json.JsonSerializationException: Es konnte keine Instanz vom Typ ITestInterface erstellt werden. Typ ist eine Schnittstelle oder abstrakte Klasse und kann nicht instanziiert werden.

Wie kann ich die List<ITestInterface>Sammlung deserialisieren ?

user1130329
quelle
Was hast du schon versucht? Haben Sie sogar die Dokumentation zu JSON.NET gelesen?! Ich bin mir ziemlich sicher, dass Serialisierung und Deserialisierung eines der ersten Dinge sind, die in dieser Dokumentation behandelt werden.
Clint
1
Ja, die Serialisierung funktioniert, aber beim Deserialisieren wird ein Fehler angezeigt: Newtonsoft.Json.JsonSerializationException: Es konnte keine Instanz vom Typ ITestInterface erstellt werden. Typ ist eine Schnittstelle oder abstrakte Klasse und kann nicht instanziiert werden.
user1130329
2
dann solltest du vielleicht damit in deiner frage führen? Anstatt so offen zu fragen: "Wie kann ich?" "Es funktioniert nicht" -Fragen, Sie müssen wirklich alle Informationen bereitstellen, um welchen Fehler handelt es sich? Wo passiert es? Was haben Sie bisher versucht, um das Problem zu beheben? Bitte bearbeiten Sie Ihre Frage mit diesen Dingen, damit die Community Ihnen besser helfen kann, anstatt Ihre Fragen zu markieren.
Clint
1
Nicholas Westby lieferte eine großartige Lösung in einem großartigen Artikel
A. Morel

Antworten:

41

Nachfolgend ein ausführliches Beispiel mit dem, was Sie tun möchten:

public interface ITestInterface
{
    string Guid { get; set; }
}

public class TestClassThatImplementsTestInterface1 : ITestInterface
{
    public string Guid { get; set; }
    public string Something1 { get; set; }
}

public class TestClassThatImplementsTestInterface2 : ITestInterface
{
    public string Guid { get; set; }
    public string Something2 { get; set; }
}

public class ClassToSerializeViaJson
{
    public ClassToSerializeViaJson()
    {
        this.CollectionToSerialize = new List<ITestInterface>();
    }
    public List<ITestInterface> CollectionToSerialize { get; set; }
}

public class TypeNameSerializationBinder : SerializationBinder
{
    public string TypeFormat { get; private set; }

    public TypeNameSerializationBinder(string typeFormat)
    {
        TypeFormat = typeFormat;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        var resolvedTypeName = string.Format(TypeFormat, typeName);
        return Type.GetType(resolvedTypeName, true);
    }
}

class Program
{
    static void Main()
    {
        var binder = new TypeNameSerializationBinder("ConsoleApplication.{0}, ConsoleApplication");
        var toserialize = new ClassToSerializeViaJson();

        toserialize.CollectionToSerialize.Add(
            new TestClassThatImplementsTestInterface1()
            {
                Guid = Guid.NewGuid().ToString(), Something1 = "Some1"
            });
        toserialize.CollectionToSerialize.Add(
            new TestClassThatImplementsTestInterface2()
            {
                Guid = Guid.NewGuid().ToString(), Something2 = "Some2"
            });

        string json = JsonConvert.SerializeObject(toserialize, Formatting.Indented, 
            new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                Binder = binder
            });
        var obj = JsonConvert.DeserializeObject<ClassToSerializeViaJson>(json, 
            new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                Binder = binder 
            });

        Console.ReadLine();
    }
}
Piotr Stapp
quelle
1
Sie können im Ordner den FullyQualifiedName anstelle des Namens verwenden und müssen den TypeFormat
Slava Shpitalny
3
Das hat bei mir gut funktioniert. Ein paar Updates, seit dies vor einiger Zeit beantwortet wurde. Es scheint, dass sie jetzt eine Schnittstelle verwenden, ISerializationBinderanstatt sie zu überschreiben SerializationBinder. Auch in BindToNameIch habe serializedType.AssemblyQualifiedNameanstelle von Name verwendet und das übergibt sowohl das assemblyNameals auch das voll qualifizierte typeNamean, BindToTypeso dass jetzt kein Konstruktor mehr benötigt wird. Dann aktualisieren Sie BindToTypemitvar resolvedTypeName = string.Format("{0}, {1}", typeName,assemblyName); and everything should work without providing the namespace & assembly in the constructor
Crob
89

Ich habe diese Frage gefunden, als ich versucht habe, dies selbst zu tun. Nachdem ich die Antwort von Piotr Stapp (Garath) implementiert hatte , war ich beeindruckt, wie einfach es schien. Wenn ich lediglich eine Methode implementiert habe, der bereits der genaue Typ (als Zeichenfolge) übergeben wurde, den ich instanziieren wollte, warum hat die Bibliothek ihn dann nicht automatisch gebunden?

Ich stellte tatsächlich fest, dass ich keine benutzerdefinierten Ordner benötigte. Json.Net konnte genau das tun, was ich brauchte, vorausgesetzt, ich sagte, dass dies das war, was ich tat.

Bei der Serialisierung:

string serializedJson = JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple
});

Bei der De-Serialisierung:

var deserializedObject = JsonConvert.DeserializeObject<ClassToSerializeViaJson>(serializedJson, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects
});

Relevante Dokumentation: Serialisierungseinstellungen für Json.NET und TypeNameHandling

Ben Jenkinson
quelle
15
Dies funktioniert, aber die Leute sollten verstehen, dass dies die Größe Ihres json groß aufblähen kann. Für mich hat es die Größe der Ausgabedatei um über das 10-fache erhöht.
11
Beachten Sie, dass das Einfügen konkreter Typnamen in den resultierenden JSON als Verlust von Implementierungsdetails angesehen werden kann und definitiv nicht sauber ist, wenn der JSON an einer anderen Stelle außerhalb Ihres eigenen Codes verwendet wird. Wenn der JSON, den Sie deserialisieren, aus einer externen Quelle stammt, ist es nicht zumutbar, dass er Ihre Typnamen enthält.
Jacek Gorgoń
4
Mit dieser Lösung sollten die eingehenden Typen bereinigt werden, um potenzielle Sicherheitsrisiken zu vermeiden. Weitere Informationen finden Sie unter TypeNameHandling-Warnung in Newtonsoft Json .
dbc
Ingeros Antwort unten minimiert die Verschmutzung von $ -Typen durch Verwendung von TypeNameHandling.Auto anstelle von TypeNameHandling.Objects
Michael Freidgeim
Ich stimme @ JacekGorgoń zu; Ich erhalte meinen JSON automatisch von einem ASP.NET-Core-Controller.
rory.ap
26

Ich war auch überrascht von der Einfachheit in Garaths und kam zu dem Schluss, dass die Json-Bibliothek dies automatisch tun kann. Aber ich dachte auch, dass es noch einfacher ist als die Antwort von Ben Jenkinson (obwohl ich sehen kann, dass es vom Entwickler der json-Bibliothek selbst geändert wurde). Nach meinen Tests müssen Sie TypeNameHandling lediglich wie folgt auf Auto setzen:

var objectToSerialize = new List<IFoo>();
// TODO: Add objects to list
var jsonString = JsonConvert.SerializeObject(objectToSerialize,
       new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });
var deserializedObject = JsonConvert.DeserializeObject<List<IFoo>>(jsonString, 
       new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });

Aus der Dokumentation zur TypeNameHandling-Aufzählung

Auto: Geben Sie den .NET-Typnamen an, wenn der Typ des zu serialisierenden Objekts nicht mit dem deklarierten Typ übereinstimmt. Beachten Sie, dass dies nicht standardmäßig das serialisierte Stammobjekt umfasst.

Inrego
quelle
2
Es ist ein paar Jahre nach dem ursprünglichen Beitrag, aber dies ist eine aktuelle und funktionierende Methode, um dies zu tun. Der Preis, den Sie zahlen, besteht darin, dass die Typnamen für jedes Objekt in JSON als "$ type" -Attribute ausgegeben werden. Dies ist jedoch unter vielen Umständen in Ordnung.
Grubl3r
1
Dies ist die richtige Lösung, auch für kompliziertere Datenstrukturen. Ich habe buchstäblich Tage TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple
gebraucht
1
Auch bei dieser Lösung sollten die eingehenden Typen bereinigt werden, um potenzielle Sicherheitsrisiken zu vermeiden. Weitere Informationen finden Sie unter TypeNameHandling-Warnung in Newtonsoft Json .
dbc
Es ist zu beachten, dass dies nur dann funktioniert, wenn Sie beim Erstellen die Kontrolle über den JSON haben. Wenn es von irgendwo anders erstellt wird, funktioniert diese Deserialisierung nicht.
Kleiner Geek
7

Mit den Standardeinstellungen können Sie nicht. JSON.NET kann nicht wissen, wie ein Array deserialisiert wird. Sie können jedoch angeben, welcher Typkonverter für Ihren Schnittstellentyp verwendet werden soll. Informationen dazu finden Sie auf dieser Seite: http://blog.greatrexpectations.com/2012/08/30/deserializing-interface-properties-using-json-net/

Informationen zu diesem Problem finden Sie auch in dieser SO-Frage: Casting-Schnittstellen für die Deserialisierung in JSON.NET

Erik Schierboom
quelle
7

Dies ist eine alte Frage, aber ich dachte, ich würde eine ausführlichere Antwort hinzufügen (in Form eines Artikels, den ich geschrieben habe): http://skrift.io/articles/archive/bulletproof-interface-deserialization-in-jsonnet /.

TLDR: Anstatt Json.NET so zu konfigurieren, dass Typnamen in das serialisierte JSON eingebettet werden, können Sie mithilfe eines JSON-Konverters herausfinden, welche Klasse mit einer beliebigen benutzerdefinierten Logik deserialisiert werden soll.

Dies hat den Vorteil, dass Sie Ihre Typen umgestalten können, ohne sich Gedanken über das Brechen der Deserialisierung machen zu müssen.

Nicholas Westby
quelle
5

Dies kann mit den Attributen JSON.NET und JsonSubTypes erfolgen :

[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Test1), "Something1")]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Test2), "Something2")]
public interface ITestInterface
{
    string Guid { get; set; }
}

public class Test1 : ITestInterface
{
    public string Guid { get; set; }
    public string Something1 { get; set; }
}

public class Test2 : ITestInterface
{
    public string Guid { get; set; }
    public string Something2 { get; set; }
}

und einfach:

var fromCode = new List<ITestInterface>();
// TODO: Add objects to list
var json = JsonConvert.SerializeObject(fromCode);
var fromJson = JsonConvert.DeserializeObject<List<ITestInterface>>(json);
manuc66
quelle
4

Ich wollte JSON deserialisieren, das von meiner Anwendung nicht serialisiert wurde, daher musste ich die konkrete Implementierung manuell angeben. Ich habe Nicholas 'Antwort erweitert.

Nehmen wir an, wir haben

public class Person
{
    public ILocation Location { get;set; }
}

und die konkrete Instanz von

public class Location: ILocation
{
    public string Address1 { get; set; }
    // etc
}

In dieser Klasse hinzufügen

public class ConfigConverter<I, T> : JsonConverter
{
    public override bool CanWrite => false;
    public override bool CanRead => true;
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(I);
    }
    public override void WriteJson(JsonWriter writer,
        object value, JsonSerializer serializer)
    {
        throw new InvalidOperationException("Use default serialization.");
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var jsonObject = JObject.Load(reader);
        var deserialized = (T)Activator.CreateInstance(typeof(T));
        serializer.Populate(jsonObject.CreateReader(), deserialized);
        return deserialized;
    }
}

Definieren Sie dann Ihre Schnittstellen mit dem JsonConverter-Attribut

public class Person
{
    [JsonConverter(typeof(ConfigConverter<ILocation, Location>))]
    public ILocation Location { get;set; }
}
Adam
quelle
Einige Implementierungen verwenden serializer.Populateandere jsonObject.ToObject. Ist da ein Unterschied?
xr280xr
3

Fast ein Duplikat von Inregos Antwort, aber es verdient weitere Erklärung:

Wenn Sie verwenden, enthält TypeNameHandling.Autoes nur den Typ- / Baugruppennamen, wenn dies erforderlich ist (dh Schnittstellen und Basis- / abgeleitete Klassen). Ihr JSON ist also sauberer, kleiner und spezifischer.

Welches ist nicht eines der Hauptverkaufsargumente für XML / SOAP?

Sliderhouserules
quelle
2

Vermeiden Sie nach Möglichkeit TypeNameHandling.Auto, insbesondere bei benutzerdefinierbaren Werten.

Sie müssen Ihren eigenen Deserializer für den Sammlungstyp schreiben .

Anstatt andere zu wiederholen, die bereits Boilerplate-Konvertercode veröffentlicht haben (insbesondere Nicholas Westby , dessen Blog-Beitrag sehr nützlich war und oben verlinkt ist ), habe ich die relevanten Änderungen für die Deserialisierung einer Sammlung von Schnittstellen aufgenommen (ich hatte eine Enum-Interface-Eigenschaft zur Unterscheidung von Implementierern ):

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        Collection<T> result = new Collection<T>();
        var array = JArray.Load(reader);
        foreach (JObject jsonObject in array)
        { 
            var rule = default(T);
            var value = jsonObject.Value<string>("MyDistinguisher");
            MyEnum distinguisher;
            Enum.TryParse(value, out distinguisher);
            switch (distinguisher)
            {
                case MyEnum.Value1:
                    rule = serializer.Deserialize<Type1>(jsonObject.CreateReader());
                    break;
                case MyEnum.Value2:
                    rule = serializer.Deserialize<Type2>(jsonObject.CreateReader());
                    break;
                default:
                    rule = serializer.Deserialize<Type3>(jsonObject.CreateReader());
                    break;
            }
            result.Add(rule);
        }
        return result;
    }

Ich hoffe, dies ist hilfreich für die nächste Person, die nach einem Deserializer für die Schnittstellensammlung sucht.

Als schädlich angesehen
quelle
Wie würde dies mit system.text.json implementiert werden?
Anton Eriksson