C# - Serialização e Desserialização


  Neste artigo vamos recordar os conceitos sobre serialização e desserialização mostrando como usar estes recursos na plataforma .NET.

Na plataforma .NET a serialização e desserialização são processos que permitem converter objetos em uma representação serializada (como texto ou bytes) para que possam ser armazenados ou transmitidos e, em seguida, reconstruídos novamente em objetos em memória.

A serialização é útil quando você precisa enviar objetos pela rede, salvar em arquivos ou armazenar em bancos de dados. Ao serializar um objeto, você o transforma em uma sequência de bytes ou texto que pode ser facilmente transmitida ou armazenada.

A desserialização, por sua vez, faz o processo inverso, reconstruindo o objeto a partir dos dados serializados.

Existem várias razões pelas quais a serialização é necessária :

1- Persistência: A serialização permite que você salve objetos em arquivos ou bancos de dados para posterior recuperação. Ao serializar um objeto, você captura seu estado atual e pode restaurá-lo posteriormente.

2- Comunicação: Ao enviar objetos pela rede, é necessário serializá-los para que possam ser transmitidos em formato binário ou texto. Na extremidade receptora, a desserialização é usada para reconstruir o objeto original.

3- Armazenamento em cache: A serialização é usada para armazenar objetos em cache, permitindo que você os recupere rapidamente sem precisar reconstruí-los a partir de fontes de dados.

A plataforma .NET oferece recursos integrados para serialização e desserialização sendo o principal mecanismo de serialização a classe System.Text.Json.JsonSerializer, que classe permite fazer a serialização e desserialização de objetos para JSON (JavaScript Object Notation), um formato amplamente utilizado para comunicação entre sistemas.

Além disso, a plataforma .NET também suporta outros formatos de serialização, como XML e binário, por meio das classes :

Essas classes oferecem funcionalidades mais avançadas para casos específicos de uso.

Considerando a biblioteca System.Text.Json temos que ela oferece vários recursos para serialização e desserialização de objetos em formato JSON.

Os principais recursos desta biblioteca são:

Serialização de objetos para JSON
- JsonSerializer.Serialize - Converte um objeto em formato JSON. Você pode fornecer opções de serialização para personalizar o processo, como formatação, conversão de nomes de propriedades e tratamento de valores nulos.

Desserialização de JSON para objetos -  JsonSerializer.Deserialize -  Reconstrói um objeto a partir de uma representação JSON. Essa função permite que você especifique o tipo de objeto esperado como um parâmetro genérico ou como um argumento explícito.

JsonDocument: A classe JsonDocument permite analisar e manipular dados JSON de forma eficiente. Ela representa uma árvore de objetos JSON e fornece métodos para navegar pelos nós, ler os valores das propriedades e converter os valores em tipos .NET.

Personalização da serialização:  A biblioteca System.Text.Json fornece vários atributos que você pode aplicar às classes e propriedades para controlar o processo de serialização.

Alguns exemplos são [JsonPropertyName], [JsonIgnore] e [JsonConverter] onde:

JsonConverter permite criar suas próprias classes derivadas de JsonConverter para personalizar o processo de serialização e desserialização para tipos específicos.

- JsonIgnore: Atributo que indica que uma propriedade deve ser ignorada durante a serialização.

- JsonPropertyName:  Atributo que permite especificar um nome diferente para uma propriedade durante a serialização.

- JsonSerializerOptions.IgnoreNullValues: Opção que permite controlar se valores nulos devem ser incluídos na serialização ou não.

- JsonSerializerOptions.WriteIndented: Opção que indica se o JSON deve ser formatado com indentação para facilitar a leitura.

Como exemplo vamos supor que temos uma classe Produto contendo as seguintes propriedades :

public class Produto
{
 
public int Id { get; set; }
 
public string Nome { get; set; }
 
public decimal Preco { get; set; }
 
public int Estoque { get; set; }
 
public DateTime DataCadastro { get; set; }
}

Aqui estão alguns exemplos de como você pode usar a serialização e desserialização na sua API:

a- Serializar um objeto Produto para JSON

using System.Text.Json;

