# 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`) ```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` | ### Использование в хендлерах ```go // 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`) ```go 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, }) } ``` **Ответ**: ```json { "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 → человекочитаемое сообщение: ```go 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()`: ```go 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)**: ```json { "type": "https://api.photoplaces.dev/errors/validation", "title": "Validation Failed", "status": 422, "errors": [ { "field": "email", "message": "invalid email format" } ] } ``` 3. **Cross-field validation** — `gtfield`, `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