oapi

package module
v0.0.0-...-ca3b2fc Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 27 Imported by: 0

README

oapi

Turn typed Go handlers into HTTP endpoints whose request and response structs are the single source of truth for binding, validation and OpenAPI 3 docs.

The struct tags you already write to bind and validate a request are the same tags the OpenAPI generator reads, so the docs are generated from the exact Go types the handler binds — they can never drift from the running code.

Go Reference Go Version

Status: pre-1.0 — the API may still change before a tagged v1.

Swagger UI for the example Catalog API Redoc reference for the example Catalog API

OpenAPI docs generated straight from the Go types — Swagger UI (left) and Redoc (right). Run it yourself from examples/.

Features

  • Typed handlersfunc(ctx, Request[Header, Param, Query, Body]) (*Response, error); each part binds from a different source, struct{} for the parts you don't use.
  • Typed middlewareWithTypedBefore sees the same parsed request as the handler (parsed once, shared).
  • Five adapters, one route set — the same []Route runs unchanged on net/http, gin, Fiber v2, chi and Echo v4.
  • OpenAPI 3 generation — a Registry turns the routes into a validated spec (JSON/YAML/Write) from the same struct tags used for binding.
  • Pluggable seams — the validator, response envelope and error parser are swappable interfaces; the core ships none and depends on no validation library.
  • Scoped config — bundle validator/envelope/error-parser/body-cap into an immutable App and attach it per route with WithApp, instead of process-wide globals.
  • Files — multipart uploads ([]*multipart.FileHeader) bind like any field; downloads stream via NewResult(bytes).WithFile(...).
  • Envelopes & paging — default {"data": ...} (+ meta), with per-route custom or raw responses.
  • Safe errorsHTTPError, per-route ErrorMapper, process-wide ErrorParser; unrecognised errors render a generic 500 and never leak internals.

Install

go get github.com/antlss/oapi

The net/http adapter ships with the core. Each other adapter is its own module, so you pull in only what you import:

go get github.com/antlss/oapi/adapter/gin
go get github.com/antlss/oapi/adapter/fiber
go get github.com/antlss/oapi/adapter/chi
go get github.com/antlss/oapi/adapter/echo

Validation is opt-in (the core ships no validator). Copy the go-playground/validator reference in examples/validation, or implement the small Validator interface yourself.

Quickstart

Let's build a complete, runnable API with OpenAPI generation in 5 simple steps.

Step 1: Initialize your project

mkdir oapi-quickstart && cd oapi-quickstart
go mod init oapi-quickstart
go get github.com/antlss/oapi

Step 2: Define your API and Routes (api/api.go) Create a new directory api and add api.go. This holds your endpoints and OpenAPI registry.

package api

import (
	"context"
	"net/http"

	"github.com/antlss/oapi"
)

// 1. Define your types with binding/validation tags
type CreateProductBody struct {
	Name     string  `json:"name"     binding:"required,min=2,max=120"     example:"Mechanical Keyboard"`
	Price    float64 `json:"price"    binding:"required,gt=0"              example:"49.90"`
	Currency string  `json:"currency" binding:"required,oneof=USD EUR JPY" example:"USD"`
}

type Product struct {
	ID       int     `json:"id"       example:"1001"`
	Name     string  `json:"name"     example:"Mechanical Keyboard"`
	Price    float64 `json:"price"    example:"49.90"`
	Currency string  `json:"currency" example:"USD"`
}

// 2. Create the Route
// Header/Param/Query are unused, so struct{}. Returning *Product wraps it in the default {"data": ...} envelope.
var CreateProduct = oapi.NewRoute(
	http.MethodPost, "/products",
	func(_ context.Context, req oapi.Request[struct{}, struct{}, struct{}, CreateProductBody]) (*Product, error) {
		return &Product{ID: 1001, Name: req.Body.Name, Price: req.Body.Price, Currency: req.Body.Currency}, nil
	},
	oapi.WithSummary("Create a product"),
	oapi.WithTags("catalog"),
	oapi.WithSuccessStatus(http.StatusCreated),
)

