Validation with go-playground/validator
encoding/json maps bytes to Go types; it does not enforce business rules.
Search across all documentation pages
encoding/json maps bytes to Go types; it does not enforce business rules.
github.com/go-playground/validator/v10 (verify version at build) reads validate struct tags and returns field-level errors suitable for HTTP 400 responses.
Decode JSON first, then call validator.Struct on the populated struct.
Tags express common constraints (required, email, gte, oneof) and custom validators extend the engine for domain-specific rules.
Validation stays separate from serialization so the same struct can round-trip to JSON without embedding policy in wire format.
Quick-reference recipe card - copy-paste ready.
import "github.com/go-playground/validator/v10"
var validate = validator.New()
type CreateOrder struct {
SKU string `json:"sku" validate:"required,alphanum,min=3,max=32"`
Qty int `json:"qty" validate:"required,gte=1,lte=1000"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { /* 400 */ }
if err := validate.Struct(req); err != nil {
verrs := err.(validator.ValidationErrors)
// map to JSON problem details
}When to reach for this:
json tags for reviewability.package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type Signup struct {
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=13,lte=120"`
Country string `json:"country" validate:"required,len=2,alpha"`
Referral string `json:"referral" validate:"omitempty,alphanum"`
}
func formatErrors(err error) string {
var b strings.Builder
for _, fe := range err.(validator.ValidationErrors) {
fmt.Fprintf(&b, "%s failed %s; ", fe.Field(), fe.Tag())
}
return strings.TrimSuffix(b.String(), "; ")
}
func main() {
validate := validator.New()
raw := []byte(`{"email":"not-an-email","age":10,"country":"USA"}`)
var s Signup
_ = json.Unmarshal(raw, &s)
if err := validate.Struct(s); err != nil {
fmt.Println(formatErrors(err))
}
good := Signup{Email: "ada@example.com", Age: 30, Country: "US"}
fmt.Println(validate.Struct(good))
}What this demonstrates:
validator.ValidationErrors exposes field, tag, and param metadata.omitempty skips rules when the field is zero.| Tag | Meaning | Example |
|---|---|---|
required | Non-zero value | IDs, emails |
omitempty | Skip other rules if zero | Optional referral |
email | Email format | User signup |
gte / lte | Numeric bounds | Quantities |
len | String/array length | Country codes |
oneof | Enum strings | oneof=red green blue |
dive | Validate slice elements | validate:"dive,email" |
Full tag list is in the upstream docs (verify version at build).
validate := validator.New()
_ = validate.RegisterValidation("sku", func(fl validator.FieldLevel) bool {
s := fl.Field().String()
return strings.HasPrefix(s, "SKU-")
})Use custom tags for domain rules that are awkward as regex.
Keep validators pure and fast; they run per request.
http.MaxBytesReader.json.Decoder with optional DisallowUnknownFields.validate.Struct.gin offers binding tags that wrap validator; echo has similar binders.
You can still use validator directly for non-framework code.
validator.ValidationErrors can feed en_translations or custom translators for end-user messages.
Log technical detail server-side; return safe messages client-side.
// validate:"-" skips a field entirely (server-set timestamps)
// Use pointers so required can distinguish missing vs zero for numbersPair with Struct Tags for JSON, DB & Validation for tag alignment.
Struct runs. Fix: validate explicitly in handlers.0 fails required for ints. Fix: use pointers for optional numbers or drop required when 0 is valid.Struct on root; inner structs need validate tags on nested fields. Fix: use dive for slices of nested DTOs.validate instance; register custom validators in init.| Alternative | Use When | Don't Use When |
|---|---|---|
| go-playground/validator | Tag-driven HTTP APIs | Complex graph rules |
| Hand-written checks | Tiny handlers | Large forms |
| OpenAPI / JSON Schema | Contract-first CI | Runtime-only validation |
| CEL or policy engines | Authorization rules | Simple field formats |
| Protobuf constraints | gRPC with protovalidate | Plain REST only |
After - unmarshaling must populate the struct first.
Yes - bind query to a struct and call Struct.
Framework binders often share the same tag engine.
Use separate partial structs or pointers so required does not fire on absent fields.
Use struct-level validation with RegisterStructValidation.
Keep cross-field logic readable and tested.
A configured Validate instance is safe for concurrent Struct calls.
Register custom validators before serving traffic.
Validate at the edge; keep DB constraints as last resort.
Duplicate rules are acceptable for defense in depth.
Yes - any populated struct qualifies.
Tags are transport-agnostic.
400 with machine-readable field errors for public APIs.
Log full detail server-side.
It does not run validator.
Use unit tests per DTO and optional linters for tag naming alignment.
Table-test valid and invalid JSON fixtures per endpoint.
Assert error tags, not only error presence.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 18, 2026