Procedimiento para escribir convertidores personalizados para la serialización de JSON (cálculo de referencias) en .NET

En este artículo se muestra cómo crear convertidores personalizados para las clases de serialización de JSON que se proporcionan en el espacio de nombres System.Text.Json. Para disponer de una introducción a System.Text.Json, vea Cómo serializar y deserializar JSON en .NET.

Un convertidor es una clase que convierte un objeto o un valor en JSON; también admite conversiones a partir de este formato. El espacio de nombres System.Text.Json tiene convertidores integrados para la mayoría de los tipos primitivos que se asignan a primitivos de JavaScript. Puede escribir convertidores personalizados:

  • Para reemplazar el comportamiento predeterminado de un convertidor integrado. Por ejemplo, puede que quiera que los valores de DateTime se representen con el formato mm/dd/aaaa. De forma predeterminada, se admite ISO 8601-1:2019, incluido el perfil RFC 3339. Para obtener más información, consulte Compatibilidad con DateTime y DateTimeOffset en System.Text.Json.
  • Para admitir un tipo de valor personalizado. Por ejemplo, una estructura PhoneNumber.

También puede escribir convertidores personalizados para personalizar o extender System.Text.Json con funcionalidad no incluida en la versión actual. Los siguientes escenarios se describen más adelante en este artículo:

En el código que escriba para un convertidor personalizado, tenga en cuenta la importante penalización de rendimiento para el uso de nuevas instancias de JsonSerializerOptions. Para obtener más información, vea Reutilización de instancias de JsonSerializerOptions.

Patrones de convertidores personalizados

Hay dos patrones para crear un convertidor personalizado: el patrón básico y el patrón de fábrica. El patrón de fábrica es para los convertidores que controlan el tipo Enum o los valores genéricos abiertos. El patrón básico es para los tipos no genéricos y los tipos genéricos y cerrados. Por ejemplo, los convertidores de los siguientes tipos requieren el patrón de fábrica:

Entre los ejemplos de tipos que puede controlar el patrón básico se incluyen los siguientes:

  • Dictionary<int, string>
  • WeekdaysEnum
  • List<DateTimeOffset>
  • DateTime
  • Int32

El patrón básico crea una clase que puede controlar un tipo. El patrón de fábrica crea una clase que determina, en tiempo de ejecución, qué tipo específico es necesario y crea dinámicamente el convertidor adecuado.

Convertidor básico de ejemplo

El ejemplo siguiente es un convertidor que reemplaza la serialización predeterminada para un tipo de datos existente. El convertidor usa el formato mm/dd/aaaa para las propiedades DateTimeOffset.

using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DateTimeOffsetJsonConverter : JsonConverter<DateTimeOffset>
    {
        public override DateTimeOffset Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                DateTimeOffset.ParseExact(reader.GetString(),
                    "MM/dd/yyyy", CultureInfo.InvariantCulture);

        public override void Write(
            Utf8JsonWriter writer,
            DateTimeOffset dateTimeValue,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(dateTimeValue.ToString(
                    "MM/dd/yyyy", CultureInfo.InvariantCulture));
    }
}

Convertidor de patrones de fábrica de ejemplo

En el código siguiente se muestra un convertidor personalizado que funciona con Dictionary<Enum,TValue>. El código sigue el patrón de fábrica porque el primer parámetro de tipo genérico es Enum y el segundo es abierto. El método CanConvert devuelve true solo para un elemento Dictionary con dos parámetros genéricos, el primero de los cuales es un tipo Enum. El convertidor interno obtiene un convertidor existente para controlar el tipo que se proporciona en tiempo de ejecución para TValue.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

El código anterior es el mismo que el que se muestra en Compatibilidad para diccionarios con una clave que no sea de cadena, más adelante en este artículo.

Pasos para seguir el patrón básico

