Validation
binder binds; it does not validate. What it offers is a single hook, so that your own rules run as part of the same call and a handler has one error path rather than two.
The Validator interface
type Validator interface {
Validate() error
}
If the target implements it, Bind calls
Validate after every field is bound and returns whatever it
returns. Nothing to register, nothing to configure:
type CreateUser struct {
Name string `body:"name"`
Email string `body:"email"`
Age int `body:"age"`
}
func (r CreateUser) Validate() error {
if r.Name == "" {
return errors.New("name is required")
}
if !strings.Contains(r.Email, "@") {
return errors.New("email must be an address")
}
if r.Age < 18 {
return errors.New("user must be 18 or older")
}
return nil
}
func handler(w http.ResponseWriter, r *http.Request) {
var req CreateUser
// One step: bind, then validate.
if err := binder.Bind(r, &req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// req is bound and valid.
json.NewEncoder(w).Encode(createUser(req))
}
A value receiver, as above, works on the pointer Bind is
given. A pointer receiver works too, and is what you want if
Validate normalises as well as checks — trimming whitespace,
lower-casing an address, filling a default.
Reporting several problems
Binding stops at the first failure, but Validate is your
code and can collect as many as it likes. errors.Join is
usually enough:
func (r CreateUser) Validate() error {
var problems []error
if r.Name == "" {
problems = append(problems, errors.New("name is required"))
}
if r.Age < 18 {
problems = append(problems, errors.New("must be 18 or older"))
}
return errors.Join(problems...)
}
Returning your own error type works just as well, and
errors.As will find it through binder — so a handler can
render a field-by-field JSON response while still treating a bind failure
and a validation failure the same way.
Why there are no validation tags
A min, max, email tag vocabulary is
the obvious next feature and deliberately absent. Adding it would mean
inventing a rule language, an error format and a translation story, and
the result would be a worse version of what
go-playground/validator
already does well. binder stays small enough to read in one sitting, and
composes with whichever validator you prefer:
var validate = validator.New()
type CreateUser struct {
Email string `body:"email" validate:"required,email"`
Age int `body:"age" validate:"gte=18"`
}
func (r CreateUser) Validate() error {
return validate.Struct(r)
}
The two tags coexist: binder reads body, the validator reads
validate, and neither knows about the other.
required in a binder tag is not validation — it asks whether
the request supplied the value, before any conversion. “Must be
present” is a binding question; “must be a valid email” is a validation
one. See Options.