Skip to content

Binding and Validation

Request structs describe what generated code binds before a handler runs.

type GetProjectRequest struct {
ProjectID project.ID `param:"projectId" validate:"required"`
Actor actor.ID `local:"actor" validate:"required"`
}

Anvil binds request fields, runs generated validation, then calls the handler. Invalid input returns an expected sdk.Failure through the global error pipeline. HTTP drivers write that failure with the HTTP status carried by the failure.

The status depends on the failure point:

  • Text binding, missing locals, invalid params, invalid query values, invalid headers, invalid body bytes, and generated validation failures use 400 Bad Request.
  • Unsupported request media types use 415 Unsupported Media Type.
  • The standard net/http driver returns 413 Request Entity Too Large when MaxBodyBytes is exceeded while the body is read. Other HTTP drivers use their own native body-limit behavior before Anvil reaches the handler.

Supported binding sources:

TagSource
param:"projectId":projectId path segment
query:"page"query string value
header:"X-Request-ID"request header
local:"actor"middleware local value
json:"name"decoded request body field name used by manifests and OpenAPI

Param, query, and header fields are converted from text. Local fields are read from ctx.Locals() and assigned by exact Go type. Exported non-anonymous request fields without param, query, header, or local tags are treated as body fields. The generated handler asks ctx.Request().Decode(&req) to decode the whole request model through the driver’s codec registry.

Generated routes reject unsupported text binding targets during compilation. The runtime binder supports:

  • encoding.TextUnmarshaler, checked before built-in scalar parsing.
  • Strings.
  • Booleans parsed with strconv.ParseBool.
  • Signed and unsigned integers parsed as base-10 numbers.
  • float32 and float64.

xml tags are not Anvil binding tags. They still matter to the XML codec at runtime because encoding/xml reads them while decoding the body. Anvil uses json tags for body field names in the manifest and OpenAPI output.

Route paths use Go web-framework style params:

Routes struct {
Get sdk.GET `path:"/:projectId"`
}

The request struct binds the same name:

type GetProjectRequest struct {
ProjectID uuid.UUID `param:"projectId" validate:"required,uuid"`
}

Binding the path param is optional at runtime. A handler can always read the raw string with ctx.Request().Param("projectId"). Add a request field with param:"projectId" when you want typed conversion, validation, manifest metadata, OpenAPI parameter output, and generated testbed inputs.

The compiler validates path syntax, but it does not require every route parameter to appear in the request model. Leaving a param unbound is allowed; it simply means Anvil will not type-convert, validate, or describe that param for tooling.

Path, query, header, and cookie string values can bind into:

  • Strings.
  • Booleans.
  • Signed and unsigned integer types.
  • Floating-point types.
  • Custom types implementing encoding.TextUnmarshaler.
func (id *ID) UnmarshalText(value []byte) error

Cookie bindings use the same scalar conversion:

type RefreshSessionRequest struct {
Session string `cookie:"session" validate:"required"`
}

Use ctx.Request().Cookie("session") when the handler needs the raw value without generated binding.

Unsupported binding targets are compiler diagnostics for generated routes. If application code calls the lower-level binder directly, unsupported targets return an ordinary error instead.

Local bindings are different:

type Request struct {
Actor actor.ID `local:"actor" validate:"required"`
}

The middleware must store the same concrete type:

ctx.Locals().Set("actor", actorID)

If the local is missing or has the wrong type, the generated binding returns a 400 Bad Request failure before the handler runs.

Body decoding is driver-owned. Drivers install codecs for media types. The standard Anvil codec registry includes JSON and XML, with JSON used when Content-Type is empty.

The default registry recognizes:

CodecMedia Types
JSONapplication/json
XMLapplication/xml, text/xml

An unsupported Content-Type returns 415 Unsupported Media Type. A body that cannot be decoded by the selected codec returns 400 Bad Request. In the standard net/http driver, requests over MaxBodyBytes return 413 Request Entity Too Large when the body is read. Fiber enforces its own BodyLimit before Anvil’s handler code runs.

Handlers can still use raw request data when needed:

payload := ctx.Request().Body()
if err := verifySignature(ctx.Request().Header("X-Signature"), payload); err != nil {
return nil, ctx.Errors().Failure(http.StatusForbidden, "invalid signature")
}

Examples:

type CreateProjectRequest struct {
Name string `json:"name" validate:"required,min=2,max=80"`
Email string `json:"email" validate:"email"`
Page int `query:"page" validate:"gte=1,lte=100"`
Invite string `json:"invite" validate:"len=6"`
Slug string `json:"slug" validate:"regex=^[a-z0-9-]+$"`
Kind string `json:"kind" validate:"oneof=public|private"`
Password string `json:"password" validate:"required,min=12"`
Confirm string `json:"confirm" validate:"eqfield=Password"`
}

Supported generated rules:

  • required
  • email
  • uuid
  • url
  • oneof=a|b|c
  • regex=expr
  • eqfield=Field
  • nefield=Field
  • len=N
  • min=N
  • max=N
  • gt=N
  • gte=N
  • lt=N
  • lte=N

Validation runs after binding and before the handler.

There is no omitempty rule. Optional string format checks such as email, uuid, url, oneof, and regex only run when the string is not empty. Use required when the field must be present.

Format rule details:

  • email uses Go’s net/mail.ParseAddress.
  • uuid accepts RFC 4122-style UUID strings with versions 1 through 5.
  • url accepts absolute http and https URLs with a host. Relative URLs and custom schemes are rejected.
  • regex is compiled by Go’s regexp package.

Go’s regexp package uses RE2-style regular expressions with linear-time matching. Validation regexes compile at generation time; invalid patterns are compiler diagnostics.

Generated code stores validation regexes as package-level compiled values, so normal request handling uses the compiled pattern directly. Keep regex rules for bounded structural checks. Use a custom type with UnmarshalText or request-level Validate when the rule needs domain logic.

Use Validate(ctx sdk.Ctx) error for cross-field or domain checks:

func (req ScheduleRequest) Validate(ctx sdk.Ctx) error {
if req.StartsAt.After(req.EndsAt) {
return ctx.Errors().Validation().
Field("startsAt", "must be before end").
Err()
}
return nil
}

The compiler records validateRequest: true in the manifest when the request type has a valid Validate(ctx sdk.Ctx) error method.

Generated validation errors use ctx.Errors().Validation() and produce:

  • HTTP status: 400 Bad Request
  • Expected: true
  • Fields: one message per failed field
  • Context phase: generated validation and request-level Validate use the route handler context; binding helpers use bind; codec failures use decode.

Drivers convert that failure to protocol-native responses.

Anvil rejects invalid request models before runtime:

  • Duplicate binding keys in the same request type
  • Conflicting binding tags on one field
  • Empty binding names
  • Unsupported scalar target types
  • Validation rules on incompatible field kinds
  • Invalid regex patterns
  • Empty oneof values
  • Negative exact lengths
  • Cross-field validation against missing, unexported, non-comparable, or incompatible fields

Diagnostics include the source file, line, code, message, and hint when Anvil can produce one.