Módulo 4 · APIs e sua Arquitetura — Capítulo 03

DTOs, Serialização e Versionamento

Controlando exatamente o formato JSON que entra e sai da API, e como evoluí-la sem quebrar quem já a consome.

1. Serialização — de objeto C# para JSON, e vice-versa

O ASP.NET Core usa System.Text.Json por padrão para converter objetos C# em JSON (na resposta) e JSON em objetos C# (no corpo de um POST/PUT) — automaticamente, sem você escrever esse código.

C# DTO simples e o JSON gerado
public class ClienteDto
{
    public int Id { get; set; }
    public string Nome { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

// return Ok(new ClienteDto { Id = 1, Nome = "Ana", Email = "ana@x.com" });
// gera:
// { "id": 1, "nome": "Ana", "email": "ana@x.com" }
Nota

Por padrão, o ASP.NET Core converte nomes de propriedade PascalCase (convenção C#, capítulo 14 do Módulo 1) para camelCase no JSON (convenção JavaScript) — Nome vira nome. Isso já resolve a maior fricção de convenção entre as duas linguagens automaticamente.

2. Controlando a serialização com atributos

C# JsonPropertyName, JsonIgnore
public class ClienteDto
{
    public int Id { get; set; }

    [JsonPropertyName("nomeCompleto")] // nome customizado no JSON
    public string Nome { get; set; } = string.Empty;

    [JsonIgnore] // nunca serializado — nunca sai no JSON
    public string SenhaHash { get; set; } = string.Empty;

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string? Telefone { get; set; } // omitido do JSON só quando for null
}
Por que isso importa tanto quanto no Módulo 3

O mesmo cuidado do capítulo 4 do Módulo 3 (nunca expor a entidade de domínio direto) vale em dobro numa API: um DTO mal desenhado pode vazar dados sensíveis (hash de senha, campos internos) simplesmente por serem propriedades públicas da entidade que acabou sendo serializada sem filtro.

3. Records — DTOs mais concisos e imutáveis

Um record (C# 9+) é ideal para DTOs — sintaxe mais curta que uma classe, e imutável por padrão:

C# record como DTO
public record ClienteDto(int Id, string Nome, string Email);

// equivalente à classe tradicional, mas em uma linha,
// com igualdade por valor (dois records com os mesmos dados são "iguais")
var dto1 = new ClienteDto(1, "Ana", "ana@x.com");
var dto2 = new ClienteDto(1, "Ana", "ana@x.com");
Console.WriteLine(dto1 == dto2); // true — records comparam por valor, classes por referência

4. Versionamento de API

Quando uma API já tem clientes em produção (apps mobile publicados, integrações de terceiros), mudar um contrato existente quebra quem já depende dele. Versionamento permite evoluir sem quebrar o que já funciona.

EstratégiaExemploObservação
Versão na URL/api/v1/clientes, /api/v2/clientesMais simples de entender e testar; a mais comum
Versão no headerX-Api-Version: 2URLs ficam "limpas", mas menos visível para quem explora a API
Versão no Accept headerAccept: application/json;v=2Mais "correto" segundo REST puro, raramente usado na prática
C# versionamento por URL — a forma mais direta
[ApiController]
[Route("api/v1/clientes")]
public class ClienteV1Controller : ControllerBase
{
    [HttpGet("{id:int}")]
    public async Task<ActionResult<ClienteDtoV1>> ObterPorId(int id) { ... }
}

[ApiController]
[Route("api/v2/clientes")]
public class ClienteV2Controller : ControllerBase
{
    // v2 pode ter um formato de DTO diferente, sem afetar quem ainda usa v1
    [HttpGet("{id:int}")]
    public async Task<ActionResult<ClienteDtoV2>> ObterPorId(int id) { ... }
}
Dica

Só versione quando realmente precisar quebrar um contrato existente (remover um campo, mudar seu tipo/significado). Adicionar um campo novo opcional a um DTO geralmente não exige nova versão — a maioria dos clientes ignora campos desconhecidos no JSON.

5. Onde isso te leva

Com o contrato de dados sob controle, o próximo capítulo cobre quem pode acessar cada endpoint: autenticação (quem é você) e autorização (o que você pode fazer) com JWT.

📌 Resumo do capítulo

  • System.Text.Json converte C# ↔ JSON automaticamente, incluindo PascalCase → camelCase.
  • [JsonIgnore] e [JsonPropertyName] controlam exatamente o que sai no JSON — essencial para nunca vazar campos sensíveis.
  • record é uma forma concisa e imutável de declarar DTOs, com igualdade por valor.
  • Versionamento (mais comum: na URL, /api/v1/...) evita quebrar clientes existentes ao evoluir a API.

✏️ Praticando

  1. Reescreva ClienteDto como record e confirme que a serialização JSON continua idêntica.
  2. Adicione um campo SenhaHash na entidade Cliente e confirme, testando a API, que ele nunca aparece no DTO exposto.
  3. Crie uma segunda versão (v2) do endpoint de clientes que devolve um campo calculado adicional, sem alterar a v1 existente.