---
title: "Validação de Dados em Rust: validator, garde e Serde em 2026"
url: "https://rustlang.com.br/blog/rust-validacao-dados-validator-garde-serde-2026/"
markdown_url: "https://rustlang.com.br/blog/rust-validacao-dados-validator-garde-serde-2026.MD"
description: "Como desenhar validação de dados em APIs Rust com Serde, validator, garde, tipos de domínio, erros claros, testes e limites entre input e regra de negócio."
date: "2026-06-12"
author: "Equipe Rust Brasil"
---

# Validação de Dados em Rust: validator, garde e Serde em 2026

Como desenhar validação de dados em APIs Rust com Serde, validator, garde, tipos de domínio, erros claros, testes e limites entre input e regra de negócio.


## Por que validação de dados importa tanto em Rust

Rust é ótimo para impedir uma classe enorme de bugs de memória, concorrência e tratamento de erro. Mas isso não significa que uma API escrita em Rust aceita apenas dados bons. JSON malformado, campos vazios, datas impossíveis, IDs trocados, enumeração fora do contrato, e-mail falso, senha fraca, preço negativo e payload grande demais continuam sendo problemas de produto, segurança e operação.

A diferença é que Rust dá ferramentas muito boas para transformar validação em parte explícita da arquitetura. Em vez de espalhar `if campo.is_empty()` em cada handler, você pode separar a fronteira do sistema, validar uma vez, converter para tipos de domínio e impedir que o restante do código carregue incerteza desnecessária. Isso deixa APIs com [Axum](/ecossistema/axum/), serviços com [Tokio](/ecossistema/tokio/), workers, CLIs e jobs mais previsíveis.

Para quem acompanha [vagas Rust](/vagas/) e trilhas de [carreira Rust](/carreira/), validação também é um ótimo assunto de entrevista. Ela mostra se a pessoa sabe sair do exemplo feliz e pensar em bordas reais: contrato de API, erro para usuário, segurança, logs, testes, compatibilidade e evolução de schema. Em produção, isso vale tanto quanto saber escrever um `struct` bonito.

## Três camadas: parse, validação e domínio

O erro mais comum é misturar tudo em uma etapa só. Em uma API HTTP, o primeiro passo é **parse**: transformar bytes em uma estrutura Rust. Com Serde, isso normalmente significa desserializar JSON para um DTO de entrada. Essa etapa responde perguntas básicas: o JSON é válido? O campo existe? O tipo combina? Uma string chegou como string? Um número chegou como número?

A segunda etapa é **validação**. Aqui entram regras que não são apenas tipo: tamanho mínimo, máximo, formato, intervalo, lista permitida, dependência entre campos e limites de segurança. Um `String` pode ser uma string válida para o compilador e ainda assim ser um nome vazio, um e-mail ruim ou uma descrição com 50 mil caracteres.

A terceira etapa é **domínio**. Depois que a entrada passa pela validação, você converte para tipos que representam dados confiáveis. Pode ser um `Email`, `UserName`, `Money`, `Slug`, `PasswordHash`, `DateRange` ou `NewJobPost`. A ideia é simples: o código de negócio não deveria ficar perguntando a cada linha se o dado ainda é válido. Se ele recebeu um tipo de domínio, a validação já aconteceu.

## Serde: excelente para contrato, insuficiente para regra

Serde é a base de muitas APIs Rust porque torna a fronteira de dados previsível. Com `#[derive(Deserialize)]`, você recebe um DTO tipado e deixa o framework rejeitar JSON impossível antes de chegar ao caso de uso. Isso já elimina muito ruído.

Um DTO simples para cadastro poderia começar assim:

```rust
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct CriarContaInput {
    nome: String,
    email: String,
    senha: String,
    aceite_termos: bool,
}
```

