---
title: "Валидация в Gin: binding"
description: "gin binding validation: ShouldBindJSON и ShouldBindUri запускают validator v10 по тегам binding. ValidationErrors превращаем в честный 400 с деталями по полям."
course: "Валидация в Go: go-playground/validator"
url: "https://golangmaster.ru/courses/validator/lessons/12-validator-i-gin/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Валидация в Gin: binding

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

Gin binding validation спрятан в один вызов: ShouldBindJSON декодирует тело и сам запускает движок validator v10 по тегам binding, вам остаётся разобрать ошибку. В этом уроке открываем капот — binding.Validator, SetTagName и дверь Engine() — прогоняем параметры маршрута через ShouldBindUri и собираем конверт 400, в котором каждая запись ValidationErrors превращена в понятное сообщение по полю.

Gin — самый распространённый способ встретить validator в карьере: большая часть проектов на gin пользуется им, не устанавливая библиотеку руками. Теги binding мы видели в уроках курса Gin — пришло время совместить их со всем, что вы умеете из первых одиннадцати уроков: с dive и вложенными структурами, условными тегами, своими функциями и StructLevel. Механика одна и та же — тот же движок v10, те же ValidationErrors и FieldError, — различие в обвязке: gin держит валидатор внутри и переключает его словарь с validate на binding. Отсюда два практических навыка урока: писать хендлер, который за один вызов привязывает и проверяет тело, и превращать сырую ошибку валидации в ответ, который фронтенд сможет подсветить по полям. Побочно наведём порядок в семье ShouldBind-методов: тело, query, заголовки и параметры пути проверяются одинаково, но разными точками входа.

### gin binding validation: что внутри ShouldBindJSON

Вызов c.ShouldBindJSON(&dto) делает две вещи подряд: декодирует JSON-тело в структуру и прогоняет её через валидатор. Вторая половина — не магия gin, а наш validator v10: пакетная переменная binding.Validator указывает на реализацию интерфейса StructValidator из двух методов — ValidateStruct(obj any) error и Engine() any. Дефолтная реализация держит *validator.Validate, который создаётся лениво под sync.Once, и в тот же момент вызывает SetTagName("binding") — вот почему правила в gin-проектах пишутся в теге binding, а не validate: словарь движка переключён, и теги validate он молча не читает. Gin validator кэширует разбор структур так же, как «голый» валидатор из второго урока, — один экземпляр на всё приложение gin создаёт и греет сам. Errors по-прежнему идут типом validator.ValidationErrors, и доставать их положено errors.As с указателем на срез — ровно та дисциплина пятого урока. Два отличия, о которых стоит помнить заранее: стат ShouldBindJSON за вас никто не ставит — ошибку в 400 превращает ваш хендлер, и массив верхнего уровня gin валидирует собственным циклом с типом binding.SliceValidationError — про него есть подробный разбор в [уроке про валидацию курса Gin](/courses/gin/lessons/09-validaciya/). Пропускная способность у ValidateStruct широкая, но выборочная: nil вернёт nil, указатель на структуру уедет в движок, срез или массив верхнего уровня — в цикл по элементам, а всё остальное — карта, строка, число — пройдёт молча без ошибки: движок проверяет только структуры. Отсюда правило DTO-дизайна: на вход принимайте указатель на структуру — единственный вид, для которого binding вообще что-то проверяет.

### Хендлер регистрации: bind, разбор, конверт 400

```go
type SignupDTO struct {
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=8"`
        Age      int    `json:"age" binding:"omitempty,gte=0,lte=130"`
}

