Validation
Okapi provides a powerful validation system that allows you to easily validate incoming request data against defined rules and constraints. This helps ensure that your API receives well-formed and expected data, improving the robustness and reliability of your application.
Validation and Default Values
Okapi provides declarative validation and automatic default value assignment using struct tags.
Basic Validation Tags
| Field Type | Tag | Description |
|---|---|---|
string | minLength:"10" | Ensures the string has at least 10 characters. |
string | maxLength:"50" | Ensures the string does not exceed 50 characters. |
number | min:"5" | Ensures the number is greater than or equal to 5. |
number | max:"100" | Ensures the number is less than or equal to 100. |
number | multipleOf:"5" | Ensures the number is a multiple of the given value. |
slice | maxItems:"5" | Ensures the slice contains at most 5 items. |
slice | minItems:"2" | Ensures the slice contains at least 2 items. |
slice | uniqueItems:"true" | Ensures all items in the slice are unique. |
any | required:"true" | Marks the field as required. |
any | default:"..." | Assigns a default value when the field is missing/empty. |
any | enum:"pending,paid,canceled" | Restricts the field to one of the listed values. |
any | format:"email" | Enables format validation (e.g. email, uuid, etc.). |
any | pattern:"^[a-zA-Z]+$" | Validates the field against a regular expression. |
Data Type & Format Validation
| Field Type | Tag / Attribute | Description |
|---|---|---|
date | format:"date" | Validates the field as a date (YYYY-MM-DD). |
date-time | format:"date-time" | Validates the field as a date and time (RFC3339). |
email | format:"email" | Validates the field as a valid email address. |
duration | format:"duration" | Validates the field as a Go duration (e.g., 1h30m). |
uuid | format:"uuid" | Validates the field as a valid UUID. |
hostname | format:"hostname" | Validates the field as a valid hostname. |
ipv4 | format:"ipv4" | Validates the field as a valid IPv4 address. |
ipv6 | format:"ipv6" | Validates the field as a valid IPv6 address. |
uri | format:"uri" | Validates the field as a valid URI. |
regex | format:"regex" pattern="^\+?[1-9]\d{1,14}$" | Validates the field using a custom regular expression. |
Example
type CreateUserRequest struct {
Email string `json:"email" required:"true" format:"email" example:"user@example.com"`
Password string `json:"password" minLength:"8" description:"User password"`
Age int `json:"age" min:"18" max:"120" default:"18"`
Roles []string `json:"roles" minItems:"1" uniqueItems:"true"`
}
Validation and Binding Methods
Okapi provides multiple ways to validate and bind incoming request data, each suited for different use cases.
Method 1: Using c.Bind()
The simplest approach to bind and validate the request data within your handler:
o.Post("/users", func(c *okapi.Context) error {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
return c.ErrorBadRequest(err)
}
// Proceed with creating the user using validated data
return c.JSON(http.StatusOK, req)
})
Method 2: Using okapi.Handle() (Input Validation)
Use okapi.Handle() when you want automatic input binding and validation with a typed handler signature:
type Book struct {
ID int `json:"id" path:"id"`
Name string `json:"name" form:"name" maxLength:"50" required:"true"`
Price int `json:"price" form:"price" min:"0" max:"500" default:"0"`
Status string `json:"status" enum:"paid,unpaid,canceled" required:"true"`
}
o.Post("/books", okapi.Handle(func(c *okapi.Context, book *Book) error {
book.ID = generateID()
return c.Created(book)
}),
okapi.DocRequestBody(&Book{}),
okapi.DocResponse(&Book{}),
)
Method 3: Using okapi.H() (Shorthand for Handle)
okapi.H() is a shorter version of okapi.Handle() when you only need input validation:
type BookDetailInput struct {
ID int `json:"id" path:"id"`
}
o.Get("/books/{id:int}", okapi.H(func(c *okapi.Context, input *BookDetailInput) error {
book := findBookByID(input.ID)
if book == nil {
return c.AbortNotFound("Book not found")
}
return c.OK(book)
}),
okapi.DocResponse(&Book{}),
)
Method 4: Using okapi.HandleIO() (Input and Output)
Use okapi.HandleIO() when you want to define both input and output structs separately. This is useful for complex operations where the response structure differs from the input:
type BookEditInput struct {
ID int `json:"id" path:"id" required:"true"`
Body Book `json:"body"`
}
type BookOutput struct {
Status int
Body Book
}
o.Put("/books/{id:int}", okapi.HandleIO(func(c *okapi.Context, input *BookEditInput) (*BookOutput, error) {
book := updateBook(input.ID, input.Body)
if book == nil {
return nil, c.AbortNotFound("Book not found")
}
return &BookOutput{Body: *book}, nil
})).WithIO(&BookEditInput{}, &BookOutput{})
Note:
WithIO()generates OpenAPI documentation for both input and output schemas. The output struct should follow the body style convention.
Method 5: Using okapi.HandleO() (Output Only)
Use okapi.HandleO() when you only need a custom output struct without specific input validation:
type BooksResponse struct {
Body []Book `json:"books"`
}
o.Get("/books", okapi.HandleO(func(c *okapi.Context) (*BooksResponse, error) {
return &BooksResponse{Body: getAllBooks()}, nil
})).WithOutput(&BooksResponse{})
Note: The output struct must follow the body style convention. The response content type is based on the
Acceptheader requested by the client, defaulting toapplication/json.
Input Sources
Okapi can bind data from multiple sources based on struct tags:
| Tag | Source | Example |
|---|---|---|
json | Request body | json:"name" |
form | Form data | form:"name" |
query | Query parameters | query:"page" |
path | Path parameters | path:"id" |
header | Request headers | header:"Authorization" |
You can combine multiple source tags on the same field:
type BookInput struct {
ID int `json:"id" path:"id"`
Name string `json:"name" form:"name" query:"name"`
Price int `json:"price" form:"price" query:"price"`
}
OpenAPI Documentation Helpers
Okapi provides helper methods to generate OpenAPI documentation:
| Method | Description |
|---|---|
okapi.DocRequestBody(&T{}) | Documents the request body schema |
okapi.DocResponse(&T{}) | Documents the response schema |
.WithInput(&T{}) | Documents input schema (for okapi.H()) |
.WithOutput(&T{}) | Documents output schema (for okapi.HandleO()) |
.WithIO(&In{}, &Out{}) | Documents both input and output (for okapi.HandleIO()) |
Complete Example
package main
import (
"fmt"
"github.com/jkaninda/okapi"
)
type Book struct {
ID int `json:"id" path:"id"`
Name string `json:"name" form:"name" maxLength:"50" example:"The Go Programming Language" required:"true"`
Price int `json:"price" form:"price" query:"price" min:"0" default:"0" max:"500"`
Qty int `json:"qty" form:"qty" query:"qty" default:"0"`
Status string `json:"status" form:"status" enum:"paid,unpaid,canceled" required:"true" example:"paid"`
}
type BookEditInput struct {
ID int `json:"id" path:"id" required:"true"`
Body Book `json:"body"`
}
type BookDetailInput struct {
ID int `json:"id" path:"id"`
}
type BookOutput struct {
Status int
Body Book
}
type BooksResponse struct {
Body []Book `json:"books"`
}
var books = []Book{
{ID: 1, Name: "The Go Programming Language", Price: 30, Qty: 100},
}
func main() {
o := okapi.Default()
api := o.Group("api")
// CREATE - Using okapi.Handle with automatic validation
api.Post("/books", okapi.Handle(func(c *okapi.Context, book *Book) error {
book.ID = len(books) + 1
books = append(books, *book)
return c.Created(book)
}),
okapi.DocRequestBody(&Book{}),
okapi.DocResponse(&Book{}),
)
// READ ONE - Using okapi.H (shorthand)
api.Get("/books/{id:int}", okapi.H(func(c *okapi.Context, input *BookDetailInput) error {
for _, b := range books {
if b.ID == input.ID {
return c.OK(b)
}
}
return c.AbortNotFound(fmt.Sprintf("Book not found: %d", input.ID))
}),
okapi.DocResponse(&Book{}),
)
// READ ALL - Using okapi.HandleO for custom output
api.Get("/books", okapi.HandleO(func(c *okapi.Context) (*BooksResponse, error) {
return &BooksResponse{Body: books}, nil
})).WithOutput(&BooksResponse{})
// UPDATE - Using okapi.HandleIO for input/output
api.Put("/books/{id:int}", okapi.HandleIO(func(c *okapi.Context, input *BookEditInput) (*BookOutput, error) {
for i, b := range books {
if b.ID == input.ID {
books[i] = input.Body
books[i].ID = input.ID
return &BookOutput{Body: books[i]}, nil
}
}
return nil, c.AbortNotFound(fmt.Sprintf("Book not found: %d", input.ID))
})).WithIO(&BookEditInput{}, &BookOutput{})
// DELETE - Using okapi.H with path parameter
api.Delete("/books/{id:int}", okapi.H(func(c *okapi.Context, input *BookDetailInput) error {
for i, b := range books {
if b.ID == input.ID {
books = append(books[:i], books[i+1:]...)
return c.NoContent()
}
}
return c.AbortNotFound(fmt.Sprintf("Book not found: %d", input.ID))
})).WithInput(&BookDetailInput{})
if err := o.Start(); err != nil {
panic(err)
}
}