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/httpdriver returns413 Request Entity Too LargewhenMaxBodyBytesis exceeded while the body is read. Other HTTP drivers use their own native body-limit behavior before Anvil reaches the handler.
Binding Tags
Section titled “Binding Tags”Supported binding sources:
| Tag | Source |
|---|---|
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.
float32andfloat64.
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.
Path Params
Section titled “Path Params”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.
Scalar Conversion
Section titled “Scalar Conversion”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) errorCookie 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
Section titled “Body Decoding”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:
| Codec | Media Types |
|---|---|
| JSON | application/json |
| XML | application/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")}Validation Tags
Section titled “Validation Tags”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:
requiredemailuuidurloneof=a|b|cregex=expreqfield=Fieldnefield=Fieldlen=Nmin=Nmax=Ngt=Ngte=Nlt=Nlte=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:
emailuses Go’snet/mail.ParseAddress.uuidaccepts RFC 4122-style UUID strings with versions1through5.urlaccepts absolutehttpandhttpsURLs with a host. Relative URLs and custom schemes are rejected.regexis compiled by Go’sregexppackage.
Regex Safety
Section titled “Regex Safety”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.
Request-Level Validation
Section titled “Request-Level Validation”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.
Error Output
Section titled “Error Output”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
Validateuse the route handler context; binding helpers usebind; codec failures usedecode.
Drivers convert that failure to protocol-native responses.
Compiler Diagnostics
Section titled “Compiler Diagnostics”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
oneofvalues - 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.