go 113 lines · 4 tabs

Binding and Validating Nested JSON in Gin With Custom Validators

Shared by codesnips Sep 2026
4 tabs
package order

type CreateOrderRequest struct {
	Reference string        `json:"reference" binding:"required,min=3,max=40"`
	Currency  string        `json:"currency" binding:"required,currency"`
	Customer  Customer      `json:"customer" binding:"required"`
	Items     []OrderItem   `json:"items" binding:"required,min=1,dive"`
	Note      *string       `json:"note" binding:"omitempty,max=200"`
}

type Customer struct {
	Name  string `json:"name" binding:"required"`
	Email string `json:"email" binding:"required,email"`
	Phone string `json:"phone" binding:"omitempty,e164"`
}

type OrderItem struct {
	SKU       string  `json:"sku" binding:"required,sku"`
	Quantity  int     `json:"quantity" binding:"required,min=1,max=999"`
	UnitPrice float64 `json:"unit_price" binding:"required,gt=0"`
}
4 files · go Explain with highlit

This snippet shows how a Gin HTTP API binds a nested JSON payload into Go structs and validates it using go-playground/validator with both built-in tags and a custom rule registered against the validator engine. The example models an order-creation endpoint where the request has nested objects (Customer) and a slice of Items, each with their own constraints.

In order_dto.go, the request types are pure data-transfer objects decorated with struct tags. The binding tag is what Gin reads: required, email, gte, dive, and the custom currency rule all live there. The dive tag on Items []OrderItem is the key to nested validation — it tells the validator to descend into each element of the slice and apply the element's own tags, so min=1 on Quantity and sku on SKU are enforced per item. Nested structs like Customer are validated automatically because the validator recurses into struct fields by default.

In validators.go, two custom validators are registered on Gin's shared *validator.Validate instance obtained through binding.Validator.Engine(). validateCurrency restricts a string to a small allow-list of ISO currency codes, and validateSKU enforces a format with a regular expression. Registering them once at startup via RegisterCustomValidators makes the currency and sku tags usable across every struct in the app. Doing the type assertion to *validator.Validate is necessary because Engine() returns any.

In order_handler.go, CreateOrder calls c.ShouldBindJSON, which both decodes the JSON and runs validation in one step. On failure the error is a validator.ValidationErrors, which the handler unpacks into a field-keyed map so the client gets actionable messages instead of an opaque string. This separation keeps binding declarative in the DTO while translation of errors stays in the handler.

A pitfall worth noting: custom tags must be registered before any request is bound, and dive only works on slices/maps — forgetting it silently skips per-element checks. The trade-off is that validation logic is spread across tags and registered functions, which is concise but can be harder to unit test than explicit imperative checks.


Related snips

Share this code

Here's the card — post it anywhere.

Binding and Validating Nested JSON in Gin With Custom Validators — share card
Link copied