La biblioteca System.Text.Json crea un contrato JSON de cada tipo de .NET que define cómo se debe serializar y deserializar el tipo. El contrato se deriva de la forma del tipo, que incluye características como sus propiedades y campos y si implementa la interfaz IEnumerable o IDictionary. Los tipos se asignan a contratos en tiempo de ejecución mediante reflexión o en tiempo de compilación mediante el generador de orígenes.
A partir de .NET 7, estos contratos JSON se pueden personalizar para proporcionar más control sobre cómo se convierten los tipos en JSON (y viceversa). Estos son solo algunos ejemplos de los tipos de personalizaciones de serialización y deserialización que se pueden realizar:
Serialización de propiedades y campos privados
Admisión de varios nombres para una misma propiedad (por ejemplo, si una versión de biblioteca anterior usó un nombre diferente)
Omisión de las propiedades con un nombre, un tipo o un valor específicos
Distinción entre valores null explícitos y ausencia de un valor en la carga JSON
Producción de una excepción si el JSON incluye una propiedad que no forma parte del tipo de destino. Para obtener más información, consulte Control de los miembros que faltan.
Cómo acceder
Existen dos maneras de acceder a estas personalizaciones. Ambas conllevan obtener un solucionador, cuyo trabajo es proporcionar una instancia de JsonTypeInfo para cada tipo que deba serializarse.
El solucionador personalizado se puede combinar también con otros, por ejemplo, con el solucionador predeterminado. Los solucionadores se consultarán en orden hasta que se devuelva un valor de JsonTypeInfo distinto de NULL para el tipo.
Aspectos configurables
La propiedad JsonTypeInfo.Kind indica cómo serializa el convertidor un tipo determinado (por ejemplo, como un objeto o como una matriz) y si sus propiedades se serializan. Puede consultar esta propiedad para saber qué aspectos del contrato JSON de un tipo se pueden configurar. Hay cuatro clases de propiedad diferentes:
El convertidor serializará el tipo en un objeto JSON y usa sus propiedades. Se usa con la mayoría de los tipos de clase y estructura y es la más flexible de todas.
El convertidor no especifica cómo serializará el tipo ni las propiedades JsonTypeInfo que usará. Se usa con tipos como System.Object, int y string, así como con todos los tipos que usan un convertidor personalizado.
Modificadores
Un modificador es un parámetro Action<JsonTypeInfo> o un método con un parámetro JsonTypeInfo que obtiene el estado actual del contrato como argumento y realiza modificaciones en el contrato. Por ejemplo, podría recorrer en iteración las propiedades rellenadas previamente en el parámetro JsonTypeInfo especificado para hallar la que le interese y, a continuación, modificar su propiedad (de serialización) JsonPropertyInfo.Get o su propiedad (de deserialización) JsonPropertyInfo.Set. También se puede crear una nueva propiedad mediante JsonTypeInfo.CreateJsonPropertyInfo(Type, String) y agregarla a la colección JsonTypeInfo.Properties.
En la siguiente tabla se muestran las modificaciones posibles y cómo realizarlas.
Vea el siguiente ejemplo, donde el modificador incrementa el valor de una propiedad determinada en la deserialización, modificando para ello su delegado JsonPropertyInfo.Set. Además de definir el modificador, el ejemplo también presenta un nuevo atributo que usa para localizar la propiedad cuyo valor debe incrementarse. Este es un ejemplo de personalización de una propiedad.
C#
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespaceSerialization
{
// Custom attribute to annotate the property// we want to be incremented.
[AttributeUsage(AttributeTargets.Property)]
classSerializationCountAttribute : Attribute
{
}
// Example type to serialize and deserialize.classProduct
{
publicstring Name { get; set; } = "";
[SerializationCount]
publicint RoundTrips { get; set; }
}
publicclassSerializationCountExample
{
// Custom modifier that increments the value// of a specific property on deserialization.staticvoidIncrementCounterModifier(JsonTypeInfo typeInfo)
{
foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
{
if (propertyInfo.PropertyType != typeof(int))
continue;
object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;
if (attribute != null)
{
Action<object, object?>? setProperty = propertyInfo.Set;
if (setProperty isnotnull)
{
propertyInfo.Set = (obj, value) =>
{
if (value != null)
{
// Increment the value by 1.value = (int)value + 1;
}
setProperty (obj, value);
};
}
}
}
}
publicstaticvoidRunIt()
{
var product = new Product
{
Name = "Aquafresh"
};
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { IncrementCounterModifier }
}
};
// First serialization and deserialization.string serialized = JsonSerializer.Serialize(product, options);
Console.WriteLine(serialized);
// {"Name":"Aquafresh","RoundTrips":0}
Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
Console.WriteLine($"{deserialized.RoundTrips}");
// 1// Second serialization and deserialization.
serialized = JsonSerializer.Serialize(deserialized, options);
Console.WriteLine(serialized);
// { "Name":"Aquafresh","RoundTrips":1}
deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
Console.WriteLine($"{deserialized.RoundTrips}");
// 2
}
}
}
Fíjese en que, en la salida, el valor de RoundTrips se incrementa cada vez que la instancia de Product se deserializa.
Ejemplo: Serializar campos privados
De forma predeterminada, System.Text.Json omite las propiedades y los campos privados. En este ejemplo se agrega un nuevo atributo para toda la clase, JsonIncludePrivateFieldsAttribute, para cambiar ese valor predeterminado. Si el modificador halla ese atributo en un tipo, agrega todos los campos privados del tipo a JsonTypeInfo como propiedades nuevas.
C#
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespaceSerialization
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
publicclassJsonIncludePrivateFieldsAttribute : Attribute { }
[JsonIncludePrivateFields]
publicclassHuman
{
privatestring _name;
privateint _age;
publicHuman()
{
// This constructor should be used only by deserializers.
_name = null!;
_age = 0;
}
publicstatic Human Create(string name, int age)
{
Human h = new()
{
_name = name,
_age = age
};
return h;
}
[JsonIgnore]
publicstring Name
{
get => _name;
set => thrownew NotSupportedException();
}
[JsonIgnore]
publicint Age
{
get => _age;
set => thrownew NotSupportedException();
}
}
publicclassPrivateFieldsExample
{
staticvoidAddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
return;
if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
return;
foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
{
JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
jsonPropertyInfo.Get = field.GetValue;
jsonPropertyInfo.Set = field.SetValue;
jsonTypeInfo.Properties.Add(jsonPropertyInfo);
}
}
publicstaticvoidRunIt()
{
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { AddPrivateFieldsModifier }
}
};
var human = Human.Create("Julius", 37);
string json = JsonSerializer.Serialize(human, options);
Console.WriteLine(json);
// {"_name":"Julius","_age":37}
Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
// [Name=Julius; Age=37]
}
}
}
Sugerencia
Si los nombres de los campos privados comienzan por caracteres de subrayado, considere la posibilidad de quitar esos caracteres al agregar los campos como propiedades JSON nuevas.
Ejemplo: Omitir propiedades con un tipo específico
Puede que el modelo tenga propiedades con nombres o tipos específicos que no quiera exponer a los usuarios. Por ejemplo, puede tener una propiedad que almacene credenciales o alguna información que no tiene sentido incluir en la carga.
En el siguiente ejemplo se muestra cómo filtrar las propiedades con un tipo específico, SecretHolder. Para ello, se usa un método de extensión IList<T> para quitar de la lista de JsonTypeInfo.Properties aquellas propiedades que tengan el tipo especificado. Las propiedades filtradas desaparecen completamente del contrato, lo que significa que System.Text.Json no las examina durante la serialización o deserialización.
C#
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespaceSerialization
{
classExampleClass
{
publicstring Name { get; set; } = "";
public SecretHolder? Secret { get; set; }
}
classSecretHolder
{
publicstring Value { get; set; } = "";
}
classIgnorePropertiesWithType
{
privatereadonly Type[] _ignoredTypes;
publicIgnorePropertiesWithType(params Type[] ignoredTypes)
=> _ignoredTypes = ignoredTypes;
publicvoidModifyTypeInfo(JsonTypeInfo ti)
{
if (ti.Kind != JsonTypeInfoKind.Object)
return;
ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
}
}
publicclassIgnoreTypeExample
{
publicstaticvoidRunIt()
{
var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { modifier.ModifyTypeInfo }
}
};
ExampleClass obj = new()
{
Name = "Password",
Secret = new SecretHolder { Value = "MySecret" }
};
string output = JsonSerializer.Serialize(obj, options);
Console.WriteLine(output);
// {"Name":"Password"}
}
}
publicstaticclassListHelpers
{
// IList<T> implementation of List<T>.RemoveAll method.publicstaticvoid RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
{
for (int i = 0; i < list.Count; i++)
{
if (predicate(list[i]))
{
list.RemoveAt(i--);
}
}
}
}
}
Ejemplo: Permitir que los valores int sean cadenas
Puede que el JSON de entrada contenga uno de los tipos numéricos entre comillas, pero otros no. Si tuviera control sobre la clase, podría colocar JsonNumberHandlingAttribute en el tipo para corregirlo, pero no lo tiene. Antes de .NET 7, tendría que escribir un convertidor personalizado para corregir este comportamiento, lo que requiere escribir un poco de código. Con la personalización del contrato, puede personalizar el comportamiento de tratamiento de números de cualquier tipo.
En el siguiente ejemplo se cambia el comportamiento de todos los valores int. Este ejemplo se puede adaptar fácilmente para usarlo con cualquier tipo o con una propiedad específica de cualquier tipo.
C#
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespaceSerialization
{
publicclassPoint
{
publicint X { get; set; }
publicint Y { get; set; }
}
publicclassAllowIntsAsStringsExample
{
staticvoidSetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Type == typeof(int))
{
jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
}
}
publicstaticvoidRunIt()
{
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { SetNumberHandlingModifier }
}
};
// Triple-quote syntax is a C# 11 feature.
Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
Console.WriteLine($"({point.X},{point.Y})");
// (12,3)
}
}
}
Sin el modificador que permite la lectura de valores int de una cadena, el programa habría terminado con una excepción:
Excepción no controlada. System.Text.Json.JsonException: El valor JSON no se pudo convertir a System.Int32. Ruta: $.X | LineNumber: 0 | BytePositionInLine: 9.
Otras formas de personalizar la serialización
Además de personalizar un contrato, existen otras formas de influir en el comportamiento de serialización y deserialización, como las siguientes:
Modificando JsonSerializerOptions, por ejemplo, para establecer una directiva de nomenclatura o para serializar los valores de enumeración como cadenas en lugar de números
Escribiendo un convertidor personalizado que realice de facto la tarea de escribir el JSON y, durante la deserialización, construyendo un objeto
La personalización de contratos es una mejora con respecto a estas personalizaciones preexistentes, ya que es posible que no tenga acceso al tipo para agregar atributos. Además, escribir un convertidor personalizado es complejo y afecta al rendimiento.
El origen de este contenido se puede encontrar en GitHub, donde también puede crear y revisar problemas y solicitudes de incorporación de cambios. Para más información, consulte nuestra guía para colaboradores.
Comentarios de .NET
.NET es un proyecto de código abierto. Seleccione un vínculo para proporcionar comentarios:
Únase a la serie de reuniones para crear soluciones de inteligencia artificial escalables basadas en casos de uso reales con compañeros desarrolladores y expertos.