JSON.net: ¿cómo deserializar sin usar el constructor predeterminado?

136

Tengo una clase que tiene un constructor predeterminado y también un constructor sobrecargado que toma un conjunto de parámetros. Estos parámetros coinciden con los campos del objeto y se asignan en la construcción. En este punto, necesito el constructor predeterminado para otros fines, por lo que me gustaría mantenerlo si puedo.

Mi problema: si elimino el constructor predeterminado y paso la cadena JSON, el objeto se deserializa correctamente y pasa los parámetros del constructor sin ningún problema. Termino recuperando el objeto poblado de la forma que esperaría. Sin embargo, tan pronto como agrego el constructor predeterminado en el objeto, cuando llamo a JsonConvert.DeserializeObject<Result>(jsontext)las propiedades ya no se rellenan.

En este punto, he intentado agregar new JsonSerializerSettings(){CheckAdditionalContent = true}a la llamada de deserialización. eso no hizo nada.

Otra nota. los parámetros del constructor coinciden exactamente con los nombres de los campos, excepto que los parámetros comienzan con una letra minúscula. No creo que esto importe ya que, como mencioné, la deserialización funciona bien sin un constructor predeterminado.

Aquí hay una muestra de mis constructores:

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
fuente

Respuestas:

208

Json.Net prefiere usar el constructor predeterminado (sin parámetros) en un objeto si hay uno. Si hay varios constructores y desea que Json.Net use uno no predeterminado, puede agregar el [JsonConstructor]atributo al constructor al que desea que llame Json.Net.

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

Es importante que los nombres de los parámetros del constructor coincidan con los nombres de propiedad correspondientes del objeto JSON (ignorando mayúsculas y minúsculas) para que esto funcione correctamente. Sin embargo, no necesariamente tiene que tener un parámetro constructor para cada propiedad del objeto. Para aquellas propiedades de objeto JSON que no están cubiertas por los parámetros del constructor, Json.Net intentará usar los accesos de propiedad pública (o propiedades / campos marcados con [JsonProperty]) para llenar el objeto después de construirlo.

Si no desea agregar atributos a su clase o no controla el código fuente de la clase que está tratando de deserializar, otra alternativa es crear un JsonConverter personalizado para crear instancias y llenar su objeto. Por ejemplo:

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

Luego, agregue el convertidor a la configuración de su serializador y use la configuración cuando deserialice:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Brian Rogers
fuente
44
Esto funcionó. Es una lástima que ahora tenga que tomar la dependencia JSON.net en mi proyecto de modelos, pero qué bueno. Marcaré esto como la respuesta.
kmacdonald
3
Hay otras opciones: puede crear una costumbre JsonConverterpara su clase. Esto eliminaría la dependencia, pero luego tendría que manejar la creación de instancias y llenar el objeto usted mismo en el convertidor. También podría ser posible escribir una costumbre ContractResolverque dirija a Json.Net a usar el otro constructor cambiando su JsonObjectContract, pero esto podría resultar un poco más complicado de lo que parece.
Brian Rogers
Sí, creo que el atributo funcionará bien. La llamada deserializar es realmente genérica, por lo que podría ser cualquier tipo de objeto. Creo que su respuesta original funcionará bien. ¡Gracias por la info!
kmacdonald
2
Realmente ayudaría si fuera posible establecer otra convención para la selección del constructor. Por ejemplo, creo que el contenedor de Unity lo admite. Entonces podría hacerlo de modo que siempre seleccione el constructor con la mayoría de los parámetros en lugar de recurrir al predeterminado. ¿Existe alguna posibilidad de que tal punto de extensión en Json.Net?
julealgon
1
No olvidesusing Newtonsoft.Json;
Bruno Bieri
36

Un poco tarde y no exactamente adecuado aquí, pero agregaré mi solución aquí, porque mi pregunta se había cerrado como un duplicado de esta, y porque esta solución es completamente diferente.

Necesitaba una forma general de instruir Json.NETpara preferir el constructor más específico para un tipo de estructura definido por el usuario, por lo que puedo omitir los JsonConstructoratributos que agregarían una dependencia al proyecto donde se define cada estructura.

Realicé ingeniería inversa un poco e implementé un solucionador de contratos personalizado donde anulé el CreateObjectContractmétodo para agregar mi lógica de creación 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);
    }
}

Lo estoy usando así.

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
fuente
2
Actualmente estoy usando la respuesta aceptada arriba, ¡pero quiero agradecerle por mostrar su solución también!
DotBert
1
Eliminé la restricción en las estructuras (la verificación de objectType.IsValueType) y esto funciona muy bien, ¡gracias!
Alex Angas
@AlexAngas Sí, aplicar esta estrategia en general tiene sentido gracias por sus comentarios.
Zoltán Tamási
3

Basado en algunas de las respuestas aquí, he escrito un CustomConstructorResolver para usar en un proyecto actual, y pensé que podría ayudar a alguien más.

Admite los siguientes mecanismos de resolución, todos configurables:

  • Seleccione un solo constructor privado para que pueda definir un constructor privado sin tener que marcarlo con un atributo.
  • Seleccione el constructor privado más específico para que pueda tener múltiples sobrecargas, aún sin tener que usar atributos.
  • Seleccione el constructor marcado con un atributo de un nombre específico, como el solucionador predeterminado, pero sin depender del paquete Json.Net porque necesita hacer referencia 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;
    }
}

Aquí está la versión completa con documentación XML como un resumen: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Comentarios apreciados.

Björn Jarisch
fuente
Gran solución! Gracias por compartir.
thomai
1

El comportamiento predeterminado de Newtonsoft.Json va a encontrar los publicconstructores. Si el constructor por defecto sólo se utiliza en la contención de la clase o del mismo conjunto, se puede reducir el nivel de acceso a protected, o internalde manera que Newtonsoft.Json escogerá su deseada publicconstructor.

Es cierto que esta solución está 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
fuente
-1

Solución:

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
fuente