Capítulo 03: Data Binding e Validação de Structs com Validator
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Corpo da Requisição (JSON/Form/Query)"] --> B["c.ShouldBind* (reflection)"]
B --> C["Struct Go com tags `json` e `binding`"]
C --> D["go-playground/validator"]
D -->|"válido"| E["Handler processa dados tipados"]
D -->|"inválido"| F["validator.ValidationErrors"]
F --> G["Formatador de Erros -> JSON amigável"]
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
style F fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style G fill:#fffde7,stroke:#fbc02d,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Data Binding e Validação de Structs com Validator
O Gin resolve o problema de “transformar bytes de uma requisição HTTP em uma struct Go tipada e validada” através de reflection combinada com a biblioteca go-playground/validator, embutida por padrão no binding engine do framework. O contrato é declarado diretamente nas tags da struct: a tag json:"campo" controla o nome usado na (des)serialização, enquanto binding:"required,email,gte=18" descreve as regras de validação que serão avaliadas logo após o unmarshal.
Existe uma diferença de controle de fluxo importante entre as duas famílias de métodos de binding:
c.BindJSON(&obj)(e as variantesc.Bind,c.BindQuery, etc.) já respondem automaticamente com400 Bad Requeste chamamc.Abort()internamente se a validação falhar — conveniente, mas retira do desenvolvedor o controle sobre o formato da resposta de erro.c.ShouldBindJSON(&obj)(e as variantesShould*) apenas retornam oerror, deixando a decisão de como responder inteiramente a cargo do handler. Em APIs corporativas,Should*é quase sempre a escolha certa, pois permite padronizar um envelope de erro único em toda a aplicação.
O binding não se limita a JSON: c.ShouldBindQuery(&filtro) mapeia query strings para campos usando a tag form:"chave" (com suporte a form:"limit,default=10"), e c.ShouldBind(&obj) decide automaticamente a origem dos dados com base no método HTTP e no Content-Type (JSON, form-urlencoded ou multipart). Para uploads, c.FormFile("campo") devolve um *multipart.FileHeader sem carregar o arquivo inteiro em memória de uma vez, permitindo então c.SaveUploadedFile(arquivo, destino).
Quando as regras built-in do validator (required, min, max, gte, lte, oneof, email, uuid, etc.) não bastam — por exemplo, para validar um CPF/CNPJ brasileiro — é possível registrar uma função customizada diretamente no *validator.Validate interno do engine de binding do Gin: binding.Validator.Engine().(*validator.Validate).RegisterValidation("nome_da_regra", funcaoCustomizada). Isso deve ser feito uma única vez, na inicialização da aplicação, antes de o servidor começar a aceitar requisições.
Por fim, quando a validação falha, o error retornado por ShouldBindJSON pode (na maioria dos casos) ser convertido via type assertion para validator.ValidationErrors — um slice de FieldError que expõe .Field(), .Tag() (a regra que falhou) e .Param() (o parâmetro da regra, como o 18 em gte=18). Mapear isso para um JSON como [{"campo": "email", "mensagem": "formato inválido"}] é o que torna os erros de validação realmente úteis para quem consome a API.
type CriarUsuarioInput struct {
Nome string `json:"nome" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Idade int `json:"idade" binding:"gte=18"`
}
func CriarUsuarioHandler(c *gin.Context) {
var input CriarUsuarioInput
if err := c.ShouldBindJSON(&input); err != nil {
if erros, ok := err.(validator.ValidationErrors); ok {
c.JSON(http.StatusBadRequest, gin.H{"erros": FormatarErrosValidacao(erros)})
return
}
c.JSON(http.StatusBadRequest, gin.H{"erro": "payload malformado"})
return
}
c.JSON(http.StatusCreated, gin.H{"email": input.Email})
}
💻 2. Código de Demonstração Corporativo
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
type CriarPedidoInput struct {
ClienteID uint `json:"cliente_id" binding:"required"`
ValorTotal float64 `json:"valor_total" binding:"required,gt=0"`
CupomCPF string `json:"cupom_cpf" binding:"omitempty,valida_cpf"`
}
// validarCPF é uma regra customizada: comprimento simplificado, apenas
// como exemplo didático (uma implementação real checaria dígitos verificadores).
var validarCPF validator.Func = func(fl validator.FieldLevel) bool {
cpf := fl.Field().String()
return len(cpf) == 11
}
func registrarValidadoresCustomizados() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("valida_cpf", validarCPF)
}
}
type ErroCampo struct {
Campo string `json:"campo"`
Mensagem string `json:"mensagem"`
}
func formatarErros(err error) []ErroCampo {
var erros []ErroCampo
if valErrs, ok := err.(validator.ValidationErrors); ok {
for _, f := range valErrs {
erros = append(erros, ErroCampo{Campo: f.Field(), Mensagem: "falha na regra: " + f.Tag()})
}
}
return erros
}
func main() {
registrarValidadoresCustomizados()
engine := gin.Default()
engine.POST("/pedidos", func(c *gin.Context) {
var input CriarPedidoInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erros": formatarErros(err)})
return
}
c.JSON(http.StatusCreated, input)
})
engine.Run(":8080")
}