func signup(c *gin.Context) {
        var in SignupDTO
        if err := c.ShouldBindJSON(&in); err != nil {
                var verrs validator.ValidationErrors
                if errors.As(err, &verrs) {
                        c.JSON(http.StatusBadRequest, gin.H{
                                "errors": respondFields(verrs), // карта поле → текст
                        })
                        return
                }
                c.JSON(http.StatusBadRequest, gin.H{"error": "битое тело запроса"})
                return
        }
        // in проверен: дальше бизнес-слой
        c.JSON(http.StatusCreated, gin.H{"status": "ok"})
}
```

ShouldBindJSON вернул err — статус ставим сами: это честная точка решения, а не скрытый 400. Первая ветка errors.As ловит ValidationErrors и отдаёт карту по полям, собранную из Field(), Tag() и Param() каталогом из шестого урока. Всё остальное — битый JSON, не-JSON, переполнение типов: общая фраза без деталей, потому что данных для подсветки нет.

### Проверка: почему тег binding

**Вопрос:** В gin-проекте правила пишут в binding:"required", а не validate:"required". Почему?

- ✅ Дефолтный валидатор gin вызывает SetTagName("binding") при инициализации движка
- Тег validate в gin не поддерживается синтаксисом структур
- binding — алиас для validate, они равнозначны
- Тег validate читает только JSON-декодер

Словарь — настройка движка: внутри lazyinit под sync.Once gin делает validator.New() и SetTagName("binding"). После этого движок ищет правила только в теге binding; validate-теги остаются мёртвым текстом, и структура проходит проверку, как будто правил нет. Алиасов между словарями нет — оба тега одновременно движок не читает.

### ShouldBind против ShouldBindJSON: кто что читает

Семейство ShouldBind устроено как розетки одного валидатора: сначала раскладка, затем проверка. ShouldBind выбирает источник сам: у GET — query-параметры, у остальных методов — JSON-тело, поэтому DELETE при таком подходе обязан прислать тело, как POST; удобно для общих хендлеров, но в явных API точечные вызовы читаются честнее. ShouldBindJSON — только тело по Content-Type json. ShouldBindQuery разбирает query-строку по тегу form, ShouldBindHeader — заголовки по тегу header, ShouldBindUri — параметры маршрута по тегу uri. Рядом живут и служебные точки входа, о которых туториалы молчат: ShouldBindXML и ShouldBindYAML читают соответствующие форматы, а ShouldBindBodyForRepeatableRead буферизует тело и позволяет привязать его второй раз — например, когда после валидации нужно перечитать сырой JSON для лога или подписи. Все они заканчиваются одним и тем же: валидация по тегам binding тем самым движком, поэтому ошибки и обработка не различаются от ветки к ветке. Синтаксис правил — весь курс целиком: min, max, oneof, dive по срезам, условные required_if, ваши RegisterValidation и StructLevel — ничего переучивать не нужно. Единственный нюанс раскладки, который бьёт по валидации: конвертация строк в числа происходит при привязке, и «abc» для int-поля — ошибка декодера, а не провал тега; в ответе это тот же 400, но без записей ValidationErrors, и вашей ошибке-обёртке лучше об этом честно сказать клиенту.

### Семейство ShouldBind: источник, тег, проверка

| Метод | Источник данных | Тег раскладки | Валидация binding |
| --- | --- | --- | --- |
| ShouldBind | по методу: GET — query, остальные — тело | form / json | да |
| ShouldBindJSON | тело запроса | json | да |
| ShouldBindQuery | query-строка | form | да |
| ShouldBindHeader | HTTP-заголовки | header | да |
| ShouldBindUri | параметры маршрута :id | uri | да |

### ShouldBindUri: параметры маршрута под правилами

Параметры пути — самое незащищённое место типичного gin-проекта: c.Param("id") отдаёт строку, и до базы доезжает всё, что вписали в URL. ShouldBindUri закрывает дыру структурой: gin собирает c.Params в карту, поля заполняются по тегу uri, и дальше — стандартная валидация binding. Тип конвертируется сам: ID int64 снимет strconv, а для строковых идентификаторов пригодится uuid из четвёртого урока. required на uri-поле обычно бессмыслен: сегмент, не подходящий под :id, просто не сматчит маршрут, — зато oneof, min и uuid работают в полный рост и отсекают мусор до бизнес-логики. Ошибки — те же ValidationErrors, и обработка не отличается от тела запроса: та же ветка errors.As, тот же каталог сообщений. Учтите и обратную сторону: значения из пути приходят строками, конвертация в числа происходит при раскладке, и «abc» в int-поле вернёт ошибку конвертера — она приедет из ShouldBindUri как bind-ошибка, без записей ValidationErrors, и обработчик у неё общий с «битым телом». Полезная привычка — держать структурки параметров рядом с маршрутом и не тащить в них json-теги: источник данных у параметров один, и двойной словарь тегов только путает ревьюера.

### Маршрут заказа: uuid и oneof на параметрах

```go
type OrderParams struct {
        ID     string `uri:"id" binding:"required,uuid"`
        Status string `uri:"status" binding:"required,oneof=new paid done"`
}

