Partilhar via


Como escrever conversores personalizados para serialização JSON (marshalling) no .NET

Este artigo mostra como criar conversores personalizados para as classes de serialização JSON fornecidas no System.Text.Json namespace. Para obter uma introdução ao System.Text.Json, consulte Como serializar e desserializar JSON no .NET.

Um conversor é uma classe que converte um objeto ou um valor de e para JSON. O System.Text.Json namespace tem conversores internos para a maioria dos tipos primitivos que mapeiam para primitivos JavaScript. Você pode escrever conversores personalizados para substituir o comportamento padrão de um conversor interno. Por exemplo:

  • Talvez você queira que DateTime os valores sejam representados pelo formato mm/dd/aa. Por padrão, a ISO 8601-1:2019 é suportada, incluindo o perfil RFC 3339. Para obter mais informações, consulte Suporte a DateTime e DateTimeOffset em System.Text.Json.
  • Talvez você queira serializar um POCO como cadeia de caracteres JSON, por exemplo, com um PhoneNumber tipo.

Você também pode escrever conversores personalizados para personalizar ou estender System.Text.Json com novas funcionalidades. Os seguintes cenários são abordados mais adiante neste artigo:

Visual Basic não pode ser usado para escrever conversores personalizados, mas pode chamar conversores que são implementados em bibliotecas C#. Para obter mais informações, consulte Suporte do Visual Basic.

Padrões de conversores personalizados

Existem dois padrões para criar um conversor personalizado: o padrão básico e o padrão de fábrica. O padrão de fábrica é para conversores que lidam com genéricos do tipo Enum ou abertos. O padrão básico é para tipos genéricos não genéricos e fechados. Por exemplo, os conversores para os seguintes tipos requerem o padrão de fábrica:

Alguns exemplos de tipos que podem ser manipulados pelo padrão básico incluem:

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

O padrão básico cria uma classe que pode manipular um tipo. O padrão de fábrica cria uma classe que determina, em tempo de execução, qual tipo específico é necessário e cria dinamicamente o conversor apropriado.

Exemplo de conversor básico

O exemplo a seguir é um conversor que substitui a serialização padrão para um tipo de dados existente. O conversor usa o formato mm/dd/aa para DateTimeOffset propriedades.

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

Conversor de padrão de fábrica de amostra

O código a seguir mostra um conversor personalizado que funciona com Dictionary<Enum,TValue>o . O código segue o padrão de fábrica porque o primeiro parâmetro de tipo genérico é Enum e o segundo é aberto. O CanConvert método retorna true apenas para um Dictionary com dois parâmetros genéricos, o primeiro dos quais é um Enum tipo. O conversor interno obtém um conversor existente para lidar com qualquer tipo fornecido em tempo de execução para TValue.

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[] typeArguments = type.GetGenericArguments();
            Type keyType = typeArguments[0];
            Type valueType = typeArguments[1];

            JsonConverter converter = (JsonConverter)Activator.CreateInstance(
                typeof(DictionaryEnumConverterInner<,>).MakeGenericType(
                    [keyType, valueType]),
                BindingFlags.Instance | BindingFlags.Public,
                binder: null,
                args: [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.
                _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.
                    reader.Read();
                    TValue value = _valueConverter.Read(ref reader, _valueType, 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)
                {
                    string propertyName = key.ToString();
                    writer.WritePropertyName
                        (options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName);

                    _valueConverter.Write(writer, value, options);
                }

                writer.WriteEndObject();
            }
        }
    }
}

Etapas para seguir o padrão básico