Produto produto = new Produto
{
   Id = 1,
   Nome =
"Produto A",
   Preco = 9.99m,
   Estoque = 10,
   DataCadastro = DateTime.Now
};

string json = JsonSerializer.Serialize(produto);
Console.WriteLine(json);
 

Isso produzirá a saída JSON correspondente ao objeto Produto:

{
"Id":1,"Nome":"Produto A","Preco":9.99,"Estoque":10,"DataCadastro":"2023-05-21T16:48:57.2401147-03:00"
}

b- Desserializar JSON em um objeto Produto

using System.Text.Json;

string json = "{\"Id\":1,\"Nome\":\"Produto A\",\"Preco\":9.99," +
             
"\"Estoque\":10,\"DataCadastro\":\"2023-05-20T12:34:56.789Z\"}";

Produto produto = JsonSerializer.Deserialize<Produto>(json);

Console.WriteLine($"Id: {produto.Id}, Nome: {produto.Nome}, Preço: {produto.Preco}, " +
                 
$"Estoque: {produto.Estoque}, Data de Cadastro: {produto.DataCadastro}");
 

Isso irá desserializar o JSON em um objeto Produto e imprimir as propriedades do produto:


 Id: 1, Nome: Produto A, Preço: 9,99, Estoque: 10, Data de Cadastro: 20/05/2023 12:34:56     
 

c- Personalizar a serialização e desserialização com atributos:

public class Produto
{
  
public int Id { get; set; }
   [JsonPropertyName(
"nome")]
  
public string? Nome { get; set; }
  
public decimal Preco { get; set; }
   [JsonIgnore]
  
public int Estoque { get; set; }
   [JsonConverter(
typeof(JsonDateTimeConverter))]
  
public DateTime DataCadastro { get; set; }
}

public class JsonDateTimeConverter : JsonConverter<DateTime>
{
  
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert,
                                 JsonSerializerOptions options)
   {
      
return DateTime.ParseExact(reader.GetString(),
                   
"yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
   }
  
public override void Write(Utf8JsonWriter writer, DateTime value,
                        JsonSerializerOptions options)
   {
          writer.WriteStringValue(value.ToString(
"yyyy-MM-dd HH:mm:ss",
            CultureInfo.InvariantCulture));
   }
}

Neste exemplo, utilizamos atributos como [JsonPropertyName] para personalizar o nome da propriedade, [JsonIgnore] para ignorar a propriedade na serialização e [JsonConverter] para usar um conversor personalizado para a propriedade DataCadastro.

Loop de referência cruzada

Na situação em que uma entidade faz referência a outra entidade e há a possibilidade de ocorrer um loop de referência cruzada, é necessário ter cuidado ao realizar a serialização dessas entidades para evitar o problema.

Vamos considerar o exemplo em que a entidade Produto referencia a entidade Categoria.

A classe Produto poderia ser definida assim:

public class Produto
{
  public int Id { get; set; }
 
public string? Nome { get; set; }
 
public decimal Preco { get; set; }
 
public int Estoque { get; set; }
 
public DateTime DataCadastro { get; set; }
 
public Categoria? Categoria { get; set; }
}

E a classe Categoria poderia ser definida assim:

public class Categoria
{
   public int Id { get; set; }
  
public string? Nome { get; set; }
  
public List<Produto>? Produtos { get; set; }
}

Aqui está um exemplo de como o loop de referência cruzada pode ocorrer:

Categoria categoria = new Categoria
{
    Id = 1,
    Nome =
"Eletrônicos"
};

Produto produto = new Produto
{
   Id = 1,
   Nome =
"Celular",
   Preco = 999.99m,
   Estoque = 10,
   DataCadastro = DateTime.Now,
   Categoria = categoria
};

categoria.Produtos = new List<Produto> { produto };
 

Nesse exemplo, temos uma referência circular entre o objeto produto e o objeto categoria, uma vez que produto.Categoria aponta para categoria e categoria.Produtos contém o objeto produto.

Ao tentar serializar esses objetos diretamente usando a biblioteca System.Text.Json, você enfrentará um erro devido ao loop de referência cruzada. Para evitar esse problema, é possível utilizar algumas opções de configuração.

Uma abordagem para resolver esse problema é usar o atributo [JsonIgnore] em uma das propriedades envolvidas na referência circular. Por exemplo, você pode usar o atributo [JsonIgnore] na propriedade Categoria da classe Produto para evitar que a serialização tente incluir essa propriedade:

public class Produto
{
   public int Id { get; set; }
  
public string Nome { get; set; }
  
public decimal Preco { get; set; }
  
public int Estoque { get; set; }
  
public DateTime DataCadastro { get; set; }

   [JsonIgnore]
  
public Categoria Categoria { get; set; }
}

Outra opção é usar as opções de configuração da biblioteca System.Text.Json para lidar com referências circulares. Uma das opções disponíveis é o uso do JsonSerializerOptions.ReferenceHandler com a opção ReferenceHandler.Preserve:

var options = new JsonSerializerOptions
{
   ReferenceHandler = ReferenceHandler.Preserve
};

string json = JsonSerializer.Serialize(produto, options);
 

Dessa forma, o ReferenceHandler.Preserve garante que as referências circulares sejam preservadas, mas o serializador será capaz de lidar com elas corretamente, evitando o loop de referência cruzada.

Essas são algumas abordagens para evitar o problema de loop de referência cruzada ao serializar objetos que possuem referências circulares. A escolha entre as opções depende do contexto.

Definindo o formato de resposta

Ao desenvolver uma API ASP.NET Core, você pode decidir o formato de resposta de acordo com as necessidades e requisitos do seu projeto. Existem várias opções disponíveis para definir o formato de resposta na ASP.NET Core. Aqui estão algumas abordagens comuns:

1- Formato JSON (padrão):

O formato JSON é amplamente utilizado para comunicação entre sistemas e é o formato padrão para as respostas da maioria das APIs. A ASP.NET Core possui suporte nativo para serialização e desserialização de objetos JSON usando a biblioteca System.Text.Json, e, para retornar uma resposta JSON, você pode simplesmente retornar um objeto serializado ou usar o método Json() da classe ControllerBase :

[HttpGet]
public
IActionResult Get()
{
  
var data = // Obtenha os dados do seu repositório
  
return Ok(data); // Retorna a resposta em JSON
}

2- Formato XML:

Embora menos comum do que o formato JSON para APIs, a ASP.NET Core também oferece suporte à serialização e desserialização de objetos XML usando a biblioteca System.Xml.Serialization. Para retornar uma resposta XML, você pode usar o método Xml() da classe ControllerBase.

Para adicionar suporte ao formato XML em uma API ASP.NET Core, você precisa configurar o suporte a XML no pipeline da aplicação e definir o comportamento da serialização e desserialização para XML. Aqui está um exemplo de como adicionar suporte ao formato XML:

Adicione a dependência ao pacote Microsoft.AspNetCore.Mvc.Formatters.Xml no seu projeto. Você pode fazer isso manualmente, adicionando a referência ao pacote no arquivo .csproj, ou usando o NuGet Package Manager no Visual Studio.

No método ConfigureServices do arquivo Startup.cs, adicione o suporte ao formato XML usando o método AddXmlSerializerFormatters() no serviço MvcOptions. Isso habilita a serialização e desserialização XML na API.

A partir do .NET 6.0 a mesma configuração pode ser feita no arquivo Program da seguinte forma:

builder.Services.AddControllers(options =>
{ // Opcional: permite negociação de conteúdo
    options.RespectBrowserAcceptHeader =
true;
})
.AddXmlSerializerFormatters();
// Adiciona o suporte ao formato XML
}

Opcionalmente, você pode personalizar as opções de serialização XML no método ConfigureServices. Por exemplo, você pode alterar a codificação XML, adicionar namespaces ou configurar formatação específica.

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers(options =>
   {
// Opcional: permite negociação de conteúdo
     options.RespectBrowserAcceptHeader =
true;
   })
   .AddXmlSerializerFormatters(options =>
   {
      options.UseXmlWriterSettings =
true;
      options.XmlWriterSettings.OmitXmlDeclaration =
true;
      options.XmlWriterSettings.NamespaceHandling = NamespaceHandling.OmitDuplicates;
      options.XmlWriterSettings.Encoding = Encoding.UTF8;
    });
