Skip to content

Guide

Binding

Decode path parameters, query strings, headers, and request bodies into typed Go structs, then validate them.

Binding turns request data into a typed struct in one call. Struct tags say where each field comes from, and Zinc converts strings to the field types for you.

type ListOrders struct {
Customer int `path:"customer"`
Status string `query:"status"`
Page int `query:"page"`
}
app.Get("/customers/{customer}/orders", func(c *zinc.Context) error {
var in ListOrders
if err := c.Bind().All(&in); err != nil {
return zinc.ErrBadRequest.WithMessage("invalid request").WithCause(err)
}
return c.JSON(in)
})

GET /customers/7/orders?status=open&page=2 fills Customer: 7, Status: "open", and Page: 2.

Tag Source Bind method
path:"id" Route parameters Path
query:"page" Query string Query
header:"x-tenant" Request headers Header
form:"name" URL-encoded or multipart form Form
json, xml, yaml, toml Request body JSON, XML, YAML, TOML

c.Bind().All(&in) is the usual choice for API handlers. It binds, in order:

  1. route parameters,
  2. query values,
  3. the body, choosing JSON, XML, YAML, TOML, text, or form decoding from Content-Type,

and then runs your validator, if one is configured.

type CreateOrder struct {
Customer int `path:"customer"`
DryRun bool `query:"dry_run"`
Items []string `json:"items"`
Note string `json:"note"`
}

When a handler should accept input from exactly one place, name it:

var in CreateOrder
if err := c.Bind().Path(&in); err != nil {
return zinc.ErrBadRequest.WithCause(err)
}
if err := c.Bind().JSON(&in); err != nil {
return zinc.ErrBadRequest.WithCause(err)
}

The available methods are All, Path, Query, Header, Form, Body (chosen by Content-Type), and the explicit body formats JSON, XML, YAML, TOML, and Text.

A failed bind returns a *zinc.BindError that names the source and field:

var be *zinc.BindError
if errors.As(err, &be) {
// be.Source is "path", "query", "header", "form", or "body"; be.Field is the struct field.
}

Zinc does not ship a validator. Plug in any library by implementing one method:

type Validator interface {
Validate(any) error
}

For example, with go-playground/validator:

type structValidator struct{ v *validator.Validate }
func (s structValidator) Validate(target any) error {
return s.v.Struct(target)
}
cfg := zinc.DefaultConfig
cfg.Validator = structValidator{v: validator.New()}
app := zinc.NewWithConfig(cfg)
type SignUp struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=12"`
}

Every bind method runs the validator after decoding, so handlers stay short. Validation errors come back from the bind call; wrap or map them the same way as binding errors, often as 422 Unprocessable Entity.

Multipart file fields bind directly:

type UploadInput struct {
Title string `form:"title"`
Avatar *multipart.FileHeader `form:"avatar"`
Files []*multipart.FileHeader `form:"files"`
}

Both multipart.FileHeader and *multipart.FileHeader work, as single values or slices.

Binding reads at most Config.BodyLimit bytes, 4 MB by default. Larger bodies return 413 Request Entity Too Large. Raise or lower the limit in configuration, or per route with the Body Limit middleware.

  • Errors turns binding and validation failures into consistent responses.
  • Request Data reads single values without a struct.
  • Binding API documents RequestBinder for replacing the decoder.

Struct targets passed to All consistently merge path, query, then body for JSON, XML, YAML, and TOML. Scalar/map YAML and TOML targets and plain text remain body-only. Validation runs once after the combined bind. Source-specific operations each validate immediately; use All for the supported combined phase or a custom RequestBinder when composing other sources.

Path, query, header, and form scalar fields support encoding.TextUnmarshaler, including time.Time and custom IDs. Optional scalar pointers stay nil when absent and are allocated when present; explicit zero values remain distinguishable from absence. Conversion errors retain their source and field. Use request DTOs: exported untagged fields still participate, and later All sources can overwrite earlier values.