Go Admin

Validação de Dados em APIs Go com go-playground/validator: Do Básico ao Avançado Já leu

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 é 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 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

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ório
  • email: Formato válido de email
  • min=X e max=X: Tamanho mínimo e máximo de string ou slice
  • gt=X e lt=X: Greater than e less than para números
  • gte=X e lte=X: Greater than or equal, less than or equal
  • url: Deve ser uma URL válida
  • uuid: Deve ser um UUID válido
  • numeric: Apenas caracteres numéricos
  • oneof=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:

  1. Validação declarativa via struct tags é mais eficiente que validação imperativa: A abordagem do go-playground/validator permite descrever regras uma única vez e reutilizá-las em toda a aplicação, reduzindo código duplicado e tornando mudanças triviais.

  2. 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.

  3. 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.

Referências


Artigos relacionados