// Adiciona o suporte ao formato XML com opções personalizadas
}

A partir do .NET 6.0 esta configuração pode ser feita na classe Program assim:

...  
   builder.Services.AddControllers(options =>
   {
// Opcional: permite negociação de conteúdo
     options.RespectBrowserAcceptHeader =
true;
   })
   .AddXmlSerializerFormatters(options =>
   {
      options.UseXmlWriterSettings =
true;
      options.XmlWriterSettings.OmitXmlDeclaration =
true;
      options.XmlWriterSettings.NamespaceHandling = NamespaceHandling.OmitDuplicates;
      options.XmlWriterSettings.Encoding = Encoding.UTF8;
    });
// Adiciona o suporte ao formato XML com opções personalizadas
...

3- Outros formatos personalizados

Se você precisa retornar um formato de resposta personalizado, você pode usar a classe ContentResult para criar uma resposta com o conteúdo desejado. Você pode definir o tipo de conteúdo (Content-Type) e o corpo da resposta manualmente :

[HttpGet]
public
IActionResult Get()
{
  
var content = // Construa o conteúdo da resposta personalizada
  
return new ContentResult
   {
      Content = content,
      ContentType =
"application/custom-type"
   };
}

Essas são algumas das maneiras de decidir o formato de resposta na ASP.NET Core. A escolha do formato depende dos requisitos da sua API, preferências do cliente e capacidades do servidor. Lembre-se de considerar fatores como a facilidade de consumo, eficiência da comunicação e compatibilidade com as tecnologias utilizadas pelos clientes da sua API.

Configurando os formatadores

Se você deseja configurar os formatadores baseados no System.Text.Json em uma API ASP.NET Core, você precisará fazer algumas alterações nas opções de formatação padrão. A seguir temos um exemplo de como configuração destess formatadores.

A partir do .NET 6.0, adicione o suporte ao formato JSON usando o método AddControllers().AddJsonOptions() na class Program e configure as opções de serialização JSON.

...
builder.Services.AddControllers()
   .AddJsonOptions(options =>
   {  // Permite a manutenção dos nomes das propriedades como estão
      options.JsonSerializerOptions.PropertyNamingPolicy =
null;     
     
// Ignora propriedades nulas na serialização
      options.JsonSerializerOptions.IgnoreNullValues =
true;   
     
// Adicione conversores personalizados, se necessário
      options.JsonSerializerOptions.Converters.Add(
new MyCustomConverter());
   });
...

Você também pode configurar as opções globais de serialização JSON na classe Program para que se apliquem a todas as ações dos controllers da sua API.

...
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {  // Converte nomes das propriedades para camelCase
       options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
       // Ignora propriedades nulas na serialização
       options.JsonSerializerOptions.IgnoreNullValues =
true;
      
// Formata a saída JSON com indentação
       options.JsonSerializerOptions.WriteIndented =
true;
});
...

A seguir, em uma action específica de um controller, você pode anotar a action com [Produces] para indicar o formato de resposta específico :

[HttpGet]
[Produces(
"application/json")]
public
IActionResult Get()
{
   var data = // Obter dados
   // A resposta será retornada no formato JSON
   return Ok(data);
}

Com essas configurações, a API ASP.NET Core utilizará os formatadores baseados no System.Text.Json para a serialização e desserialização de objetos JSON.

Lembre-se de que a configuração destes formatadores JSON  é flexível e pode ser adaptada às necessidades específicas da sua API e dos clientes que consomem a API.

E estamos conversados.

"Deus nos ressuscitou com Cristo e com ele nos fez assentar nos lugares celestiais em Cristo Jesus,para mostrar, nas eras que hão de vir, a incomparável riqueza de sua graça, demonstrada em sua bondade para conosco em Cristo Jesus. "
Efésios 2:6-7

Referências:


José Carlos Macoratti