Verwenden von Json.NET-Konvertern zum Deserialisieren von Eigenschaften

88

Ich habe eine Klassendefinition, die eine Eigenschaft enthält, die eine Schnittstelle zurückgibt.

public class Foo
{ 
    public int Number { get; set; }

    public ISomething Thing { get; set; }
}

Beim Versuch, die Foo-Klasse mit Json.NET zu serialisieren, wird die folgende Fehlermeldung angezeigt: "Es konnte keine Instanz vom Typ 'ISomething' erstellt werden. ISomething kann eine Schnittstelle oder eine abstrakte Klasse sein."

Gibt es ein Json.NET-Attribut oder einen Konverter, mit dem ich eine konkrete SomethingKlasse angeben kann , die während der Deserialisierung verwendet werden soll?

dthrasher
quelle
Ich glaube, Sie müssen einen Eigenschaftsnamen angeben, der ISomething
ram
Ich habe. Ich verwende die Kurzform für automatisch implementierte Eigenschaften, die in C # 3.5 eingeführt wurden. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher
4
Ist nicht etwas der Typ? Ich denke, RAM ist richtig, Sie brauchen noch einen Eigenschaftsnamen. Ich weiß, dass dies nicht mit Ihrem Problem zusammenhängt, aber Ihr Kommentar oben hat mich glauben lassen, dass mir eine neue Funktion in .NET fehlt, mit der Sie eine Eigenschaft ohne Namen angeben konnten.
Herr Moose

Antworten:

91

Mit Json.NET können Sie unter anderem Folgendes tun :

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;

JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

Das TypeNameHandlingFlag fügt $typedem JSON eine Eigenschaft hinzu, mit der Json.NET erkennt, in welchen konkreten Typ das Objekt deserialisiert werden soll. Auf diese Weise können Sie ein Objekt deserialisieren, während Sie eine Schnittstelle oder eine abstrakte Basisklasse erfüllen.

Der Nachteil ist jedoch, dass dies sehr Json.NET-spezifisch ist. Das $typewird ein vollständig qualifizierter Typ sein, also , wenn Sie es mit Typ - Informationen sind die Serialisierung ,, die Deserializer muss in der Lage sein , es so gut zu verstehen.

Dokumentation: Serialisierungseinstellungen mit Json.NET

Daniel T.
quelle
Interessant. Ich muss damit herumspielen. Netter Tipp!
Dthrasher
2
Für Newtonsoft.Json funktioniert es ähnlich, aber die Eigenschaft ist "$ type"
Jaap
Das war zu einfach!
Shimmy Weitzhandler
1
Achten Sie bei der Verwendung hier auf mögliche Sicherheitsprobleme TypeNameHandling. Weitere Informationen finden Sie unter TypeNameHandling-Warnung in Newtonsoft Json .
dbc
Ich hatte gestern wie verrückt mit Konvertern zu kämpfen, und das war viel besser und verständlicher, danke !!!
Horothenic
52

Sie können dies durch die Verwendung der JsonConverter-Klasse erreichen. Angenommen, Sie haben eine Klasse mit einer Schnittstelleneigenschaft.

public class Organisation {
  public string Name { get; set; }

  [JsonConverter(typeof(TycoonConverter))]
  public IPerson Owner { get; set; }
}

public interface IPerson {
  string Name { get; set; }
}

public class Tycoon : IPerson {
  public string Name { get; set; }
}

Ihr JsonConverter ist für die Serialisierung und De-Serialisierung der zugrunde liegenden Eigenschaft verantwortlich.

public class TycoonConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return (objectType == typeof(IPerson));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    return serializer.Deserialize<Tycoon>(reader);
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}

Wenn Sie mit einer über Json.Net deserialisierten Organisation arbeiten, ist die zugrunde liegende IPerson für die Owner-Eigenschaft vom Typ Tycoon.

MrMDavidson
quelle
Sehr schön. Ich muss es mit dem Konverter versuchen.
Dthrasher
4
Würde das Tag "[JsonConverter (typeof (TycoonConverter))]" immer noch funktionieren, wenn es in einer Liste der Schnittstelle enthalten wäre?
Zwik
40

Anstatt ein benutzerdefiniertes JsonSerializerSettings-Objekt mit der Option TypeNameHandling.Objects an JsonConvert.SerializeObject () zu übergeben, können Sie diese bestimmte Schnittstelleneigenschaft einfach mit einem Attribut markieren, damit der generierte JSON nicht mit den Eigenschaften "$ type" aufgebläht wird auf JEDEM Objekt:

public class Foo
{
    public int Number { get; set; }

