backend-validation.md 7.3 KB

Backend: Валидация — go-playground/validator + кастомные правила

Контекст: Во всех хендлерах (places.go, auth.go, services.go и др.) валидация входящих данных выполняется через go-playground/validator с struct tags и кастомными валидаторами. Обновлено 2026-06 — код уже использует go-playground/validator, а не ручную валидацию, как было указано в первой версии заметки.

Суть

В проекте реализован единый слой валидации в backend/internal/validator/validator.go:

  • Struct tags: validate:"required,email,max=255"
  • Кастомные валидаторы: uuid, slug, latitude, longitude, place_type, user_role, currency, datetime и др.
  • Функции: Validate(s interface{}) и ValidateVar(field interface{}, tag string)
  • Алиас: ValidationErrors = validator.ValidationErrors

Реализация

Пакет валидатора (validator/validator.go)

var validate *validator.Validate

func init() {
    validate = validator.New()

    _ = validate.RegisterValidation("uuid", validateUUID)
    _ = validate.RegisterValidation("slug", validateSlug)
    _ = validate.RegisterValidation("latitude", validateLatitude)
    _ = validate.RegisterValidation("longitude", validateLongitude)
    _ = validate.RegisterValidation("place_type", validatePlaceType)
    _ = validate.RegisterValidation("user_role", validateUserRole)
    _ = validate.RegisterValidation("currency", validateCurrency)
    _ = validate.RegisterValidation("user_status", validateUserStatus)
    _ = validate.RegisterValidation("place_status", validatePlaceStatus)
    _ = validate.RegisterValidation("service_status", validateServiceStatus)
    _ = validate.RegisterValidation("booking_status", validateBookingStatus)
    _ = validate.RegisterValidation("datetime", validateDateTime)
}

func Validate(s interface{}) error { return validate.Struct(s) }
func ValidateVar(field interface{}, tag string) error { return validate.Var(field, tag) }

Кастомные валидаторы

Валидатор Формат Пример
uuid ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ 550e8400-e29b-41d4-a716-446655440000
slug ^[a-z0-9-]+$ portrait-photography
latitude >= -90 && <= 90 55.7558
longitude >= -180 && <= 180 37.6173
place_type place или studio
user_role customer, landlord, executor, moderator, superadmin
currency ^[A-Z]{3}$ RUB, USD
datetime time.Parse(time.RFC3339) 2026-06-24T12:00:00Z

Использование в хендлерах

// handlers/places.go — 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,latitude"`
    Lng         float64  `json:"lng" validate:"required,longitude"`
    Type        string   `json:"type" validate:"required,place_type"`
    AccessInfo  *string  `json:"access_info" validate:"omitempty,max=2000"`
    CoverImage  *string  `json:"cover_image" validate:"omitempty,url,max=500"`
    Tags        []string `json:"tags" validate:"dive,slug,max=50"`
    Features    []string `json:"features" validate:"dive,slug,max=50"`
    HourlyRate  *int     `json:"hourly_rate" validate:"omitempty,min=0"`
    Currency    string   `json:"currency" validate:"omitempty,currency,len=3"`
    MinHours    int      `json:"min_hours" validate:"min=0,max=100"`
}

func (h *PlaceHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req createPlaceRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid request body", err)
        return
    }
    if err := validator.Validate(req); err != nil {
        writeValidationError(w, err) // 422 Unprocessable Entity
        return
    }
    // ... бизнес-логика
}

Обработка ошибок валидации

writeValidationError (handlers/errors.go:39-48)

func writeValidationError(w http.ResponseWriter, err error) {
    details := err.Error()
    if AppEnv == "production" {
        details = "validation failed" // не раскрываем детали в production
    }
    writeJSON(w, http.StatusUnprocessableEntity, map[string]interface{}{
        "error":   "validation failed",
        "details": details,
    })
}

Ответ:

{
  "error": "validation failed",
  "details": "Key: 'createPlaceRequest.Title' Error:Field validation for 'Title' failed on the 'required' tag"
}

В production details заменяется на "validation failed" — защита от утечки структуры данных.

validationMessage (handlers/auth.go:195-221)

Отдельная функция для маппинга validation tag → человекочитаемое сообщение:

func validationMessage(tag, param string) string {
    switch tag {
    case "required": return "field is required"
    case "email":    return "invalid email format"
    case "latitude": return "latitude must be between -90 and 90"
    // ...
    }
}

⚠️ Недостаток: это дублирование логики go-playground/validator. Лучше использовать RegisterTranslation() вместо ручного switch. См. раздел "Улучшения".

Улучшения (Todo)

  1. Регистрация переводов — вместо validationMessage использовать RegisterTranslation():

    validate.RegisterTranslation("required", trans, 
       func(ut ut.Translator) error { return ut.Add("required", "{0} обязателен", true) },
       func(ut ut.Translator, fe validator.FieldError) string { t, _ := ut.T("required", fe.Field()); return t },
    )
    
  2. Структурированный ответ (RFC 7807):

    {
     "type": "https://api.photoplaces.dev/errors/validation",
     "title": "Validation Failed",
     "status": 422,
     "errors": [
       { "field": "email", "message": "invalid email format" }
     ]
    }
    
  3. Cross-field validationgtfield, ltefield для зависимых полей (например, start_time < end_time)

  4. ПайплайныRegisterStructCtx для кастомной логики после базовой валидации

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

  • [[decision-validation-library]] — Decision record: почему go-playground/validator
  • [[decision-validation-error-production]] — Скрытие деталей ошибок
  • [[architecture-overview]] — Общий обзор
  • [[atomic-global-appenv-package-var]] — Проблема глобальной AppEnv

Источник

Code review PhotoPlaces 2026-06. Актуализация — go-playground/validator уже внедрён во все хендлеры.

Теги

#backend #validation #go #best-practice