JSON.net: Wie deserialisiere ich ohne den Standardkonstruktor?

136

Ich habe eine Klasse mit einem Standardkonstruktor und einem überladenen Konstruktor, der eine Reihe von Parametern akzeptiert. Diese Parameter stimmen mit Feldern im Objekt überein und werden bei der Erstellung zugewiesen. An diesem Punkt brauche ich den Standardkonstruktor für andere Zwecke, also möchte ich ihn behalten, wenn ich kann.

Mein Problem: Wenn ich den Standardkonstruktor entferne und die JSON-Zeichenfolge übergebe, deserialisiert das Objekt korrekt und übergibt die Konstruktorparameter ohne Probleme. Am Ende bekomme ich das Objekt so zurück, wie ich es erwartet hätte. Sobald ich jedoch den Standardkonstruktor zum Objekt hinzufüge, werden beim Aufrufen JsonConvert.DeserializeObject<Result>(jsontext)die Eigenschaften nicht mehr ausgefüllt.

Zu diesem Zeitpunkt habe ich versucht new JsonSerializerSettings(){CheckAdditionalContent = true}, den Deserialisierungsaufruf zu ergänzen. das hat nichts getan.

Noch ein Hinweis. Die Konstruktorparameter stimmen genau mit den Namen der Felder überein, außer dass die Parameter mit einem Kleinbuchstaben beginnen. Ich würde nicht denken, dass dies eine Rolle spielen würde, da die Deserialisierung, wie bereits erwähnt, ohne Standardkonstruktor einwandfrei funktioniert.

Hier ist ein Beispiel meiner Konstruktoren:

public Result() { }

public Result(int? code, string format, Dictionary<string, string> details = null)
{
    Code = code ?? ERROR_CODE;
    Format = format;

    if (details == null)
        Details = new Dictionary<string, string>();
    else
        Details = details;
}
kmacdonald
quelle
Vielleicht kann dies helfen, stackoverflow.com/questions/8254503/…
csharpwinphonexaml

Antworten:

208

Json.Net bevorzugt die Verwendung des Standardkonstruktors (ohne Parameter) für ein Objekt, falls vorhanden. Wenn mehrere Konstruktoren vorhanden sind und Json.Net einen nicht standardmäßigen verwenden soll, können Sie das [JsonConstructor]Attribut dem Konstruktor hinzufügen, den Json.Net aufrufen soll.

[JsonConstructor]
public Result(int? code, string format, Dictionary<string, string> details = null)
{
    ...
}

Es ist wichtig, dass die Konstruktorparameternamen mit den entsprechenden Eigenschaftsnamen des JSON-Objekts übereinstimmen (Groß- und Kleinschreibung ignorieren), damit dies ordnungsgemäß funktioniert. Sie müssen jedoch nicht unbedingt für jede Eigenschaft des Objekts einen Konstruktorparameter haben. Für JSON-Objekteigenschaften, die nicht von den Konstruktorparametern abgedeckt werden, versucht Json.Net, die öffentlichen Eigenschaftszugriffsfunktionen (oder mit gekennzeichneten Eigenschaften / Feldern [JsonProperty]) zu verwenden, um das Objekt nach dem Erstellen zu füllen .

Wenn Sie Ihrer Klasse keine Attribute hinzufügen oder den Quellcode für die Klasse, die Sie deserialisieren möchten, nicht anderweitig steuern möchten, können Sie auch einen benutzerdefinierten JsonConverter erstellen, um Ihr Objekt zu instanziieren und zu füllen . Beispielsweise:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load the JSON for the Result into a JObject
        JObject jo = JObject.Load(reader);

        // Read the properties which will be used as constructor parameters
        int? code = (int?)jo["Code"];
        string format = (string)jo["Format"];

        // Construct the Result object using the non-default constructor
        Result result = new Result(code, format);

        // (If anything else needs to be populated on the result object, do that here)

        // Return the result
        return result;
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