As etapas a seguir explicam como criar um conversor seguindo o padrão básico:

  • Crie uma classe que deriva de JsonConverter<T> onde T é o tipo a ser serializado e desserializado.
  • Substitua o Read método para desserializar o JSON de entrada e convertê-lo em tipo T. Use o Utf8JsonReader que é passado para o método para ler o JSON. Você não precisa se preocupar em lidar com dados parciais, pois o serializador passa todos os dados para o escopo JSON atual. Por isso, não é necessário ligar Skip ou TrySkip validar esse Read retorno true.
  • Substitua o Write método para serializar o objeto de entrada do tipo T. Use o Utf8JsonWriter que é passado para o método para escrever o JSON.
  • Substitua o CanConvert método somente se necessário. A implementação padrão retorna true quando o tipo a ser convertido é do tipo T. Portanto, os conversores que suportam apenas o tipo T não precisam substituir esse método. Para obter um exemplo de um conversor que precisa substituir esse método, consulte a seção de desserialização polimórfica mais adiante neste artigo.

Você pode consultar o código-fonte dos conversores internos como implementações de referência para escrever conversores personalizados.

Passos para seguir o padrão de fábrica

As etapas a seguir explicam como criar um conversor seguindo o padrão de fábrica:

  • Crie uma classe que derive de JsonConverterFactory.
  • Substitua o CanConvert método a ser retornado true quando o tipo a ser convertido for aquele que o conversor pode manipular. Por exemplo, se o conversor for para List<T>, ele pode manipular List<int>apenas , List<string>e List<DateTime>.
  • Substitua o CreateConverter método para retornar uma instância de uma classe de conversor que manipulará o tipo para converter fornecido em tempo de execução.
  • Crie a classe de conversor que o CreateConverter método instancia.

O padrão de fábrica é necessário para genéricos abertos porque o código para converter um objeto de e para uma cadeia de caracteres não é o mesmo para todos os tipos. Um conversor para um tipo genérico aberto (List<T>, por exemplo) tem que criar um conversor para um tipo genérico fechado (List<DateTime>, por exemplo) nos bastidores. O código deve ser escrito para lidar com cada tipo genérico fechado que o conversor pode manipular.

O Enum tipo é semelhante a um tipo genérico aberto: um conversor para Enum tem que criar um conversor para um específico Enum (WeekdaysEnum, por exemplo) nos bastidores.

O uso de Utf8JsonReader Read no método

Se o conversor estiver convertendo um objeto JSON, o Utf8JsonReader será posicionado no token de objeto begin quando o Read método começar. Em seguida, você deve ler todos os tokens nesse objeto e sair do método com o leitor posicionado no token de objeto final correspondente. Se você ler além do final do objeto, ou se parar antes de atingir o token final correspondente, obterá uma JsonException exceção indicando que:

O conversor 'ConverterName' leu demais ou não o suficiente.

Para obter um exemplo, consulte o conversor de exemplo de padrão de fábrica anterior. O Read método começa verificando se o leitor está posicionado em um token de objeto inicial. Ele lê até descobrir que está posicionado no próximo token de objeto final. Ele para no token de objeto de extremidade seguinte porque não há tokens de objeto de início intervenientes que indicariam um objeto dentro do objeto. A mesma regra sobre token inicial e token final se aplica se você estiver convertendo uma matriz. Para obter um exemplo, consulte o Stack<T> conversor de exemplo mais adiante neste artigo.

Processamento de erros

O serializador fornece tratamento especial para tipos JsonException de exceção e NotSupportedException.

JsonExceção

Se você lançar um JsonException sem uma mensagem, o serializador criará uma mensagem que inclui o caminho para a parte do JSON que causou o erro. Por exemplo, a instrução throw new JsonException() produz uma mensagem de erro como o exemplo a seguir:

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

Se você fornecer uma mensagem (por exemplo, throw new JsonException("Error occurred")), o serializador ainda definirá as Pathpropriedades , LineNumbere .BytePositionInLine

NotSupportedException

Se você lançar um NotSupportedException, você sempre obtém as informações de caminho na mensagem. Se você fornecer uma mensagem, as informações do caminho serão anexadas a ela. Por exemplo, a instrução throw new NotSupportedException("Error occurred.") produz uma mensagem de erro como o exemplo a seguir:

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