    // Add "$type" property containing type info of concrete class.
    [JsonProperty( TypeNameHandling = TypeNameHandling.Objects )]
    public ISomething { get; set; }
}
Erhhung
quelle
Brillant. Danke :)
Darren Young
4
Für Sammlungen von Schnittstellen oder abstrakten Klassen lautet die Eigenschaft "ItemTypeNameHandling". Beispiel: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Anthony F
Danke dafür!
Brudert
23

In der neuesten Version des Newtonsoft Json-Konverters eines Drittanbieters können Sie einen Konstruktor mit einem konkreten Typ festlegen, der sich auf die Schnittstelle bezieht.

public class Foo
{ 
    public int Number { get; private set; }

    public ISomething IsSomething { get; private set; }

    public Foo(int number, Something concreteType)
    {
        Number = number;
        IsSomething = concreteType;
    }
}

Solange etwas etwas implementiert, sollte dies funktionieren. Setzen Sie auch keinen leeren Standardkonstruktor, falls der JSon-Konverter versucht, diesen zu verwenden. Sie müssen ihn zwingen, den Konstruktor zu verwenden, der den konkreten Typ enthält.

PS. Auf diese Weise können Sie Ihre Setter auch privat machen.

SamuelDavis
quelle
6
Dies sollte von den Dächern geschrien werden! Es stimmt zwar, dass die konkrete Implementierung eingeschränkt wird, aber es ist viel einfacher als die anderen Ansätze für Situationen, in denen es verwendet werden kann.
Mark Meuer
3
Was ist, wenn wir mehr als einen Konstruktor mit mehreren konkreten Typen haben, wird er es trotzdem wissen?
Teoman Shipahi
1
Diese Antwort ist so elegant im Vergleich zu all dem verworrenen Unsinn, den Sie sonst machen müssten. Dies sollte die akzeptierte Antwort sein. Eine Einschränkung in meinem Fall war jedoch, dass ich [JsonConstructor] vor dem Konstruktor hinzufügen musste, damit es funktioniert. Ich vermute, dass die Verwendung dieser Option bei nur EINEM Ihrer konkreten Konstrukteure Ihr (4 Jahre altes) Problem lösen würde @ Teomanshipahi
Nacitar Sevaht
@nacitarsevaht Ich kann jetzt zurückgehen und mein Problem beheben :) Trotzdem erinnere ich mich nicht einmal daran, was es war, aber wenn ich es mir noch einmal anschaue, ist dies eine gute Lösung für bestimmte Fälle.
Teoman Shipahi
Wir verwenden dies auch, aber ich bevorzuge in den meisten Fällen die Konvertierung, da die Kopplung des Betontyps an den Konstruktor den Zweck der Verwendung einer Schnittstelle für die Eigenschaft in erster Linie zunichte macht!
gabe
18

Hatte das gleiche Problem, also kam ich auf meinen eigenen Konverter, der bekannte Typenargumente verwendet.

public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x =>typeName.Contains("."+x.Name+",")));
        }

        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

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

Ich habe zwei Erweiterungsmethoden zum Deserialisieren und Serialisieren definiert:

public static class AltiJsonSerializer
{
    public static T DeserializeJson<T>(this string jsonString, IEnumerable<Type> knownTypes = null)
    {
        if (string.IsNullOrEmpty(jsonString))
            return default(T);

        return JsonConvert.DeserializeObject<T>(jsonString,
                new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto, 
                    Converters = new List<JsonConverter>
                        (
                            new JsonConverter[]
                            {
                                new JsonKnownTypeConverter(knownTypes)
                            }
                        )
                }
            );
    }

    public static string SerializeJson(this object objectToSerialize)
    {
        return JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented,
        new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
    }
}

Sie können Ihre eigene Art des Vergleichens und Identifizierens von Typen in den Konvertierungen definieren. Ich verwende nur den Klassennamen.

Bruno Altinet
quelle
1
Dieser JsonConverter ist großartig, ich habe ihn verwendet, aber einige Probleme hatte ich auf diese Weise gelöst: - Verwenden Sie stattdessen JsonSerializer.CreateDefault () zum Auffüllen, da mein Objekt eine tiefere Hierarchie hatte. - Verwenden von Reflection, um den Konstruktor abzurufen und in der Create () -Methode zu instanziieren
Aurel
3

Normalerweise habe ich die Lösung immer mit verwendet, TypeNameHandlingwie von DanielT vorgeschlagen, aber in Fällen, in denen ich hier keine Kontrolle über den eingehenden JSON hatte (und daher nicht sicherstellen kann, dass er eine $typeEigenschaft enthält), habe ich einen benutzerdefinierten Konverter geschrieben, mit dem Sie nur explizit angeben können der konkrete Typ:

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