Fügen Sie dann den Konverter zu Ihren Serializer-Einstellungen hinzu und verwenden Sie die Einstellungen beim Deserialisieren:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Brian Rogers
quelle
4
Das hat funktioniert. Es ist schade, dass ich jetzt die JSON.net-Abhängigkeit in meinem Modellprojekt übernehmen muss, aber was solls. Ich werde dies als Antwort markieren.
kmacdonald
3
Es gibt andere Optionen - Sie können eine benutzerdefinierte Option JsonConverterfür Ihre Klasse erstellen . Dies würde die Abhängigkeit entfernen, aber dann müssten Sie das Instanziieren und Auffüllen des Objekts selbst im Konverter durchführen. Es könnte auch möglich sein, einen benutzerdefinierten ContractResolverBenutzer zu schreiben , der Json.Net anweist, den anderen Konstruktor durch Ändern seines zu verwenden JsonObjectContract. Dies könnte sich jedoch als etwas schwieriger erweisen, als es sich anhört.
Brian Rogers
Ja, ich denke, das Attribut wird gut funktionieren. Der Deserialize-Aufruf ist eigentlich generisch, sodass es sich um einen beliebigen Objekttyp handeln kann. Ich denke, Ihre ursprüngliche Antwort wird gut funktionieren. Danke für die Information!
kmacdonald
2
Es wäre wirklich hilfreich, wenn es möglich wäre, eine andere Konvention für die Konstruktorauswahl festzulegen. Ich denke zum Beispiel, dass der Unity-Container dies unterstützt. Dann können Sie es so gestalten, dass immer der Konstruktor mit den meisten Parametern ausgewählt wird, anstatt auf den Standard zurückzugreifen. Gibt es eine Möglichkeit, dass ein solcher Erweiterungspunkt in Json.Net vorhanden ist?
Julealgon
1
Vergessen Sie nichtusing Newtonsoft.Json;
Bruno Bieri
36

Ein bisschen spät und hier nicht genau geeignet, aber ich werde meine Lösung hier hinzufügen, weil meine Frage als Duplikat dieser geschlossen wurde und weil diese Lösung völlig anders ist.

Ich brauchte eine allgemeine Methode, um anzuweisen Json.NET, den spezifischsten Konstruktor für einen benutzerdefinierten Strukturtyp zu bevorzugen, damit ich die JsonConstructorAttribute weglassen kann, die dem Projekt, in dem jede solche Struktur definiert ist, eine Abhängigkeit hinzufügen würden.

Ich habe ein wenig rückentwickelt und einen benutzerdefinierten Vertragsauflöser implementiert, bei dem ich die CreateObjectContractMethode zum Hinzufügen meiner benutzerdefinierten Erstellungslogik überschrieben habe .

public class CustomContractResolver : DefaultContractResolver {

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var c = base.CreateObjectContract(objectType);
        if (!IsCustomStruct(objectType)) return c;

        IList<ConstructorInfo> list = objectType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).OrderBy(e => e.GetParameters().Length).ToList();
        var mostSpecific = list.LastOrDefault();
        if (mostSpecific != null)
        {
            c.OverrideCreator = CreateParameterizedConstructor(mostSpecific);
            c.CreatorParameters.AddRange(CreateConstructorParameters(mostSpecific, c.Properties));
        }

        return c;
    }

    protected virtual bool IsCustomStruct(Type objectType)
    {
        return objectType.IsValueType && !objectType.IsPrimitive && !objectType.IsEnum && !objectType.Namespace.IsNullOrEmpty() && !objectType.Namespace.StartsWith("System.");
    }

    private ObjectConstructor<object> CreateParameterizedConstructor(MethodBase method)
    {
        method.ThrowIfNull("method");
        var c = method as ConstructorInfo;
        if (c != null)
            return a => c.Invoke(a);
        return a => method.Invoke(null, a);
    }
}

Ich benutze es so.

public struct Test {
  public readonly int A;
  public readonly string B;

  public Test(int a, string b) {
    A = a;
    B = b;
  }
}

var json = JsonConvert.SerializeObject(new Test(1, "Test"), new JsonSerializerSettings {
  ContractResolver = new CustomContractResolver()
});
var t = JsonConvert.DeserializeObject<Test>(json);
t.A.ShouldEqual(1);
t.B.ShouldEqual("Test");
Zoltán Tamási
quelle
2
Ich verwende derzeit die oben akzeptierte Antwort, möchte mich aber auch bei Ihnen dafür bedanken, dass Sie Ihre Lösung gezeigt haben!
DotBert
1
Ich habe die Einschränkung für Strukturen (die Prüfung auf objectType.IsValueType) aufgehoben und das funktioniert großartig, danke!
Alex Angas
@AlexAngas Ja, die Anwendung dieser Strategie im Allgemeinen ist sinnvoll. Vielen Dank für Ihr Feedback.
Zoltán Tamási
3

Basierend auf einigen der Antworten hier habe ich eine CustomConstructorResolverzur Verwendung in einem aktuellen Projekt geschrieben und dachte, es könnte jemand anderem helfen.

