---
title: "ValidationErrors: что именно сломалось"
description: "validationerrors go под микроскопом: errors.As, методы FieldError — Field(), Tag(), Param(), Value(), Namespace() — и понятный ответ 400 вместо Key: '...'."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/05-oshibki-validacii/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# ValidationErrors: что именно сломалось

*Курс «Валидация в Go: go-playground/validator», урок 5 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/validator/lessons/05-oshibki-validacii/*

После провала правил validate.Struct возвращает не абстрактную ошибку, а структуру с полным описанием: validationerrors go хранит имя поля, споткнувшийся тег и его параметр — достаточно, чтобы ответить клиенту по-человечески. В этом уроке разбираем ValidationErrors и FieldError по методам: errors.As, Field(), Tag(), Param(), Value(), Namespace() — и собираем понятный ответ 400 вместо английской простыни Key: '...'.

Вот эту строку вы видели в уроке про первый запуск: Key: 'SignupForm.Password' Error:Field validation for 'Password' failed on the 'min' tag. Английская, подробная и совершенно непригодная для пользователя — но присмотритесь, сколько в ней информации: имя структуры, имя поля, провалившийся тег. Всё это валидатор складывает в структуру ошибки, и сегодня мы научимся доставать части по отдельности: тип ValidationErrors, каждый его элемент FieldError, методы Field, Tag, Param и компания. К концу урока простыня Key: '...' превратится в сырьё для понятных ответов 400, а полная система сообщений на русском станет темой шестого урока. Умение читать структуру ошибки — это навык, который отделяет валидацию «как в туториале» от валидации, за которую не стыдно на ревью.

### validationerrors go: два типа ошибок из Struct

err, который возвращает validate.Struct, — один из двух типов, и различать их нужно на уровне кода. Первый — validator.ValidationErrors: срез FieldError, по записи на каждое провалившееся поле; это нормальный рабочий сценарий — данные не прошли правила, отвечаем 400 по полям. Второй — *validator.InvalidValidationError: валидатору передали не структуру — nil, число, срез; это ошибка программиста, и её место в логах или панике на старте, а не в ответе клиенту. Оба типа — конкретные значения, реализующие error, поэтому достают их через errors.As с указателем на нужный тип: это безопаснее прямого приведения типов, потому что err может быть завёрнут другим слоем. Порядок веток имеет значение по соображениям читаемости: сначала узкая InvalidValidationError, потом широкая ValidationErrors — формально они не пересекаются, но такой порядок читается как чек-лист: баг кода, отказ данных, всё остальное. Стандартный паттерн — в блоке ниже.

### Две ветки errors.As — канонический разбор

```go
err := validate.Struct(&form)
if err != nil {
        var invalid *validator.InvalidValidationError
        if errors.As(err, &invalid) {
                // не структура: баг кода, не данные
                panic(fmt.Sprintf("validate.Struct: %v", invalid))
        }

        var verrs validator.ValidationErrors
        if errors.As(err, &verrs) {
                for _, e := range verrs {
                        fmt.Println(e.Field(), e.Tag(), e.Param())
                }
                // Password min 8
                return
        }
        // всё остальное — неожиданно, общий обработчик
}
```

Две ветки errors.As по типам: InvalidValidationError — не-структура на входе, ValidationErrors — провал правил. В цикле по verrs каждая запись отдаёт имя поля, тег и параметр тремя методами; для Password с тегом min=8 цикл напечатает Password min 8. Всё остальное в err считайте неожиданным и уходите в общий обработчик ошибок.

### validator field tag: методы FieldError

FieldError — маленькая, но говорящая структура. Field() возвращает имя поля структуры: Password; для элементов среза после dive — имя с индексом, вроде Tags[2]. Tag() — тег, на котором проверка споткнулась: required, min, email. Param() — параметр этого тега строкой: «8» для min=8, «admin user» для oneof, пустая строка для required — у него параметров нет. Value() — само значение поля, которое не прошло: удобно для логов, но не для ответа клиенту. Namespace() — полный путь с точками: SignupForm.Address.City; он оживёт в уроке про вложенные структуры, а пока запомните сам факт. ActualTag() возвращает реальный тег без алиасов — на простых тегах совпадает с Tag(), разница появляется с RegisterAlias, когда одному правилу придумывают своё имя. Kind() и Type() — reflect-категория и тип поля, если хочется ветвиться по ним: строка это или число, int или time.Time; так, каталог сообщений может для числовых полей говорить «минимум N», а для строковых — «минимум N символов», не заводя отдельные теги. И метод Error(), который собирает ту самую простыню Key: '...' — теперь понятно, из каких частей. Для сообщений на русском нужны первые три — Field, Tag и Param — плюс словарь; остальное пригодится в специфических сценариях. И ещё разница, которую полезно понимать заранее: Field() и Namespace() отдают имена по настоящим полям структуры, без переименований по json-меткам — валидатор про сериализацию не знает, у него свой взгляд на имена; будем помнить при маппинге.

### Методы FieldError на одном примере

| Метод | Что возвращает | Пример для Password при min=8 |
| --- | --- | --- |
| Field() | имя поля структуры | Password |
| Tag() | тег, на котором провалилась проверка | min |
| Param() | параметр тега строкой | 8 |
| Value() | значение поля, которое не прошло | abc |
| Namespace() | полный путь через точки | SignupForm.Password |
| ActualTag() | тег без алиасов | min |
| Error() | собранная строка Key: '...' Error:... | Key: 'SignupForm.Password' Error:... |

### Проверка: Param у oneof

**Вопрос:** Role string с тегом oneof=admin user получил значение root. Что вернёт e.Param()?

- ✅ admin user — перечень допустимых значений
- oneof — имя тега
- root — провалившееся значение
- Пустую строку — у oneof нет параметров

Param() — параметр споткнувшегося тега: у oneof это перечень допустимых значений, «admin user». Имя тега — Tag(), провалившееся значение — Value(). Комбинация Tag() и Param() позволяет одному сообщению «допустимые значения: %s» обслужить все oneof-поля проекта разом.

### Первый провалившийся тег побеждает

Важное свойство исполнения: поле проверяется по правилам слева направо, и первый провалившийся тег завершает проверку этого поля. Password string с тегом required,min=8 и значением «» вернёт одну запись с тегом required — до min дело не дойдёт, пустая строка уже провалила первый тег. Из этого два следствия. Первое: не рассчитывайте увидеть в ValidationErrors все причины по одному полю — полный список причин по полю валидатор не соберёт; если бизнесу нужен пошаговый диалог, проверяйте повторно после каждого исправления или делите форму на шаги, где каждая валидация отвечает только про текущий шаг. Второе: порядок правил в теге управляет тем, какое сообщение получит пользователь: required первым даёт «укажите поле», а min первым — «слишком коротко» даже для пустой строки. В курсе мы ставим required первым — это договорённость между тегом и текстом подсказки, которая делает сообщения предсказуемыми. На практике из этого свойства вырастает целая дисциплина UX: длинная форма с десятью ошибками не должна возвращать все десять, если девять из них — следствия одной пустой строки; пошаговые формы с валидацией текущего шага дают клиенту короткий список и держат сообщения осмысленными. А если продукт требует показать все проблемы разом, собирайте их отдельным проходом по полям — правила слева направо это механика валидатора, а не закон интерфейса. При этом сами поля независимы: три сломанных поля дадут три записи, и клиент получит весь список разом — это и есть преимущество собирающей валидации перед стоп-на-первой-ошибке. И последнее свойство, которое стоит прочувствовать: порядок записей в срезе следует порядку полей структуры — Password раньше Age. Не полагайтесь на это в логике ответа, но для чтения логов знание полезное: запись с незнакомым полем значит, что смотрят не ту версию структуры.

### Сколько записей и какие теги

```go
type Profile struct {
        Password string `validate:"required,min=8"`
        Age      int    `validate:"gte=0"`
}

p := Profile{Password: "", Age: -1}
err := validate.Struct(&p)
// сколько записей в ValidationErrors и какие теги?
```

**Что выведет программа?**

- Одна запись: тег required — валидатор остановился на первом поле
- ✅ Две записи: Password по required, Age по gte=0
- Одна запись: тег min — валидатор проверяет последнее правило
- Ни одной: пустой Password и отрицательный Age пройдут

На каждом поле проверка останавливается на первом провалившемся теге — Password вернёт required, а не min, — но поля независимы: Age с −1 провалит gte=0 и даст вторую запись. Итог — две записи FieldError, по одной на поле; это же поведение predict-блока первого урока, теперь вы знаете его механику.

### Мини-каталог: от тега к тексту

```go
func fieldMessage(e validator.FieldError) string {
        switch e.Tag() {
        case "required":
                return "обязательное поле"
        case "min":
                return fmt.Sprintf("минимум %s символов", e.Param())
        case "max":
                return fmt.Sprintf("максимум %s символов", e.Param())
        case "email":
                return "укажите адрес почты"
        default:
                return e.Field() + ": проверка " + e.ActualTag() + " не прошла"
        }
}
```

Мини-каталог: текст подбирается по Tag(), параметр тега подставляется в сообщение — минимум 8 символов. Ветка default с ActualTag() — страховка: незнакомый тег не вернёт пустоту, а даст честную техническую строку, по которой вы узнаете о пропущенном кейсе. Полная система с русскими текстами и уточнениями по полям — тема шестого урока.

> **Field() — имя поля структуры, а не JSON.** Field() возвращает имя поля СТРУКТУРЫ, а фронтенд знает JSON-имя. Поле Password string с json:"password" в ошибке — «Password», а клиент подсвечивает поля по «password». На camelCase-проектах это совпадает с точностью до регистра; на snake_case-фронтах не совпадает вовсе, и подсветка молча не срабатывает. Решения: маппинг структурных имён в JSON через reflect, отправка пути Namespace(), который фронт умеет разбирать, или конвенция одинаковых имён. Классический симптом непочиненного маппинга: 400 приходит, текст правильный, а инпут не подсвечен — а причина в первой букве.

### InvalidValidationError: когда сломался код

*InvalidValidationError заслуживает отдельного блока, потому что его появление почти всегда означает баг в вашем коде, а не в данных. Валидатор возвращает его, когда в Struct передали nil, не-указатель, указатель на не-структуру. Типичные сценарии из жизни: функция принимает any и доходит до validate.Struct(data) с числом внутри; тест передаёт nil «а вдруг сработает»; рефакторинг сменил сигнатуру, и вместо структуры поехала ошибка предыдущего шага. Обращение однообразное: это не пользовательская история — логируйте с контекстом или паникуйте на старте, чтобы баг всплыл при первом же прогоне, а не жил годами в тёмной ветке. Практика с паникой на старте выглядит жёстко, но у неё есть обоснование: InvalidValidationError возможен только там, где потерян тип данных — после рефакторинга или в generic-обёртке; такие места всплывают на первом интеграционном тесте, и лучше упасть в тестовом окружении с внятной причиной, чем молча пропускать проверку в проде. Если паника в вашем сервисе недопустима, логируйте на уровне error и возвращайте 500 — главное, не превращайте её в 400: пользователь не виноват в том, что код передал валидатору не структуру. Отсюда же правило для обёрток: функция-помощник validateOf(data any) error обязана различать оба типа и документировать, что InvalidValidationError наружу не отдаётся. В тестах правил — отдельный урок про тесты — InvalidValidationError проверяют явно: подают nil и ожидают именно этот тип, а не ValidationErrors; это фиксирует контракт проверки в коде.

### Проверка: что вернёт Error()

**Вопрос:** Что вернёт err.Error() у ValidationErrors с тремя записями?

- Первую строку Key: '...' — только первую запись
- ✅ Три строки, склеенные переводами строк: по строке на запись
- JSON-массив сообщений
- Пустую строку: Error() есть только у FieldError

ValidationErrors реализует error, склеивая Error() каждой записи переводами строк — поэтому печать err даёт простыню из трёх блоков Key: '...'. Для ответа клиенту эту склейку не используют, но в логе она удобна: весь список провалов одним значением.

> **err.Error() в логе, не в ответе.** Простыня Key: 'SignupForm.Password' Error:... не годится клиенту, но хороша в журнале: одна строка с полным списком провалов делает запись о 400 самодостаточной для расследования. Компромисс, который работает: клиенту — Details с понятными сообщениями, в лог — err.Error() целиком. Главное — не наоборот.

### От простыни к конверту 400

Соберём практику урока в принципы ответа клиенту. Не отдавайте err.Error() как есть: английская простыня, утечка внутренних имён полей и структуры — и всё это вместо двух слов по-русски. Не отдавайте и Value() провалившихся значений: в ошибке уедет то, что ввёл пользователь, включая пароли и паспорта; значение — для логов, и не для всех полей. Отвечайте списком Details: по записи на каждое сломанное поле, собранной из Field, Tag, Param и словаря сообщений, — так фронтенд подсвечивает инпуты один к одному. Этот конверт — тема двух уроков: [шестой](/courses/validator/lessons/06-russkie-soobshcheniya/) соберёт каталог русских текстов, [двадцать четвёртый](/courses/validator/lessons/24-soobshcheniya-api/) оформит единый формат ошибок API с кодами и полем. А если вы работаете в Gin, тот же паттерн уже разобран в [двенадцатом уроке](/courses/validator/lessons/12-validator-i-gin/) — с тегами binding и ShouldBindJSON; механика FieldError при этом идентична. Практическая проверка принципов простая: откройте любой свой ответ 400 и посчитайте, сколько в нём утечёк из прошлого — технических имён, английских тегов, значений полей. Каждая утечка — это ровно то, что мы сегодня разложили по методам.

### Что вытащить из FieldError для ответа

- Field() — имя поля для маппинга в JSON-имя и подсветки инпута
- Tag() — ключ каталога сообщений: один текст на все поля с этим тегом
- Param() — параметр тега: граница диапазона или перечень oneof подставляются в текст
- Value() — провалившееся значение: в лог, но не в ответ клиенту
- Namespace() — полный путь через точки: понадобится во вложенных структурах

### Соберите разбор ошибок

Восстановите тип ошибки, errors.As и два метода FieldError — четыре пропуска.

```go
var verrs validator.___
if errors.___(err, &verrs) {
        for _, e := range verrs {
                switch e.___() {
                case "min":
                        fmt.Printf("минимум %s символов", e.___())
                }
        }
}
```

Подсказка: Тип среза ошибок — ValidationErrors; достаёт его As; ключ каталога — Tag(); параметр тега — Param().

Правильные ответы в пропусках: `ValidationErrors`, `As`, `Tag`, `Param`

### Соберите путь ошибки в ответ 400

От err до Details в ответе — расставьте шаги по порядку.

Правильный порядок строк:

1. `validate.Struct(&form) вернул err не nil`
2. `errors.As достаёт validator.ValidationErrors`
3. `Цикл по записям: каждая — FieldError`
4. `Field() даёт имя поля, Tag() — споткнувшийся тег`
5. `Param() подставляет границу или перечень в текст`
6. `Каталог сообщений возвращает русский текст по тегу`
7. `Details собираются в конверт 400`
8. `err.Error() целиком уходит в лог для расследования`

### Карточки: анатомия ошибки

- **ValidationErrors** — Срез FieldError, по записи на каждое провалившееся поле. Реализует error; Error() склеивает записи переводами строк.
- **FieldError** — Запись об одном провале: поле, тег, параметр, значение. Достаётся из err через errors.As на validator.ValidationErrors.
- **Field()** — Имя поля структуры (после dive — с индексом, Tags[2]). Не JSON-имя: для подсветки на фронте нужен маппинг.
- **Tag() и Param()** — Tag() — споткнувшийся тег; Param() — его параметр строкой: «8» у min=8, перечень у oneof, пусто у required.
- **InvalidValidationError** — Валидатору передали не структуру: nil, число, срез. Ошибка программиста — лог или panic, не клиенту.
- **Namespace()** — Полный путь поля через точки: SignupForm.Address.City. Оживёт на вложенных структурах, где Field() даёт только последний сегмент.

> **Пять методов на одной строке вывода.** Программу урока соберите локально — песочница сайта валидатор не исполняет. Самая полезная тренировка: напечатать Field, Tag, Param, Value и Namespace одной строкой на живом провале. Когда увидите их на настоящей ошибке, а не в таблице, каталог сообщений соберётся почти сам.

Шестой урок — [русские сообщения](/courses/validator/lessons/06-russkie-soobshcheniya/): двухуровневый каталог, уточнения по полям и согласование по числам. [Двенадцатый](/courses/validator/lessons/12-validator-i-gin/) — тот же механизм в Gin с тегами binding и ShouldBindJSON. [Двадцать четвёртый](/courses/validator/lessons/24-soobshcheniya-api/) — единый формат ошибок API с кодами и Details. А соседний разбор из курса Gin — [валидация в Gin](/courses/gin/lessons/09-validaciya/) — покажет, как тот же FieldError живёт внутри фреймворка и собирает Envelope-конверт с кодом VALIDATION_FAILED: синтаксис там отличается только словарём тегов.

## Коротко: что запомнить

- Struct возвращает один из двух типов: ValidationErrors — отказ правил, InvalidValidationError — не-структура на входе.
- Оба типа достаются errors.As с указателем на тип; сначала узкая ветка про баг кода, потом рабочая про данные.
- Field() — имя поля структуры, Tag() — споткнувшийся тег, Param() — его параметр строкой; этого хватает для каталога сообщений.
- Первый провалившийся тег завершает проверку поля: Password с «» даст required, а не min.
- Поля независимы: три сломанных поля — три записи в ValidationErrors, клиент получает список разом.
- Field() — не JSON-имя: для подсветки полей на фронте нужен маппинг или Namespace().
- err.Error() — для лога; клиенту едут Details из каталога сообщений, Value() наружу не отдаётся.

## Домашнее задание

Возьмите форму из прошлых уроков с тремя полями и сломайте её двумя способами: пустыми значениями и значениями вне диапазона. Напечатайте для каждого случая все пять методов FieldError — Field, Tag, Param, Value, Namespace — и убедитесь, что у Password с пустой строкой тег required, а не min. Потом напишите fieldMessage с ветками на required, min и max и соберите ответ, где для каждой записи есть русское сообщение.

## Вопросы и ответы по уроку

### Почему в ошибке тег required, хотя в поле стоит и min?

Поле проверяется по правилам слева направо до первого провала. Пустая строка провалила required, и до min дело не дошло. Исправьте required — следующая проверка ответит уже про min. Это же свойство используют тесты: подавая заведомо пустые данные, ожидайте required, а не диапазон, — так контракт правил фиксируется в коде.

### Как отличить ошибку данных от бага кода?

По типу: validator.ValidationErrors — нормальный отказ правил, данные не прошли; *validator.InvalidValidationError — валидатору передали не структуру, это баг. Разводите их двумя ветками errors.As и обрабатывайте по-разному.

### Можно ли отправлять клиенту Value() провалившегося поля?

Не стоит: в значении может быть пароль, телефон или что-то конфиденциальное. Value() — инструмент для логов, причём не для всех полей; клиенту едут Field, Tag и Param, из которых собираются понятные сообщения без утечек.