Quando lançar qual tipo de exceção

Quando a carga JSON contiver tokens que não são válidos para o tipo que está sendo desserializado, lance um JsonExceptionarquivo .

Quando você quiser não permitir certos tipos, lance um NotSupportedExceptionarquivo . Essa exceção é o que o serializador lança automaticamente para tipos que não são suportados. Por exemplo, System.Type não é suportado por razões de segurança, portanto, uma tentativa de desserializá-lo resulta em um NotSupportedExceptionarquivo .

Você pode lançar outras exceções conforme necessário, mas elas não incluem automaticamente informações de caminho JSON.

Registar um conversor personalizado

Registre um conversor personalizado para fazer os Serialize métodos e Deserialize usá-lo. Escolha uma das seguintes abordagens:

Exemplo de registo - Coleção de conversores

Aqui está um exemplo que torna o DateTimeOffsetJsonConverter o padrão para propriedades do tipo DateTimeOffset:

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

jsonString = JsonSerializer.Serialize(weatherForecast, serializeOptions);

Suponha que você serialize uma instância do seguinte tipo:

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

Aqui está um exemplo de saída JSON que mostra que o conversor personalizado foi usado:

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

O código a seguir usa a mesma abordagem para desserializar usando o conversor personalizado DateTimeOffset :

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

Exemplo de registro - [JsonConverter] em uma propriedade

O código a seguir seleciona um conversor personalizado para a Date propriedade:

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

O código a ser serializado WeatherForecastWithConverterAttribute não requer o uso de JsonSerializeOptions.Converters:

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

O código para desserializar também não requer o uso de Converters:

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

Exemplo de registro - [JsonConverter] em um tipo

Aqui está o código que cria um struct e aplica o [JsonConverter] atributo a ele:

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

Aqui está o conversor personalizado para a estrutura anterior:

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

O [JsonConverter] atributo no struct registra o conversor personalizado como o padrão para propriedades do tipo Temperature. O conversor é usado automaticamente na TemperatureCelsius propriedade do seguinte tipo quando você serializa ou desserializa:

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

Precedência de registro do conversor

Durante a serialização ou desserialização, um conversor é escolhido para cada elemento JSON na seguinte ordem, listado da prioridade mais alta para a mais baixa:

  • [JsonConverter] aplicado a um imóvel.
  • Um conversor adicionado à Converters coleção.
  • [JsonConverter] aplicado a um tipo de valor personalizado ou POCO.

Se vários conversores personalizados para um tipo forem registrados na Converters coleção, o primeiro conversor que retorna true para CanConvert será usado.

Um conversor integrado é escolhido somente se nenhum conversor personalizado aplicável estiver registrado.

Exemplos de conversores para cenários comuns

As seções a seguir fornecem exemplos de conversores que abordam alguns cenários comuns que a funcionalidade interna não manipula.

Para obter um conversor de exemplo DataTable , consulte Tipos de coleção suportados.

Desserializar tipos inferidos para propriedades de objeto

Ao desserializar para uma propriedade do tipo object, um JsonElement objeto é criado. O motivo é que o desserializador não sabe qual tipo de CLR criar e não tenta adivinhar. Por exemplo, se uma propriedade JSON tiver "true", o desserializador não inferirá que o valor é um Boolean, e se um elemento tiver "01/01/2019", o desserializador não inferirá que é um DateTime.

A inferência de tipo pode ser imprecisa. Se o desserializador analisar um número JSON que não tenha ponto decimal como um long, isso pode resultar em problemas fora do intervalo se o valor foi originalmente serializado como um ulong ou BigInteger. Analisar um número que tem um ponto decimal como um double pode perder a precisão se o número foi originalmente serializado como um decimal.

Para cenários que exigem inferência de tipo, o código a seguir mostra um conversor personalizado para object propriedades. O código converte:

  • true e false para Boolean
  • Números sem decimal a long
  • Números com uma casa decimal a double
  • Datas para DateTime
  • Strings para string
  • Tudo o resto para JsonElement