En los pasos siguientes se explica cómo crear un convertidor siguiendo el patrón básico:

  • Cree una clase que derive de JsonConverter<T>, donde T es el tipo que se va a serializar y deserializar.
  • Reemplace el método Read para deserializar el JSON de entrada y convertirlo al tipo T. Use el elemento Utf8JsonReader que se pasa al método para leer el JSON.
  • Invalide el método Write para serializar el objeto de entrada de tipo T. Use el elemento Utf8JsonWriter que se pasa al método para escribir el JSON.
  • Reemplace el método CanConvert solo si es necesario. La implementación predeterminada devuelve true cuando el tipo que se va a convertir es de tipo T. Por lo tanto, los convertidores que solo admiten el tipo T no necesitan reemplazar este método. Para obtener un ejemplo de un convertidor que necesita reemplazar este método, consulte la sección sobre la deserialización polimórfica, más adelante en este artículo.

Puede hacer referencia al código fuente de convertidores integrados como implementaciones de referencia para escribir convertidores personalizados.

Pasos para seguir el patrón de fábrica

En los pasos siguientes se explica cómo crear un convertidor siguiendo el patrón de fábrica:

  • Cree una clase que derive de JsonConverterFactory.
  • Reemplace el método CanConvert para devolver "true" cuando el tipo que se va a convertir sea uno que el convertidor puede controlar. Por ejemplo, si el convertidor es para List<T>, solo puede controlar List<int>, List<string> y List<DateTime>.
  • Invalide el método CreateConverter para devolver una instancia de una clase de convertidor que controlará el tipo de conversión que se proporciona en tiempo de ejecución.
  • Cree la clase de convertidor de la que el método CreateConverter crea instancias.

El patrón de fábrica es necesario para los genéricos abiertos porque el código para convertir un objeto en una cadena (y también para realizar una conversión a partir de esta) no es el mismo para todos los tipos. Un convertidor para un tipo genérico abierto (List<T>, por ejemplo) tiene que crear un convertidor para un tipo genérico cerrado (List<DateTime>, por ejemplo) en segundo plano. Es necesario escribir código para controlar cada tipo genérico cerrado que el convertidor puede controlar.

El tipo Enum es similar a un tipo genérico abierto: un convertidor para Enum tiene que crear un convertidor para un elemento Enum específico (WeekdaysEnum, por ejemplo) en segundo plano.

Control de errores

El serializador proporciona un control especial para los tipos de excepción JsonException y NotSupportedException.

JsonException

Si produce una excepción JsonException sin ningún mensaje, el serializador crea un mensaje que incluye la ruta a la parte del JSON que causó el error. Por ejemplo, la instrucción throw new JsonException() genera un mensaje de error como el ejemplo siguiente:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to System.Object.
Path: $.Date | LineNumber: 1 | BytePositionInLine: 37.

Si proporciona un mensaje (por ejemplo, throw new JsonException("Error occurred")), el serializador establece igualmente las propiedades Path, LineNumber y BytePositionInLine.

NotSupportedException

Si produce una excepción NotSupportedException, siempre obtendrá la información de la ruta en el mensaje. Si proporciona un mensaje, la información de la ruta estará anexada. Por ejemplo, la instrucción throw new NotSupportedException("Error occurred.") genera un mensaje de error como el ejemplo siguiente:

Error occurred. The unsupported member type is located on type
'System.Collections.Generic.Dictionary`2[Samples.SummaryWords,System.Int32]'.
Path: $.TemperatureRanges | LineNumber: 4 | BytePositionInLine: 24

Cuándo se debe producir cada tipo de excepción

Cuando la carga de JSON contiene tokens que no son válidos para el tipo que se está deserializando, produzca una excepción JsonException.

Si quiere impedir determinados tipos, produzca una excepción NotSupportedException. Esta excepción es lo que el serializador produce de forma automática para los tipos que no se admiten. Por ejemplo, System.Type no se admite por motivos de seguridad, por lo que un intento de deserializarlo da como resultado una excepción NotSupportedException.

