Skip to main content
Version: v0.11.0

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 TypeTagDescription
stringminLength:"10"Ensures the string has at least 10 characters.
stringmaxLength:"50"Ensures the string does not exceed 50 characters.
numbermin:"5"Ensures the number is greater than or equal to 5.
numbermax:"100"Ensures the number is less than or equal to 100.
numberexclusiveMin:"0"Ensures the number is strictly greater than 0.
numberexclusiveMax:"100"Ensures the number is strictly less than 100.
numbermultipleOf:"5"Ensures the number is a multiple of the given value.
slicemaxItems:"5"Ensures the slice contains at most 5 items.
sliceminItems:"2"Ensures the slice contains at least 2 items.
sliceuniqueItems:"true"Ensures all items in the slice are unique.
mapminProperties:"1"Ensures the map has at least 1 entry.
mapmaxProperties:"10"Ensures the map has at most 10 entries.
anyrequired:"true"Marks the field as required.
anydefault:"..."Assigns a default value when the field is missing/empty.
string / []stringenum:"pending,paid,canceled"Restricts the field to one of the listed values.
string / []stringconst:"active"Requires the field to equal a fixed value.
string / []stringformat:"email"Enables format validation (e.g. email, uuid, etc.).
string / []stringpattern:"^[a-zA-Z]+$"Validates the field against a regular expression.
string / []stringcontains:"@"Ensures the string contains the given substring.
string / []stringnotContains:" "Ensures the string does not contain the given substring.

Slices: enum, const, format, pattern, contains, and notContains apply to each element of a []string field. Failures are reported per index, e.g. element [2]: ....

Empty values: enum, const, format, pattern, contains, and notContains skip empty strings — combine with required:"true" to also enforce presence.

Conditional Required Tags​

These make a field's requiredness depend on sibling fields in the same struct. Referenced fields use their Go field name, not the JSON name.

TagDescription
requiredIf:"Type card"Required when the sibling field Type equals card.
requiredWith:"Pass"Required when any of the listed sibling fields (comma-separated) is non-empty.
requiredWithout:"Email"Required when any of the listed sibling fields (comma-separated) is empty.
type Payment struct {
Type string `json:"type"`
Card string `json:"card" requiredIf:"Type card"`
Pass string `json:"pass"`
Confirm string `json:"confirm" requiredWith:"Pass"`
Email string `json:"email"`
Phone string `json:"phone" requiredWithout:"Email"`
}

Schema Annotations​

These emit OpenAPI schema keywords and are documentation-only (no runtime enforcement).

TagDescription
readOnly:"true"Marks the property readOnly (returned but not sent by clients).
writeOnly:"true"Marks the property writeOnly (sent but not returned).
nullable:"true"Marks the property as nullable. Pointer fields are nullable automatically.

Data Type & Format Validation​

Format validation is enabled with the format tag. All formats apply to string fields (and each element of []string fields).

Date & time​

FormatTagDescription
dateformat:"date"Date in YYYY-MM-DD form.
date-timeformat:"date-time"Date and time (RFC3339).
timeformat:"time"Time of day (RFC3339 full-time, e.g. 15:04:05Z).
durationformat:"duration"Go duration (e.g. 1h30m, 300ms).

Network, web & identifiers​

FormatTagDescription
emailformat:"email"Valid email address.
hostnameformat:"hostname"Valid hostname.
ipv4format:"ipv4"Valid IPv4 address.
ipv6format:"ipv6"Valid IPv6 address.
macformat:"mac"Valid MAC address.
cidrformat:"cidr"CIDR notation (e.g. 192.168.1.0/24).
uriformat:"uri"Valid URI (any scheme).
uri-referenceformat:"uri-reference"URI reference (relative references allowed).
urlformat:"url"Absolute URL using the http or https scheme.
uuidformat:"uuid"Valid UUID.
ulidformat:"ulid"Valid ULID.
e164 / phoneformat:"e164"Phone number in E.164 format (e.g. +14155552671).
credit-cardformat:"credit-card"Credit card number (passes the Luhn checksum).
semverformat:"semver"Semantic version (e.g. 1.2.3-alpha.1).
json-pointerformat:"json-pointer"JSON Pointer (RFC 6901).
byte / base64format:"byte"Base64-encoded value.
base64urlformat:"base64url"URL-safe Base64 (padded or unpadded).
jwtformat:"jwt"JSON Web Token (three base64url segments).
portformat:"port"TCP/UDP port number (1–65535).

String content​

FormatTagDescription
alphaformat:"alpha"Letters only (a–z, A–Z).
alphanumericformat:"alphanumeric"Letters and digits only.
numericformat:"numeric"A numeric string (e.g. 123, -12.5).
asciiformat:"ascii"ASCII characters only.
lowercaseformat:"lowercase"No uppercase characters.
uppercaseformat:"uppercase"No lowercase characters.
slugformat:"slug"URL slug (e.g. my-post-123).
hexcolorformat:"hexcolor"Hex color (#RGB or #RRGGBB).
jsonformat:"json"A syntactically valid JSON string.

Geo & time zones​

FormatTagDescription
latitudeformat:"latitude"Decimal latitude in the range -90 to 90.
longitudeformat:"longitude"Decimal longitude in the range -180 to 180.
timezoneformat:"timezone"IANA time zone name (e.g. America/New_York).

Custom pattern​

FormatTag / AttributeDescription
regexformat:"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" exclusiveMin:"0" max:"120" default:"18"`
Website string `json:"website" format:"url"`
Kind string `json:"kind" const:"user"`
Roles []string `json:"roles" minItems:"1" uniqueItems:"true" enum:"admin,editor,viewer"`
Metadata map[string]string `json:"metadata" minProperties:"1" maxProperties:"10"`
}

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 Accept header requested by the client, defaulting to application/json.

Input Sources​

Okapi can bind data from multiple sources based on struct tags:

TagSourceExample
jsonRequest bodyjson:"name"
formForm dataform:"name"
queryQuery parametersquery:"page"
pathPath parameterspath:"id"
headerRequest headersheader:"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:

MethodDescription
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)
}
}