JSON.net: comment désérialiser sans utiliser le constructeur par défaut?

136

J'ai une classe qui a un constructeur par défaut et aussi un constructeur surchargé qui prend dans un ensemble de paramètres. Ces paramètres correspondent aux champs de l'objet et sont affectés lors de la construction. À ce stade, j'ai besoin du constructeur par défaut à d'autres fins, je voudrais donc le conserver si je le peux.

Mon problème: si je supprime le constructeur par défaut et passe la chaîne JSON, l'objet se désérialise correctement et transmet les paramètres du constructeur sans aucun problème. Je finis par récupérer l'objet peuplé comme je m'y attendais. Cependant, dès que j'ajoute le constructeur par défaut dans l'objet, lorsque j'appelle, JsonConvert.DeserializeObject<Result>(jsontext)les propriétés ne sont plus remplies.

À ce stade, j'ai essayé d'ajouter new JsonSerializerSettings(){CheckAdditionalContent = true}à l'appel de désérialisation. cela n'a rien fait.

Une autre note. les paramètres du constructeur correspondent exactement aux noms des champs, sauf que les paramètres commencent par une lettre minuscule. Je ne pense pas que cela importerait car, comme je l'ai mentionné, la désérialisation fonctionne bien sans constructeur par défaut.

Voici un échantillon de mes constructeurs:

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
la source

Réponses:

208

Json.Net préfère utiliser le constructeur par défaut (sans paramètre) sur un objet s'il y en a un. S'il existe plusieurs constructeurs et que vous souhaitez que Json.Net en utilise un autre que celui par défaut, vous pouvez ajouter l' [JsonConstructor]attribut au constructeur que vous souhaitez que Json.Net appelle.

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

Il est important que les noms des paramètres du constructeur correspondent aux noms de propriété correspondants de l'objet JSON (en ignorant la casse) pour que cela fonctionne correctement. Cependant, vous n'avez pas nécessairement besoin d'un paramètre de constructeur pour chaque propriété de l'objet. Pour les propriétés d'objet JSON qui ne sont pas couvertes par les paramètres du constructeur, Json.Net essaiera d'utiliser les accesseurs de propriété publique (ou les propriétés / champs marqués par [JsonProperty]) pour remplir l'objet après sa construction.

Si vous ne souhaitez pas ajouter d'attributs à votre classe ou si vous ne contrôlez pas le code source de la classe que vous essayez de désérialiser, une autre alternative consiste à créer un JsonConverter personnalisé pour instancier et peupler votre objet. Par exemple:

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

Ensuite, ajoutez le convertisseur à vos paramètres de sérialiseur et utilisez les paramètres lorsque vous désérialisez:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Brian Rogers
la source
4
Cela a fonctionné. Ça craint que je doive maintenant prendre la dépendance JSON.net dans mon projet de modèles, mais qu'est-ce que c'est. Je marquerai ceci comme la réponse.
kmacdonald
3
Il existe d'autres options - vous pouvez créer une personnalisation JsonConverterpour votre classe. Cela supprimerait la dépendance, mais vous devrez ensuite gérer l'instanciation et le remplissage de l'objet vous-même dans le convertisseur. Il pourrait également être possible d'écrire une coutume ContractResolverqui dirigerait Json.Net pour utiliser l'autre constructeur en changeant son JsonObjectContract, mais cela pourrait s'avérer un peu plus compliqué qu'il n'y paraît.
Brian Rogers
Ouais, je pense que l'attribut fonctionnera bien. L'appel de désérialisation est en fait générique afin qu'il puisse s'agir de n'importe quel type d'objet. Je pense que votre réponse originale fonctionnera très bien. Merci pour l'info!
kmacdonald
2
Cela aiderait vraiment s'il était possible de définir une autre convention pour la sélection du constructeur. Par exemple, je pense que le conteneur Unity prend en charge cela. Ensuite, vous pouvez faire en sorte qu'il sélectionne toujours le constructeur avec la plupart des paramètres au lieu de revenir au constructeur par défaut. Une telle possibilité existe-t-elle dans Json.Net?
julealgon
1
N'oubliez pasusing Newtonsoft.Json;
Bruno Bieri
36

Un peu tard et pas tout à fait adapté ici, mais je vais ajouter ma solution ici, car ma question avait été fermée comme un double de celle-ci, et parce que cette solution est complètement différente.

J'avais besoin d'un moyen général pour demander Json.NETde préférer le constructeur le plus spécifique pour un type de structure défini par l'utilisateur, afin que je puisse omettre les JsonConstructorattributs qui ajouteraient une dépendance au projet où chaque structure de ce type est définie.

J'ai un peu d'ingénierie inverse et implémenté un résolveur de contrat personnalisé où j'ai remplacé la CreateObjectContractméthode pour ajouter ma logique de création personnalisée.

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

Je l'utilise comme ça.

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
la source
2
J'utilise actuellement la réponse acceptée ci-dessus, mais je tiens également à vous remercier d'avoir montré votre solution!
DotBert
1
J'ai supprimé la restriction sur les structures (la vérification objectType.IsValueType) et cela fonctionne très bien, merci!
Alex Angas le
@AlexAngas Oui, appliquer cette stratégie en général a du sens, merci pour vos commentaires.
Zoltán Tamási
3

Sur la base de certaines des réponses ici, j'ai écrit un CustomConstructorResolverpour une utilisation dans un projet en cours, et j'ai pensé que cela pourrait aider quelqu'un d'autre.

Il prend en charge les mécanismes de résolution suivants, tous configurables:

  • Sélectionnez un seul constructeur privé afin de pouvoir définir un constructeur privé sans avoir à le marquer avec un attribut.
  • Sélectionnez le constructeur privé le plus spécifique pour pouvoir avoir plusieurs surcharges, toujours sans avoir à utiliser d'attributs.
  • Sélectionnez le constructeur marqué avec un attribut d'un nom spécifique - comme le résolveur par défaut, mais sans dépendance sur le package Json.Net car vous devez référencer 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;
    }
}

Voici la version complète avec la documentation XML comme base: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Commentaires appréciés.

Björn Jarisch
la source
Excellente solution! Merci d'avoir partagé.
thomai
1

Le comportement par défaut de Newtonsoft.Json va trouver les publicconstructeurs. Si votre constructeur par défaut est uniquement utilisé pour contenir la classe ou le même assembly, vous pouvez réduire le niveau d'accès à protectedou internalpour que Newtonsoft.Json choisisse le publicconstructeur souhaité .

Certes, cette solution est plutôt très limitée à des cas spécifiques.

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
la source
-1

Solution:

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

Modèle:

public class modelname {
    public long parameter{ get; set; }
    public int parameter{ get; set; }
    public int parameter{ get; set; }
    public string parameter{ get; set; }
}
sachin
la source