Es unterstützt die folgenden Auflösungsmechanismen, die alle konfigurierbar sind:

  • Wählen Sie einen einzelnen privaten Konstruktor aus, damit Sie einen privaten Konstruktor definieren können, ohne ihn mit einem Attribut markieren zu müssen.
  • Wählen Sie den spezifischsten privaten Konstruktor aus, damit Sie mehrere Überladungen haben können, ohne Attribute verwenden zu müssen.
  • Wählen Sie den Konstruktor aus, der mit einem Attribut eines bestimmten Namens gekennzeichnet ist - wie der Standardauflöser, jedoch ohne Abhängigkeit vom Json.Net-Paket, da Sie darauf verweisen müssen Newtonsoft.Json.JsonConstructorAttribute.
public class CustomConstructorResolver : DefaultContractResolver
{
    public string ConstructorAttributeName { get; set; } = "JsonConstructorAttribute";
    public bool IgnoreAttributeConstructor { get; set; } = false;
    public bool IgnoreSinglePrivateConstructor { get; set; } = false;
    public bool IgnoreMostSpecificConstructor { get; set; } = false;

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        // Use default contract for non-object types.
        if (objectType.IsPrimitive || objectType.IsEnum) return contract;

        // Look for constructor with attribute first, then single private, then most specific.
        var overrideConstructor = 
               (this.IgnoreAttributeConstructor ? null : GetAttributeConstructor(objectType)) 
            ?? (this.IgnoreSinglePrivateConstructor ? null : GetSinglePrivateConstructor(objectType)) 
            ?? (this.IgnoreMostSpecificConstructor ? null : GetMostSpecificConstructor(objectType));

        // Set override constructor if found, otherwise use default contract.
        if (overrideConstructor != null)
        {
            SetOverrideCreator(contract, overrideConstructor);
        }

        return contract;
    }

    private void SetOverrideCreator(JsonObjectContract contract, ConstructorInfo attributeConstructor)
    {
        contract.OverrideCreator = CreateParameterizedConstructor(attributeConstructor);
        contract.CreatorParameters.Clear();
        foreach (var constructorParameter in base.CreateConstructorParameters(attributeConstructor, contract.Properties))
        {
            contract.CreatorParameters.Add(constructorParameter);
        }
    }

    private ObjectConstructor<object> CreateParameterizedConstructor(MethodBase method)
    {
        var c = method as ConstructorInfo;
        if (c != null)
            return a => c.Invoke(a);
        return a => method.Invoke(null, a);
    }

    protected virtual ConstructorInfo GetAttributeConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .Where(c => c.GetCustomAttributes().Any(a => a.GetType().Name == this.ConstructorAttributeName)).ToList();

        if (constructors.Count == 1) return constructors[0];
        if (constructors.Count > 1)
            throw new JsonException($"Multiple constructors with a {this.ConstructorAttributeName}.");

        return null;
    }

    protected virtual ConstructorInfo GetSinglePrivateConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);

        return constructors.Length == 1 ? constructors[0] : null;
    }

    protected virtual ConstructorInfo GetMostSpecificConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .OrderBy(e => e.GetParameters().Length);

        var mostSpecific = constructors.LastOrDefault();
        return mostSpecific;
    }
}

Hier ist die vollständige Version mit XML-Dokumentation als Kern: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Feedback erwünscht.

Björn Jarisch
quelle
Tolle Lösung! Danke für das Teilen.
Thomai
1

Das Standardverhalten von Newtonsoft.Json wird die publicKonstruktoren finden. Wenn Ihr Standardkonstruktor nur zum Enthalten von Klassen oder derselben Assembly verwendet wird, können Sie die Zugriffsebene auf protectedoder internalso reduzieren , dass Newtonsoft.Json den gewünschten publicKonstruktor auswählt .

Zugegebenermaßen ist diese Lösung eher auf bestimmte Fälle beschränkt.

internal Result() { }

public Result(int? code, string format, Dictionary<string, string> details = null)
{
    Code = code ?? ERROR_CODE;
    Format = format;

    if (details == null)
        Details = new Dictionary<string, string>();
    else
        Details = details;
}
Darkato
quelle
0

Lösung:

public Response Get(string jsonData) {
    var json = JsonConvert.DeserializeObject<modelname>(jsonData);
    var data = StoredProcedure.procedureName(json.Parameter, json.Parameter, json.Parameter, json.Parameter);
    return data;
}

Modell:

public class modelname {
    public long parameter{ get; set; }
    public int parameter{ get; set; }
    public int parameter{ get; set; }
    public string parameter{ get; set; }
}
Sachin
quelle