# Backend: Валидация входящих данных **Контекст**: Во всех handlers (`places.go`, `services.go`, `auth.go` и др.) валидация делается вручную через `if req.Field == ""` проверки. Нет единой схемы валидации, нет структурированных ошибок. ## Суть **Ручная валидация — не масштабируема и ошибочна**: - Дублирование кода проверок - Неконсистентные сообщения об ошибках - Нет валидации типов, форматов (email, URL, UUID, координаты) - Сложно поддерживать и тестировать ## Код (проблемное место) ```go // backend/internal/handlers/places.go:120-147 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") return } if req.Title == "" { // Ручная проверка writeError(w, http.StatusBadRequest, "title is required") return } if req.Type == "" { // Ручная проверка writeError(w, http.StatusBadRequest, "type is required") return } // ... нет валидации: lat/lng range, URL формат, enum для type, длина строк } ``` ## Решение: go-playground/validator v10 Стандарт де-факто в Go экосистеме. Поддерживает: - Struct tags: `validate:"required,email,max=255"` - Custom validators - Перевод ошибок - Вложенные структуры ### Пример внедрения: ```go // internal/validator/validator.go package validator import ( "github.com/go-playground/validator/v10" ) var Validate *validator.Validate func init() { Validate = validator.New() // Регистрация кастомных валидаторов Validate.RegisterValidation("coordinate", validateCoordinate) } func validateCoordinate(fl validator.FieldLevel) bool { lat := fl.Field().Float() return lat >= -90 && lat <= 90 } ``` ```go // internal/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,coordinate,min=-90,max=90"` Lng float64 `json:"lng" validate:"required,coordinate,min=-180,max=180"` Type string `json:"type" validate:"required,oneof=place studio"` Tags []string `json:"tags" validate:"dive,required,alphanum"` Features []string `json:"features" validate:"dive,required,alphanum"` HourlyRate *int `json:"hourly_rate" validate:"omitempty,min=0"` Currency string `json:"currency" validate:"omitempty,len=3,uppercase"` MinHours int `json:"min_hours" validate:"min=0"` } ``` ```go // В handler: 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") return } if err := validator.Validate.Struct(req); err != nil { var ve validator.ValidationErrors if errors.As(err, &ve) { writeValidationError(w, ve) // Структурированный ответ return } writeError(w, http.StatusBadRequest, err.Error()) return } // ... } ``` ## Структурированный ответ об ошибках (RFC 7807 / Problem Details) ```json { "type": "https://api.photoplaces.ru/errors/validation-error", "title": "Validation Failed", "status": 422, "detail": "One or more fields failed validation", "errors": [ { "field": "title", "message": "title is required" }, { "field": "lat", "message": "lat must be between -90 and 90" } ] } ``` ## Альтернативы | Библиотека | Плюсы | Минусы | |------------|-------|--------| | **go-playground/validator (рекомендую)** | Стандарт, быстрый, теги, кастомные правила | Рефлексия (небольшой оверхед) | | **go-ozzo/ozzo-validation** | Функциональный стиль, без рефлексии | Менее популярный | | **Ручная (текущее)** | Нет зависимостей | Неподдерживаемо, баги | ## Связанные заметки - [[decision-validation-library]] - [[backend-auth-security]] - [[architecture-overview]] ## Источник Задача: Code review PhotoPlaces — отсутствие валидации входных данных ## Теги #backend #validation #go #best-practice #technical-debt