A client needs to know what to send and how to interpret both success and failure. Operation callbacks record that contract beside your handler registration. Reuse the handler’s input schema and output type where possible, but keep decoding, validation, and response writing explicit: declarations do not bind or execute those steps for you.
These examples assume an existing routes router and the Greeting output type
from the overview. They use net/http,
arkoren.dev/core/http/request, arkoren.dev/core/http/response,
arkoren.dev/core/http/router, and arkoren.dev/foundation/validate.
Describe Input
To let a caller choose the greeting’s name, define a narrow request type and its validation schema:
type GreetingInput struct {
Name string `json:"name"`
}
var greetingInput = validate.Struct(func(object *validate.StructBuilder[GreetingInput]) {
object.Field("Name", func(name *validate.StringBuilder[string]) {
name.Required()
})
})
The handler must actually decode and validate that input before producing its typed response:
func createGreeting(writer http.ResponseWriter, incoming *http.Request) error {
input, err := request.ParseJSON(incoming, greetingInput)
if err != nil {
return err
}
return response.JSON(writer, http.StatusOK, Greeting{Message: "Hello, " + input.Name})
}
ParseJSON returns the parsed, validated input, with schema defaults applied.
An invalid request returns an error instead of reaching the success response.
Now reuse the schema in the operation description:
routes.Post("/greeting", createGreeting, func(operation *router.OperationBuilder) {
operation.ID("createGreeting")
operation.JSONBody(greetingInput).BodyRequired(true)
operation.JSONResponse(http.StatusOK, validate.Infer[Greeting]())
})
JSONBody describes application/json; use Body(media, schema) for another
representation. Body presence is optional unless you declare
BodyRequired(true). That flag is separate from a schema’s required properties,
and neither declaration rejects requests by itself.
Describe Failures
The success declaration does not discover the errors returned by ParseJSON.
Add its known input problems inside the same registration callback:
operation.Problem(request.ErrInvalidJSON)
operation.Problem(request.ErrValidationFailed)
operation.Problem(request.ErrBodyTooLarge)
These describe malformed JSON at 400, decoding or validation violations at
422, and an oversized body at 413. Operational failures can also escape
parsing; this list is not a claim that every failure has one of those identities.
See request validation for the
runtime boundary.
Problem accepts problem.Details and describes the router’s negotiated RFC 9457
wire response at its status: application/problem+json, application/json, and
text/plain. A zero status becomes 500. The schema does not expose the Go
value’s private fields or error cause. Declare application and middleware problems
explicitly, matching your actual error mapping.
Add Parameters
For a route such as /greetings/{id}, describe an identifier and optional inputs
independently of the body. Inside its operation callback:
operation.Parameter("id", "path", router.RawSchema(`{"type":"integer","minimum":1}`))
operation.Parameter("language", "query", router.RawSchema(`{"type":"string"}`))
operation.Parameter("X-Request-ID", "header", router.RawSchema(`{"type":"string"}`))
The supported locations are path, query, header, and cookie. Path parameters
are always required; all other parameters are optional in the document. There is
no setting to require a non-path parameter or choose its serialization style.
Schema constraints do not change these limits or parse the incoming value.
Export checks path parameter names against the final path, including group prefixes. Undescribed single-segment wildcards receive required string parameters; an explicit declaration supplies the schema instead. Duplicate names within one location fail export, with case-insensitive comparison for header parameters.
Describe Responses
Choose declarations that match what the handler writes. JSONResponse describes
a JSON body; Response describes a status without adding a body. For a redirect,
declare its status and destination header:
operation.Response(http.StatusSeeOther, "Continue at the returned location")
operation.ResponseHeader(http.StatusSeeOther, "Location", router.RawSchema(`{"type":"string","format":"uri-reference"}`))
Statuses must be integers from 100 through 599; there are no status-range or
default response declarations. An empty description uses standard HTTP status
text. Response preserves existing bodies and headers at that status; it does not
remove them to make a previously declared response bodyless.
For a binary download, describe the media type rather than inventing a JSON body:
operation.MediaResponse(http.StatusOK, "application/octet-stream", router.RawSchema(`{"type":"string","format":"binary"}`))
operation.ResponseHeader(http.StatusOK, "Content-Disposition", router.RawSchema(`{"type":"string"}`))
Different media types at one status describe alternative representations; repeating a media type replaces its representation. Request and response media types must parse as a type/subtype. These declarations do not select an encoder, set headers, negotiate content, or validate output. Check the actual behavior of downloads and other integrations, including streaming responses.
Response header names match case-insensitively within one status. A later
x-request-id replaces an earlier X-Request-ID, including its exported spelling
and schema; other statuses are unaffected. Invalid header names fail export.
Request Examples
To show clients a valid greeting request, declare the body representation before adding its named example:
operation.JSONBody(greetingInput).BodyRequired(true)
operation.Example("application/json", "personalGreeting", GreetingInput{Name: "Ada"})
The example is for the request body, not a response. Its JSON encoding is captured immediately, but it is not validated against the schema. Encoding failures become export errors. Adding an example before its media type records an error even if you declare that body representation later.
Declare Security
If your routes enforce bearer authentication, name that scheme when exporting:
document, err := routes.OpenAPI("Greeting API", "1.0.0", func(definition *router.OpenAPIBuilder) {
definition.SecurityScheme("bearer", router.RawSchema(`{"type":"http","scheme":"bearer"}`))
})
Then reference it in each protected route’s operation callback:
operation.Security("bearer")
This declares a requirement; it does not install authentication or authorization.
Configure the actual authentication policy
separately and declare its relevant error responses. Middleware does not supply
schemes or requirements automatically. Pass the same document configuration to
OpenAPIHandler if you also serve the JSON.
The supported schemes are HTTP, API-key, and mutual TLS. HTTP requires a valid
scheme token. API-key requires a nonempty name and an in location of query,
header, or cookie; a header name must be an HTTP token. For example:
{
"type": "apiKey",
"name": "X-API-Key",
"in": "header"
}
OAuth flows and OpenID Connect scheme definitions are unsupported. Scheme names
use letters, digits, ., _, or -; unsupported fields are rejected except
x- extensions. Standard supported fields must be strings.
Every referenced scheme must be declared. Repeated Security calls describe
alternatives (OR), not a combined requirement (AND). Nonempty scopes fail export.
The document API has no server URL setting.
Schema Limits
Bodies, responses, parameters, and headers accept router.Schema, an interface
with JSONSchema() ([]byte, error). Published validation schemas implement it,
but export includes only their representable rules. Cross-field comparisons,
conditions, custom checks, normalizers, and clock-based rules remain runtime
behavior; see JSON Schema.
validate.Infer[T]() describes a Go type’s JSON output without applying value
rules. Interfaces and opaque custom MarshalJSON implementations produce open
schemas unless a supported explicit type provider describes their shape.
Recursive types become open at the repeated level, and pointer nil values are
not modeled. Types without a JSON representation produce inference errors.
If inference cannot describe your wire shape, supply router.RawSchema. Its root
must be a JSON object or boolean, not an array, string, or null. The router checks
syntax and root shape, not arbitrary JSON Schema keywords. Schema bytes are
captured at declaration time and remain inline; references are neither rewritten
nor fetched. Successful export is not proof of equivalence with your runtime
validator or encoder.
Check Declarations
Use Summary, Description, and Tags to identify and organize operations.
Descriptions pass CommonMark through without interpretation or sanitization.
Deprecated(true) labels an operation but does not disable it or add a warning
header.
Nonempty operation IDs must be unique across the whole document. If one
registration expands to several methods, omit its ID or register each method
separately with a unique ID. Equivalent templates such as /users/{id} and
/users/{name} also conflict, even under different methods. QUERY has its OpenAPI
3.2 method field; CONNECT and extension methods appear in additionalOperations.
Leave mounts and catch-all registrations unannotated, or set Hidden(true).
Visible annotated opaque paths fail export. Hiding an operation suppresses its
metadata errors too, but does not prevent requests or remove it from Routes().
Invalid metadata does not stop route execution. The first declaration error is retained even if a later setter replaces the invalid value. Run offline export before deployment to catch those errors, invalid media types, duplicate IDs, and path conflicts rather than discovering them on the first documentation request.
For all declaration methods, see the
router API reference.