JSON.net: come deserializzare senza usare il costruttore predefinito?


136

Ho una classe che ha un costruttore predefinito e anche un costruttore sovraccarico che accetta un set di parametri. Questi parametri corrispondono ai campi sull'oggetto e sono assegnati in fase di costruzione. A questo punto ho bisogno del costruttore predefinito per altri scopi, quindi vorrei tenerlo se posso.

Il mio problema: se rimuovo il costruttore predefinito e passo la stringa JSON, l'oggetto si deserializza correttamente e passa i parametri del costruttore senza problemi. Finisco per recuperare l'oggetto popolato come mi aspetterei. Tuttavia, non appena aggiungo il costruttore predefinito nell'oggetto, quando chiamo JsonConvert.DeserializeObject<Result>(jsontext)le proprietà non vengono più popolate.

A questo punto ho provato ad aggiungere new JsonSerializerSettings(){CheckAdditionalContent = true}alla chiamata di deserializzazione. quello non ha fatto niente.

Un'altra nota i parametri dei contruttori corrispondono esattamente ai nomi dei campi, tranne per il fatto che i parametri iniziano con una lettera minuscola. Non penserei che questo importerebbe poiché, come ho già detto, la deserializzazione funziona bene senza un costruttore predefinito.

Ecco un esempio dei miei costruttori:

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


Risposte:


208

Json.Net preferisce utilizzare il costruttore predefinito (senza parametri) su un oggetto, se presente. Se sono presenti più costruttori e si desidera che Json.Net ne utilizzi uno non predefinito, è possibile aggiungere l' [JsonConstructor]attributo al costruttore che si desidera chiamare da Json.Net.

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

È importante che i nomi dei parametri del costruttore corrispondano ai nomi delle proprietà corrispondenti dell'oggetto JSON (ignorando il caso) affinché funzioni correttamente. Tuttavia, non è necessario disporre di un parametro costruttore per ogni proprietà dell'oggetto. Per le proprietà dell'oggetto JSON che non sono coperte dai parametri del costruttore, Json.Net proverà a utilizzare gli accessori di proprietà pubblica (o proprietà / campi contrassegnati con [JsonProperty]) per popolare l'oggetto dopo averlo costruito.

Se non si desidera aggiungere attributi alla propria classe o non si controlla in altro modo il codice sorgente per la classe che si sta tentando di deserializzare, un'altra alternativa è quella di creare un JsonConverter personalizzato per creare un'istanza e popolare il proprio oggetto. Per esempio:

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

Quindi, aggiungere il convertitore alle impostazioni del serializzatore e utilizzare le impostazioni quando si deserializza:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);

4
Questo ha funzionato. In un certo senso fa schifo che ora devo prendere la dipendenza JSON.net nel mio progetto di modelli, ma che diamine. Lo segnerò come risposta.
kmacdonald,

3
Ci sono altre opzioni: potresti creare un'abitudine JsonConverterper la tua classe. Ciò eliminerebbe la dipendenza, ma poi dovresti gestire da solo l'istanza e il popolamento dell'oggetto nel convertitore. Potrebbe anche essere possibile scrivere un'abitudine ContractResolverche induca Json.Net a utilizzare l'altro costruttore modificandone la modifica JsonObjectContract, ma ciò potrebbe rivelarsi un po 'più complicato di quanto sembri.
Brian Rogers,

Sì, penso che l'attributo funzionerà bene. La chiamata deserialize è in realtà generica in modo che possa essere qualsiasi tipo di oggetto. penso che la tua risposta originale funzionerà bene. grazie per le informazioni!
kmacdonald,

2
Sarebbe davvero utile se fosse possibile stabilire un'altra convenzione per la selezione del costruttore. Ad esempio, penso che il contenitore Unity lo supporti. Quindi potresti farlo in modo che abbia sempre selezionato il costruttore con la maggior parte dei parametri invece di tornare a quello predefinito. Qualche possibilità esiste un punto di estensione in Json.Net?
julealgon,

1
Non dimenticareusing Newtonsoft.Json;
Bruno Bieri,

36

Un po 'tardi e non proprio adatto qui, ma aggiungerò la mia soluzione qui, perché la mia domanda era stata chiusa come duplicata di questa e perché questa soluzione è completamente diversa.

Avevo bisogno di un modo generale per istruire Json.NETa preferire il costruttore più specifico per un tipo di struttura definito dall'utente, in modo da poter omettere gli JsonConstructorattributi che aggiungerebbero una dipendenza al progetto in cui ogni struttura è definita.

Ho invertito un po 'l'ingegneria e implementato un risolutore di contratti personalizzati in cui ho ignorato il CreateObjectContractmetodo per aggiungere la mia logica di creazione personalizzata.

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

Lo sto usando in questo modo.

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

2
Attualmente sto usando la risposta accettata sopra, ma voglio ringraziarti per aver mostrato anche la tua soluzione!
DotBert

1
Ho rimosso la restrizione sulle strutture (il controllo per objectType.IsValueType) e questo funziona alla grande, grazie!
Alex Angas,

@AlexAngas Sì, applicare questa strategia in generale ha senso grazie per il tuo feedback.
Zoltán Tamási,

3

Sulla base di alcune delle risposte qui, ho scritto un CustomConstructorResolverper l'uso in un progetto corrente e ho pensato che potesse aiutare qualcun altro.

Supporta i seguenti meccanismi di risoluzione, tutti configurabili:

  • Seleziona un singolo costruttore privato in modo da poter definire un costruttore privato senza contrassegnarlo con un attributo.
  • Seleziona il costruttore privato più specifico in modo da poter avere più sovraccarichi, senza tuttavia utilizzare gli attributi.
  • Seleziona il costruttore contrassegnato con un attributo di un nome specifico, ad esempio il risolutore predefinito, ma senza una dipendenza dal pacchetto Json.Net perché devi fare riferimento 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;
    }
}

Ecco la versione completa con la documentazione XML come sintesi: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Feedback apprezzato.


Ottima soluzione! Grazie per la condivisione.
Thom

1

Il comportamento predefinito di Newtonsoft.Json sta per trovare i publiccostruttori. Se il costruttore di default viene utilizzato solo in classe o che contiene lo stesso assieme, è possibile ridurre il livello di accesso alle protectedo internalin modo che Newtonsoft.Json prenderà il tuo desiderato publiccostruttore.

Certo, questa soluzione è piuttosto limitata a casi specifici.

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

-1

Soluzione:

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

Modello:

public class modelname {
    public long parameter{ get; set; }
    public int parameter{ get; set; }
    public int parameter{ get; set; }
    public string parameter{ get; set; }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.