Esse código garante que `nome`, `email`, `senha` e `aceite_termos` existem nos tipos esperados. Mas ele não garante que `nome` tem conteúdo, que `email` parece e-mail, que `senha` tem tamanho mínimo ou que `aceite_termos` veio como `true`. Esses detalhes são validação de entrada.

Também vale resistir à tentação de colocar regra demais em atributos de desserialização. `rename`, `default`, `deny_unknown_fields` e conversões customizadas são úteis, mas Serde deve continuar sendo a camada de contrato. Se a regra precisa de mensagem clara, teste específico ou contexto de negócio, ela provavelmente merece uma validação explícita.

## validator: validações comuns com derive

A crate `validator` é uma escolha conhecida para validações declarativas. Ela permite anotar campos com regras comuns e chamar `validate()` antes de seguir. Para formulários e DTOs simples, a ergonomia é boa.

```rust
use serde::Deserialize;
use validator::Validate;

#[derive(Debug, Deserialize, Validate)]
struct CriarContaInput {
    #[validate(length(min = 2, max = 80))]
    nome: String,

    #[validate(email)]
    email: String,

    #[validate(length(min = 12, max = 128))]
    senha: String,

    #[validate(must_match(other = "confirmacao_senha"))]
    confirmacao_senha: String,
}
```

O valor desse estilo é legibilidade perto do contrato. Quem abre o DTO entende rapidamente quais regras de formato existem. Isso ajuda em APIs CRUD, painéis internos, formulários e endpoints onde as regras são estáveis.

O cuidado é não transformar atributos em uma linguagem de negócio escondida. Se a regra depende de banco, feature flag, país, plano do cliente, data atual ou política complexa, escreva função explícita. Atributos são ótimos para validações locais; regras com contexto merecem código comum, nomeado e testado.

## garde: validação expressiva e composição moderna

A crate `garde` aparece como alternativa moderna para validação declarativa em Rust. Ela também usa derive e atributos, mas oferece uma API interessante para compor regras, validar coleções e organizar mensagens. Em projetos novos, vale comparar a ergonomia com `validator` antes de padronizar.

Um DTO com `garde` pode seguir a mesma ideia:

```rust
use garde::Validate;
use serde::Deserialize;

#[derive(Debug, Deserialize, Validate)]
struct CriarProjetoInput {
    #[garde(length(min = 3, max = 60))]
    nome: String,

    #[garde(length(min = 10, max = 160))]
    descricao: String,

    #[garde(skip)]
    tags: Vec<String>,
}
```

A escolha entre `validator` e `garde` não deveria virar torcida. Em produção, pergunte: a crate é mantida? As mensagens de erro são fáceis de traduzir? A equipe entende os atributos? Validações customizadas ficam claras? Funciona bem com seus frameworks? O formato de erro conversa com o frontend?

Para times brasileiros, outro ponto prático é localização. A mensagem enviada ao usuário precisa ser natural em português, mas logs internos podem manter códigos técnicos. Não dependa apenas da frase padrão de uma crate se o produto precisa de UX consistente.

## Tipos de domínio: validado uma vez, confiável depois

A parte mais poderosa vem depois da validação. Em vez de passar `String` para todo lado, crie tipos pequenos que só existem quando o valor é válido.

```rust
#[derive(Debug, Clone)]
struct Email(String);

impl Email {
    fn parse(valor: String) -> Result<Self, ValidacaoErro> {
        let normalizado = valor.trim().to_lowercase();
        if normalizado.len() > 254 || !normalizado.contains('@') {
            return Err(ValidacaoErro::email_invalido());
        }
        Ok(Self(normalizado))
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}
```

Esse exemplo é simples de propósito. Em um produto real, você talvez use uma crate especializada para e-mail. O ponto arquitetural é que `Email` não deveria aceitar qualquer string. Depois que o tipo existe, o resto do sistema ganha uma garantia.

A mesma técnica serve para `Slug`, `NomeEmpresa`, `Cep`, `CpfMascarado`, `Preco`, `IntervaloData`, `Percentual`, `SenhaEmTexto` antes do hash e `SenhaHash` depois do hash. Cada tipo reduz a chance de trocar parâmetros, duplicar regra ou salvar estado inválido.