Hierbei wird nur die Standard-Serializer-Implementierung von Json.Net verwendet, während der konkrete Typ explizit angegeben wird.

Der Quellcode und eine Übersicht finden Sie in diesem Blogbeitrag .

Steve Greatrex
quelle
1
Dies ist eine großartige Lösung. Prost.
JohnMetta
2

Ich wollte nur das Beispiel vervollständigen, das uns @Daniel T. oben gezeigt hat:

Wenn Sie diesen Code zum Serialisieren Ihres Objekts verwenden:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

Der Code zum Deserialisieren des JSON sollte folgendermaßen aussehen:

var settings = new JsonSerializerSettings(); 
settings.TypeNameHandling = TypeNameHandling.Objects;
var entity = JsonConvert.DeserializeObject<EntityType>(json, settings);

So wird ein JSON angepasst, wenn das TypeNameHandlingFlag verwendet wird:Geben Sie hier die Bildbeschreibung ein

Luis Armando
quelle
-5

Ich habe mich das Gleiche gefragt, aber ich fürchte, es geht nicht.

Schauen wir es uns so an. Sie übergeben JSon.net eine Datenfolge und einen Typ, in den Sie deserialisieren möchten. Was kann JSON.net tun, wenn es auf etwas trifft? Es kann keine neue Art von ISomething erstellen, da ISomething kein Objekt ist. Es kann auch kein Objekt erstellen, das ISomething implementiert, da es keine Ahnung hat, welches der vielen Objekte, die möglicherweise ISomething erben, verwendet werden soll. Schnittstellen können automatisch serialisiert, aber nicht automatisch deserialisiert werden.

Was ich tun würde, wäre zu versuchen, ISomething durch eine Basisklasse zu ersetzen. Auf diese Weise können Sie möglicherweise den gewünschten Effekt erzielen.

Timothy Baldridge
quelle
1
Mir ist klar, dass es nicht "out of the box" funktionieren wird. Aber ich habe mich gefragt, ob es ein Attribut wie "[JsonProperty (typeof (SomethingBase))]" gibt, das ich verwenden kann, um eine konkrete Klasse bereitzustellen.
Dthrasher
Warum also nicht SomethingBase anstelle von ISomething im obigen Code verwenden? Es könnte argumentiert werden, dass wir dies auch falsch betrachten, da Schnittstellen bei der Serialisierung nicht verwendet werden sollten, da sie einfach die Kommunikationsschnittstelle mit einer bestimmten Klasse definieren. Das technische Serialisieren einer Schnittstelle ist Unsinn, ebenso wie das Serialisieren einer abstrakten Klasse. Also, während es "getan werden könnte", würde ich argumentieren, dass es "nicht getan werden sollte".
Timothy Baldridge
Haben Sie sich eine der Klassen im Newtonsoft.Json.Serialization-Namespace angesehen? insbesondere die JsonObjectContract-Klasse?
Johnny
-9

Hier ist ein Verweis auf einen Artikel von ScottGu

Auf dieser Grundlage habe ich einen Code geschrieben, der meiner Meinung nach hilfreich sein könnte

public interface IEducationalInstitute
{
    string Name
    {
        get; set;
    }

}

public class School : IEducationalInstitute
{
    private string name;
    #region IEducationalInstitute Members

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    #endregion
}

public class Student 
{
    public IEducationalInstitute LocalSchool { get; set; }

    public int ID { get; set; }
}

public static class JSONHelper
{
    public static string ToJSON(this object obj)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        return serializer.Serialize(obj);
    }
    public  static string ToJSON(this object obj, int depth)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.RecursionLimit = depth;
        return serializer.Serialize(obj);
    }
}

Und so würden Sie es nennen

School myFavSchool = new School() { Name = "JFK High School" };
Student sam = new Student()
{
    ID = 1,
    LocalSchool = myFavSchool
};
string jSONstring = sam.ToJSON();

Console.WriteLine(jSONstring);
//Result {"LocalSchool":{"Name":"JFK High School"},"ID":1}

Wenn ich es richtig verstehe, müssen Sie meiner Meinung nach keine konkrete Klasse angeben, die die Schnittstelle für die JSON-Serialisierung implementiert.

RAM
quelle
1
Ihr Beispiel verwendet den JavaScriptSerializer, eine Klasse in .NET Framework. Ich verwende Json.NET als Serializer. codeplex.com/Json
dthrasher
3
Bezieht sich nicht auf die ursprüngliche Frage, Json.NET wurde dort ausdrücklich erwähnt.
Oliver