using System.Text.Json;
using System.Text.Json.Serialization;

namespace CustomConverterInferredTypesToObject
{
    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) =>
            JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
    }

    public class WeatherForecast
    {
        public object? Date { get; set; }
        public object? TemperatureCelsius { get; set; }
        public object? Summary { get; set; }
    }

    public class Program
    {
        public static void Main()
        {
            string jsonString = """
                {
                  "Date": "2019-08-01T00:00:00-07:00",
                  "TemperatureCelsius": 25,
                  "Summary": "Hot"
                }
                """;

            WeatherForecast weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString)!;
            Console.WriteLine($"Type of Date property   no converter = {weatherForecast.Date!.GetType()}");

            var options = new JsonSerializerOptions();
            options.WriteIndented = true;
            options.Converters.Add(new ObjectToInferredTypesConverter());
            weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString, options)!;
            Console.WriteLine($"Type of Date property with converter = {weatherForecast.Date!.GetType()}");

            Console.WriteLine(JsonSerializer.Serialize(weatherForecast, options));
        }
    }
}

// Produces output like the following example:
//
//Type of Date property   no converter = System.Text.Json.JsonElement
//Type of Date property with converter = System.DateTime
//{
//  "Date": "2019-08-01T00:00:00-07:00",
//  "TemperatureCelsius": 25,
//  "Summary": "Hot"
//}

O exemplo mostra o código do conversor e uma WeatherForecast classe com object propriedades. O Main método desserializa uma cadeia de caracteres JSON em uma WeatherForecast instância, primeiro sem usar o conversor e, em seguida, usando o conversor. A saída do console mostra que, sem o conversor, o tipo de tempo de execução para a Date propriedade é JsonElement; com o conversor, o tipo de tempo de execução é DateTime.

A pasta de testes de unidade no System.Text.Json.Serialization namespace tem mais exemplos de conversores personalizados que manipulam a desserialização para object propriedades.

Suporte a desserialização polimórfica

O .NET 7 fornece suporte para serialização e desserialização polimórficas. No entanto, em versões anteriores do .NET, havia suporte limitado à serialização polimórfica e nenhum suporte para desserialização. Se você estiver usando o .NET 6 ou uma versão anterior, a desserialização exigirá um conversor personalizado.

Suponha, por exemplo, que você tenha uma Person classe base abstrata, com Employee e Customer classes derivadas. Desserialização polimórfica significa que, em tempo de design, você pode especificar Person como o destino de desserialização e Customer Employee os objetos no JSON são desserializados corretamente em tempo de execução. Durante a desserialização, você precisa encontrar pistas que identifiquem o tipo necessário no JSON. Os tipos de pistas disponíveis variam de acordo com cada cenário. Por exemplo, uma propriedade discriminadora pode estar disponível ou você pode ter que confiar na presença ou ausência de uma propriedade específica. A versão atual do não fornece atributos para especificar como lidar com cenários de System.Text.Json desserialização polimórfica, portanto, conversores personalizados são necessários.

O código a seguir mostra uma classe base, duas classes derivadas e um conversor personalizado para elas. O conversor usa uma propriedade discriminadora para fazer desserialização polimórfica. O discriminador de tipo não está nas definições de classe, mas é criado durante a serialização e é lido durante a desserialização.

Importante

O código de exemplo requer que os pares nome/valor do objeto JSON permaneçam em ordem, o que não é um requisito padrão do JSON.

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

O código a seguir registra o conversor:

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

O conversor pode desserializar JSON que foi criado usando o mesmo conversor para serializar, por exemplo:

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

O código do conversor no exemplo anterior lê e grava cada propriedade manualmente. Uma alternativa é ligar Deserialize ou Serialize fazer parte do trabalho. Para obter um exemplo, consulte esta postagem StackOverflow.

Uma maneira alternativa de fazer desserialização polimórfica

Você pode chamar Deserialize o Read método:

  • Faça um clone da Utf8JsonReader instância. Uma vez que Utf8JsonReader é um struct, isso requer apenas uma declaração de atribuição.
  • Use o clone para ler os tokens discriminadores.
  • Ligue Deserialize usando a instância original Reader assim que souber o tipo necessário. Você pode chamar Deserialize porque a instância original Reader ainda está posicionada para ler o token de objeto begin.

Uma desvantagem desse método é que você não pode passar a instância de opções original que registra o conversor para Deserialize. Isso causaria um estouro de pilha, conforme explicado em Propriedades necessárias. O exemplo a seguir mostra um Read método que usa essa alternativa:

public override Person Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
    Utf8JsonReader readerClone = reader;

    if (readerClone.TokenType != JsonTokenType.StartObject)
    {
        throw new JsonException();
    }

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

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

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

    TypeDiscriminator typeDiscriminator = (TypeDiscriminator)readerClone.GetInt32();
    Person person = typeDiscriminator switch
    {
        TypeDiscriminator.Customer => JsonSerializer.Deserialize<Customer>(ref reader)!,
        TypeDiscriminator.Employee => JsonSerializer.Deserialize<Employee>(ref reader)!,
        _ => throw new JsonException()
    };
    return person;
}

Suporte ida e volta para Stack tipos

Se você desserializar uma cadeia de caracteres JSON em um Stack objeto e, em seguida, serializar esse objeto, o conteúdo da pilha estará na ordem inversa. Esse comportamento se aplica aos seguintes tipos e interfaces e tipos definidos pelo usuário que derivam deles:

Para suportar a serialização e desserialização que mantém a ordem original na pilha, é necessário um conversor personalizado.

O código a seguir mostra um conversor personalizado que permite a ida e volta Stack<T> de e para objetos:

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

O código a seguir registra o conversor:

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

Usar conversor de sistema padrão

Em alguns cenários, talvez você queira usar o conversor de sistema padrão em um conversor personalizado. Para fazer isso, obtenha o conversor do sistema da JsonSerializerOptions.Default propriedade, conforme mostrado no exemplo a seguir:

public class MyCustomConverter : JsonConverter<int>
{
    private readonly static JsonConverter<int> s_defaultConverter = 
        (JsonConverter<int>)JsonSerializerOptions.Default.GetConverter(typeof(int));

    // Custom serialization logic
    public override void Write(
        Utf8JsonWriter writer, int value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }

    // Fall back to default deserialization logic
    public override int Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return s_defaultConverter.Read(ref reader, typeToConvert, options);
    }
}

Processar valores nulos

Por padrão, o serializador manipula valores nulos da seguinte maneira:

  • Para os tipos e Nullable<T> tipos de referência:

    • Ele não passa null para conversores personalizados na serialização.
    • Ele não passa JsonTokenType.Null para conversores personalizados na desserialização.
    • Ele retorna uma null instância na desserialização.
    • Ele escreve null diretamente com o escritor na serialização.
  • Para tipos de valores não anuláveis:

    • Ele passa JsonTokenType.Null para conversores personalizados na desserialização. (Se nenhum conversor personalizado estiver disponível, uma JsonException exceção será lançada pelo conversor interno para o tipo.)

Esse comportamento de manipulação nula é principalmente para otimizar o desempenho ignorando uma chamada extra para o conversor. Além disso, evita forçar conversores para tipos anuláveis para verificar null no início de cada Read substituição de Write método.

Para habilitar um conversor personalizado para manipular null para um tipo de referência ou valor, substitua JsonConverter<T>.HandleNull para retornar true, conforme mostrado no exemplo a seguir:

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.

Preservar referências