Puede iniciar otras excepciones según sea necesario, pero no incluyen automáticamente la información de la ruta de JSON.

Registro de un convertidor personalizado

Registre un convertidor personalizado para que los métodos Serialize y Deserialize lo utilicen. Elija uno de los enfoques siguientes:

  • Agregue una instancia de la clase de convertidor a la colección JsonSerializerOptions.Converters.
  • Aplique el atributo [JsonConverter] a las propiedades que requieren el convertidor personalizado.
  • Aplique el atributo [JsonConverter] a una clase o una estructura que represente un tipo de valor personalizado.

Ejemplo de registro: colección de convertidores

Este es un ejemplo que hace que DateTimeOffsetConverter sea el valor predeterminado para propiedades de tipo DateTimeOffset:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    Converters =
    {
        new DateTimeOffsetJsonConverter()
    }
};

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Suponga que serializa una instancia del tipo siguiente:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Este es un ejemplo de salida JSON que muestra que se ha usado el convertidor personalizado:

{
  "Date": "08/01/2019",
  "TemperatureCelsius": 25,
  "Summary": "Hot"
}

En el código siguiente se usa el mismo enfoque para realizar una deserialización mediante el convertidor DateTimeOffset personalizado:

var deserializeOptions = new JsonSerializerOptions();
deserializeOptions.Converters.Add(new DateTimeOffsetJsonConverter());
weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, deserializeOptions);

Ejemplo de registro: [JsonConverter] en una propiedad

El código siguiente selecciona un convertidor personalizado para la propiedad Date:

public class WeatherForecastWithConverterAttribute
{
    [JsonConverter(typeof(DateTimeOffsetJsonConverter))]
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

El código para serializar WeatherForecastWithConverterAttribute no requiere el uso de JsonSerializeOptions.Converters:

var serializeOptions = new JsonSerializerOptions
{
    WriteIndented = true
};
jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

El código que se va a deserializar tampoco requiere el uso de Converters:

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithConverterAttribute>(jsonString);

Ejemplo de registro: [JsonConverter] en un tipo

Este es el código que crea una estructura y le aplica el atributo [JsonConverter]:

using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    [JsonConverter(typeof(TemperatureConverter))]
    public struct Temperature
    {
        public Temperature(int degrees, bool celsius)
        {
            Degrees = degrees;
            IsCelsius = celsius;
        }

        public int Degrees { get; }
        public bool IsCelsius { get; }
        public bool IsFahrenheit => !IsCelsius;

        public override string ToString() =>
            $"{Degrees}{(IsCelsius ? "C" : "F")}";

        public static Temperature Parse(string input)
        {
            int degrees = int.Parse(input.Substring(0, input.Length - 1));
            bool celsius = input.Substring(input.Length - 1) == "C";

            return new Temperature(degrees, celsius);
        }
    }
}

Este es el convertidor personalizado para la estructura anterior:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class TemperatureConverter : JsonConverter<Temperature>
    {
        public override Temperature Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
                Temperature.Parse(reader.GetString());

        public override void Write(
            Utf8JsonWriter writer,
            Temperature temperature,
            JsonSerializerOptions options) =>
                writer.WriteStringValue(temperature.ToString());
    }
}

El atributo [JsonConvert] de la estructura registra el convertidor personalizado como valor predeterminado para las propiedades de tipo Temperature. El convertidor se usa automáticamente en la propiedad TemperatureCelsius del tipo siguiente al serializarla o deserializarla:

public class WeatherForecastWithTemperatureStruct
{
    public DateTimeOffset Date { get; set; }
    public Temperature TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

Prioridad de registro del convertidor

Durante la serialización o la deserialización, se elige un convertidor para cada elemento JSON en el orden siguiente, que se muestra de la prioridad más alta a la más baja:

