binder / Docs / Validation

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.