// 3. Export the Registry (used for both serving and generating docs)
func BuildRegistry() *oapi.Registry {
	return oapi.NewRegistry("Catalog API", "v1").
		Describe("A tiny example API.").
		AddServer("http://localhost:8080", "Local").
		Add(CreateProduct)
}

Step 3: Create the Generator CLI (cmd/gen/main.go) Create cmd/gen/main.go. This tiny script writes your OpenAPI spec to disk.

package main

import (
	"oapi-quickstart/api"
	
	gendoc "github.com/antlss/oapi/tools/gen_doc"
)

// gendoc.Main parses flags (-out, -format, etc) and writes the files.
func main() {
	gendoc.Main(api.BuildRegistry())
}

Step 4: Create the Server (main.go) Create main.go in the root of your project. Notice the //go:generate directive at the top!

//go:generate go run ./cmd/gen -out ./openapi
package main

import (
	"log"
	"net/http"

	"oapi-quickstart/api"

	"github.com/antlss/oapi/adapter/nethttp"
)

func main() {
	mux := http.NewServeMux()
	
	// Register the routes
	nethttp.RegisterAll(mux, api.CreateProduct)

	// Serve the raw spec at /openapi.json
	mux.HandleFunc("GET /openapi.json", nethttp.SpecHandler(api.BuildRegistry()))

	log.Println("Listening on :8080 (Spec at /openapi.json)")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Step 5: Generate & Run!

Now you can generate your OpenAPI docs and run the server:

# 1. Generate OpenAPI specs to disk (creates ./openapi/openapi.json and .yaml)
go generate ./...

# 2. Run the server
go run main.go

Test it with curl:

curl -X POST http://localhost:8080/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Mechanical Keyboard", "price": 49.90, "currency": "USD"}'

POST /products binds the body, returns 201 with {"data": {...}}, and /openapi.json serves a spec whose schema — required fields, the oneof enum, the bounds — comes from the same struct.

Validation is opt-in: install a validator once at startup — oapi.SetValidator(validation.New()) — or the binding rules are skipped (with a one-time warning).

Browse the docs — Swagger UI & Redoc

/openapi.json is the raw spec. To make it browsable, serve a tiny HTML page that loads that spec into Swagger UI (interactive "Try it out") or Redoc (read-only reference) from a CDN — no extra Go dependency, no embedded assets:

const swaggerHTML = `<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Catalog API — Swagger UI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"></head>
<body><div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>window.onload = () => SwaggerUIBundle({url: "/openapi.json", dom_id: "#swagger-ui"})</script>
</body></html>`

const redocHTML = `<!DOCTYPE html><html><head><meta charset="utf-8">
<title>Catalog API — Redoc</title></head>
<body><redoc spec-url="/openapi.json"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"></script>
</body></html>`

func htmlPage(html string) http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		_, _ = w.Write([]byte(html))
	}
}

Mount them next to the spec in main. The UIs are plain static HTML, so there's nothing oapi-specific here — serve the strings with whatever your framework uses for an HTML route:

mux.HandleFunc("GET /openapi.json", nethttp.SpecHandler(reg)) // the spec (from above)
mux.HandleFunc("GET /swagger", htmlPage(swaggerHTML))         // interactive UI
mux.HandleFunc("GET /redoc", htmlPage(redocHTML))             // reference docs

Open http://localhost:8080/swagger or /redoc. Both pages only point at /openapi.json, so they track the Go types automatically — change a struct, the docs change with it.

examples/docsui ships these same pages with pinned versions + SRI integrity hashes and a landing page at /. Copy that package as-is for production rather than the floating @5/@2 tags shown here.

Adapters

The core is framework-agnostic. Every adapter exposes the same surface — Register, RegisterAll, SpecHandler — so switching frameworks is just a different RegisterAll call over the same routes.

Framework Adapter package (under github.com/antlss/oapi) Notes
net/http adapter/nethttp Ships with the core, no extra deps (Go 1.22+ method-aware ServeMux).
gin adapter/gin Separate module.
Fiber v2 adapter/fiber Separate module.
chi adapter/chi Separate module (go-chi/chi v5).
Echo v4 adapter/echo Separate module.

