Introdução: Por Que Validar Dados em APIs?
Quando você constrói uma API em Go, recebe dados de clientes externos — JSON, formulários, query parameters — e precisa garantir que esses dados atendem aos requisitos da sua aplicação antes de processá-los. Validação inadequada leva a erros em tempo de execução, comportamentos inesperados e vulnerabilidades de segurança. O go-playground/validator é a biblioteca Go mais popular para resolver esse problema de forma elegante e eficiente, permitindo que você defina regras de validação através de struct tags com uma sintaxe clara e expressiva.
Neste artigo, você aprenderá não apenas como usar a biblioteca, mas entenderá por quê cada abordagem funciona dessa forma e como aplicar isso em cenários reais de desenvolvimento de APIs.
Conceitos Fundamentais do go-playground/validator
O que é Validação Declarativa?
O go-playground/validator utiliza um padrão chamado validação declarativa, onde você descreve as regras de validação diretamente nas struct tags, sem escrever lógica condicional manual. Essa abordagem é superior a validações imperativas porque separa a lógica de negócio da lógica de validação, tornando o código mais legível e mantível.
Quando você marca um campo com tags como validate:"required,email", você está dizendo: "este campo é obrigatório e deve ser um email válido". A biblioteca interpreta essas tags em tempo de validação e retorna erros estruturados caso as regras não sejam atendidas.
Instalação e Setup Básico
Para instalar a biblioteca:
go get github.com/go-playground/validator/v10
Uma validação básica em Go requer apenas:
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type User struct {
Name string `validate:"required,min=3"`
Email string `validate:"required,email"`
Age int `validate:"required,min=18,max=120"`
}
func main() {
validate := validator.New()
user := User{
Name: "Jo",
Email: "invalid-email",
Age: 15,
}
err := validate.Struct(user)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
fmt.Printf("Campo: %s, Tag: %s, Valor: %v\n",
err.Field(), err.Tag(), err.Value())
}
}
}
Neste exemplo, a validação falhará em três pontos: Name tem menos de 3 caracteres, Email não é um formato válido de email, e Age é menor que 18. A biblioteca retorna um tipo ValidationErrors que você pode iterar para obter detalhes de cada falha.
Integrando Validação em APIs Go
Validação em Handlers HTTP
Em uma API RESTful real, você valida dados recebidos em requisições. A integração mais natural é fazer a validação logo após o unmarshal do JSON:
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/go-playground/validator/v10"
)
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=3,max=100"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Age int `json:"age" validate:"required,min=18,max=120"`
}
var validate = validator.New()
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
// Decodificar JSON
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "JSON inválido", http.StatusBadRequest)
return
}
// Validar estrutura
if err := validate.Struct(req); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
// Construir resposta de erro detalhada
validationErrors := err.(validator.ValidationErrors)
errorResponse := make(map[string]string)
for _, err := range validationErrors {
errorResponse[err.Field()] = fmt.Sprintf(
"Falha na validação '%s'", err.Tag())
}
json.NewEncoder(w).Encode(map[string]interface{}{
"errors": errorResponse,
})
return
}
// Processar dados válidos
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]string{
"message": "Usuário criado com sucesso",
"name": req.Name,
})
}
func main() {
http.HandleFunc("/users", createUserHandler)
http.ListenAndServe(":8080", nil)
}
Aqui, quando um POST chega em /users, o handler decodifica o JSON, valida a estrutura contra as tags, e retorna erros específicos para cada campo inválido. Isso é muito mais eficiente que validações manuais com dozens de if statements.
Tags de Validação Mais Comuns
As tags mais utilizadas no desenvolvimento real são:
required: Campo obrigatórioemail: Formato válido de emailmin=Xemax=X: Tamanho mínimo e máximo de string ou slicegt=Xelt=X: Greater than e less than para númerosgte=Xelte=X: Greater than or equal, less than or equalurl: Deve ser uma URL válidauuid: Deve ser um UUID válidonumeric: Apenas caracteres numéricosoneof=X Y Z: Valor deve ser um de X, Y ou Z
type Product struct {
ID string `validate:"required,uuid"`
Name string `validate:"required,min=1,max=255"`
Price float64 `validate:"required,gt=0"`
Status string `validate:"required,oneof=active inactive archived"`
Website string `validate:"omitempty,url"`
SKU string `validate:"numeric"`
}
Validações Avançadas e Customizadas
Validadores Customizados
O poder real da biblioteca emerge quando você precisa de lógicas de validação específicas ao seu domínio. Você pode registrar validadores customizados antes de usar o validator:
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
"regexp"
)
var validate = validator.New()
// Registrar validador customizado
func init() {
validate.RegisterValidation("cpf", validateCPF)
validate.RegisterValidation("strongpassword", validateStrongPassword)
}
// Validador para CPF (lógica simplificada)
func validateCPF(fl validator.FieldLevel) bool {
cpf := fl.Field().String()
// Verificar se tem 11 dígitos
if len(cpf) != 11 {
return false
}
// Verificar se não é sequência repetida
for i := 0; i < 10; i++ {
if cpf == string(rune(cpf[0])) {
return false
}
}
return true
}
// Validador para senha forte
func validateStrongPassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
hasUpper := regexp.MustCompile("[A-Z]").MatchString(password)
hasLower := regexp.MustCompile("[a-z]").MatchString(password)
hasDigit := regexp.MustCompile("[0-9]").MatchString(password)
hasSpecial := regexp.MustCompile("[!@#$%^&*]").MatchString(password)
return hasUpper && hasLower && hasDigit && hasSpecial && len(password) >= 12
}
type BrazilianUser struct {
Name string `validate:"required,min=3"`
CPF string `validate:"required,cpf"`
Password string `validate:"required,strongpassword"`
}
func main() {
user := BrazilianUser{
Name: "João Silva",
CPF: "12345678900",
Password: "MyP@ssw0rd123",
}
if err := validate.Struct(user); err != nil {
fmt.Println("Validação falhou:", err)
} else {
fmt.Println("Usuário válido!")
}
}
Validação com Condições (Cross-Field)
Às vezes, a validação de um campo depende do valor de outro campo. Use a tag eqfield ou registre validadores que acessem múltiplos campos:
type PasswordReset struct {
NewPassword string `validate:"required,min=8"`
ConfirmPassword string `validate:"required,eqfield=NewPassword"`
}
type DateRange struct {
StartDate string `validate:"required,datetime=2006-01-02"`
EndDate string `validate:"required,datetime=2006-01-02"`
}
Na estrutura PasswordReset, o ConfirmPassword deve ser exatamente igual ao NewPassword. Isso é validação cross-field integrada. Para lógicas mais complexas, você cria validadores customizados que recebem a struct inteira:
validate.RegisterStructValidation(func(sl validator.StructLevel) {
obj := sl.Current().Interface().(DateRange)
if obj.EndDate <= obj.StartDate {
sl.ReportError(obj.EndDate, "EndDate", "enddate", "enddate", "")
}
}, DateRange{})
Boas Práticas e Padrões em Produção
Criando um Middleware de Validação
Em aplicações maiores, você quer reutilizar a lógica de validação. Um middleware genérico reduz repetição:
package middleware
import (
"encoding/json"
"net/http"
"github.com/go-playground/validator/v10"
)
var validate = validator.New()
func ValidateJSON(expectedType interface{}) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Criar nova instância do tipo esperado
ptr := reflect.New(reflect.TypeOf(expectedType)).Interface()
if err := json.NewDecoder(r.Body).Decode(ptr); err != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"error": "JSON inválido",
})
return
}
if err := validate.Struct(ptr); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]interface{}{
"errors": formatValidationErrors(err),
})
return
}
// Passar dados validados via context
ctx := context.WithValue(r.Context(), "validated", ptr)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func formatValidationErrors(err error) map[string]string {
result := make(map[string]string)
for _, err := range err.(validator.ValidationErrors) {
result[err.Field()] = err.Tag()
}
return result
}
Mensagens de Erro Amigáveis
Erros técnicos (tag validation failed) não são úteis para clientes. Implemente um tradutor de mensagens:
package validation
import "github.com/go-playground/validator/v10"
var fieldMessages = map[string]map[string]string{
"Email": {
"required": "O email é obrigatório",
"email": "O email deve ser válido",
},
"Age": {
"required": "A idade é obrigatória",
"min": "A idade mínima é 18 anos",
"max": "A idade máxima é 120 anos",
},
}
func GetErrorMessage(err validator.FieldError) string {
field := err.Field()
tag := err.Tag()
if messages, exists := fieldMessages[field]; exists {
if msg, exists := messages[tag]; exists {
return msg
}
}
return "Campo inválido: " + field
}
Agora seus erros são legíveis pelo cliente:
validationErrors := err.(validator.ValidationErrors)
for _, err := range validationErrors {
fmt.Println(GetErrorMessage(err))
// Saída: "O email deve ser válido"
}
Conclusão
Você aprendeu três pontos essenciais sobre validação em Go:
-
Validação declarativa via struct tags é mais eficiente que validação imperativa: A abordagem do
go-playground/validatorpermite descrever regras uma única vez e reutilizá-las em toda a aplicação, reduzindo código duplicado e tornando mudanças triviais. -
Validadores customizados resolvem lógicas de domínio específicas: CPF, senhas fortes, datas futuras — você registra uma vez e usa em qualquer struct, mantendo código DRY e testável.
-
Integração em middleware padroniza validação em APIs: Ao encapsular validação em middleware ou handlers reutilizáveis, você garante consistência, reduz bugs e deixa handlers focados em lógica de negócio, não em validação repetitiva.
A chave para usar bem essa biblioteca é não apenas aplicar tags indiscriminadamente, mas pensar em quais regras definem a integridade dos seus dados e registrá-las uma vez de forma clara.