Skip to content

Validation ​

Kori integrates go-playground/validator and runs validation automatically as part of binding. There is no separate validation step.

Validator integration ​

Kori initializes a shared *validator.Validate instance with WithRequiredStructEnabled, which makes the required tag apply to structs and non-pointer fields consistently.

The validator is shared across all binding calls and is initialized once. You do not need to configure or instantiate it yourself.

Struct tags ​

Add validate tags to fields to declare constraints:

go
type CreateUserInput struct {
    Name  string `json:"name"  validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age"   validate:"required,gte=0,lte=130"`
    Role  string `json:"role"  validate:"required,oneof=admin editor viewer"`
}

Any tag supported by go-playground/validator is accepted. Common tags:

TagMeaning
requiredField must be present and non-zero
omitemptySkip validation if the field is zero
min=NMinimum length (string) or value (number)
max=NMaximum length (string) or value (number)
gte=NGreater than or equal to N
lte=NLess than or equal to N
gt=NGreater than N
lt=NLess than N
emailMust be a valid email address
urlMust be a valid URL
uuid4Must be a valid UUID v4
uuidMust be a valid UUID (any version)
oneof=a b cMust be one of the listed values
len=NMust have exactly N characters

The tags above produce a human-readable message in the error response. Any other validator tag still works, but its message falls back to the raw error from go-playground/validator.

Automatic validation ​

Every binding function runs validation after decoding. No explicit call is required:

go
func createUser(w http.ResponseWriter, r *http.Request) error {
    var input CreateUserInput
    if err := kori.BindJSON(r, &input); err != nil {
        return err // 400 on bad JSON, 422 on validation failure
    }
    // input is valid here
    return kori.JSON(w, http.StatusCreated, input)
}

The binding functions that validate are:

FunctionDecodes
kori.BindJSON(r, dst)Request body (JSON)
kori.BindQuery(r, dst)URL query parameters
kori.BindPath(r, dst)Chi URL path parameters
kori.BindHeader(r, dst)HTTP headers
kori.BindForm(r, dst)Form values (application/x-www-form-urlencoded)
kori.BindMultipart(r, dst)Multipart form data
kori.Bind(r, dst)Path + query + headers combined

Validation errors ​

When validation fails, Kori returns a 422 Unprocessable Entity with a structured body:

json
{
  "message": "validation failed",
  "details": [
    {
      "field": "email",
      "tag": "email",
      "message": "email must be a valid email address"
    },
    {
      "field": "name",
      "tag": "min",
      "param": "2",
      "message": "name must be at least 2"
    }
  ]
}

Each entry corresponds to a ValidationDetail:

go
type ValidationDetail struct {
    Field   string `json:"field"`
    Tag     string `json:"tag"`
    Param   string `json:"param,omitempty"`
    Message string `json:"message"`
}

Field names in errors ​

Kori registers a tag name function on the validator. When reporting errors, field names are resolved from the binding tag of the struct — not the Go field name:

go
type Query struct {
    PageSize int `query:"page_size" validate:"min=1,max=100"`
}

A validation failure on PageSize reports "field": "page_size", not "field": "PageSize". The resolver checks tags in this order: json, query, path, header, form.

Validation during binding ​

Binding and validation share the same struct. Tags for decoding (json, query, path, header, form) and tags for validation (validate) live on the same field:

go
type ListTodosParams struct {
    Completed *bool  `query:"completed"`
    Search    string `query:"search"    validate:"omitempty,max=100"`
    Page      int    `query:"page"      validate:"omitempty,min=1"`
    PageSize  int    `query:"page_size" validate:"omitempty,min=1,max=100"`
}

func listTodos(w http.ResponseWriter, r *http.Request) error {
    var q ListTodosParams
    if err := kori.BindQuery(r, &q); err != nil {
        return err
    }
    // q is decoded and validated
    return kori.JSON(w, http.StatusOK, results)
}

Path parameters work the same way:

go
type PathID struct {
    ID int `path:"id" validate:"required,min=1"`
}

func getTodo(w http.ResponseWriter, r *http.Request) error {
    var p PathID
    if err := kori.BindPath(r, &p); err != nil {
        return err
    }
    // p.ID is an int, decoded from the URL and validated
    return kori.JSON(w, http.StatusOK, todo)
}