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:
- Desserialize tipos inferidos para propriedades de objeto.
- Suporte a desserialização polimórfica.
- Suporte ida e volta para
Stack
tipos. - 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 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 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
Write
método para serializar o objeto de entrada do tipoT
. 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 retornatrue
quando o tipo a ser convertido é do tipoT
. Portanto, os conversores que suportam apenas o tipoT
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 retornadotrue
quando 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>
. - 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 JsonException
arquivo .
Quando você quiser não permitir certos tipos, lance um NotSupportedException
arquivo . 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 NotSupportedException
arquivo .
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,
Converters =
{
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.
- Desserialize tipos inferidos para propriedades de objeto.
- Suporte ida e volta para
Stack
tipos. - Use o conversor de sistema padrão.
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
efalse
paraBoolean
- 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 queUtf8JsonReader
é 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 originalReader
assim que souber 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 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
{
Converters = { 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.
- Ele não passa
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, umaJsonException
exceçã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 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 é umEmployee
arquivo . 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:
- 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.