  • Elemento [JsonConverter] aplicado a una propiedad.
  • Convertidor agregado a la colección Converters.
  • Elemento [JsonConverter] aplicado a un tipo de valor personalizado o POCO.

Si se registran varios convertidores personalizados para un tipo en la colección Converters, se usa el primer convertidor que devuelve "true" para CanConvert.

Solo se elige un convertidor integrado si no se registra ningún convertidor personalizado aplicable.

Ejemplos de convertidor para escenarios comunes

En las secciones siguientes se proporcionan ejemplos de convertidor que abordan algunos escenarios comunes que la funcionalidad integrada no controla.

Deserialización de los tipos inferidos en propiedades de objeto

Al realizar una deserialización en una propiedad de tipo object, se crea un objeto JsonElement. La razón es que el deserializador no sabe qué tipo de CLR debe crear, y tampoco intenta averiguarlo. Por ejemplo, si una propiedad JSON tiene "true", el deserializador no infiere que el valor es de tipo Boolean, y si un elemento tiene "01/01/2019", el deserializador no infiere que es de tipo DateTime.

La inferencia de tipos puede ser incorrecta. Si el deserializador analiza un número JSON que no tiene un separador decimal como long, pueden producirse problemas de salida del intervalo si el valor se serializó originalmente como ulong o BigInteger. El análisis de un número que tiene un separador decimal como double puede perder precisión si el número se ha serializado originalmente como decimal.

En escenarios que requieren inferencia de tipos, el código siguiente muestra un convertidor personalizado para las propiedades object. El código convierte:

  • true y false, en Boolean
  • Números sin decimales, en long
  • Números con un decimal, en double
  • Fechas, en DateTime
  • Cadenas, en string
  • Todo lo demás, en JsonElement
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class ObjectToInferredTypesConverter
        : JsonConverter<object>
    {
        public override object Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) => reader.TokenType switch
            {
                JsonTokenType.True => true,
                JsonTokenType.False => false,
                JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
                JsonTokenType.Number => reader.GetDouble(),
                JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
                JsonTokenType.String => reader.GetString(),
                _ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
            };

        public override void Write(
            Utf8JsonWriter writer,
            object objectToWrite,
            JsonSerializerOptions options) =>
            throw new InvalidOperationException("Should not get here.");
    }
}

El código siguiente registra el convertidor:

var deserializeOptions = new JsonSerializerOptions
{
    Converters =
    {
        new ObjectToInferredTypesConverter()
    }
};

Este es un ejemplo de tipo con propiedades object:

public class WeatherForecastWithObjectProperties
{
    public object Date { get; set; }
    public object TemperatureCelsius { get; set; }
    public object Summary { get; set; }
}

El siguiente ejemplo de JSON para deserializar contiene valores que se deserializarán como DateTime, long y string:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
}

Sin el convertidor personalizado, la deserialización coloca un elemento JsonElement en cada propiedad.

La carpeta de pruebas unitarias en el espacio de nombres System.Text.Json.Serialization tiene más ejemplos de convertidores personalizados que controlan la deserialización en propiedades object.

Compatibilidad para diccionarios con una clave que no sea de cadena

La compatibilidad integrada con colecciones de diccionarios es para Dictionary<string, TValue>. Por lo tanto, la clave debe ser una cadena. Para admitir un diccionario con un entero o algún otro tipo como clave, se requiere un convertidor personalizado.

