Контекст: Во всех хендлерах (places.go, auth.go, services.go и др.) валидация входящих данных выполняется через go-playground/validator с struct tags и кастомными валидаторами. Обновлено 2026-06 — код уже использует go-playground/validator, а не ручную валидацию, как было указано в первой версии заметки.
В проекте реализован единый слой валидации в backend/internal/validator/validator.go:
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.ValidationErrorsvalidator/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. См. раздел "Улучшения".
Регистрация переводов — вместо 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 },
)
Структурированный ответ (RFC 7807):
{
"type": "https://api.photoplaces.dev/errors/validation",
"title": "Validation Failed",
"status": 422,
"errors": [
{ "field": "email", "message": "invalid email format" }
]
}
Cross-field validation — gtfield, ltefield для зависимых полей (например, start_time < end_time)
Пайплайны — RegisterStructCtx для кастомной логики после базовой валидации
AppEnvCode review PhotoPlaces 2026-06. Актуализация — go-playground/validator уже внедрён во все хендлеры.
#backend #validation #go #best-practice