JSON.net: como desserializar sem usar o construtor padrão?

136

Eu tenho uma classe que tem um construtor padrão e também um construtor sobrecarregado que recebe um conjunto de parâmetros. Esses parâmetros correspondem aos campos no objeto e são atribuídos na construção. Neste ponto, preciso do construtor padrão para outros fins, portanto, gostaria de mantê-lo, se puder.

Meu problema: se eu remover o construtor padrão e passar a string JSON, o objeto desserializa corretamente e passa os parâmetros do construtor sem problemas. Acabo voltando ao objeto preenchido da maneira que eu esperaria. No entanto, assim que adiciono o construtor padrão ao objeto, quando eu chamo JsonConvert.DeserializeObject<Result>(jsontext)as propriedades não são mais preenchidas.

Neste ponto, tentei adicionar new JsonSerializerSettings(){CheckAdditionalContent = true}à chamada de desserialização. isso não fez nada.

Outra nota. os parâmetros do contratante correspondem exatamente aos nomes dos campos, exceto que os parâmetros são iniciados com uma letra minúscula. Eu não acho que isso importaria, pois, como mencionei, a desserialização funciona bem sem construtor padrão.

Aqui está uma amostra dos meus construtores:

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
fonte

Respostas:

208

O Json.Net prefere usar o construtor padrão (sem parâmetros) em um objeto, se houver algum. Se houver vários construtores e você desejar que o Json.Net use um não padrão, você poderá adicionar o [JsonConstructor]atributo ao construtor que deseja que o Json.Net chame.

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

É importante que os nomes dos parâmetros do construtor correspondam aos nomes de propriedades correspondentes do objeto JSON (caso de ignorância) para que isso funcione corretamente. Entretanto, você não precisa necessariamente ter um parâmetro construtor para todas as propriedades do objeto. Para aquelas propriedades de objeto JSON que não são cobertas pelos parâmetros do construtor, o Json.Net tentará usar os acessadores de propriedade pública (ou propriedades / campos marcados com [JsonProperty]) para preencher o objeto depois de construí-lo.

Se você não deseja adicionar atributos à sua classe ou não controlar o código-fonte da classe que está tentando desserializar, outra alternativa é criar um JsonConverter personalizado para instanciar e preencher seu objeto. Por exemplo:

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

Em seguida, adicione o conversor às configurações do serializador e use-as quando desserializar:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Brian Rogers
fonte
4
Isso funcionou. É meio chato que agora eu tenha que aceitar a dependência do JSON.net no meu projeto de modelos, mas ei. Vou marcar isso como a resposta.
kmacdonald
3
Existem outras opções - você pode criar um costume JsonConverterpara sua turma. Isso removeria a dependência, mas você teria que lidar com a instanciação e o preenchimento do objeto no conversor. Também pode ser possível escrever um costume ContractResolverque instrua o Json.Net a usar o outro construtor alterando o seu JsonObjectContract, mas isso pode ser um pouco mais complicado do que parece.
9789 Brian
Sim, acho que o atributo funcionará bem. A chamada de desserialização é realmente genérica, portanto pode ser qualquer tipo de objeto. Eu acho que sua resposta original funcionará bem. Obrigado pela informação!
precisa saber é o seguinte
2
Seria realmente útil se fosse possível definir outra convenção para a seleção de construtores. Por exemplo, acho que o contêiner Unity suporta isso. Em seguida, você poderia fazer isso para que ele sempre selecionasse o construtor com a maioria dos parâmetros, em vez de voltar ao padrão. Existe a possibilidade de existir um ponto de extensão no Json.Net?
precisa saber é
1
Não esqueçausing Newtonsoft.Json;
Bruno Bieri
36

Um pouco tarde e não exatamente adequado aqui, mas vou adicionar minha solução aqui, porque minha pergunta foi encerrada como uma duplicata desta e porque essa solução é completamente diferente.

Eu precisava de uma maneira geral de instruir Json.NETa preferir o construtor mais específico para um tipo de estrutura definido pelo usuário, para poder omitir os JsonConstructoratributos que adicionariam uma dependência ao projeto em que cada estrutura é definida.

Eu fiz engenharia reversa um pouco e implementei um resolvedor de contrato personalizado, onde substituí o CreateObjectContractmétodo para adicionar minha lógica de criação personalizada.

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

Estou usando assim.

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
fonte
2
Atualmente, estou usando a resposta aceita acima, mas também quero agradecer por ter mostrado sua solução!
DotBert 31/03/16
1
Eu removi a restrição de estruturas (a verificação objectType.IsValueType) e isso funciona muito bem, obrigado!
Alex Angas
@AlexAngas Sim, a aplicação dessa estratégia em geral faz sentido, obrigado por seus comentários.
Zoltán Tamási
3

Com base em algumas das respostas aqui, escrevi um CustomConstructorResolverpara uso em um projeto atual e achei que poderia ajudar outra pessoa.

Ele suporta os seguintes mecanismos de resolução, todos configuráveis:

  • Selecione um único construtor privado para poder definir um construtor privado sem precisar marcá-lo com um atributo.
  • Selecione o construtor privado mais específico para que você possa ter várias sobrecargas, ainda sem precisar usar atributos.
  • Selecione o construtor marcado com um atributo de um nome específico - como o resolvedor padrão, mas sem uma dependência do pacote Json.Net porque você precisa fazer referência 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;
    }
}

Aqui está a versão completa com documentação XML como uma essência: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Feedback apreciado.

Björn Jarisch
fonte
Ótima solução! Obrigado por compartilhar.
thomai 9/03
1

O comportamento padrão do Newtonsoft.Json vai encontrar os publicconstrutores. Se o construtor padrão for usado apenas na classe ou na mesma montagem, você poderá reduzir o nível de acesso para protectedou internalpara que o Newtonsoft.Json escolha o publicconstrutor desejado .

É certo que esta solução é bastante limitada a casos específicos.

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
fonte
-1

Solução:

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

Modelo:

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