## DTO não é modelo de banco

Outro erro recorrente é usar a mesma `struct` para JSON de entrada, domínio e persistência. Parece econômico no começo, mas costuma piorar com o tempo. Entrada HTTP tem campos opcionais, nomes pensados para API pública, mensagens de erro e compatibilidade. Domínio tem invariantes. Banco tem IDs, timestamps, índices, campos normalizados e detalhes de storage.

Separar não significa criar burocracia infinita. Para endpoints pequenos, três structs podem parecer demais. Mas em partes importantes do sistema, a separação paga rápido:

- `CriarContaInput`: representa o payload externo;
- `NovaConta`: representa dados já validados para o caso de uso;
- `ContaRow`: representa a linha retornada pelo banco.

Com essa divisão, mudanças externas não vazam diretamente para o banco. Você consegue aceitar um alias temporário no JSON, manter a regra de domínio estável e alterar persistência sem quebrar o contrato público.

## Erros bons: código estável, mensagem humana

Validação ruim devolve `400 Bad Request` com texto genérico. Validação boa devolve erro estruturado, com campo, código e mensagem. O frontend consegue destacar o campo certo; logs conseguem agrupar problemas; testes conseguem comparar códigos estáveis sem depender de frase.

Um formato prático:

```json
{
  "erro": "validacao_falhou",
  "campos": [
    { "campo": "email", "codigo": "email_invalido", "mensagem": "Informe um e-mail válido." },
    { "campo": "senha", "codigo": "senha_curta", "mensagem": "Use pelo menos 12 caracteres." }
  ]
}
```

Em Rust, modele isso com enum e structs. Evite montar JSON de erro com strings soltas em cada handler. O erro de validação deve ter uma conversão central para resposta HTTP, seja em Axum, Actix Web ou outro framework.

Também cuidado com segurança: nem toda regra deve revelar detalhe. Para login, por exemplo, responder "usuário não existe" e "senha errada" separadamente pode facilitar enumeração. Para cadastro, mensagem específica ajuda UX. O contexto decide.

## Validação síncrona, assíncrona e com banco

Muitas validações são puras e rápidas: tamanho, formato, intervalo, enumeração, relação entre dois campos do mesmo payload. Essas devem rodar antes de qualquer chamada externa. Elas economizam banco, fila e logs.

Outras validações precisam de contexto: e-mail já cadastrado, plano permite tal recurso, organização existe, usuário tem permissão, cupom ainda está ativo. Essas regras podem exigir banco ou serviço externo. Não misture tudo em uma única macro. Faça primeiro a validação local; depois chame o caso de uso para validações de negócio.

Uma sequência saudável para `POST /contas`:

1. desserializar JSON com Serde;
2. validar forma local com `validator`, `garde` ou função própria;
3. converter para `NovaConta` com tipos de domínio;
4. no caso de uso, verificar unicidade, plano e permissões;
5. persistir e emitir resposta.

Esse fluxo deixa claro onde cada erro nasce. Também facilita teste: você testa validação local sem banco e testa regra de negócio com repositório fake ou banco de integração.

## Testes que pegam regressão de validação

Validação sem teste vira decoração. Escreva casos pequenos para entradas válidas, entradas inválidas e bordas. Não precisa testar cada atributo de uma crate como se você mantivesse a crate; teste o contrato do seu produto.

Exemplos de casos úteis:

- nome com espaços antes/depois é normalizado ou rejeitado conforme a regra;
- e-mail em maiúsculas vira minúsculo quando isso é decisão do produto;
- senha com 11 caracteres falha e com 12 passa;
- descrição no limite máximo passa, acima do limite falha;
- datas com início depois do fim falham;
- campo desconhecido é rejeitado quando o contrato exige `deny_unknown_fields`;
- payload grande demais é barrado antes de alocar trabalho caro.

Para APIs, inclua pelo menos um teste de integração do formato de erro. Isso protege o frontend e evita regressão silenciosa quando alguém troca a crate de validação.

## Observabilidade sem vazar dado sensível

Validação gera muitos erros esperados. Usuário erra formulário, bot envia payload estranho, cliente antigo usa campo removido. Você precisa observar padrões sem transformar logs em depósito de dado pessoal.

Registre código de erro, rota, versão do cliente, tamanho do payload, ID de correlação e quantidade de campos inválidos. Evite logar senha, token, documento, corpo completo de requisição ou dados sensíveis. Com [Tracing](/ecossistema/tracing/), crie spans úteis e campos seguros.

Métricas também ajudam. Um aumento súbito em `validacao_falhou` depois de deploy pode indicar contrato quebrado. Muitos erros em um campo específico podem mostrar UX ruim. Muitos payloads enormes podem indicar abuso ou falta de limite no proxy.

## Limites de payload e defesa de borda

Validação de campo não substitui limites de infraestrutura. Antes mesmo de desserializar JSON, defina tamanho máximo de body. Uma API que aceita 100 MB para criar uma conta tem problema antes de chegar em `validator`.

Em Axum e outros frameworks, configure limites de request, timeouts e camadas de middleware. Para uploads, use fluxo próprio. Para JSON comum, seja conservador. Depois, valide campos internos. Essa combinação evita gastar CPU e memória com entrada absurda.

Também pense em listas. Um array com 10 itens pode ser normal; um array com 100 mil itens talvez derrube o endpoint mesmo que cada item seja válido. Valide tamanho da coleção e profundidade quando o formato permitir objetos aninhados.

## O que colocar no portfólio de carreira

Um bom projeto de portfólio em Rust não precisa ser grande. Uma API pequena com validação bem desenhada já demonstra maturidade. Mostre DTOs de entrada, conversão para domínio, erros estruturados, testes de borda, limites de payload, logs seguros e documentação do contrato.

Se quiser ir além, implemente o mesmo caso com Axum, Serde, uma crate de validação, tipos newtype e testes. Publique exemplos de erro e explique decisões. Esse material conversa com vagas backend, plataforma, segurança de API e ferramentas internas.

Para comparação de linguagem, vale estudar como Go trata validação com structs e tags no ecossistema do <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Golang Brasil</a>. A diferença de ergonomia ajuda a explicar por que Rust costuma empurrar mais invariantes para tipos, enquanto Go depende mais de disciplina em runtime.

## Checklist prático

Antes de considerar validação pronta em uma API Rust, confira:

- Serde rejeita tipos incompatíveis e campos obrigatórios ausentes;
- payload máximo está configurado antes da desserialização;
- regras locais ficam em uma camada explícita;
- regras de negócio com banco não estão escondidas em macro de DTO;
- tipos de domínio impedem passar string solta para o núcleo do sistema;
- erros têm campo, código estável e mensagem em português;
- logs não vazam senha, token, documento nem corpo completo;
- testes cobrem bordas e formato de erro;
- frontend ou consumidor externo sabe como tratar falhas;
- documentação mostra exemplos de payload válido e inválido.

## Conclusão

Validação de dados em Rust é menos sobre escolher uma crate e mais sobre desenhar fronteiras confiáveis. Serde resolve o contrato inicial. `validator`, `garde` ou funções próprias ajudam a declarar regras de entrada. Tipos de domínio fazem a garantia sobreviver depois do handler. Erros estruturados e testes impedem que a API vire uma caixa-preta.

Essa disciplina combina muito bem com Rust porque usa a linguagem a favor da arquitetura: dados ruins ficam na borda, dados bons entram no núcleo com tipos fortes, e cada camada tem responsabilidade clara. Para produção e carreira, esse é o tipo de detalhe que separa um exemplo de tutorial de um serviço que aguenta uso real.