Each adapter caps the request body at DefaultMaxRequestBytes (10 MiB; set 0 to disable), overridable per route via an App's WithMaxRequestBytes.

OpenAPI generation

A Registry collects routes and document metadata, then renders the spec:

reg := oapi.NewRegistry("Catalog API", "v1").
	Describe("...").
	Contact("API Team", "https://example.com/support", "api@example.com").
	License("Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0").
	AddServer("https://api.example.com", "Production").
	AddSecurityScheme("bearerAuth", oapi.BearerAuth()).
	AddTag("catalog", "Browse and manage products").
	Add(routes...)

data, err := reg.JSON() // or reg.YAML()
err = reg.Validate(ctx) // check against the OpenAPI 3 schema

Also available: TermsOfService, ExternalDocs, Logo/LogoWith, TagGroup, and UseComponents() (emit shared types as $ref under components/schemas instead of inlining). A Base document supplies defaults the generated paths overlay (Base / LoadBaseFile).

Write the spec to disk. Write validates first (unless NoValidate), then emits JSON and/or YAML, returning the paths it wrote:

written, err := reg.Write(ctx, oapi.GenConfig{Dir: "openapi"})
// -> ["openapi/openapi.json", "openapi/openapi.yaml"], validated before writing

Generate it as a CLI / go generate step. tools/gen_doc is a turnkey main that parses flags, validates and writes — so your generator command is one line. Drop a //go:generate directive next to your routes and the spec is rebuilt with go generate:

//go:generate go run ./cmd/openapi-gen -out ./openapi
package main

import (
	gendoc "github.com/antlss/oapi/tools/gen_doc"
	"example.com/app/api"
)

func main() { gendoc.Main(api.Registry()) } // flags: -out -format json,yaml -base FILE -no-validate

See it generate, end to end. The examples/ module ships exactly this wiring — a real cmd/openapi-gen, a //go:generate directive in api/routes.go, and the committed output under examples/openapi/. Run it yourself:

cd examples
go run ./cmd/openapi-gen -out ./openapi   # validates, then writes openapi/openapi.{json,yaml}
go generate ./...                         # the same, via the //go:generate directive

Because the output is committed, regenerating and diffing it in review is how spec drift is caught: change a struct, rerun, and the JSON/YAML change with it — or the diff tells you a doc went stale.

Concepts

Request parts

Request[Header, Param, Query, Body] — each part binds from a different source; struct{} means "this endpoint doesn't use it":

Part Source Tag
Header request headers header:"..."
Param path parameters uri:"..."
Query query string form:"..."
Body JSON body json:"..."
Body urlencoded / multipart form:"..." (+ []*multipart.FileHeader for files)

The binding tag carries validation rules that also become OpenAPI constraints (required; oneof→enum; min/max/gt→bounds; uuid/email/url→formats). example tags set the docs samples.

Constructors
  • NewRoute — handler returns *Response, wrapped by the envelope; nil204 No Content.
  • NewRichRoute — handler returns a fully built *Result (paging, headers, status, file download). Add WithResponseType[T]() or WithBinaryResponse(...) so the docs match what it returns.
  • NewBodyRoute / NewQueryRoute / NewParamRoute — shortcuts for single-part endpoints, so you skip the struct{} placeholders.
Result & envelope
  • Build a *Result with NewDataResult (enveloped), NewListDataResult (+ paging meta) or NewResult (raw); chain .WithStatus, .WithHeader, .WithMeta, .WithPaging, .WithFile.
  • The envelope is a ResponseEnvelope seam (default DataEnvelope{"data": ...}). Override per route with WithEnvelope(KeyedEnvelope{...}) / WithRawResponse(), per App with WithResponseEnvelope, or globally with SetResponseEnvelope. One definition drives both the wire body and its documented schema.
Errors
  • HTTPError — any error with HTTPStatus() int controls its own status (and, via ErrorBody, its JSON). Build one with oapi.NewError(...), or the standard field-level 400 with oapi.NewValidationError(...).
  • ErrorMapper (per-route, WithErrorMapper) and ErrorParser (global, SetErrorParser) own the full wire body; ErrorParser also documents it.
  • Resolution order: per-route mapper → ErrorParserHTTPError → aerror-shaped duck typing → generic 500. Unrecognised errors never leak; they're recorded on the carrier for logging middleware.
