Documentation
¶
Overview ¶
Package oapi turns typed Go handlers into HTTP endpoints whose request and response structs are the single source of truth for binding, validation and OpenAPI 3 documentation.
The core is framework-agnostic: handlers, middleware, the response envelope and the OpenAPI generator all operate over the small Carrier seam, never a concrete web framework. It depends only on what every route needs (binding + OpenAPI generation) and on no validation library. The net/http adapter ships with the core; each other transport lives in its own module so a consumer pulls in only the one it imports:
- github.com/antlss/oapi/adapter/nethttp — net/http (ships with the core, no extra dependencies)
- github.com/antlss/oapi/adapter/gin — gin
- github.com/antlss/oapi/adapter/fiber — Fiber v2
- github.com/antlss/oapi/adapter/chi — go-chi/chi v5
- github.com/antlss/oapi/adapter/echo — Echo v4
Validation is a pluggable seam (Validator + SetValidator); the library ships no validator. A ready go-playground/validator implementation is provided as reference code in the examples module (examples/validation).
Index ¶
- Variables
- func APIKeyAuth(in, name string) *openapi3.SecurityScheme
- func BearerAuth() *openapi3.SecurityScheme
- func LoadBaseFile(path string) (*openapi3.T, error)
- func SetErrorParser(p ErrorParser)
- func SetResponseEnvelope(e ResponseEnvelope)
- func SetValidator(v Validator)
- func WireName(field reflect.StructField, source string) string
- type App
- type Carrier
- type Error
- type ErrorBody
- type ErrorMapper
- type ErrorParser
- type FieldError
- type GenConfig
- type HTTPCarrier
- func (c *HTTPCarrier) Abort()
- func (c *HTTPCarrier) Body() ([]byte, error)
- func (c *HTTPCarrier) Cleanup()
- func (c *HTTPCarrier) ContentType() string
- func (c *HTTPCarrier) Context() context.Context
- func (c *HTTPCarrier) Errors() []error
- func (c *HTTPCarrier) Header(name string) string
- func (c *HTTPCarrier) HeaderValues(name string) []string
- func (c *HTTPCarrier) Method() string
- func (c *HTTPCarrier) MultipartForm() (*multipart.Form, error)
- func (c *HTTPCarrier) Query() url.Values
- func (c *HTTPCarrier) RecordError(err error)
- func (c *HTTPCarrier) SetContext(ctx context.Context)
- func (c *HTTPCarrier) SetHeader(key, value string)
- func (c *HTTPCarrier) WriteBytes(status int, contentType string, data []byte) error
- func (c *HTTPCarrier) WriteEmpty(status int) error
- func (c *HTTPCarrier) WriteJSON(status int, body any) error
- type HTTPError
- type KeyedEnvelope
- type Middleware
- type Option
- type PagingMeta
- type Registry
- func (rg *Registry) Add(routes ...Route) *Registry
- func (rg *Registry) AddSecurityScheme(name string, scheme *openapi3.SecurityScheme) *Registry
- func (rg *Registry) AddServer(url, description string) *Registry
- func (rg *Registry) AddTag(name, description string) *Registry
- func (rg *Registry) Base(doc *openapi3.T) *Registry
- func (rg *Registry) Contact(name, url, email string) *Registry
- func (rg *Registry) Describe(description string) *Registry
- func (rg *Registry) ExternalDocs(description, url string) *Registry
- func (rg *Registry) JSON() ([]byte, error)
- func (rg *Registry) License(name, url string) *Registry
- func (rg *Registry) Logo(url string) *Registry
- func (rg *Registry) LogoWith(logo map[string]any) *Registry
- func (rg *Registry) OpenAPI() *openapi3.T
- func (rg *Registry) Routes() []Route
- func (rg *Registry) SpecBytesOnce() func() ([]byte, error)
- func (rg *Registry) TagGroup(name string, tags ...string) *Registry
- func (rg *Registry) TermsOfService(url string) *Registry
- func (rg *Registry) UseComponents() *Registry
- func (rg *Registry) Validate(ctx context.Context) error
- func (rg *Registry) Write(ctx context.Context, cfg GenConfig) ([]string, error)
- func (rg *Registry) YAML() ([]byte, error)
- type Request
- type RequestHandler
- type ResponseEnvelope
- type Result
- func (r *Result) WithError(err error) *Result
- func (r *Result) WithFile(filename string) *Result
- func (r *Result) WithHeader(key, value string) *Result
- func (r *Result) WithMeta(meta any) *Result
- func (r *Result) WithPaging(count int64, perPage, current int) *Result
- func (r *Result) WithStatus(status int) *Result
- type RichHandler
- type Route
- func NewBodyRoute[Body, Response any](method, path string, ...) Route
- func NewParamRoute[Param, Response any](method, path string, ...) Route
- func NewQueryRoute[Query, Response any](method, path string, ...) Route
- func NewRichRoute[Header, Param, Query, Body any](method string, path string, handler RichHandler[Header, Param, Query, Body], ...) Route
- func NewRoute[Header, Param, Query, Body, Response any](method string, path string, ...) Route
- func (route Route) Description() string
- func (route Route) Invoke(c Carrier)
- func (route Route) MaxRequestBytes() (limit int64, ok bool)
- func (route Route) MaxRequestBytesOr(fallback int64) int64
- func (route Route) Method() string
- func (route Route) Path() string
- func (route Route) Schema() RouteSchema
- func (route Route) SuccessStatus() int
- func (route Route) Summary() string
- func (route Route) Tags() []string
- type RouteOption
- func WithApp(a *App) RouteOption
- func WithBinaryResponse(contentType, description string) RouteOption
- func WithDeprecated() RouteOption
- func WithDescription(description string) RouteOption
- func WithEnvelope(e ResponseEnvelope) RouteOption
- func WithErrorMapper(mapper ErrorMapper) RouteOption
- func WithMetaType[T any]() RouteOption
- func WithRawResponse() RouteOption
- func WithResponse[T any](status int, description string) RouteOption
- func WithResponseType[T any]() RouteOption
- func WithSecurity(scheme string, scopes ...string) RouteOption
- func WithSuccessStatus(status int) RouteOption
- func WithSummary(summary string) RouteOption
- func WithTags(tags ...string) RouteOption
- func WithTypedBefore[Header, Param, Query, Body any](middlewares ...Middleware[Header, Param, Query, Body]) RouteOption
- type RouteSchema
- type Validator
Constants ¶
This section is empty.
Variables ¶
var DataEnvelope = KeyedEnvelope{DataKey: "data", MetaKey: "meta"} //nolint:exhaustruct,gochecknoglobals
DataEnvelope is the library default: the standard {"data": ...} envelope, with a "meta" key added when the route documents/attaches meta. A fresh install behaves exactly as the library did before envelopes were pluggable.
var RuleTag = "binding"
RuleTag is the struct tag the library reads for validation rules. It is the single name shared by two readers: the OpenAPI generator (which turns the rules into schema constraints — required/enum/format/bounds) and the configured Validator (which enforces them at runtime). It defaults to "binding" (gin's convention); set it ONCE at startup to match your project — e.g. "validate" (the go-playground/validator default) or "validation" — before constructing routes and the validator. Source-location tags (header/uri/form/ json) are separate and not affected.
Functions ¶
func APIKeyAuth ¶
func APIKeyAuth(in, name string) *openapi3.SecurityScheme
APIKeyAuth is an API-key security scheme carried in the given location ("header", "query" or "cookie") under the given parameter name.
func BearerAuth ¶
func BearerAuth() *openapi3.SecurityScheme
BearerAuth is an HTTP bearer-token security scheme (e.g. JWT).
func LoadBaseFile ¶
LoadBaseFile reads an OpenAPI document (JSON or YAML) from disk for use as a Registry base via Base, returning the parsed document or an error so a missing/invalid file is handled explicitly:
base, err := oapi.LoadBaseFile("openapi/common.json")
if err != nil { ... }
reg.Base(base)
func SetErrorParser ¶
func SetErrorParser(p ErrorParser)
SetErrorParser installs the process-wide ErrorParser. Call it once during startup, before serving requests. Passing nil clears it (errors fall back to the built-in handling). The swap is atomic, so calling it while requests are in flight is safe.
A parser whose ErrorType is nil cannot describe its body for the docs; since an all-in-one parser almost always renders a custom shape, this logs a one-time warning to catch the drift early.
func SetResponseEnvelope ¶
func SetResponseEnvelope(e ResponseEnvelope)
SetResponseEnvelope installs the process-wide ResponseEnvelope used to shape every success response that does not override it per route. Call it once during startup, before serving. Passing nil restores the default DataEnvelope.
The swap is atomic, so calling it while requests are in flight is safe.
func SetValidator ¶
func SetValidator(v Validator)
SetValidator installs the process-wide Validator used to check every bound request. Call it once during startup, before serving requests. Passing nil disables validation explicitly (and silences the "no validator configured" warning), which is useful when a service validates elsewhere.
The swap is atomic, so calling it while requests are in flight is safe (each request reads either the old or the new validator, never a torn value).
func WireName ¶
func WireName(field reflect.StructField, source string) string
WireName returns the name a request field is bound under for the given source ("header", "uri", "form" or "json") — the name clients actually use — falling back to the Go field name when the tag is absent or "-". Validator implementations use it so the field names they report match the binding source, exactly as the OpenAPI parameters and schema do.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is an immutable bundle of the configuration the library otherwise reads from process-wide globals — the Validator, the success ResponseEnvelope, the ErrorParser and the request body cap. Build one with New, attach it to routes with WithApp, and every request those routes serve reads its configuration from the App rather than the globals.
An App makes configuration explicit, thread-safe and composable: two Apps with different validators or envelopes can serve in the same process without racing on shared globals, and a test can run with its own App without disturbing the rest of the program. The process-wide SetValidator/SetResponseEnvelope API keeps working unchanged for routes built without an App (they read the globals at request time, exactly as before).
An App is safe for concurrent use: its configuration is snapshotted at New time and never mutated afterwards.
Scope note: the RuleTag is intentionally process-wide — it is a reflection-time struct-tag name read by the doc generator, not per-request state — so set the exported RuleTag variable directly rather than per App. Everything that varies per request (validator, response envelope, error parser and request body cap) is App-scoped.
func New ¶
New builds an immutable App. It snapshots the current process-wide defaults (whatever SetValidator/SetResponseEnvelope last installed) as the baseline, then applies opts, so an App is fully self-contained: later global Set* calls do not change an App that already exists. Call it once at startup, before serving.
type Carrier ¶
type Carrier interface {
// Method returns the request method (for diagnostics; routing is the
// adapter's job).
Method() string
// Header returns the first value of the named request header, or "".
// Lookup is case-insensitive (canonicalized) on every adapter.
Header(name string) string
// HeaderValues returns every value of the named request header.
HeaderValues(name string) []string
// Param returns the named path parameter, or "".
Param(name string) string
// Query returns the parsed, multi-value-safe query string.
Query() url.Values
// ContentType returns the request media type with parameters (charset,
// boundary) stripped, e.g. "application/json".
ContentType() string
// Body returns the raw request body. Implementations cache it so it can be
// read more than once (a typed middleware and the handler may both bind it).
Body() ([]byte, error)
// MultipartForm parses and returns the multipart form.
MultipartForm() (*multipart.Form, error)
// SetHeader sets a response header. Must precede any Write* call.
SetHeader(key, value string)
// WriteJSON writes status and a JSON-encoded body.
WriteJSON(status int, body any) error
// WriteBytes writes status, content type and a raw body.
WriteBytes(status int, contentType string, data []byte) error
// WriteEmpty writes status with no body (e.g. 204).
WriteEmpty(status int) error
// Context returns the per-request context (carrying values injected by typed
// middleware).
Context() context.Context
// SetContext replaces the per-request context.
SetContext(ctx context.Context)
// Abort marks the request handled so adapter-side after-middleware is
// skipped. Called by the core when it renders an error.
Abort()
// RecordError records a non-fatal error for logging middleware to observe.
// Best-effort and adapter-defined (gin appends to c.Errors; others store it).
RecordError(err error)
}
Carrier is the adapter seam between the framework-agnostic core and a concrete HTTP transport (gin, net/http, fiber, ...). Each adapter implements it once, wrapping a single in-flight request/response. The core reads the request through the read-side methods, runs the typed handler chain, and writes the outcome through the write-side methods.
Contract:
- All SetHeader calls happen before the first Write* call.
- Exactly one of WriteJSON / WriteBytes / WriteEmpty is called per request.
- The core never calls Body and MultipartForm for the same request.
type Error ¶
type Error struct {
Status int `json:"-"`
Code string `json:"code,omitempty"`
Message string `json:"message"`
Fields []FieldError `json:"fields,omitempty"`
// contains filtered or unexported fields
}
Error is the library's built-in HTTPError, used for binding and validation failures and available to callers who want a simple status-carrying error.
func NewValidationError ¶
func NewValidationError(message string, fields []FieldError) *Error
NewValidationError builds the library's standard field-level 400 — the same {"error":{"code":"bad_request","message":...,"fields":[...]}} envelope the built-in binder produces. A Validator implementation should return this so custom validation renders identically to the library's own binding errors.
func (*Error) ErrorBody ¶
ErrorBody renders the error itself (code/message/fields) as the envelope body.
func (*Error) HTTPStatus ¶
type ErrorBody ¶
type ErrorBody interface {
ErrorBody() any
}
ErrorBody lets an HTTPError provide the exact JSON value placed under the "error" key of the response envelope. Return any json-marshalable value.
type ErrorMapper ¶
ErrorMapper maps an arbitrary error to an HTTP response for a single route (register it with WithErrorMapper). When ok is true it OWNS the full wire body: body is marshalled and written as-is, with no {"error": ...} wrapper, so a mapper controls the exact shape clients see. Return ok=false to defer to the global ErrorParser, then the built-in HTTPError/aerror recognition and the 500 fallback. body may be any json-marshalable value; a json.RawMessage is written verbatim.
type ErrorParser ¶
type ErrorParser interface {
// Resolve maps err to a status and the FULL json-marshalable wire body (no
// {"error": ...} wrapper is added). Return ok=false to defer to the next
// resolver. A [json.RawMessage] body is written verbatim.
Resolve(err error) (status int, body any, ok bool)
// ErrorType returns the Go type of the error body, whose `json`/`example` tags
// drive its documented schema — exactly like a response type. Return nil to
// document errors with the built-in {"error": {...}} schema (but then Resolve
// should also produce that shape, or the docs will not match).
ErrorType() reflect.Type
}
ErrorParser is the process-wide, all-in-one error seam — the error counterpart of Validator. It owns a project's error handling end to end: recognising the project's error types, mapping them to a status, producing the exact wire body, AND describing that body's shape for the OpenAPI docs (so error responses stay type-driven and cannot drift). Install one with SetErrorParser.
It runs after any per-route ErrorMapper and before the built-in HTTPError / aerror recognition and the 500 fallback, so unclaimed errors still get the library's safe, non-leaking default.
type FieldError ¶
type FieldError struct {
Field string `json:"field"`
Rule string `json:"rule,omitempty"`
Message string `json:"message,omitempty"`
}
FieldError describes a single request field that failed binding or validation.
type GenConfig ¶
type GenConfig struct {
Dir string // output directory; default "."
JSONFile string // JSON filename; default "openapi.json"; "-" disables JSON
YAMLFile string // YAML filename; default "openapi.yaml"; "-" disables YAML
NoValidate bool // skip the pre-write OpenAPI 3 validation (validation is on by default)
BaseFile string // optional base/common document (JSON or YAML) to overlay; see Base
}
GenConfig configures Registry.Write. The zero value is valid: an empty Dir, JSONFile or YAMLFile falls back to its default, so GenConfig{} writes openapi.json and openapi.yaml into the current directory after validating.
type HTTPCarrier ¶
type HTTPCarrier struct {
W http.ResponseWriter
R *http.Request
MaxBody int64 // request body cap in bytes; <= 0 means unlimited
// contains filtered or unexported fields
}
HTTPCarrier is the shared net/http implementation of Carrier used by every net/http-based adapter (nethttp, chi, echo). Those three carriers were ~90% byte-identical, and the library requires them to behave identically (the WriteJSON/Body/error bytes must match across adapters), so the implementation lives here once where the compiler keeps that invariant.
An adapter embeds *HTTPCarrier in its own carrier and seeds W, R and MaxBody at construction, overriding only the framework-specific methods — Param everywhere, plus SetContext for echo, whose request lives inside echo.Context rather than being owned directly. MaxBody <= 0 disables the request body cap.
func (*HTTPCarrier) Abort ¶
func (c *HTTPCarrier) Abort()
Abort is a no-op for net/http-style adapters: native middleware wraps the whole handler, so it cannot observe an abort from inside. The core calls it when rendering an error; gin uses it for real.
func (*HTTPCarrier) Body ¶
func (c *HTTPCarrier) Body() ([]byte, error)
func (*HTTPCarrier) Cleanup ¶
func (c *HTTPCarrier) Cleanup()
Cleanup removes any temp files net/http spilled to disk while parsing a multipart form. net/http never does this for you, so without it large uploads leak files into the temp dir for the lifetime of the process. Adapters call it in a deferred cleanup from their handler.
func (*HTTPCarrier) ContentType ¶
func (c *HTTPCarrier) ContentType() string
func (*HTTPCarrier) Context ¶
func (c *HTTPCarrier) Context() context.Context
func (*HTTPCarrier) Errors ¶
func (c *HTTPCarrier) Errors() []error
Errors exposes recorded errors for logging middleware.
func (*HTTPCarrier) Header ¶
func (c *HTTPCarrier) Header(name string) string
func (*HTTPCarrier) HeaderValues ¶
func (c *HTTPCarrier) HeaderValues(name string) []string
func (*HTTPCarrier) Method ¶
func (c *HTTPCarrier) Method() string
func (*HTTPCarrier) MultipartForm ¶
func (c *HTTPCarrier) MultipartForm() (*multipart.Form, error)
func (*HTTPCarrier) Query ¶
func (c *HTTPCarrier) Query() url.Values
func (*HTTPCarrier) RecordError ¶
func (c *HTTPCarrier) RecordError(err error)
func (*HTTPCarrier) SetContext ¶
func (c *HTTPCarrier) SetContext(ctx context.Context)
SetContext swaps the request's context. nethttp and chi own the request directly, so this default is enough; echo overrides it to also push the new request back into its echo.Context.
func (*HTTPCarrier) SetHeader ¶
func (c *HTTPCarrier) SetHeader(key, value string)
func (*HTTPCarrier) WriteBytes ¶
func (c *HTTPCarrier) WriteBytes(status int, contentType string, data []byte) error
func (*HTTPCarrier) WriteEmpty ¶
func (c *HTTPCarrier) WriteEmpty(status int) error
type HTTPError ¶
HTTPError is the contract an error can satisfy to control the HTTP response it produces. Any error returned by a handler or middleware that implements it is rendered with the given status; everything else falls back to 500. This is the only error abstraction the library depends on, which keeps the package free of any private error module.
Optionally implement ErrorBody to supply a custom JSON body; otherwise the standard {"error": {"code": ..., "message": ...}} envelope is used.
type KeyedEnvelope ¶
type KeyedEnvelope struct {
DataKey string // key the payload nests under; "" defaults to "data"
MetaKey string // key the meta object nests under; "" omits meta entirely
Constants map[string]any // fixed fields merged into every success body
}
KeyedEnvelope is the declarative, common-case ResponseEnvelope: it nests the payload under DataKey and the meta object under MetaKey, optionally merging a set of constant fields (e.g. {"success": true, "code": 0}). It expresses both the runtime body and the documented schema from one definition, so the two cannot diverge — and a project configures its envelope without ever importing openapi3.
oapi.SetResponseEnvelope(oapi.KeyedEnvelope{DataKey: "result", MetaKey: "meta",
Constants: map[string]any{"success": true}})
func (KeyedEnvelope) Wrap ¶
func (e KeyedEnvelope) Wrap(data, meta any) any
Wrap builds the success body: constant fields, then the payload under the data key (omitted when nil, matching the previous "data,omitempty" behaviour) and the meta object under the meta key (only when a meta key is configured and meta is non-nil).
func (KeyedEnvelope) WrapSchema ¶
func (e KeyedEnvelope) WrapSchema(data, meta *openapi3.SchemaRef) *openapi3.SchemaRef
WrapSchema mirrors Wrap key-for-key: the same constant/data/meta keys, so the documented schema always matches the wire body.
type Middleware ¶
type Middleware[Header, Param, Query, Body any] func( ctx context.Context, req Request[Header, Param, Query, Body], ) (context.Context, error)
Middleware is a typed, framework-agnostic middleware. Like a RequestHandler it operates on the already-parsed Request instead of a raw framework context, so it is easy to unit test and impossible to misuse the binding.
It may return a derived context.Context (e.g. carrying an authenticated user or resolved tenant) which is propagated to every downstream middleware and to the handler. Return the incoming ctx unchanged when there is nothing to add. A non-nil error aborts the chain and renders the error via the Result envelope, exactly like a handler error.
type Option ¶
type Option func(*appConfig)
Option configures an App at construction time.
func WithErrorParser ¶
func WithErrorParser(p ErrorParser) Option
WithErrorParser sets the ErrorParser this App's routes use to render AND document errors, scoping per App what SetErrorParser does process-wide. Passing nil disables the parser for the App (errors fall back to the built-in HTTPError/aerror recognition). Like the global setter, a parser whose ErrorType() is nil logs a one-time docs-drift warning.
func WithMaxRequestBytes ¶
WithMaxRequestBytes sets the request body cap (in bytes) an adapter applies for this App's routes; 0 disables the cap. Adapters read it via Route.MaxRequestBytes; until an adapter is App-aware it falls back to its own DefaultMaxRequestBytes.
func WithResponseEnvelope ¶
func WithResponseEnvelope(e ResponseEnvelope) Option
WithResponseEnvelope sets the default success ResponseEnvelope for this App's routes (a route may still override it with WithEnvelope/WithRawResponse). Passing nil restores the standard DataEnvelope.
func WithValidator ¶
WithValidator sets the Validator this App uses to check every bound request part. Passing nil disables validation for the App explicitly (and silences the "no validator configured" warning for its routes).
type PagingMeta ¶
type PagingMeta struct {
Count int64 `json:"count" example:"137"`
Pages int `json:"pages" example:"7"`
PerPage int `json:"per_page" example:"20"`
Current int `json:"current" example:"1"`
}
PagingMeta is the standard pagination metadata attached by Result.WithPaging under the envelope's "meta" key. It is exported so a route can document it with WithMetaType[PagingMeta](); the example:"" tags give the docs sample values.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry collects Routes and turns them into an OpenAPI 3 document. Because it reads the RouteSchema reflection types captured at NewRoute time, the docs are generated from the exact same Go types the handler binds — they cannot drift.
Registry is framework-agnostic: it produces bytes (JSON/YAML) and an *openapi3.T. Serve those with any adapter (e.g. adapter/gin's SpecHandler).
func NewRegistry ¶
NewRegistry creates a documentation registry with the given API title/version.
func (*Registry) AddSecurityScheme ¶
func (rg *Registry) AddSecurityScheme(name string, scheme *openapi3.SecurityScheme) *Registry
AddSecurityScheme registers a named security scheme (referenced by routes via WithSecurity). Use BearerAuth / APIKeyAuth for the common cases.
func (*Registry) AddServer ¶
AddServer adds a server entry (base URL + description) to the document.
func (*Registry) AddTag ¶
AddTag declares a top-level tag with a description. Declaration order controls the order tags appear in Redoc's navigation; calling it again for an existing tag name updates that tag's description.
func (*Registry) Base ¶
Base sets a base OpenAPI document the generator overlays onto: the base supplies defaults (info, externalDocs, vendor extensions such as x-tagGroups, …) while the generated paths/components and any value set through the Registry's own setters take precedence. Use LoadBaseFile to read one from disk (e.g. openapi/common.json). Pass nil to clear.
func (*Registry) Contact ¶
Contact sets the API contact (name/url/email) shown in the docs. Empty arguments are omitted from the spec.
func (*Registry) ExternalDocs ¶
ExternalDocs adds a link to external documentation shown near the top of the docs.
func (*Registry) Logo ¶
Logo sets a logo image URL shown in Redoc's sidebar via the x-logo extension (Swagger UI ignores it). Use LogoWith to also set background colour, alt text or a click-through href.
func (*Registry) LogoWith ¶
LogoWith sets the full Redoc x-logo object (keys: url, backgroundColor, altText, href), replacing any value set by Logo.
func (*Registry) OpenAPI ¶
OpenAPI builds the OpenAPI 3 document for every registered route.
When a base document is configured (see Base/LoadBaseFile) the generator starts from a copy of it and overlays the rest. Precedence is deterministic: the base supplies defaults; values set through the Registry override them when non-empty; and the generated paths/components are always merged in last.
func (*Registry) SpecBytesOnce ¶
SpecBytesOnce returns a closure that renders the document to indented JSON exactly once and caches the result (bytes or error) for every later call. It is the shared engine behind each adapter's SpecHandler: the adapter keeps only the few framework-specific lines that write the bytes, while the lazy render-once behaviour lives here next to JSON. Each call to SpecBytesOnce gets its own cache, so one per SpecHandler registration.
func (*Registry) TagGroup ¶
TagGroup defines a Redoc x-tagGroups navigation section grouping the named tags under a heading (Swagger UI ignores it).
func (*Registry) TermsOfService ¶
TermsOfService sets the URL of the API's terms of service.
func (*Registry) UseComponents ¶
UseComponents makes the generator emit each named response/data/meta/error struct type once under components/schemas and reference it by $ref wherever it appears, instead of inlining a copy at every use. This produces smaller, more idiomatic specs for APIs that share types across endpoints. It is opt-in: the default inlines every schema (so output is unchanged unless you call this). Request bodies stay inline, since they carry per-field binding constraints a shared response component must not.
func (*Registry) Validate ¶
Validate builds the document and checks it against the OpenAPI 3 schema. A nil return means the spec is well-formed and loadable by any standard tool.
func (*Registry) Write ¶
Write renders the OpenAPI document and writes it to disk as JSON and/or YAML, creating the output directory as needed. Unless NoValidate is set it validates the document first and returns an error without touching the filesystem when the spec is invalid, so a successful call guarantees well-formed artifacts. It returns the paths actually written, in a stable order (JSON then YAML).
When BaseFile is set and no base has been configured yet, it is loaded and applied via Base before generation.
type Request ¶
type Request[Header, Param, Query, Body any] struct { Header Header Param Param Query Query Body Body }
Request is the typed, framework-agnostic representation of an inbound HTTP request. Each part is bound from a different source:
Header -> `header:"..."` tags Param -> `uri:"..."` tags (path params) Query -> `form:"..."` tags (query string) Body -> `json:"..."` (JSON) or `form:"..."` (multipart / urlencoded) tags
Use struct{} for any part the endpoint does not consume.
type RequestHandler ¶
type RequestHandler[Header, Param, Query, Body, Response any] func( ctx context.Context, req Request[Header, Param, Query, Body], ) (*Response, error)
RequestHandler is the common handler shape: pure business logic over a typed request, returning a typed response pointer. nil response => 204 No Content.
type ResponseEnvelope ¶
type ResponseEnvelope interface {
// Wrap returns the value to JSON-marshal for a success response. data is the
// handler payload (nil for a body-less response); meta is whatever
// WithMeta/WithPaging attached (nil if none).
Wrap(data any, meta any) any
// WrapSchema mirrors Wrap for documentation: given the schema of the data type
// (and meta type, nil when none), return the schema of the wrapped body.
WrapSchema(data, meta *openapi3.SchemaRef) *openapi3.SchemaRef
}
ResponseEnvelope governs how a successful payload (plus optional meta) becomes the response body, AND how that wrapping is described in the OpenAPI docs. The two halves are defined together so the documented schema can never drift from the bytes on the wire — the same invariant the rest of the library upholds for request binding.
Install one process-wide with SetResponseEnvelope, or per route with WithEnvelope / WithRawResponse. The default is DataEnvelope (the standard {"data": ...} / {"data": ..., "meta": ...} shape).
Implement this directly only for exotic shapes; most projects are served by the declarative KeyedEnvelope (which never touches openapi3) or by RawEnvelope.
Contract: WrapSchema MUST mirror Wrap — they must agree on which keys exist — or the docs will misdescribe the response.
var RawEnvelope ResponseEnvelope = rawEnvelope{} //nolint:gochecknoglobals
RawEnvelope renders the payload as-is, with no wrapper (the pure handler model). It has nowhere to put meta, so any attached meta is dropped — prefer documenting meta-bearing routes with the default envelope. NewResult selects it; a route opts in with WithRawResponse.
type Result ¶
type Result struct {
Status int `json:"-"`
Data any `json:"data,omitempty"`
Error *json.RawMessage `json:"error,omitempty"`
Meta any `json:"meta,omitempty"`
// contains filtered or unexported fields
}
Result is the library's HTTP response value. Its success body is shaped by the route's ResponseEnvelope (the default DataEnvelope gives the {"data": ...} / {"meta": ...} shape; configure it with SetResponseEnvelope/WithEnvelope or drop it with WithRawResponse); errors render through the resolution chain. The zero value is not usable — build one with NewResult (raw model) or NewDataResult (enveloped).
func NewDataResult ¶
NewDataResult creates a 200 OK result whose payload is wrapped by the route's configured ResponseEnvelope (the default DataEnvelope gives {"data": ...}). Leaving the envelope unset lets the route or process-wide setting decide.
func NewListDataResult ¶
NewListDataResult is a convenience for a paged collection: the items wrapped by the configured envelope, with standard pagination meta attached (see WithPaging).
func NewResult ¶
NewResult creates a 200 OK result carrying data rendered AS-IS — the raw handler model, with no envelope. Use NewDataResult for the standard {"data": ...} wrapping (or whatever envelope the route/process configures). Use the With* builders to adjust status, turn it into an error or a file download.
A raw result pins itself to RawEnvelope, so it stays raw even on a route whose configured envelope wraps — the explicit constructor choice wins.
func (*Result) WithError ¶
WithError turns the result into an error response. The status and JSON body are resolved at render time via resolveError — an optional custom mapper, then HTTPError, then aerror-compatible duck typing, then a 500 fallback — so the route's ErrorMapper applies even when a RichHandler calls WithError before that mapper is attached. An explicit WithStatus still overrides the resolved status. The original error is recorded on the carrier (see render) so logging middleware can report it.
func (*Result) WithFile ¶
WithFile marks the result as a binary file download. The data passed to NewResult must be the file bytes ([]byte).
func (*Result) WithHeader ¶
WithHeader sets a response header written just before the body, letting a handler attach metadata it computed (Location on a 201, ETag, Cache-Control, a single Set-Cookie, rate-limit headers, ...) without dropping down to a native framework handler. Calls are applied in order with Set semantics, so a repeated key overwrites; for multiple values of the same header (e.g. several Set-Cookie) use native adapter middleware.
func (*Result) WithMeta ¶
WithMeta attaches an arbitrary meta object (e.g. custom pagination cursors).
func (*Result) WithPaging ¶
WithPaging attaches standard pagination meta computed from the total count.
func (*Result) WithStatus ¶
WithStatus overrides the HTTP status code. On an error result it takes precedence over the status resolved from the error at render time.
type RichHandler ¶
type RichHandler[Header, Param, Query, Body any] func( ctx context.Context, req Request[Header, Param, Query, Body], ) (*Result, error)
RichHandler is the escape hatch for endpoints that need full control over the response envelope (paging, meta, file download, custom status). It returns a fully built *Result. nil => 204 No Content.
type Route ¶
type Route struct {
// contains filtered or unexported fields
}
Route is a fully described HTTP endpoint: method, path, the folded typed handler chain (typed middlewares -> handler), the success status and the reflection schema used for documentation. It is built once at startup via NewRoute / NewRichRoute and is immutable afterwards.
A Route is framework-agnostic: it exposes Invoke(Carrier), and an adapter (adapter/gin, adapter/nethttp, adapter/fiber) turns it into a native handler.
func NewBodyRoute ¶
func NewBodyRoute[Body, Response any]( method, path string, handler func(ctx context.Context, body Body) (*Response, error), opts ...RouteOption, ) Route
NewBodyRoute builds an endpoint whose handler receives only the decoded request Body — the common shape for a create/update endpoint with no header, path or query binding. Equivalent to NewRoute with Request[struct{}, struct{}, struct{}, Body].
func NewParamRoute ¶
func NewParamRoute[Param, Response any]( method, path string, handler func(ctx context.Context, param Param) (*Response, error), opts ...RouteOption, ) Route
NewParamRoute builds an endpoint whose handler receives only the decoded path parameters (the Param struct) — the common shape for a GET/DELETE by id. Equivalent to NewRoute with Request[struct{}, Param, struct{}, struct{}].
func NewQueryRoute ¶
func NewQueryRoute[Query, Response any]( method, path string, handler func(ctx context.Context, query Query) (*Response, error), opts ...RouteOption, ) Route
NewQueryRoute builds an endpoint whose handler receives only the decoded Query struct — the common shape for a list/search GET. Equivalent to NewRoute with Request[struct{}, struct{}, Query, struct{}].
func NewRichRoute ¶
func NewRichRoute[Header, Param, Query, Body any]( method string, path string, handler RichHandler[Header, Param, Query, Body], opts ...RouteOption, ) Route
NewRichRoute builds an endpoint from a RichHandler that returns a fully formed *Result (paging, file, custom status, ...). Use WithResponseType to keep the generated docs accurate.
func NewRoute ¶
func NewRoute[Header, Param, Query, Body, Response any]( method string, path string, handler RequestHandler[Header, Param, Query, Body, Response], opts ...RouteOption, ) Route
NewRoute builds an endpoint from a typed RequestHandler. The request is parsed exactly once (shared with any typed middleware), the response is wrapped by the route's configured envelope (the default DataEnvelope gives {"data": ...}) and errors are rendered via Result.
func (Route) Description ¶
func (Route) Invoke ¶
Invoke runs the full typed chain (typed middlewares -> handler -> render) for one request, through the given carrier. Adapters call this from their native handler.
func (Route) MaxRequestBytes ¶
MaxRequestBytes reports the per-route request body cap configured via an App (WithApp + WithMaxRequestBytes). ok is false when the route inherits no App-level cap, in which case an adapter falls back to its own DefaultMaxRequestBytes. A returned value of 0 means "no cap".
func (Route) MaxRequestBytesOr ¶
MaxRequestBytesOr returns the route's App-configured request body cap, or fallback when the route inherits no App-level cap. A configured cap of 0 ("no cap") is returned as 0. It is the one-liner every adapter needs to resolve the effective cap from its own package-level DefaultMaxRequestBytes:
cap := route.MaxRequestBytesOr(DefaultMaxRequestBytes)
func (Route) Method ¶
Method, Path, Schema and the documentation getters expose route metadata for the OpenAPI generator and for adapter integration.
func (Route) Schema ¶
func (route Route) Schema() RouteSchema
func (Route) SuccessStatus ¶
type RouteOption ¶
type RouteOption func(*Route)
RouteOption customises a Route at construction time. Options run before the handler closure is built, so the closure always observes the final values.
func WithApp ¶
func WithApp(a *App) RouteOption
WithApp attaches an App's configuration to a route. It is the bridge between the App and the generic route constructors (Go does not allow generic methods, so the App cannot expose NewRoute itself): pass it as a RouteOption.
app := oapi.New(oapi.WithValidator(v), oapi.WithResponseEnvelope(env))
r := oapi.NewRoute(method, path, handler, oapi.WithApp(app), oapi.WithSummary("..."))
It sets the route's success envelope from the App only when the route has not already chosen one with WithEnvelope/WithRawResponse, so an explicit per-route envelope always wins regardless of option order.
func WithBinaryResponse ¶
func WithBinaryResponse(contentType, description string) RouteOption
WithBinaryResponse documents the success response as a binary stream — a file download — with the given media type (empty defaults to "application/octet-stream") and description. Use it for file-download RichHandlers (those returning Result.WithFile), whose body cannot be inferred from generics: without it the docs would default to 204 No Content while the handler returns 200 with bytes. It sets the documented success status to 200 unless an explicit WithSuccessStatus overrides it. Documentation only — the handler still streams the bytes at runtime.
func WithDeprecated ¶
func WithDeprecated() RouteOption
WithDeprecated marks the operation as deprecated in the docs.
func WithDescription ¶
func WithDescription(description string) RouteOption
WithDescription sets the OpenAPI operation description.
func WithEnvelope ¶
func WithEnvelope(e ResponseEnvelope) RouteOption
WithEnvelope overrides the success-response envelope for this route, taking precedence over the process-wide SetResponseEnvelope default. It drives both the wire body and the documented schema, so they stay in lockstep.
func WithErrorMapper ¶
func WithErrorMapper(mapper ErrorMapper) RouteOption
WithErrorMapper sets a custom error mapper for this route, taking precedence over the global ErrorParser and the default HTTPError / aerror resolution.
func WithMetaType ¶
func WithMetaType[T any]() RouteOption
WithMetaType documents the type placed under the response envelope's "meta" key (e.g. PagingMeta for paged endpoints), so a success response that carries meta is fully described. Documentation only — the handler still attaches the meta at runtime via Result.WithMeta / WithPaging.
func WithRawResponse ¶
func WithRawResponse() RouteOption
WithRawResponse renders this route's success body as the raw handler model with no envelope (and documents it the same way). Shorthand for WithEnvelope(RawEnvelope).
func WithResponse ¶
func WithResponse[T any](status int, description string) RouteOption
WithResponse documents an additional response (typically an error or an alternative success status), e.g. 201, 404, 409, 422. T is the body type under the standard envelope; use struct{} for a body-less response. This is documentation only — the handler still decides what to return at runtime.
func WithResponseType ¶
func WithResponseType[T any]() RouteOption
WithResponseType declares the response body type for documentation when it cannot be inferred from generics (i.e. for NewRichRoute handlers).
func WithSecurity ¶
func WithSecurity(scheme string, scopes ...string) RouteOption
WithSecurity declares that the operation requires the named security scheme (registered on the Registry via AddSecurityScheme) with the given scopes. This documents the requirement; enforcement remains the middleware's job.
func WithSuccessStatus ¶
func WithSuccessStatus(status int) RouteOption
WithSuccessStatus overrides the HTTP status returned on success (default 200, or 204 when there is no response body). Setting 204 forces an empty body.
func WithSummary ¶
func WithSummary(summary string) RouteOption
WithSummary sets the OpenAPI operation summary.
func WithTags ¶
func WithTags(tags ...string) RouteOption
WithTags sets the OpenAPI operation tags (used to group endpoints in docs).
func WithTypedBefore ¶
func WithTypedBefore[Header, Param, Query, Body any]( middlewares ...Middleware[Header, Param, Query, Body], ) RouteOption
WithTypedBefore registers one or more typed middlewares to run before the handler. They reuse the same parse-once cache as the handler, so declaring the request shape here does not cause the request to be parsed twice.
The request shape is independent of the handler's: a middleware may declare only the parts it needs (e.g. Request[AuthHeader, struct{}, struct{}, struct{}]) and still compose with any handler on the route.
type RouteSchema ¶
type RouteSchema struct {
Header reflect.Type
Param reflect.Type
Query reflect.Type
Body reflect.Type
Response reflect.Type
Meta reflect.Type
}
RouteSchema captures the concrete Go types of every part of the request and of the response. nil means "this part is absent". It is the single source of truth for OpenAPI generation so the docs can never drift from the handler.
type Validator ¶
Validator runs project-defined validation on a freshly bound request part. The library calls it after decoding each part (header, path, query, body), so a single rule source can drive runtime checks while the OpenAPI generator reads the same `binding` tags for the docs.
source is the wire location of the part — one of "header", "uri", "form" or "json" — so an implementation can report field names the way clients send them (see WireName). value is a pointer to the bound struct.
Return nil when value is valid. Return an error to abort the request: an error implementing HTTPError controls its own status and body, anything else is rendered as a generic 500. Build the library's standard field-level 400 with NewValidationError.
The core depends on no validation library and ships no Validator. Install one once at startup with SetValidator. A ready go-playground/validator-backed implementation that reads the `binding` tag is provided as reference code in the examples module (examples/validation).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
adapter
|
|
|
nethttp
Package nethttp adapts the framework-agnostic oapi core onto the net/http standard library (Go 1.22+ method-aware ServeMux patterns).
|
Package nethttp adapts the framework-agnostic oapi core onto the net/http standard library (Go 1.22+ method-aware ServeMux patterns). |
|
gin
module
|
|
|
tools
|
|
|
gen_doc
Package gendoc provides a turnkey main for generating a Registry's OpenAPI document from the command line, so an application's generator command is a single line:
|
Package gendoc provides a turnkey main for generating a Registry's OpenAPI document from the command line, so an application's generator command is a single line: |