En el código siguiente se muestra un convertidor personalizado que funciona con Dictionary<Enum,TValue>:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class DictionaryTKeyEnumTValueConverter : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
        {
            if (!typeToConvert.IsGenericType)
            {
                return false;
            }

            if (typeToConvert.GetGenericTypeDefinition() != typeof(Dictionary<,>))
            {
                return false;
            }

            return typeToConvert.GetGenericArguments()[0].IsEnum;
        }

        public override JsonConverter CreateConverter(
            Type type,
            JsonSerializerOptions options)
        {
            Type keyType = type.GetGenericArguments()[0];
            Type valueType = type.GetGenericArguments()[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    new Type[] { keyType, valueType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: new object[] { options },
                culture: null);

            return converter;
        }

        private class DictionaryEnumConverterInner<TKey, TValue> :
            JsonConverter<Dictionary<TKey, TValue>> where TKey : struct, Enum
        {
            private readonly JsonConverter<TValue> _valueConverter;
            private readonly Type _keyType;
            private readonly Type _valueType;

            public DictionaryEnumConverterInner(JsonSerializerOptions options)
            {
                // For performance, use the existing converter if available.
                _valueConverter = (JsonConverter<TValue>)options
                    .GetConverter(typeof(TValue));

                // Cache the key and value types.
                _keyType = typeof(TKey);
                _valueType = typeof(TValue);
            }

            public override Dictionary<TKey, TValue> Read(
                ref Utf8JsonReader reader,
                Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }

                var dictionary = new Dictionary<TKey, TValue>();

                while (reader.Read())
                {
                    if (reader.TokenType == JsonTokenType.EndObject)
                    {
                        return dictionary;
                    }

                    // Get the key.
                    if (reader.TokenType != JsonTokenType.PropertyName)
                    {
                        throw new JsonException();
                    }

                    string propertyName = reader.GetString();

                    // For performance, parse with ignoreCase:false first.
                    if (!Enum.TryParse(propertyName, ignoreCase: false, out TKey key) &&
                        !Enum.TryParse(propertyName, ignoreCase: true, out key))
                    {
                        throw new JsonException(
                            $"Unable to convert \"{propertyName}\" to Enum \"{_keyType}\".");
                    }

                    // Get the value.
                    TValue value;
                    if (_valueConverter != null)
                    {
                        reader.Read();
                        value = _valueConverter.Read(ref reader, _valueType, options);
                    }
                    else
                    {
                        value = JsonSerializer.Deserialize<TValue>(ref reader, options);
                    }

                    // Add to dictionary.
                    dictionary.Add(key, value);
                }

                throw new JsonException();
            }

            public override void Write(
                Utf8JsonWriter writer,
                Dictionary<TKey, TValue> dictionary,
                JsonSerializerOptions options)
            {
                writer.WriteStartObject();

                foreach ((TKey key, TValue value) in dictionary)
                {
                    var propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    if (_valueConverter != null)
                    {
                        _valueConverter.Write(writer, value, options);
                    }
                    else
                    {
                        JsonSerializer.Serialize(writer, value, options);
                    }
                }

                writer.WriteEndObject();
            }
        }
    }
}

El código siguiente registra el convertidor:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new DictionaryTKeyEnumTValueConverter());

El convertidor puede serializar y deserializar la propiedad TemperatureRanges de la siguiente clase que usa el elemento Enum siguiente:

public class WeatherForecastWithEnumDictionary
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
    public Dictionary<SummaryWordsEnum, int> TemperatureRanges { get; set; }
}

public enum SummaryWordsEnum
{
    Cold, Hot
}

La salida JSON de la serialización es similar al ejemplo siguiente:

{
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "TemperatureRanges": {
    "Cold": 20,
    "Hot": 40
  }
}

La carpeta de pruebas unitarias en el espacio de nombres System.Text.Json.Serialization tiene más ejemplos de convertidores personalizados que controlan los diccionarios con una clave que no sea de cadena.

Compatibilidad con la deserialización polimórfica

Las características integradas proporcionan una gama limitada de serialización polimórfica, pero no admiten ningún tipo de deserialización. La deserialización requiere un convertidor personalizado.

Suponga, por ejemplo, que tiene una clase base abstracta Person, con clases derivadas Employee y Customer. La deserialización polimórfica significa que puede especificar Person como destino de la deserialización en tiempo de diseño, y los objetos Customer y Employee en el JSON se deserializan correctamente en tiempo de ejecución. Durante la deserialización, debe buscar pistas que identifiquen el tipo requerido en JSON. Los tipos de pistas disponibles varían según cada escenario. Por ejemplo, una propiedad de discriminador puede estar disponible o puede tener que confiar en la presencia o ausencia de una propiedad determinada. La versión actual de System.Text.Json no proporciona atributos para especificar cómo controlar los escenarios de deserialización polimórficos, por lo que se requieren convertidores personalizados.

En el código siguiente se muestra una clase base, dos clases derivadas y un convertidor personalizado para ellas. El convertidor usa una propiedad de discriminador para realizar la deserialización polimórfica. El discriminador de tipo no se encuentra en las definiciones de clase, pero se crea durante la serialización y se lee durante la deserialización.

public class Person
{
    public string Name { get; set; }
}

public class Customer : Person
{
    public decimal CreditLimit { get; set; }
}

public class Employee : Person
{
    public string OfficeNumber { get; set; }
}
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
    {
        enum TypeDiscriminator
        {
            Customer = 1,
            Employee = 2
        }

        public override bool CanConvert(Type typeToConvert) =>
            typeof(Person).IsAssignableFrom(typeToConvert);

        public override Person Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartObject)
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.PropertyName)
            {
                throw new JsonException();
            }

            string propertyName = reader.GetString();
            if (propertyName != "TypeDiscriminator")
            {
                throw new JsonException();
            }

            reader.Read();
            if (reader.TokenType != JsonTokenType.Number)
            {
                throw new JsonException();
            }

            TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
            Person person = typeDiscriminator switch
            {
                TypeDiscriminator.Customer => new Customer(),
                TypeDiscriminator.Employee => new Employee(),
                _ => throw new JsonException()
            };

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return person;
                }

                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                    reader.Read();
                    switch (propertyName)
                    {
                        case "CreditLimit":
                            decimal creditLimit = reader.GetDecimal();
                            ((Customer)person).CreditLimit = creditLimit;
                            break;
                        case "OfficeNumber":
                            string officeNumber = reader.GetString();
                            ((Employee)person).OfficeNumber = officeNumber;
                            break;
                        case "Name":
                            string name = reader.GetString();
                            person.Name = name;
                            break;
                    }
                }
            }

            throw new JsonException();
        }

        public override void Write(
            Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        {
            writer.WriteStartObject();

            if (person is Customer customer)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Customer);
                writer.WriteNumber("CreditLimit", customer.CreditLimit);
            }
            else if (person is Employee employee)
            {
                writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.Employee);
                writer.WriteString("OfficeNumber", employee.OfficeNumber);
            }

            writer.WriteString("Name", person.Name);

            writer.WriteEndObject();
        }
    }
}

El código siguiente registra el convertidor:

var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());

El convertidor puede deserializar el JSON que se creó con el mismo convertidor para serializar, por ejemplo:

[
  {
    "TypeDiscriminator": 1,
    "CreditLimit": 10000,
    "Name": "John"
  },
  {
    "TypeDiscriminator": 2,
    "OfficeNumber": "555-1234",
    "Name": "Nancy"
  }
]

El código del convertidor en el ejemplo anterior lee y escribe cada propiedad manualmente. Una alternativa es llamar a Deserialize o Serialize para realizar parte del trabajo. Para obtener un ejemplo, vea esta publicación de StackOverflow.

Compatibilidad con el recorrido de ida y vuelta para Stack<T>

Si deserializa una cadena JSON en un objeto Stack<T> y después serializa ese objeto, el contenido de la pila está en orden inverso. Este comportamiento se aplica a los siguientes tipos e interfaz, así como a los tipos definidos por el usuario que derivan de ellos:

Para admitir la serialización y deserialización que conserva el orden original en la pila, se requiere un convertidor personalizado.

En el código siguiente se muestra un convertidor personalizado que permite el recorrido de ida y vuelta hacia y desde objetos Stack<T>:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SystemTextJsonSamples
{
    public class JsonConverterFactoryForStackOfT : JsonConverterFactory
    {
        public override bool CanConvert(Type typeToConvert)
            => typeToConvert.IsGenericType
            && typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>);

        public override JsonConverter CreateConverter(
            Type typeToConvert, JsonSerializerOptions options)
        {
            Debug.Assert(typeToConvert.IsGenericType &&
                typeToConvert.GetGenericTypeDefinition() == typeof(Stack<>));

            Type elementType = typeToConvert.GetGenericArguments()[0];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(JsonConverterForStackOfT<>)
                    .MakeGenericType(new Type[] { elementType }),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: null,
                culture: null)!;

            return converter;
        }
    }

    public class JsonConverterForStackOfT<T> : JsonConverter<Stack<T>>
    {
        public override Stack<T> Read(
            ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException();
            }
            reader.Read();

            var elements = new Stack<T>();

            while (reader.TokenType != JsonTokenType.EndArray)
            {
                elements.Push(JsonSerializer.Deserialize<T>(ref reader, options));

                reader.Read();
            }

            return elements;
        }

        public override void Write(
            Utf8JsonWriter writer, Stack<T> value, JsonSerializerOptions options)
        {
            writer.WriteStartArray();

            var reversed = new Stack<T>(value);

            foreach (T item in reversed)
            {
                JsonSerializer.Serialize(writer, item, options);
            }

            writer.WriteEndArray();
        }
    }
}

El código siguiente registra el convertidor:

var options = new JsonSerializerOptions
{
    Converters = { new JsonConverterFactoryForStackOfT() },
};

Control de valores NULL

De forma predeterminada, el serializador controla los valores NULL de la siguiente manera:

  • Para tipos de referencia y tipos de Nullable<T>:

    • No pasa null a los convertidores personalizados en la serialización.
    • No pasa JsonTokenType.Null a los convertidores personalizados en la deserialización.
    • Devuelve una instancia de null en la deserialización.
    • Escribe null directamente con el escritor en la serialización.
  • Para los tipos de valor distintos a NULL:

    • Pasa JsonTokenType.Null a los convertidores personalizados en la deserialización. (Si no hay ningún convertidor personalizado disponible, el convertidor interno produce una excepción JsonException para el tipo).

Este comportamiento de administración de valores NULL sirve principalmente para optimizar el rendimiento omitiendo una llamada adicional al convertidor. Además, evita la aplicación de convertidores para tipos que aceptan valores NULL que comprobar para null al principio de cada invalidación de método Read y Write.

Para permitir que un convertidor personalizado administre null para un tipo de valor o referencia, invalide JsonConverter<T>.HandleNull para devolver true, como se muestra en el ejemplo siguiente:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterHandleNull
{
    public class Point
    {
        public int X { get; set; }
        public int Y { get; set; }

        [JsonConverter(typeof(DescriptionConverter))]
        public string Description { get; set; }
    }

    public class DescriptionConverter : JsonConverter<string>
    {
        public override bool HandleNull => true;

        public override string Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options) =>
            reader.GetString() ?? "No description provided.";

        public override void Write(
            Utf8JsonWriter writer,
            string value,
            JsonSerializerOptions options) =>
            writer.WriteStringValue(value);
    }

    public class Program
    {
        public static void Main()
        {
            string json = @"{""x"":1,""y"":2,""Description"":null}";

            Point point = JsonSerializer.Deserialize<Point>(json);
            Console.WriteLine($"Description: {point.Description}");
        }
    }
}

// Produces output like the following example:
//
//Description: No description provided.

Otros ejemplos de convertidor personalizado

El artículo Migración de Newtonsoft.Json a System.Text.Json contiene ejemplos adicionales de convertidores personalizados.

La carpeta de pruebas unitarias en el código fuente de System.Text.Json.Serialization incluye otros ejemplos de convertidor personalizado, como:

Si necesita crear un convertidor que modifique el comportamiento de un convertidor integrado existente, puede obtener el código fuente del convertidor existente para que sirva como punto de partida para la personalización.

Recursos adicionales