๐Ÿ“– 3 min read (~ 500 words).

Validation hooks

OpenAPI specifies most validation declaratively (required fields, pattern, min/max, enum, etc.). go-swagger turns those rules into code on the generated model types via two interfaces:

type Validatable interface {
    Validate(strfmt.Registry) error
}

type ContextValidatable interface {
    ContextValidate(context.Context, strfmt.Registry) error
}

Both live in the root runtime package โ€” see runtime.Validatable and runtime.ContextValidatable for the authoritative definitions. The strfmt.Registry argument carries the active string-format registry (date-time, UUID, โ€ฆ) so format-aware validation has access to it.

ContextValidatable is the context-aware version; it should be preferred in new code because some validations (read-only / write-only flags, async-driven cross-field checks) genuinely need request scope.

When the runtime calls them

Server-side, validation runs as part of Context.BindValidRequest, which fires after security and just after parameter binding:

flowchart TD
    sec["Security ยท Context.Authorize<br/>Authenticator โ†’ Authorizer"]
    bind["Binder<br/>Consumer decodes body into the parameter struct"]
    val["Validator (per parameter)<br/>1. spec-driven validation (required, pattern, โ€ฆ)<br/>2. if Validatable: Validate(formats)<br/>3. if ContextValidatable: ContextValidate(ctx, formats)"]
    err{{"errors.CompositeValidationError<br/>aggregates every parameter-level violation<br/>(does not stop on first failure)"}}

    sec --> bind --> val
    val -. on error .-> err

Two consequences worth being aware of:

  • Multiple errors per parameter set. A request with three invalid fields produces a CompositeValidationError containing three entries, not a single one.
  • Both layers run. Implementing Validatable does not turn off spec-driven validation; the two layers compose. Use Validatable for rules the spec cannot express (cross-field invariants, business rules).

Client-side, generated request models implement the same interfaces, and the generated Validate method runs before the body is serialised โ€” a malformed payload fails locally instead of producing a server-side 422.

Custom validation in your own types

Most users never write these by hand โ€” they fall out of swagger generate. But for hand-rolled types you can add cross-field checks like this:

// DateRange illustrates a cross-field invariant on a hand-written type.
type DateRange struct {
	From strfmt.Date `json:"from"`
	To   strfmt.Date `json:"to"`
}

// Validate enforces that To is not before From. The strfmt.Registry
// argument is unused here because the rule does not involve any
// registered string format.
func (d DateRange) Validate(_ strfmt.Registry) error {
	if time.Time(d.To).Before(time.Time(d.From)) {
		return errors.New("DateRange.to must not be before DateRange.from")
	}
	return nil
}

Full source: docs/examples/core/validation/main.go

For checks that depend on the request:

// ContextValidate enforces that on_behalf_of is only set when the
// request context carries an authenticated user.
func (r MyRequest) ContextValidate(ctx context.Context, _ strfmt.Registry) error {
	if reqUser(ctx) == "" && r.OnBehalfOf != "" {
		return errors.New("on_behalf_of is only valid when authenticated")
	}
	return nil
}

Full source: docs/examples/core/validation/main.go

Strfmt registry

Both methods take a strfmt.Registry, which is how the runtime carries named formats (date-time, uuid, email, โ€ฆ) into the validator. You rarely build one by hand โ€” the server’s *Context and the client Runtime each carry one and pass it down. To register a custom format (x-go-type style), call strfmt.Default.Add(...) once at startup; the default registry is what both sides use unless overridden.