Como gravar 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 namespace System.Text.Json. Para obter uma introdução a System.Text.Json
, confira Como serializar e desserializar o JSON no .NET.
Um conversor é uma classe que converte um objeto ou um valor de e para JSON. O namespace System.Text.Json
tem conversores internos para a maioria dos tipos primitivos que são mapeados para primitivos JavaScript. Você pode escrever conversores personalizados para substituir o comportamento padrão de um conversor interno. Por exemplo:
- Talvez você queira que os valores
DateTime
sejam representados pelo formato mm/dd/yyyyy. Por padrão, há suporte para ISO 8601-1:2019, incluindo o perfil RFC 3339. Para obter mais informações, confira Suporte para DateTime e DateTimeOffset em System.Text.Json. - Talvez você queira serializar um POCO como uma cadeia de caracteres JSON, por exemplo, com um tipo
PhoneNumber
.
Você também pode escrever conversores personalizados ou estender System.Text.Json
com novas funcionalidades. Os seguintes cenários são abordados posteriormente neste artigo:
- Desserializar tipos inferidos para propriedades de objeto.
- Dar suporte para desserialização polimórfica.
- Suporte para ida e volta de
Stack
tipos. - Usar o conversor de sistema padrão.
O Visual Basic não pode ser usado para gravar conversores personalizados, mas pode chamar conversores implementados em bibliotecas C#. Para saber mais, confira Suporte para Visual Basic.
Padrões de conversor personalizado
Há 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 tipos Enum
ou genéricos abertos. O padrão básico é para tipos genéricos não genéricos e fechados. Por exemplo, os conversores para os seguintes tipos exigem o padrão de fábrica:
Alguns exemplos de tipos que podem ser tratados pelo padrão básico incluem:
O padrão básico cria uma classe que pode lidar com 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.
Conversor básico de exemplo
O exemplo a seguir é um conversor que substitui a serialização padrão para um tipo de dados. O conversor usa o formato mm/dd/aaaa para propriedades DateTimeOffset
.
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ões de fábrica de exemplo
O código a seguir mostra um conversor personalizado que funciona com Dictionary<Enum,TValue>
. O código segue o padrão de fábrica porque o primeiro parâmetro de tipo genérico é Enum
e o segundo está aberto. O método CanConvert
retorna true
apenas para um Dictionary
com dois parâmetros genéricos, o primeiro deles é um tipo Enum
. O conversor interno obtém um conversor 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 seguintes etapas explicam como criar um conversor seguindo o padrão básico:
- Crie uma classe que deriva de JsonConverter<T> em que
T
é o tipo a ser serializado e desserializado. - Substitua o método
Read
para desserializar o JSON de entrada e convertê-lo no tipoT
. Use o Utf8JsonReader que é passado para o método para ler o JSON. Você não precisa se preocupar em lidar com os dados parciais, pois o serializador passa todos os dados para o escopo JSON atual. Portanto, não é necessário chamar Skip ou TrySkip para validar que Read retornatrue
. - Substitua o método
Write
para serializar o objeto de entrada do tipoT
. Use o Utf8JsonWriter que é passado para o método para escrever o JSON. - Substitua o método
CanConvert
somente se necessário. A implementação padrão retornatrue
quando o tipo a ser convertido é do tipoT
. Portanto, os conversores que dão suporte apenas ao tipoT
não precisam substituir esse método. Para obter um exemplo de um conversor que precisa substituir esse método, confira a seção desserialização polimórfica mais adiante neste artigo.
Você pode consultar código-fonte dos conversores internos como implementações de referência para escrever conversores personalizados.
Etapas para seguir o padrão de fábrica
As seguintes etapas explicam como criar um conversor seguindo o padrão de fábrica:
- Crie uma classe que deriva de JsonConverterFactory.
- Substitua o método
CanConvert
para retornartrue
quando o tipo a ser convertido é aquele que o conversor pode manipular. Por exemplo, se o conversor for paraList<T>
, ele só poderá lidar comList<int>
,List<string>
eList<DateTime>
. - Substitua o método
CreateConverter
para retornar uma instância de uma classe de conversor que lidará com o tipo a converter fornecido no tempo de execução. - Crie a classe de conversor que o método
CreateConverter
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) precisa criar um conversor para um tipo genérico fechado (List<DateTime>
, por exemplo) nos bastidores. O código deve ser gravado para lidar com cada tipo genérico fechado com que o conversor pode lidar.
O tipo Enum
é semelhante a um tipo genérico aberto: um conversor para Enum
precisa criar um conversor para um Enum
específico (WeekdaysEnum
, por exemplo) nos bastidores.
O uso do Utf8JsonReader
no método Read
Se o conversor estiver convertendo um objeto JSON, o Utf8JsonReader
será posicionado no token de objeto inicial quando o método Read
for iniciado. 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 parar antes de chegar ao token final correspondente, receberá uma exceção JsonException
indicando que:
O conversor 'ConverterName' leu demais ou não o suficiente.
Para obter um exemplo, confira o conversor de exemplo de padrão de fábrica anterior. O método Read
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 intervindo que indicariam um objeto dentro do objeto. A mesma regra sobre token de início e token final se aplicará se você estiver convertendo uma matriz. Para obter um exemplo, confira o conversor de exemplo Stack<T>
mais adiante neste artigo.
Tratamento de erros
O serializador fornece tratamento especial para tipos de exceção JsonException e NotSupportedException.
JsonException
Se você gerar um JsonException
sem mensagem, o serializador criará uma mensagem incluindo 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 seguinte exemplo:
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 propriedades e Path, LineNumber e BytePositionInLine.
NotSupportedException
Se você gerar um NotSupportedException
, sempre obterá as informações do caminho na mensagem. Se você fornecer uma mensagem, as informações do caminho serão acrescentadas a ela. Por exemplo, a instrução throw new NotSupportedException("Error occurred.")
produz uma mensagem de erro como o seguinte exemplo:
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 gerar qual tipo de exceção
Quando o conteúdo JSON contiver tokens que não são válidos para o tipo que está sendo desserializado, gere um JsonException
.
Quando você quiser desabilitar determinados tipos, gere um NotSupportedException
. Essa exceção é o que o serializador gera automaticamente para tipos que não têm suporte. Por exemplo, System.Type
não tem suporte por motivos de segurança, portanto, uma tentativa de desserializá-la resulta em uma NotSupportedException
.
Você pode gerar outras exceções conforme necessário, mas elas não incluem automaticamente informações de caminho JSON.
Registrar um conversor personalizado
Registre um conversor personalizado para fazer com que os métodos Serialize
e Deserialize
o usem. Escolha uma das seguintes abordagens:
- Adiciona uma instância da classe do conversor à coleção JsonSerializerOptions.Converters.
- Aplique o atributo [JsonConverter] às propriedades que exigem o conversor personalizado.
- Aplique o atributo [JsonConverter] a uma classe ou a um struct que representa um tipo de valor personalizado.
Exemplo de registro – coleção Converters
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 seguinte código 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 seguinte código seleciona um conversor personalizado para a propriedade Date
:
public class WeatherForecastWithConverterAttribute
{
[JsonConverter(typeof(DateTimeOffsetJsonConverter))]
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
O código para serializar 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 atributo [JsonConverter]
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 o struct 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 atributo [JsonConverter]
no struct registra o conversor personalizado como o padrão para propriedades do tipo Temperature
. O conversor é usado automaticamente na propriedade TemperatureCelsius
do seguinte tipo ao serializá-la ou desserializá-la:
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, listada da prioridade mais alta para a mais baixa:
[JsonConverter]
aplicado a uma propriedade.- Um conversor adicionado à coleção
Converters
. [JsonConverter]
aplicado a um tipo de valor personalizado ou POCO.
Se diversos conversores personalizados para um tipo forem registrados na coleção Converters
, o primeiro conversor que retornará true
para CanConvert
será usado.
Um conversor interno será escolhido somente se nenhum conversor personalizado aplicável for registrado.
Exemplos de conversor para cenários comuns
As seções a seguir apresentam exemplos de conversor que abordam alguns cenários comuns com que a funcionalidade interna não lida.
- Desserializar tipos inferidos para propriedades de objeto.
- Suporte para ida e volta de
Stack
tipos. - Usar o conversor de sistema padrão.
Para obter um conversor de exemplo DataTable, confira Tipos de coleção com suporte.
Desserializar tipos inferidos para propriedades de objeto
Ao desserializar para uma propriedade do tipo object
, um objeto JsonElement
é criado. O motivo é que o desserializador não sabe qual tipo 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
; se um elemento tiver "01/01/2019", o desserializador não inferirá que ele é uma DateTime
.
A inferência de tipos pode ser imprecisa. Se o desserializador analisar um número JSON que não tem nenhum ponto decimal como um long
, isso poderá resultar em problemas fora do intervalo se o valor tiver sido originalmente serializado como ulong
ou BigInteger
. Analisar um número que tem um ponto decimal como um double
pode perder precisão se o número tiver sido originalmente serializado como um decimal
.
Para cenários que exigem inferência de tipos, o código a seguir mostra um conversor personalizado para propriedades object
. O código converte:
true
efalse
emBoolean
- Números sem um decimal em
long
- Números com um decimal em
double
- Datas em
DateTime
- Cadeias de caracteres em
string
- Todo o resto em
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 classe WeatherForecast
com propriedades object
. O método Main
desserializa uma cadeia de caracteres JSON em uma instância WeatherForecast
, primeiro sem usar o conversor e então usando o conversor. A saída do console mostra que, sem o conversor, o tipo de tempo de execução da propriedade Date
é JsonElement
; com o conversor, o tipo de tempo de execução é DateTime
.
A pasta de testes de unidade no namespace System.Text.Json.Serialization
tem mais exemplos de conversores personalizados que lidam com a desserialização em propriedades object
.
Dar suporte para desserialização polimórfica
O .NET 7 dá suporte à serialização e à desserialização polimórficas. No entanto, em versões anteriores do .NET, havia suporte limitado para 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 classe Person
base abstrata, com as classes derivadas Employee
e Customer
. A desserialização polimórfica significa que, no momento do design, você pode especificar Person
como o alvo da desserialização e os objetos Customer
e Employee
no JSON são corretamente desserializados no 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 conforme cada cenário. Por exemplo, uma propriedade discriminatória pode estar disponível ou talvez você precise confiar na presença ou na ausência de uma propriedade específica. A versão atual de System.Text.Json
não fornece atributos para especificar como lidar com cenários de 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 discriminatória para fazer a desserialização polimórfica. O tipo discriminatório 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 pares de nome/valor de 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 seguinte código registra o conversor:
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new PersonConverterWithTypeDiscriminator());
O conversor pode desserializar o JSON 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 conversor no exemplo anterior lê e grava cada propriedade manualmente. Uma alternativa é chamar Deserialize
ou Serialize
fazer parte do trabalho. Para obter um exemplo, confira esta postagem StackOverflow.
Uma forma alternativa de fazer a desserialização polimórfica
Você pode chamar Deserialize
no método Read
:
- Faça um clone da instância
Utf8JsonReader
. ComoUtf8JsonReader
é um struct, isso requer apenas uma instrução de atribuição. - Use o clone para ler os tokens discriminatórios.
- Chame
Deserialize
usando a instância originalReader
depois de saber o tipo necessário. Você pode chamarDeserialize
porque a instância originalReader
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 nas propriedades necessárias. O seguinte exemplo mostra um método Read
que usa esta 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 de ida e volta para tipos Stack
Se você desserializar uma cadeia de caracteres JSON em um objeto Stack
e serializar esse objeto, o conteúdo da pilha estará em ordem inversa. Esse comportamento se aplica aos seguintes tipos e interfaces e aos tipos definidos pelo usuário que derivam deles:
Para dar suporte à serialização e à desserialização que retém a ordem original na pilha, é necessário um conversor personalizado.
O seguinte código mostra um conversor personalizado que permite a viagem de ida e volta de objetos Stack<T>
:
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 seguinte código registra o conversor:
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonConverterFactoryForStackOfT());
Usar o conversor de sistema padrão
Em alguns cenários, pode ser útil usar o conversor de sistema padrão em um conversor personalizado. Para fazer isso, obtenha o conversor do sistema da propriedade JsonSerializerOptions.Default, conforme mostra o seguinte exemplo:
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);
}
}
Manipular valores nulos
Por padrão, o serializador manipula valores nulos da seguinte maneira:
Para tipos de referência e tipos Nullable<T>:
- 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 instância
null
na desserialização. - Ele grava
null
diretamente com o gravador na serialização.
- Ele não passa
Para tipos de valor não anuláveis:
- Ele passa
JsonTokenType.Null
para conversores personalizados na desserialização. (Se nenhum conversor personalizado estiver disponível, uma exceçãoJsonException
será gerada pelo conversor interno do tipo.)
- Ele passa
Esse comportamento de tratamento nulo é 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 substituição de método Read
e Write
.
Para habilitar um conversor personalizado a lidar com null
para um tipo de referência ou valor, substitua JsonConverter<T>.HandleNull para retornar true
, conforme mostra o seguinte exemplo:
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ó são armazenados em cache para cada chamada a Serialize ou Deserialize. Para persistir as referências de uma chamada Serialize
/Deserialize
para outra, enraíze a instância ReferenceResolver no site de chamada de Serialize
/Deserialize
. O seguinte código mostra um exemplo para esse cenário:
- Você escreve um conversor personalizado para o tipo
Company
. - Não é recomendável serializar manualmente a propriedade
Supervisor
, que é umaEmployee
. É recomendável delegar isso ao serializador e preservar as referências que você já salvou.
Aqui estão as classes Employee
e Company
:
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 derivada 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 derivada de ReferenceHandler contém uma instância e MyReferenceResolver
cria uma nova instância somente quando necessário (em um método chamado 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 instância JsonSerializerOptions na qual a propriedade ReferenceHandler é definida como uma instância de MyReferenceHandler
. Ao seguir esse padrão, redefina o dicionário ReferenceResolver
ao terminar de serializar para evitar que ele continue crescendo 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.
Outros exemplos de conversor personalizado
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 conversor personalizado, como:
- Conversor Int32 que converte nulo em 0 ao desserializar
- Conversor Int32 que permite valores de cadeia de caracteres e números em desserializar
- Conversor de enumeração
- Conversor List<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, poderá obter o código-fonte do conversor para servir como um ponto de partida para personalização.