Por padrão, os dados de referência são armazenados em cache apenas para cada chamada para Serialize ou Deserialize. Para persistir referências de uma Serialize/Deserialize chamada para outra, enraize a ReferenceResolver instância no site de chamada do .Serialize/Deserialize O código a seguir mostra um exemplo para esse cenário:

  • Você escreve um conversor personalizado para o Company tipo.
  • Você não deseja serializar manualmente a Supervisor propriedade, que é um Employeearquivo . Você deseja delegar isso ao serializador e também deseja preservar as referências que já salvou.

Aqui estão as Employee e Company classes:

public class Employee
{
    public string? Name { get; set; }
    public Employee? Manager { get; set; }
    public List<Employee>? DirectReports { get; set; }
    public Company? Company { get; set; }
}

public class Company
{
    public string? Name { get; set; }
    public Employee? Supervisor { get; set; }
}

O conversor tem esta aparência:

class CompanyConverter : JsonConverter<Company>
{
    public override Company Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Company value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();

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

        writer.WritePropertyName("Supervisor");
        JsonSerializer.Serialize(writer, value.Supervisor, options);

        writer.WriteEndObject();
    }
}

Uma classe que deriva de ReferenceResolver armazena as referências em um dicionário:

class MyReferenceResolver : ReferenceResolver
{
    private uint _referenceCount;
    private readonly Dictionary<string, object> _referenceIdToObjectMap = new ();
    private readonly Dictionary<object, string> _objectToReferenceIdMap = new (ReferenceEqualityComparer.Instance);

    public override void AddReference(string referenceId, object value)
    {
        if (!_referenceIdToObjectMap.TryAdd(referenceId, value))
        {
            throw new JsonException();
        }
    }

    public override string GetReference(object value, out bool alreadyExists)
    {
        if (_objectToReferenceIdMap.TryGetValue(value, out string? referenceId))
        {
            alreadyExists = true;
        }
        else
        {
            _referenceCount++;
            referenceId = _referenceCount.ToString();
            _objectToReferenceIdMap.Add(value, referenceId);
            alreadyExists = false;
        }

        return referenceId;
    }

    public override object ResolveReference(string referenceId)
    {
        if (!_referenceIdToObjectMap.TryGetValue(referenceId, out object? value))
        {
            throw new JsonException();
        }

        return value;
    }
}

Uma classe que deriva de ReferenceHandler mantém uma instância de MyReferenceResolver e cria uma nova instância somente quando necessário (em um método nomeado Reset neste exemplo):

class MyReferenceHandler : ReferenceHandler
{
    public MyReferenceHandler() => Reset();

    private ReferenceResolver? _rootedResolver;
    public override ReferenceResolver CreateResolver() => _rootedResolver!;
    public void Reset() => _rootedResolver = new MyReferenceResolver();
}

Quando o código de exemplo chama o serializador, ele usa uma JsonSerializerOptions instância na qual a ReferenceHandler propriedade é definida como uma instância de MyReferenceHandler. Ao seguir esse padrão, certifique-se de redefinir o ReferenceResolver dicionário quando terminar de serializar, para evitar que ele cresça para sempre.

var options = new JsonSerializerOptions();

options.Converters.Add(new CompanyConverter());
var myReferenceHandler = new MyReferenceHandler();
options.ReferenceHandler = myReferenceHandler;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.WriteIndented = true;

string str = JsonSerializer.Serialize(tyler, options);

// Reset after serializing to avoid out of bounds memory growth in the resolver.
myReferenceHandler.Reset();

O exemplo anterior só faz serialização, mas uma abordagem semelhante pode ser adotada para desserialização.

Outras amostras de conversores personalizados

O artigo Migrar de Newtonsoft.Json para System.Text.Json contém exemplos adicionais de conversores personalizados.

A pasta de testes de unidade no código-fonte System.Text.Json.Serialization inclui outros exemplos de conversores personalizados, como:

Se você precisar fazer um conversor que modifique o comportamento de um conversor interno existente, você pode obter o código-fonte do conversor existente para servir como um ponto de partida para personalização.

Recursos adicionais