docs/guide/validation.md
Fiber does not bundle a validation library. Instead, Bind accepts any validator you plug into fiber.Config.StructValidator and runs it automatically whenever request data is bound onto a struct. The core stays dependency-free, and validation becomes a one-time setup instead of per-handler boilerplate.
The steps below use go-playground/validator, the most common choice in the Go ecosystem, but any library works as long as you wrap it in the small adapter from step 2.
go get github.com/go-playground/validator/v10
StructValidator expects a single Validate(out any) error method, so wrap the library in a tiny adapter and register it once:
import "github.com/go-playground/validator/v10"
type structValidator struct {
validate *validator.Validate
}
// Validator needs to implement the Validate method
func (v *structValidator) Validate(out any) error {
return v.validate.Struct(out)
}
// Set up your validator in the config
app := fiber.New(fiber.Config{
StructValidator: &structValidator{validate: validator.New()},
})
:::note
StructValidator runs only for struct destinations (or pointers to structs). Binding into maps and other non-struct types skips validation.
:::
Tag your structs with the validator's rules. Every bind method (Body, Query, Form, and the rest) now triggers validation and returns its errors alongside binding errors:
type User struct {
Name string `json:"name" form:"name" query:"name" validate:"required"`
Age int `json:"age" form:"age" query:"age" validate:"gte=0,lte=100"`
}
app.Post("/", func(c fiber.Ctx) error {
user := new(User)
// Works with all bind methods: Body, Query, Form, ...
if err := c.Bind().Body(user); err != nil { // validation errors are returned here
return err
}
return c.JSON(user)
})
Returned as-is, a validation error surfaces through Fiber's error handling, so you can also shape it globally in a custom error handler.
For field-level feedback, unwrap validator.ValidationErrors and answer with a structured body instead of the default text:
type User struct {
Name string `json:"name" validate:"required,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=100"`
Password string `json:"password" validate:"required,min=8"`
Website string `json:"website" validate:"url"`
}
app.Post("/user", func(c fiber.Ctx) error {
user := new(User)
if err := c.Bind().Body(user); err != nil {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
out := make([]fiber.Map, 0, len(validationErrors))
for _, e := range validationErrors {
// e.Field() - field name, e.Tag() - failed rule,
// e.Param() - rule parameter, e.Value() - invalid value
out = append(out, fiber.Map{
"field": e.Field(),
"rule": e.Tag(),
})
}
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"errors": out})
}
return err
}
return c.JSON(user)
})
Because the adapter is yours, you can layer custom checks on top of the tag rules:
// Custom validator for password strength
type PasswordValidator struct {
validate *validator.Validate
}
func (v *PasswordValidator) Validate(out any) error {
if err := v.validate.Struct(out); err != nil {
return err
}
// Custom password validation logic
if user, ok := out.(*User); ok {
if len(user.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
// Add more password validation rules here
}
return nil
}
// Usage
app := fiber.New(fiber.Config{
StructValidator: &PasswordValidator{validate: validator.New()},
})