GinJuniorCoding

Как разбирать JSON-тело запроса в Gin с помощью c.ShouldBindJSON()?

c.ShouldBindJSON(&struct{}) десериализует JSON-тело запроса в Go-структуру с валидацией тегов binding и возвращает ошибку без автоматической отправки ответа. Предпочтительнее c.BindJSON(), который скрыто вызывает Abort при ошибке.

Разбор JSON-тела запроса через c.ShouldBindJSON()

c.ShouldBindJSON() читает тело HTTP-запроса, десериализует JSON в переданную структуру и возвращает ошибку при неудаче — без автоматической отправки ответа клиенту. Это основной способ получать JSON-данные от клиента в Gin.

Базовый пример

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type CreateUserRequest struct {
	Name     string `json:"name"     binding:"required,min=2,max=100"`
	Email    string `json:"email"    binding:"required,email"`
	Age      int    `json:"age"      binding:"required,min=18,max=120"`
	Role     string `json:"role"     binding:"oneof=admin user viewer"`
}

func createUser(c *gin.Context) {
	var req CreateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// req.Name, req.Email, req.Age заполнены и валидны
	c.JSON(http.StatusCreated, gin.H{
		"id":    42,
		"name":  req.Name,
		"email": req.Email,
	})
}

func main() {
	r := gin.Default()
	r.POST("/users", createUser)
	r.Run(":8080")
}

ShouldBindJSON vs BindJSON

Gin предоставляет два варианта:

  • c.ShouldBindJSON(&obj) — возвращает ошибку, не пишет ответ автоматически. Рекомендуется.
  • c.BindJSON(&obj) — при ошибке автоматически вызывает c.AbortWithStatus(400). Это скрывает логику и мешает отправить собственный JSON-ответ с деталями ошибки.
// НЕ делайте так — BindJSON уже отправил 400, ваш c.JSON не выполнится
if err := c.BindJSON(&req); err != nil {
	c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // лишний вызов
	return
}

// Правильно:
if err := c.ShouldBindJSON(&req); err != nil {
	c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	return
}

Кастомная обработка ошибок валидации

import (
	"errors"

	"github.com/go-playground/validator/v10"
)

func formatValidationErrors(err error) []gin.H {
	var ve validator.ValidationErrors
	if !errors.As(err, &ve) {
		return []gin.H{{"message": err.Error()}}
	}

	out := make([]gin.H, len(ve))
	for i, fe := range ve {
		out[i] = gin.H{
			"field":   fe.Field(),
			"tag":     fe.Tag(),
			"value":   fe.Param(),
			"message": fe.Translate(nil),
		}
	}
	return out
}

func createUser(c *gin.Context) {
	var req CreateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"errors": formatValidationErrors(err),
		})
		return
	}
	c.JSON(http.StatusCreated, gin.H{"name": req.Name})
}

Опциональные поля и указатели

type UpdateUserRequest struct {
	Name  *string `json:"name"  binding:"omitempty,min=2"`  // nil если не передано
	Email *string `json:"email" binding:"omitempty,email"`
}

func updateUser(c *gin.Context) {
	var req UpdateUserRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if req.Name != nil {
		// обновить имя
	}
}

Подводные камни

  • Тело запроса читается один раз. После ShouldBindJSON() c.Request.Body исчерпан. Повторный вызов вернёт ошибку EOF. Если нужно несколько биндингов, прочитайте тело в []byte через c.GetRawData() и восстановите через io.NopCloser.
  • Content-Type должен быть application/json. Если клиент не выставил заголовок, ShouldBindJSON игнорирует его — но декодирование может всё равно пройти. Явно проверяйте Content-Type при необходимости.
  • Поле с binding:"required" vs omitempty. Нулевое значение (0, "", false) удовлетворяет required только если поле присутствует в JSON. Используйте указатели для различения «не передано» и «передано с нулём».
  • Ошибка валидации vs ошибка декодирования. Невалидный JSON (синтаксическая ошибка) и ошибка валидации тега — разные типы. Первая — *json.SyntaxError, вторая — validator.ValidationErrors.
  • Глубокая вложенность и тег binding. Вложенные структуры должны иметь тег binding:"required" или dive для валидации элементов слайса — без них вложенные поля не валидируются.
  • Максимальный размер тела запроса. По умолчанию Gin не ограничивает размер Body. При загрузке больших JSON используйте http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20) перед биндингом.

Common mistakes

  • Давать ответ про c.ShouldBindJSON() только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «c.ShouldBindJSON()»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать c.ShouldBindJSON() с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает c.ShouldBindJSON() и где это используется в Go-коде.
  • Связывает c.ShouldBindJSON() с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics