decision-validation-library.md 4.2 KB

Decision Record: Validation Library — go-playground/validator

Дата: 2026-06-11 Статус: Accepted (требует реализации)

Контекст

Валидация во всех handlers делается вручную через if req.Field == "" проверки. Нет единой схемы, неконсистентные ошибки, нет валидации форматов.

Решение

Использовать github.com/go-playground/validator/v10 — стандарт де-факто в Go.

Почему не альтернативы:

Библиотека Причина отказа
go-ozzo/ozzo-validation Функциональный стиль, менее популярный, меньше примеров
Ручная валидация Неподдерживаемо, баги, дублирование
CUE / JsonSchema Overengineering для REST API

План внедрения

  1. Добавить зависимость: go get github.com/go-playground/validator/v10
  2. Создать пакет internal/validator/ с инициализацией и кастомными правилами
  3. Пометить DTO тегами во всех handlers
  4. Добавить middleware/хелпер для единообразных ответов об ошибках (RFC 7807)
  5. Покрыть тестами кастомные валидаторы

Кастомные валидаторы (понадобятся)

// internal/validator/validator.go
func init() {
    Validate.RegisterValidation("coordinate", validateCoordinate)
    Validate.RegisterValidation("place_type", validatePlaceType)
    Validate.RegisterValidation("user_role", validateUserRole)
    Validate.RegisterValidation("currency", validateCurrency) // ISO 4217
}

func validateCoordinate(fl validator.FieldLevel) bool {
    v := fl.Field().Float()
    return v >= -90 && v <= 90 // для lat; для lng нужен отдельный или min/max
}

func validatePlaceType(fl validator.FieldLevel) bool {
    v := fl.Field().String()
    return v == "place" || v == "studio"
}

func validateUserRole(fl validator.FieldLevel) bool {
    roles := map[string]bool{"customer": true, "landlord": true, "executor": true, "moderator": true, "superadmin": true}
    return roles[fl.Field().String()]
}

Пример DTO с тегами

type CreatePlaceRequest struct {
    Title       string   `json:"title" validate:"required,min=1,max=255"`
    Description *string  `json:"description" validate:"omitempty,max=5000"`
    Address     *string  `json:"address" validate:"omitempty,max=500"`
    Lat         float64  `json:"lat" validate:"required,coordinate,min=-90,max=90"`
    Lng         float64  `json:"lng" validate:"required,coordinate,min=-180,max=180"`
    Type        string   `json:"type" validate:"required,place_type"`
    Tags        []string `json:"tags" validate:"dive,required,alphanum,max=50"`
    Features    []string `json:"features" validate:"dive,required,alphanum,max=50"`
    HourlyRate  *int     `json:"hourly_rate" validate:"omitempty,min=0"`
    Currency    string   `json:"currency" validate:"omitempty,len=3,uppercase,currency"`
    MinHours    int      `json:"min_hours" validate:"min=0"`
}

RFC 7807 Problem Details ответ

{
  "type": "https://api.photoplaces.ru/errors/validation-error",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields failed validation",
  "instance": "/api/v1/places",
  "errors": [
    { "field": "title", "message": "title is required" },
    { "field": "lat", "message": "lat must be between -90 and 90" }
  ]
}

Последствия

Положительные:

  • Декларативная валидация в DTO
  • Консистентные ошибки
  • Легко тестировать
  • Стандарт индустрии

Отрицательные:

  • Рефлексия (небольшой оверхед ~1-2µs)
  • Нужно обновить все handlers

Связанные заметки

  • [[backend-validation]]
  • [[architecture-overview]]

Теги

#decision-record #validation #go #best-practice