Serialização avançada com System.Text.Json em C#

A serialização e desserialização de objetos para JSON são tarefas comuns em projetos .NET. No ecossistema .NET, o “System.Text.Json” se destaca como uma biblioteca nativa, eficiente e flexível para lidar com JSON. Embora seja amplamente utilizado para casos simples, situações complexas frequentemente surgem, como trabalhar com formatos de data e hora personalizados ou lidar com enums representados como strings no JSON. Neste artigo, exploraremos como o “System.Text.Json” pode ser adaptado para atender a essas demandas, com foco na criação e uso de conversores personalizados.

Introdução ao System.Text.Json

Antes de nos aprofundarmos em técnicas mais avançadas, é importante compreender como funciona a serialização e desserialização básica com o System.Text.Json. Essa biblioteca oferece suporte nativo para converter objetos C# em JSON e vice-versa. Vejamos o exemplo abaixo:

				
					using System.Text.Json;

public class Pessoa
{
    public string Nome { get; set; }
    public int Idade { get; set; }
}

var pessoa = new Pessoa { Nome = "Joao", Idade = 30 };

// Serialização: Objeto C# para JSON
string json = JsonSerializer.Serialize(pessoa);
Console.WriteLine(json); 

//Resultado apresentado no console:  {"Nome":"João","Idade":30}

// Desserialização: JSON para Objeto C#
var desserializado = JsonSerializer.Deserialize<Pessoa>(json);
Console.WriteLine($"{desserializado.Nome}, {desserializado.Idade}");

	//Resultado apresentado no console:  Joao, 30

				
			

Neste exemplo, o “System.Text.Json” utiliza automaticamente os nomes das propriedades da classe para mapear os campos no JSON. Esse comportamento é conveniente em cenários onde o formato do JSON corresponde diretamente à estrutura da classe.

Agora que entendemos o funcionamento básico, vamos explorar como lidar com situações mais complexas

Conversores personalizados

Conversores personalizados são úteis quando:

  • O formato JSON não corresponde diretamente às propriedades da classe.
  • É necessário manipular valores especiais, como enums representados por strings ou formatos de data específicos.
  • O JSON contém propriedades dinâmicas ou ausentes.

Criando um conversor personalizado

Os conversores personalizados são implementados estendendo a classe “JsonConverter<T>”. Para usar um conversor, você o registra no nível da propriedade (com [JsonConverter]) ou globalmente, nas opções do serializador.

Imagine um sistema que utiliza o formato de data “dd/MM/yyyy”. Um conversor personalizado pode mapear esse formato para “DateTime”.

				
					{ "Cliente": "Maria", "DataEntrega ": "25/12/1990" }
				
			

Para realizarmos a serialização do json em um objeto C#, vamos criar uma classe chamada “Pedido”:

				
					public class Pedido
{
    public string Cliente { get; set; }

    [JsonConverter(typeof(DateFormatConverter))]
    public DateTime DataEntrega { get; set; }
}

				
			

A classe “Pedido” possui as propriedades “Cliente” um campo string simples, que será mapeado automaticamente no JSON e “DataEntrega” um campo “DateTime”, que usa o atributo “[JsonConverter]” para associar um conversor personalizado, “DateFormatConverter” que será criado logo abaixo.

Esse atributo é necessário porque o formato padrão de serialização para “DateTime” no “System.Text.Json” usa ISO 8601 (“yyyy-MM-ddTHH:mm:ss.fffZ”), o que não corresponde ao formato específico (“dd/MM/yyyy”) usado neste exemplo.

Agora vamos criar o conversor personalizado:

				
					public class DateFormatConverter : JsonConverter<DateTime>
{
    private const string DateFormat = "dd/MM/yyyy";

    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return DateTime.ParseExact(reader.GetString(), DateFormat, null);
    }

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString(DateFormat));
    }
}

				
			

O “DateFormatConverter” é uma classe personalizada que estende o comportamento do “System.Text.Json” ao serializar e desserializar datas no formato “dd/MM/yyyy”. Ele herda de “JsonConverter<DateTime>” e implementa dois métodos principais:

Read (Desserialização): Esse método é chamado quando um JSON está sendo convertido em um objeto C#. Ele utiliza o “Utf8JsonReader” para ler o valor da propriedade no JSON e, em seguida, usa “DateTime.ParseExact” para interpretar a string no formato específico (“dd/MM/yyyy”) e transformá-la em um objeto “DateTime”. 

Write (Serialização): Durante a serialização, este método é chamado para converter um valor “DateTime” do objeto C# em uma string JSON formatada como “dd/MM/yyyy”. O método utiliza o “Utf8JsonWriter” e chama “value.ToString(DateFormat)” para garantir que o formato de saída corresponda ao esperado.

Para testar podemos utilizar o seguinte código:

				
					var json = @"{ ""Cliente"": ""João"", ""DataEntrega"": ""15/12/2024"" }";
var pedido = JsonSerializer.Deserialize<Pedido>(json);
Console.WriteLine($"{pedido.Cliente} - {pedido.DataEntrega:yyyy-MM-dd}");

				
			

Tratamento de Enums como Strings

Enums podem ser representados como strings personalizadas no JSON, e um conversor facilita essa tradução, permitindo que os valores do enum sejam manipulados de maneira mais flexível ao serializar ou desserializar objetos:

				
					public enum Status
{
    Ativo,
    Inativo
}

public class StatusConverter : JsonConverter<Status>
{
    public override Status Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return reader.GetString() switch
        {
            "ativo" => Status.Ativo,
            "inativo" => Status.Inativo,
            _ => throw new JsonException("Valor inválido")
        };
    }

    public override void Write(Utf8JsonWriter writer, Status value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value == Status.Ativo ? "ativo" : "inativo");
    }
}

				
			

No código acima, o método “Read” verifica qual valor no JSON corresponde aos casos definidos no “switch” e retorna o valor do enum correspondente. Já o método “Write” converte o valor do enum em uma string para ser escrita no JSON.

Agora, vamos criar a classe “Usuario” que irá receber o enum “Status” com o conversor personalizado:

				
					public class Usuario
{
    [JsonConverter(typeof(StatusConverter))]
    public Status Status { get; set; }
}

				
			

Com isso, podemos criar um exemplo para testar o processo de desserialização:

				
					var usuarioJson = @"{ ""Status"": ""ativo"" }";
var usuario = JsonSerializer.Deserialize<Usuario>(usuarioJson);
Console.WriteLine(usuario.Status); 

				
			

Neste exemplo, o JSON com a string “ativo” é convertido para o valor “Status.Ativo” na propriedade “Status” do objeto “Usuario”. Esse processo permite um mapeamento flexível entre os valores do enum e suas representações em string no JSON, facilitando a integração com sistemas que utilizam formatos específicos para representar enums.

Manipulação de Dados Dinâmicos

Em muitos casos, ao trabalhar com dados JSON, a estrutura pode ser imprevisível ou variar de acordo com diferentes cenários, como quando consumimos APIs externas ou lidamos com sistemas de terceiros. Nesses casos, não é prático ou possível definir previamente classes para representar todos os formatos possíveis de JSON.

Para esses cenários, o “System.Text.Json” fornece ferramentas como “JsonElement” e “JsonDocument”, que permitem acessar e manipular os dados de forma dinâmica, sem a necessidade de modelos específicos. 

Para entendermos melhor, imagine que você está consumindo uma API que retorna informações de pedidos, mas o formato do JSON pode variar dependendo dos itens no pedido ou de atualizações no sistema.

				
					var json = @"
        {
            ""PedidoId"": 12345,
            ""Cliente"": ""João Silva"",
            ""Itens"": [
                { ""Produto"": ""Notebook"", ""Quantidade"": 1, ""Preco"": 3500.00 },
                { ""Produto"": ""Mouse"", ""Quantidade"": 2, ""Preco"": 150.00 }
            ],
            ""Data"": ""2024-12-16"",
            ""Status"": ""Enviado""
        }";

        using var document = JsonDocument.Parse(json);

        var root = document.RootElement;

        // Acessando propriedades principais
        Console.WriteLine($"Pedido ID: {root.GetProperty("PedidoId").GetInt32()}");
        Console.WriteLine($"Cliente: {root.GetProperty("Cliente").GetString()}");
        Console.WriteLine($"Data: {root.GetProperty("Data").GetString()}");
        Console.WriteLine($"Status: {root.GetProperty("Status").GetString()}");

        // Acessando a lista de itens
        Console.WriteLine("\nItens do Pedido:");
        foreach (var item in root.GetProperty("Itens").EnumerateArray())
        {
            var produto = item.GetProperty("Produto").GetString();
            var quantidade = item.GetProperty("Quantidade").GetInt32();
            var preco = item.GetProperty("Preco").GetDecimal();
            Console.WriteLine($"- {produto}: {quantidade} x R${preco:F2}");
        }
    }

				
			

Neste cenário, o “JsonDocument.Parse(json)” analisa a string JSON e cria um objeto “JsonDocument” para acessar sua estrutura. Após isso, o “GetProperty()” é usado para acessar valores como “PedidoId”, “Cliente”, “Data” e “Status”. Esses métodos retornam um “JsonElement”, do qual podemos extrair valores usando métodos como “GetString()”, “GetInt32()” e “GetDecimal()”.

O resultado que teremos no console será:

				
					Pedido ID: 12345
Cliente: João Silva
Data: 2024-12-16
Status: Enviado

Itens do Pedido:
- Notebook: 1 x R$3500,00
- Mouse: 2 x R$150,00

				
			

Acelere a sua carreira conosco!

Se você é Desenvolvedor .NET Júnior e quer acelerar sua carreira até nível Pleno com salário de R$7k+, ou mesmo busca a primeira vaga, conheça a Mentoria .NET StartClique aqui

Se é Desenvolvedor .NET Pleno ou Sênior e quer virar referência técnica em sua equipe e mercado, com salário de R$10k+, conheça a Mentoria .NET ExpertClique aqui

Conclusão

O “System.Text.Json” oferece flexibilidade e um ótimo desempenho para manipular JSON em aplicações .NET. Com conversores personalizados, você pode adaptar o processamento de JSON às necessidades mais complexas, garantindo que seus sistemas permaneçam robustos e eficientes. Experimente as técnicas apresentadas neste artigo para dominar a manipulação avançada de JSON!