router.GET("/orders/:id/:status", func(c *gin.Context) {
        var p OrderParams
        if err := c.ShouldBindUri(&p); err != nil {
                var verrs validator.ValidationErrors
                if errors.As(err, &verrs) {
                        c.JSON(http.StatusBadRequest,
                                gin.H{"errors": respondFields(verrs)})
                        return
                }
                c.JSON(http.StatusBadRequest, gin.H{"error": "битые параметры"})
                return
        }
        c.String(http.StatusOK, "order %s is %s", p.ID, p.Status)
})
```

Имя в uri-теге обязано совпадать с сегментом маршрута посимвольно — опечатка оставит поле нулевым молча, и провалится уже required. oneof на статусе отсекает /orders/x/cancelled ещё до хендлера. Обработка ошибки идентична телу запроса: errors.As, respondFields, 400 — у клиента один формат ответа на все источники данных.

### Что вернёт ShouldBindJSON

```go
type SignupDTO struct {
        Email    string `json:"email" binding:"required,email"`
        Password string `json:"password" binding:"required,min=8"`
}

// тело запроса:
// {"email": "ada@example.com", "password": ""}

err := c.ShouldBindJSON(&in)
```

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

- ✅ ValidationErrors с одной записью: Password по required
- ValidationErrors с двумя записями: required и min
- nil: пустая строка пройдёт оба тега
- Ошибку декодера: пароль обязателен в JSON

Механика третьего урока работает и здесь: правила поля проверяются слева направо до первого провала, и пустая строка нулевым значением ловит required — до min дело не доходит. Декодер тело прочитал нормально, значит ошибок разбора нет; одна запись ValidationErrors, клиент получит сообщение про обязательный пароль.

### Ошибки по-человечески: конверт для фронтенда

respondFields — та самая функция из пятого и шестого уроков, и в gin ей живётся спокойнее всего: единственная точка оборачивания ValidationErrors в ответ, один формат на все ShouldBind-ветки. Помните про два имени поля: Field() возвращает имя структуры, фронтенд подсвечивает json-имя — маппинг из пятого урока нужен здесь нисколько не меньше. Для массивов верхнего уровня не забывайте вторую ветку: gin соберёт ошибки элементов в binding.SliceValidationError со строками вида [1]: ..., и errors.As на ValidationErrors мимо — элементу ответа лучше указать индекс виновника. Если ответ уезжает в JSON, не отдавайте err.Error() целиком: это склейка английских простыней, которую клиент не разберёт, а лог — наоборот, увидит её с благодарностью: одна строка с полным списком провалов делает запись о 400 самодостаточной. PATCH-обновлениям в gin тоже есть место: через Engine() доступен StructPartial с перечнем пришедших полей — проверяются только они, и клиент не обязан присылать форму целиком; вызов встаёт после декода и до записи в базу, ошибки — те же ValidationErrors. И не пытайтесь перехватывать валидацию раньше времени: даже если поле пришло, проверку остановит первый провалившийся тег — те же правила порядка, что вы видели на чистом движке.

### Проверка: теги ShouldBindUri

**Вопрос:** Какие теги нужны полю структуры для ShouldBindUri?

- ✅ uri:"id" для имени параметра и binding:"required,uuid" для правил
- Только uri:"id" — он же несёт правила
- Только binding:"required" — имя параметра gin возьмёт из поля
- json:"id" и validate:"required"

Тег uri отвечает за раскладку: по нему gin сопоставляет сегмент маршрута с полем. Правила живут в binding, потому что движок читает именно этот словарь. json и validate тут ни при чём: тело запроса и словарь validate в этой ветке не участвуют.

> **Живое место: переезд структуры с validate-тегами.** Классика миграции: DTO приехал из чистого v10-сервиса с тегами validate:"required,email" — и в gin-проекте ShouldBindJSON молча пропускает все правила. Никаких ошибок: движок после SetTagName("binding") не читает validate, тело декодируется, данные едут в базу непроверенными. Ловится поздно — по мусору в таблице. Лечение: в gin-проекте писать binding; при шаринге структур между gin и фоновым кодом дублировать оба тега; а единомышленникам на прозрачность — свой валидатор через Engine() с нужным словарём. Правило одно: тег без словаря — просто текст.

### Свой тег и StructLevel внутри gin

Всё из уроков десять и одиннадцать работает в gin через официальную дверь — метод Engine() интерфейса StructValidator, который возвращает внутренний *validator.Validate. Приведение типа any к *validator.Validate с проверкой ok — и дальше обычный API: RegisterValidation для своих тегов, RegisterStructValidation для правил на всю структуру, RegisterTagNameFunc, если хотите видеть в ошибках json-имена. Регистрируйте до запуска сервера: регистрации не потокобезопасны, а движок общий на все запросы — это тот же контракт, что у одинокого экземпляра из второго урока. RegisterTagNameFunc уместен и здесь: json-имена в FieldError понадобятся respondFields, чтобы ключи конверта совпали с тем, что видит фронтенд, — регистрируйте его на том же экземпляре из Engine(), вместе со своими тегами. Отдельно решите, где живут ваши теги: если движок достаётся из binding.Validator, теги видны и ShouldBindJSON, и структурным правилам, и всему, что вы проверяете руками; если создаёте второй экземпляр через validator.New() — это отдельный мир со своим словарём, и его пересечение с gin-овским равно нулю. Про цену второго экземпляра и про то, почему валидатор один на приложение, — целиком следующий урок, там и продолжим.

> **Не создавайте validator.New() в хендлере.** gin уже держит прогретый движок: каждый validator.New() в хендлере — отдельный кэш с нуля, повторный разбор тегов и словарь без ваших тегов. До встроенного движка добираетесь через binding.Validator.Engine(), до своих проверок — тем же экземпляром. Один движок на процесс — правило и для gin, и для чистого v10.

### Чек-лист gin binding validation

- правила — в теге binding: SetTagName переключил словарь движка на него
- ShouldBindJSON = декод + валидация; статус 400 ставит ваш хендлер
- ошибка данных — validator.ValidationErrors, достаётся errors.As
- массив верхнего уровня — binding.SliceValidationError: отдельная ветка errors.As
- ShouldBindUri: uri — имя параметра, binding — правила; имя совпадает с сегментом маршрута
- конверт 400 — respondFields по Field/Tag/Param, единый на все ShouldBind-ветки
- свои теги и StructLevel — через binding.Validator.Engine(), регистрация до старта сервера
- второй validator.New() — отдельный словарь и пустой кэш: не создавайте в хендлерах

### Соберите разбор ошибки в хендлере

Четыре пропуска: метод привязки, errors.As, статус и имя поля.

```go
var in SignupDTO
if err := c.___(&in); err != nil {
        var verrs validator.ValidationErrors
        if errors.___(err, &verrs) {
                for _, e := range verrs {
                        fmt.Println(e.___())
                }
                c.JSON(http.___, gin.H{"errors": "проверьте поля"})
                return
        }
        c.String(http.StatusBadRequest, "битый JSON")
}
```

Подсказка: Привязка тела — ShouldBindJSON; доставает ValidationErrors — As; статус 400 — StatusBadRequest; имя поля — Field().

Правильные ответы в пропусках: `ShouldBindJSON`, `As`, `StatusBadRequest`, `Field`

### Соберите путь binding-тега

POST /signup с битым телом. Расставьте шаги по порядку.

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

1. `Запрос уходит в роутер gin`
2. `ShouldBindJSON декодирует JSON в SignupDTO`
3. `Движок валидации зовёт ValidateStruct(dto)`
4. `Движок читает правила из тега binding`
5. `Password пуст: провален required`
6. `ValidationErrors возвращены из ShouldBindJSON`
7. `errors.As достаёт срез в хендлере`
8. `respondFields собирает конверт 400 по полям`

### Карточки: validator в gin

- **binding.Validator** — Пакетная переменная gin, через которую фреймворк говорит с движком. Интерфейс StructValidator: ValidateStruct(obj any) error и Engine() any.
- **SetTagName("binding")** — Настройка словаря внутри ленивой инициализации дефолтного валидатора: правила ищутся в теге binding, validate-теги игнорируются молча.
- **ShouldBindUri** — Параметры маршрута по тегу uri, затем валидация binding. Ошибки — те же ValidationErrors; имя в uri-теге должно совпадать с сегментом :id.
- **SliceValidationError** — Тип gin для массивов верхнего уровня: элементы валидируются циклом, ошибки склеиваются со строками [i]:. Другой тип — другая ветка errors.As.
- **Engine()** — Метод интерфейса, возвращающий внутренний *validator.Validate. Официальная дверь к RegisterValidation и RegisterStructValidation внутри gin.
- **кто ставит 400** — Никто, кроме вас: ShouldBindJSON возвращает ошибку, статус и конверт — работа хендлера. Ветку ValidationErrors держите отдельно от ошибок декодера.

Следующий урок — [Echo, chi и свой Validation-хук](/courses/validator/lessons/13-validator-i-echo/): те же правила в фреймворках без встроенного валидатора, где конвейер bind → validate собираете вы. Разбор ошибок FieldError с методами Field, Tag и Param — в [пятом уроке](/courses/validator/lessons/05-oshibki-validacii/), цена одного экземпляра движка — в [четырнадцатом](/courses/validator/lessons/14-odin-ekzemplar/). Весь путь binding — от ShouldBindJSON до массивов — есть в [курсе Gin](/courses/gin/lessons/09-validaciya/). Код урока запускается локально: gin и validator — внешние модули, песочница сайта исполняет только чистый Go, поэтому хендлеры проверяйте через go run и curl.

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

- ShouldBindJSON = декод JSON плюс валидация: движок v10 уже внутри gin, словарь переключён на binding через SetTagName.
- Ошибки данных — validator.ValidationErrors: та же errors.As и FieldError, что на голом валидаторе; статус 400 ставит ваш хендлер.
- Семейство ShouldBind различается источником: body, query, header, uri — теги раскладки разные, валидация binding одна.
- ShouldBindUri: uri-тег задаёт имя параметра, binding — правила; uuid и oneof отсекают мусор URL до бизнес-логики.
- Массив верхнего уровня даёт binding.SliceValidationError — отдельная ветка errors.As с индексами виновников.
- Свои теги и StructLevel подключаются через binding.Validator.Engine() до старта сервера.
- validate-теги в gin мертвы: молчаливый пропуск правил — главная грабля переезда структур.

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

Перенесите SignupDTO из одиннадцатого урока в gin-проект: добавьте маршрут POST /signup с ShouldBindJSON, структурное правило passwordmatch через binding.Validator.Engine() и маршрут GET /orders/:id/:status с ShouldBindUri и тегами uuid, oneof. Соберите respondFields по каталогу сообщений и проверьте curl-ом три сценария: валидное тело, пустой пароль, битый uuid в параметрах — убедитесь, что все три отвечают 400 с деталями по полям.

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

### Почему ShouldBindJSON не возвращает 400 сам?

Он возвращает ошибку, а решение о статусе остаётся хендлеру: один проект хочет 400 с картой полей, другой — 422, третий — свой формат. Принятое разделение: валидатор решает, что данные плохи, фреймворк и ваш код решают, как об этом сказать. Главное — не потерять ошибку: ветка без обработки ошибок отправит нулевую структуру в бизнес-слой. Практика сверх того: фиксированная константа статуса на весь проект и единая функция-конверт — расхождение 400 и 422 между эндпоинтами читается клиентом как разные классы проблем, хотя это один и тот же провал валидации.

### Как добраться до движка, чтобы зарегистрировать свой тег?

Через binding.Validator.Engine(): метод возвращает any, приведите к *validator.Validate с проверкой ok и вызывайте RegisterValidation или RegisterStructValidation. Делайте это до запуска сервера: регистрации не потокобезопасны. Если приведение не сработало — кто-то уже подменил binding.Validator, и лучше упасть на старте, чем жить без тега.

### Что вернёт ShouldBindJSON на массиве верхнего уровня?

Для []TodoInput gin сам ходит по элементам и собирает ошибки в binding.SliceValidationError — срез error-ов со строками [0]:, [1]:. errors.As на validator.ValidationErrors не совпадёт: держите отдельную ветку. Внутри структуры срезы валидируются обычным движком с dive — там ошибки приезжают стандартным типом. Планируете принимать пачки часто — заведите для SliceValidationError свою ветку в respondFields: пройдитесь по элементам, достаньте ValidationErrors каждого и соберите карту «индекс → поля»; клиент получит подсветку по позициям массива без повторной валидации.