Validation

Validation is a pluggable seam — the core ships no validator and depends on no validation library, so you choose one (or none) and pull in only what you import.

// Install once at startup, before serving. The binding rules now run on every request.
oapi.SetValidator(validation.New())
  • The seam. Any type implementing Validator (Validate(value any, source string) error) works. Install it process-wide with SetValidator, or scope it to a route group with an App's WithValidator. SetValidator(nil) disables it explicitly.
  • If you skip it. With no validator installed, the binding rules are not enforced — requests bind and pass through, and the library logs a one-time warning. Schema generation is unaffected: the docs still show the constraints either way.
  • One tag, two jobs. RuleTag (default "binding") names the tag the validator reads and the generator turns into OpenAPI constraints, so a rule like binding:"required,oneof=USD EUR" can never validate one thing and document another.
  • Reference implementation. examples/validation is a ready go-playground/validator adapter (validation.New()): one engine per request part, field errors reported by their wire name (json/header/uri/form), translated into the library's field-level 400. Copy it, or implement the one-method seam yourself.

Each runnable example installs it at startup (oapi.SetValidator(validation.New())), so a request that violates a binding rule comes back as a structured 400 you can see in Swagger UI.

Scoped config (App)

Instead of process-wide globals, bundle config into an immutable App and attach it per route — two differently configured groups can then serve in one process:

app := oapi.New(
	oapi.WithValidator(validation.New()),
	oapi.WithResponseEnvelope(oapi.KeyedEnvelope{DataKey: "data", Constants: map[string]any{"success": true}}),
	oapi.WithErrorParser(api.AppErrorParser{}),
	oapi.WithMaxRequestBytes(5 << 20),
)
r := oapi.NewRoute(method, path, handler, oapi.WithApp(app))

New snapshots the current globals and is immutable after. The App scopes both the wire bytes and the generated docs, so /v1 and /v2 can each have their own envelope and error shape with no global state. (RuleTag stays process-wide.)

Examples

examples/ is a runnable "Catalog API" exercising every capability — all request parts, JSON/urlencoded/multipart bodies, file upload/download, paging, security, typed middleware, the full error model and custom envelopes. The same routes mount on net/http, gin and Fiber under examples/cmd/{nethttp,gin,fiber}. examples/cmd/customized configures the response/error shapes process-wide via Set*; examples/cmd/scoped does it per App (two groups, no globals).

Every command serves the spec at /openapi.json plus Swagger UI (/swagger), Redoc (/redoc) and a landing page (/) — the ready-made pages in examples/docsui (CDN-loaded, version-pinned, SRI-hashed). Run one and open the root URL:

cd examples && go run ./cmd/nethttp   # then open http://localhost:8081

License

MIT © 2026 Tran Long An

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func LoadBaseFile(path string) (*openapi3.T, error)

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

func New(opts ...Option) *App

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 NewError

func NewError(status int, code, message string) *Error

NewError builds an Error with the given status, code and message.

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) Error

func (e *Error) Error() string

func (*Error) ErrorBody

func (e *Error) ErrorBody() any

ErrorBody renders the error itself (code/message/fields) as the envelope body.

func (*Error) HTTPStatus

func (e *Error) HTTPStatus() int

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying cause (if any) so errors.Is/As and logging middleware can inspect the original decoder/validation failure. The cause is never placed in the client-facing body.

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

type ErrorMapper func(error) (status int, body any, ok bool)

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

func (*HTTPCarrier) WriteJSON

func (c *HTTPCarrier) WriteJSON(status int, body any) error

type HTTPError

type HTTPError interface {
	error
	HTTPStatus() int
}

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

func WithMaxRequestBytes(n int64) Option

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

func WithValidator(v Validator) Option

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

func NewRegistry(title, version string) *Registry

NewRegistry creates a documentation registry with the given API title/version.

func (*Registry) Add

func (rg *Registry) Add(routes ...Route) *Registry

Add appends routes to the registry. Returns the registry for chaining.

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

func (rg *Registry) AddServer(url, description string) *Registry

