C#:deserializar JSON con un constructor específico

C#:deserializar JSON con un constructor específico

Cuando su clase tiene varios constructores, puede usar el atributo JsonConstructor para especificar qué constructor usar durante la deserialización. He aquí un ejemplo:

using System.Text.Json.Serialization;

public class Person
{
	public string Name { get; set; }
	public int LuckyNumber { get; private set; }
	
	[JsonConstructor]
	public Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}
	public Person() { }
}
Code language: C# (cs)

Nota:JsonConstructor para System.Text.Json se agregó en .NET 5.

Ahora deserialice:

using System.Text.Json;

var person = JsonSerializer.Deserialize<Person>("{\"LuckyNumber\":7, \"Name\":\"Jason\"}");
Console.WriteLine($"{person.Name}'s lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Esto genera:

Jason's lucky number is 7Code language: plaintext (plaintext)

Esto muestra que usó el constructor Person(int luckyNumber). Pasó en el LuckyNumber propiedad JSON al constructor y luego configure las propiedades restantes que no se pasaron al constructor (solo Person.Name ).

Newtonsoft funciona con constructores casi exactamente de la misma manera que System.Text.Json, que explicaré al final.

¿Qué constructor usará System.Text.Json?

Cuando deserializa, System.Text.Json busca un constructor público usando las siguientes reglas de precedencia:

  • Utilice un constructor público con el atributo JsonConstructor.
[JsonConstructor]
public Person(int luckyNumber) //uses this one

public Person()
Code language: C# (cs)
  • Utilice un constructor público sin parámetros.
public Person(int luckyNumber)

public Person() //uses this one
Code language: C# (cs)
  • Utilice el único constructor público.
public Person(int luckyNumber) //uses this one
Code language: C# (cs)

Tenga en cuenta que no necesita agregar el atributo JsonConstructor si solo tiene un único constructor parametrizado. Sin embargo, sugiero usar JsonConstructor solo para que no te encuentres con sorpresas en el futuro si alguna vez agregas otro constructor.

Error:cuando no puede encontrar un constructor público adecuado

Si no se encuentra un constructor público adecuado, obtendrá la siguiente excepción durante la deserialización:

No se indica en el mensaje de error, pero debe tener un público constructor. Estos son algunos ejemplos de constructores que darían como resultado esta excepción.

  • No hay un constructor público.
internal Person() { }
Code language: C# (cs)
  • Hay varios constructores parametrizados y JsonConstructor no se usa.
public Person(int luckyNumber)

public Person(int luckyNumber, string name)
Code language: C# (cs)

Error:no se puede usar JsonConstructor varias veces

Solo puede colocar el atributo JsonConstructor en un constructor; de lo contrario, obtendrá la siguiente excepción durante la deserialización:

Este es un ejemplo del uso incorrecto de JsonConstructor varias veces:

[JsonConstructor]
public Person(int luckyNumber)

[JsonConstructor]
public Person(int luckyNumber, string name)
Code language: C# (cs)

Nota:este problema ocurre incluso si coloca JsonConstructor en constructores no públicos (sí, aunque System.Text.Json no usará constructores no públicos) .

Antes de .NET 5

Digamos que solo tiene un constructor parametrizado:

public Person(int luckyNumber)
Code language: C# (cs)

Antes de .NET 5, System.Text.Json requería un constructor sin parámetros. Entonces, si solo tuviera un constructor parametrizado, arrojaría la siguiente excepción:

Tienes tres opciones:

  • Actualizar a .NET 5.
  • Escriba un convertidor personalizado que cree el objeto utilizando el constructor parametrizado.
  • Use Newtonsoft en su lugar.

Newtonsoft es su mejor opción si no puede actualizar a .NET 5 y no quiere escribir un convertidor personalizado. Aquí hay un ejemplo del uso de Newtonsoft:

using Newtonsoft.Json;

var person = JsonConvert.DeserializeObject<Person>("{\"LuckyNumber\":7}");
Console.WriteLine($"Lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Esto genera lo siguiente, mostrando que maneja constructores parametrizados:

Lucky number is 7Code language: plaintext (plaintext)

Nombres de parámetros de constructores

Cuando usa un constructor parametrizado, debe seguir estas reglas:

  • El nombre de la propiedad JSON debe coincidir con un nombre de propiedad en la clase (se distingue entre mayúsculas y minúsculas de forma predeterminada).
  • El nombre del parámetro del constructor debe coincidir con un nombre de propiedad en la clase (sin distinción entre mayúsculas y minúsculas de forma predeterminada).

Si no se cumplen las condiciones del nombre del parámetro, obtendrá una excepción:

Aquí hay un ejemplo del uso de nombres que cumplen con estas condiciones. Digamos que tiene el siguiente JSON:

{
  "LuckyNumber":7
}Code language: JSON / JSON with Comments (json)

La clase necesita una propiedad llamada LuckyNumber . Por convención, los parámetros usan camelCasing, así que agregue un parámetro llamado luckyNumber :

using System.Text.Json.Serialization;

public class Person
{
	public int LuckyNumber { get; private set; }

	[JsonConstructor]
	public Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}

	public Person() { }
}
Code language: C# (cs)

Es capaz de deserializar esto.

Error:no se puede asignar a una propiedad que utiliza el atributo JsonExtensionData

Otro tipo de error con el que te puedes encontrar al deserializar con un constructor parametrizado es el siguiente:

Utiliza el atributo JsonExtensionData para capturar propiedades JSON donde no hay una propiedad coincidente en la clase. No puede tener esta propiedad como un parámetro de constructor. He aquí un ejemplo:

using System.Text.Json.Serialization;

public class Person
{
	[JsonExtensionData]
	public Dictionary<string, object> ExtensionData { get; set; }
	
	[JsonConstructor]
	public Person(Dictionary<string, object> ExtensionData)
	{
		
	}
	
	public Person() {}
}
Code language: C# (cs)

Debe eliminar el atributo JsonExtensionData de la propiedad o eliminar ese parámetro del constructor.

Cuando no puede usar el atributo JsonConstructor

La razón principal por la que no puede usar el atributo JsonConstructor es porque está tratando de deserializar una clase sobre la que no tiene control y no puede cambiar. Hay dos opciones que puedes hacer.

Opción 1:subclase y agregue un constructor

Supongamos que está utilizando la siguiente clase de terceros que no puede cambiar y desea utilizar el constructor parametrizado durante la deserialización:

public class Person
{
	public string Name { get; set; }
	public int LuckyNumber { get; private set; }

	public Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}

	public Person() { }
}
Code language: C# (cs)

