Skip to content

x/validate Primer

Experimental — API compatibility is not frozen. Evaluate before adopting in production. Check Release Posture for current maturity status.

Open this page after x/* Family when the change involves decoding a JSON request body and returning structured validation errors through contract.WriteError — without adding validation middleware or tag-based parsing.

x/validate provides two functions: BindJSON[T] for decode-only and Bind[T] for decode plus caller-owned validation. Validation rules belong in your application code; x/validate only wires decode errors and validation errors into the contract.APIError response shape.

  • a handler needs to decode a JSON body and return a structured 400 or 422 response on failure
  • you have a Validator implementation and want to compose it with contract.WriteError without boilerplate
  • you are adapting a third-party validator (e.g. go-playground/validator) to the validate.Validator interface in your own package
  • middleware-level validation before the handler runs is the goal — validate in the handler, not upstream
  • tag-based struct validation is required — keep your adapter in application code or an external module
  • the validation is unrelated to HTTP body decoding (e.g. config or environment validation) — use your own logic
  • you are adding a default global validator to x/validate — no such global exists or should be added
  1. x/validate/BindJSON, Bind, Validator interface, ValidationError
  2. reference/with-rest/ — complete example of Bind with a third-party adapter
  3. contract/errors.goTypeValidation and TypeBadRequest constants used in error responses
Keep it in x/validate when the work is aboutMove out when the work becomes
BindJSON[T]: decoding the request body into a typed valuetype-specific decoding or content-type negotiation — handle in the handler
Bind[T]: composing decode with a caller-supplied Validatora concrete validator implementation (go-playground, custom rules) — keep in application code
ValidationError: shaping decode and validation failures into contract.APIErrordomain-level validation (business rules, database uniqueness) — handle in the service layer
Validator interface: the minimal contract between Bind and caller logicglobal validator instances or tag-based registration — not in x/validate

BindJSON and Bind return *validate.ValidationError on failure. contract.WriteError handles this type natively:

Failure kindHTTP statustype field
Malformed JSON, unexpected EOF400bad_request
Validator.Validate returns non-nil422validation_error

Keep the adapter in your application code — not in x/validate:

internal/validation/adapter.go
type PlaygroundAdapter struct{ v *validator.Validate }
func New() validate.Validator { return &PlaygroundAdapter{v: validator.New()} }
func (a *PlaygroundAdapter) Validate(val any) error { return a.v.Struct(val) }
handler.go
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
var v = validation.New()
func createUserHandler(w http.ResponseWriter, r *http.Request) {
req, err := validate.Bind[CreateUserRequest](r, v)
if err != nil {
_ = contract.WriteError(w, r, err)
return
}
_ = contract.WriteResponse(w, r, http.StatusCreated, req, nil)
}

Request validation in Go web services drifts in two directions: either it disappears into middleware (making handler behavior implicit) or it scatters custom decode-and-check blocks across every handler. x/validate provides a single, explicit call site per handler with a consistent error output shape. The Validator interface keeps third-party libraries in application code where they belong, without coupling x/validate to any specific validation strategy.