AddServer adds a server entry (base URL + description) to the document.

func (*Registry) AddTag

func (rg *Registry) AddTag(name, description string) *Registry

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

func (rg *Registry) Base(doc *openapi3.T) *Registry

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

func (rg *Registry) Contact(name, url, email string) *Registry

Contact sets the API contact (name/url/email) shown in the docs. Empty arguments are omitted from the spec.

func (*Registry) Describe

func (rg *Registry) Describe(description string) *Registry

Describe sets the API description shown in the docs.

func (*Registry) ExternalDocs

func (rg *Registry) ExternalDocs(description, url string) *Registry

ExternalDocs adds a link to external documentation shown near the top of the docs.

func (*Registry) JSON

func (rg *Registry) JSON() ([]byte, error)

JSON renders the OpenAPI document as indented JSON.

func (*Registry) License

func (rg *Registry) License(name, url string) *Registry

License sets the API license (name + optional URL) shown in the docs.

func (rg *Registry) Logo(url string) *Registry

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

func (rg *Registry) LogoWith(logo map[string]any) *Registry

LogoWith sets the full Redoc x-logo object (keys: url, backgroundColor, altText, href), replacing any value set by Logo.

func (*Registry) OpenAPI

func (rg *Registry) OpenAPI() *openapi3.T

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) Routes

func (rg *Registry) Routes() []Route

Routes returns the registered routes in registration order.

func (*Registry) SpecBytesOnce

func (rg *Registry) SpecBytesOnce() func() ([]byte, error)

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

func (rg *Registry) TagGroup(name string, tags ...string) *Registry

TagGroup defines a Redoc x-tagGroups navigation section grouping the named tags under a heading (Swagger UI ignores it).

func (*Registry) TermsOfService

func (rg *Registry) TermsOfService(url string) *Registry

TermsOfService sets the URL of the API's terms of service.

func (*Registry) UseComponents

func (rg *Registry) UseComponents() *Registry

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

func (rg *Registry) Validate(ctx context.Context) error

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

func (rg *Registry) Write(ctx context.Context, cfg GenConfig) ([]string, error)

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.

func (*Registry) YAML

func (rg *Registry) YAML() ([]byte, error)

YAML renders the OpenAPI document as block-style YAML. It round-trips the JSON encoding through a yaml.Node so key order is preserved and every openapi3 custom JSON marshaller is honoured, without a YAML-specific dependency.

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

func NewDataResult(data any) *Result

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

func NewListDataResult(items any, count int64, perPage, current int) *Result

NewListDataResult is a convenience for a paged collection: the items wrapped by the configured envelope, with standard pagination meta attached (see WithPaging).

func NewResult

func NewResult(data any) *Result

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

func (r *Result) WithError(err error) *Result

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

func (r *Result) WithFile(filename string) *Result

WithFile marks the result as a binary file download. The data passed to NewResult must be the file bytes ([]byte).

func (*Result) WithHeader

func (r *Result) WithHeader(key, value string) *Result

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

func (r *Result) WithMeta(meta any) *Result

WithMeta attaches an arbitrary meta object (e.g. custom pagination cursors).

func (*Result) WithPaging

func (r *Result) WithPaging(count int64, perPage, current int) *Result

WithPaging attaches standard pagination meta computed from the total count.

func (*Result) WithStatus

func (r *Result) WithStatus(status int) *Result

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 Route) Description() string

func (Route) Invoke

func (route Route) Invoke(c Carrier)

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

func (route Route) MaxRequestBytes() (limit int64, ok bool)

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

func (route Route) MaxRequestBytesOr(fallback int64) int64

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

func (route Route) Method() string

Method, Path, Schema and the documentation getters expose route metadata for the OpenAPI generator and for adapter integration.

func (Route) Path

func (route Route) Path() string

func (Route) Schema

func (route Route) Schema() RouteSchema

func (Route) SuccessStatus

func (route Route) SuccessStatus() int

func (Route) Summary

func (route Route) Summary() string

func (Route) Tags

func (route Route) Tags() []string

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

type Validator interface {
	Validate(value any, source string) error
}

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).

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:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL