Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
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
DateTimeos 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
PhoneNumbertipo.
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:
- Desserialize tipos inferidos para propriedades de objeto.
- Suporte a desserialização polimórfica.
-
Suporte ida e volta para
Stacktipos. - Use o conversor de sistema padrão.
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:
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 o 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 interior obtém um conversor existente para processar o 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
Readmétodo para desserializar o JSON de entrada e convertê-lo em tipoT. 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 retornotrue. - Substitua o
Writemétodo para serializar o objeto de entrada do tipoT. Use o Utf8JsonWriter que é passado para o método para escrever o JSON. - Substitua o
CanConvertmétodo somente se necessário. A implementação padrão retornatruequando o tipo a ser convertido é do tipoT. Portanto, os conversores que suportam apenas o tipoTnã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
CanConvertmétodo a ser retornadotruequando o tipo a ser convertido for aquele que o conversor pode manipular. Por exemplo, se o conversor for paraList<T>, ele pode manipularList<int>apenas ,List<string>eList<DateTime>. - Substituir o método
CreateConverterpara devolver uma instância de uma classe de conversor que irá tratar do tipo a ser convertido fornecido em tempo de execução. - Crie a classe de conversor que o
CreateConvertermé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 Utf8JsonReaderRead no método
Se o conversor estiver a converter um objeto JSON, o Utf8JsonReader estará posicionado no token de início de objeto quando o método Read 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.
Tratamento de erros
O serializador fornece tratamento especial para tipos JsonException de exceção e NotSupportedException.
JsonException
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 é desserializado, lance um JsonException.
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 motivos de segurança, portanto, uma tentativa de desserializá-lo resulta em um NotSupportedException.
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:
- Adicione uma instância da classe conversor à JsonSerializerOptions.Converters coleção.
- Aplique o atributo [JsonConverter] às propriedades que exigem o conversor personalizado.
- Aplique o atributo [JsonConverter] a uma classe ou struct que representa um tipo de valor personalizado.
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 à
Converterscoleçã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.
- Desserialize tipos inferidos para propriedades de objeto.
-
Suporte ida e volta para
Stacktipos. - Use o conversor de sistema padrão.
Para um exemplo de um conversor de DataTable, consulte Tipos 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:
-
trueefalseparaBoolean - 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)
{
var runtimeType = objectToWrite.GetType();
if (runtimeType == typeof(object))
{
writer.WriteStartObject();
writer.WriteEndObject();
return;
}
JsonSerializer.Serialize(writer, objectToWrite, runtimeType, 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 Run()
{
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 da consola mostra que, sem o conversor, o tipo de runtime para a Date propriedade é JsonElement; com o conversor, o tipo de runtime é 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. A desserialização polimórfica significa que, no momento do design, pode especificar Person como o destino da desserialização, e Customer os Employee objetos no JSON são corretamente desserializados 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.
Important
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
Utf8JsonReaderinstância. Uma vez queUtf8JsonReaderé um struct, isso requer apenas uma declaração de atribuição. - Use o clone para ler os tokens discriminadores.
- Ligue
Deserializeusando a instância originalReaderassim que souber o tipo necessário. Você pode chamarDeserializeporque a instância originalReaderainda 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
nullpara conversores personalizados na serialização. - Não passa
JsonTokenType.Nullpara os conversores personalizados durante a desserialização. - Ele retorna uma
nullinstância na desserialização. - Ele escreve
nulldiretamente com o escritor na serialização.
- Ele não passa
Para tipos de valores não anuláveis:
- Ele passa
JsonTokenType.Nullpara conversores personalizados na desserialização. (Se nenhum conversor personalizado estiver disponível, umaJsonExceptionexceção será lançada pelo conversor interno para o tipo.)
- Ele passa
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 Run()
{
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
Companytipo. - Você não deseja serializar manualmente a
Supervisorpropriedade, que é umEmployeearquivo . 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.
Limitações do ReferenceResolver com conversores personalizados
Ao usar Preserve, lembre-se de que o estado de manipulação de referência não é preservado quando o serializador chama um conversor personalizado. Isso significa que, se tiver um conversor personalizado para um tipo que faça parte de um grafo de objetos que está a ser serializado ou desserializado com a preservação de referências ativada, o conversor e quaisquer chamadas de serialização aninhadas não terão acesso à instância atual ReferenceResolver.
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:
- Conversor Int32 que converte null em 0 na desserialização
- Conversor Int32 que permite valores de cadeia de caracteres e números na desserialização
- Enum conversor
- Conversor de lista<T> que aceita dados externos
- Conversor Long[] que funciona com uma lista de números delimitada por vírgulas
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.