Puede subclasificar esto, agregar un constructor y usar el atributo JsonConstructor (opcional si solo tiene un constructor):

using System.Text.Json.Serialization;

public class CustomPerson : Person
{
	[JsonConstructor]
	public CustomPerson(int luckyNumber) : base(luckyNumber) 
	{ }
}
Code language: C# (cs)

Luego deserializa usando tu subclase:

using System.Text.Json;

var person = JsonSerializer.Deserialize<CustomPerson>("{\"LuckyNumber\":13, \"Name\":\"Jason\"}");
Console.WriteLine($"{person.Name}'s lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Utilizará el constructor parametrizado. Esto genera lo siguiente:

Jason's lucky number is 13Code language: plaintext (plaintext)

Opción 2:escribir un convertidor personalizado

Si no puede usar el enfoque de subclase (por ejemplo, cuando no conoce los tipos con anticipación o si se trata de una clase sellada), puede escribir un convertidor personalizado para usar el constructor que desee.

Supongamos que tiene la siguiente clase sellada y desea utilizar el constructor parametrizado durante la deserialización:

public sealed class Person
{
	public string Name { get; set; }
	public int LuckyNumber { get; private set; }

	public Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}

	public Person() { }
}
Code language: C# (cs)

Agregue un convertidor personalizado para la clase Person. Implemente la deserialización siguiendo los siguientes pasos en el método Read():

  • Analizar JSON en un JsonDocument.
  • Obtenga las propiedades necesarias para llamar al constructor parametrizado.
  • Cree el objeto con el constructor parametrizado.
  • Establezca el resto de las propiedades.
using System.Text.Json;
using System.Text.Json.Serialization;

public class PersonConverter : JsonConverter<Person>
{
	public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (!JsonDocument.TryParseValue(ref reader, out JsonDocument jsonDoc))
			throw new JsonException("PersonConverter couldn't parse Person JSON");

		var personJson = jsonDoc.RootElement;

		//Get properties for constructor
		var luckyNumber = personJson.GetProperty(nameof(Person.LuckyNumber)).GetInt32();

		return new Person(luckyNumber)
		{
			//populate the remaining elements
			Name = personJson.GetProperty(nameof(Person.Name)).GetString()
		};
	}

	public override void Write(Utf8JsonWriter writer, Person value, JsonSerializerOptions options)
	{
		var optsWithoutThisConverter = new JsonSerializerOptions(options);
		optsWithoutThisConverter.Converters.Remove(this); //prevent recursion

		JsonSerializer.Serialize<Person>(writer, value, optsWithoutThisConverter);
	}
}
Code language: C# (cs)

Ahora use el convertidor personalizado durante la deserialización agregándolo a JsonSerializerOptions.Converters:

using System.Text.Json;

var options = new JsonSerializerOptions();
options.Converters.Add(new PersonConverter());

var person = JsonSerializer.Deserialize<Person>("{\"LuckyNumber\":137, \"Name\":\"Albert\"}",
	options);

Console.WriteLine($"{person.Name}'s lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Esto utiliza con éxito el convertidor personalizado, que llama al constructor Person(int luckyNumber) parametrizado según se desee, y genera lo siguiente:

Albert's lucky number is 137Code language: plaintext (plaintext)

Newtonsoft y constructores

Newtonsoft y System.Text.Json en su mayoría funcionan igual cuando se trata de constructores. Por ejemplo, cuando tiene varios constructores, puede usar el atributo JsonConstructor para especificar qué constructor usar:

using Newtonsoft.Json;

public class Person
{
	public int LuckyNumber { get; private set; }

	[JsonConstructor]
	public Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}
	public Person() { }
}
Code language: C# (cs)

Ahora deserialice con Newtonsoft:

using Newtonsoft.Json;

var person = JsonConvert.DeserializeObject<Person>("{\"LuckyNumber\":7}");
Console.WriteLine($"Lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Esto genera lo siguiente, mostrando que usó el constructor especificado:

Lucky number is 7

Deserializar con un constructor no público

System.Text.Json requiere que tenga un constructor público. Newtonsoft no lo hace. Puede usar constructores no públicos. He aquí un ejemplo:

using Newtonsoft.Json;

public class Person
{
	public int LuckyNumber { get; private set; }

	[JsonConstructor]
	private Person(int luckyNumber)
	{
		LuckyNumber = luckyNumber;
	}

	public Person() { }
}
Code language: C# (cs)

Nota:para hacer esto con System.Text.Json, debe escribir un convertidor personalizado y usar la reflexión para encontrar el constructor no público.

Ahora deserializar:

using Newtonsoft.Json;

var person = JsonConvert.DeserializeObject<Person>("{\"LuckyNumber\":7}");
Console.WriteLine($"Lucky number is {person.LuckyNumber}");
Code language: C# (cs)

Esto genera lo siguiente, mostrando que puede deserializarse a un constructor privado:

Lucky number is 7