Validation
After binding, ezz runs the validate tags on your request struct. If anything fails, the request stops with a 400 and your handler is never called.
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Website string `json:"website" validate:"omitempty,url"`
Age int `json:"age" validate:"min=13,max=130"`
Code string `json:"code" validate:"len=6"`
}
Available rules
| Rule | Applies to | Meaning |
|---|---|---|
required | any | Value must not be the zero value |
min=n | string, slice, map | Length is at least n |
min=n | numbers | Value is at least n |
max=n | string, slice, map | Length is at most n |
max=n | numbers | Value is at most n |
len=n | string, slice, map | Length is exactly n |
email | string | Matches an email pattern |
url | string | Starts with http:// or https:// |
omitempty | any | Skip every other rule on this field when the value is the zero value |
Rules are comma-separated and evaluated in order. Every failing rule on every field is collected, so one response can report several problems at once.
validate:"uuid" or validate:"gte=5" will not fail the request, and it will not warn you. Stick to the supported set and do anything else in the handler.Make a field optional
required and the other rules treat the zero value differently, which trips people up. An email rule on an empty string fails, because the empty string is not a valid email. When a field is genuinely optional, add omitempty first:
type UpdateProfileRequest struct {
// optional, but must be a valid email when supplied
Email string `json:"email" validate:"omitempty,email"`
// optional, but between 2 and 50 characters when supplied
Nickname string `json:"nickname" validate:"omitempty,min=2,max=50"`
}
omitempty is a marker rather than a check. When the field holds its zero value, the remaining rules on that field are skipped entirely.
Validate nested structs
Nested structs and non-nil pointers to structs are walked recursively, whether or not the parent field carries a validate tag. Error field names are prefixed with the path:
type Address struct {
City string `json:"city" validate:"required"`
Zip string `json:"zip" validate:"len=5"`
}
type CreateOrderRequest struct {
Ship Address `json:"ship"`
Bill *Address `json:"bill"`
}
A missing city on the shipping address reports as field 'Ship.City' is required. A nil Bill pointer is skipped, so mark it required if the caller must supply it.
Read the error response
Failures are returned as a single 400 with the messages joined by ; :
{
"error": true,
"message": "field 'Name' is required; field 'Email' must be a valid email",
"status": 400
}
Field names in messages are the Go field names, not the JSON names.
If you need a different error shape, validate manually in the handler and return your own error. binding.Validate gives you the structured form:
import "github.com/sulv-io/ezz/binding"
app.POST("/users", ezz.H(func(ctx *ezz.Context, req CreateUserRequest) (any, error) {
if err := binding.Validate(req); err != nil {
var verrs binding.ValidationErrors
if errors.As(err, &verrs) {
fields := make(map[string]string, len(verrs))
for _, e := range verrs {
fields[e.Field] = e.Message
}
return nil, ctx.JSON(http.StatusUnprocessableEntity, map[string]any{
"error": true,
"fields": fields,
})
}
}
return store.Create(req), nil
}))
Each binding.ValidationError carries Field, Tag, Value, and Message.
Add rules the tags cannot express
Business rules belong in the handler, where you have your dependencies:
app.POST("/users", ezz.H(func(ctx *ezz.Context, req CreateUserRequest) (*User, error) {
if store.EmailTaken(ctx, req.Email) {
return nil, ezz.Conflict("email already registered")
}
if !passwordStrongEnough(req.Password) {
return nil, ezz.UnprocessableEntity("password is too weak")
}
return store.Create(ctx, req)
}))
See the rules in your documentation
The OpenAPI generator reads the same tags, so constraints appear in the spec without repeating yourself:
| Tag | Schema output |
|---|---|
min / max on a string or array | minLength / maxLength |
min / max on a number | minimum / maximum |
len | minLength and maxLength set to the same value |
email | format: email |
url | format: uri |
required on a query or header field | Marks the parameter required |