A comprehensive OpenAPI 3.1 specification generator extension for the Rex framework.
rextension-openapi is a Rex extension that provides:
- Automatic OpenAPI 3.1 document generation from registered routes
- Rich JSON Schema generation via reflection on Go types
- Struct tag support:
json,validate(required, min, max, etc.) - Union type support: OneOf/AnyOf/AllOf via
validation.BodySchema - Per-status-code response documentation via
ValidatableRoute.Responses() - Security scheme documentation (soft-dependency on
rextension-security) - Request/response examples via
ResponseExamplesProviderandRequestBodyExamplesProvider - Response descriptions via
ResponseDescriptionProvider - Generated once at startup, when the route table is frozen — a generation failure is a startup error, not a 500 on the first request
- Router scoping: which routers' routes appear in the document, and which router serves it
- Configurable serve path (default:
/openapi.json)
go get github.com/kryovyx/rextension-openapi
package main import ( "github.com/kryovyx/rex" "github.com/kryovyx/rex/route" openapi "github.com/kryovyx/rextension-openapi" ) func main() { app := rex.New() // Add OpenAPI extension with default config app.WithOptions( openapi.WithOpenAPI(nil), ) // Register your routes (implementing OpenAPIRoute) app.RegisterRoute(&HelloRoute{}) // Run the application // OpenAPI document available at /openapi.json if err := app.Run(); err != nil { panic(err) } }
Routes must implement the OpenAPIRoute interface to be included in the generated OpenAPI document. Routes that do not implement this interface are excluded from the spec.
type OpenAPIRoute interface { OperationID() string Summary() string Description() string Tags() []string }
A route must also implement route.Route (Method(), Path(), Handler()). Combining both interfaces provides the metadata needed for the OpenAPI spec:
type HelloRoute struct{} func (r *HelloRoute) Method() string { return "GET" } func (r *HelloRoute) Path() string { return "/hello" } func (r *HelloRoute) Handler() route.HandlerFunc { return func(ctx route.Context) { ctx.JSON(200, map[string]string{"message": "Hello, World!"}) } } // OpenAPIRoute implementation func (r *HelloRoute) OperationID() string { return "getHello" } func (r *HelloRoute) Summary() string { return "Say hello" } func (r *HelloRoute) Description() string { return "Returns a greeting message" } func (r *HelloRoute) Tags() []string { return []string{"greetings"} }
If a route also implements ValidatableRoute (from rextension-validation), request and response schemas are automatically generated from the Go types via reflection.
Struct tags drive schema generation:
type CreateUserRequest struct { Name string `json:"name" validate:"required,min=1,max=100"` Email string `json:"email" validate:"required"` Age int `json:"age" validate:"min=0,max=150"` }
This produces an OpenAPI schema with required fields and minLength/maxLength/minimum/maximum constraints.
Use validation.BodySchema to declare union types:
- OneOf: Exactly one schema matches
- AnyOf: One or more schemas match
- AllOf: All schemas must match
Routes implementing ValidatableRoute can return Responses() to document each status code with its own schema type:
func (r *MyRoute) Responses() map[int]interface{} { return map[int]interface{}{ 200: SuccessResponse{}, 400: ErrorResponse{}, 401: AuthErrorResponse{}, } }
Routes that implement both OpenAPIRoute and the SecuredRoute interface (from rextension-security) automatically have security requirements added to their operations in the spec.
func (r *ProtectedRoute) RequiredSchemes() []string { return []string{"bearer"} }
Security schemes registered via rextension-security are discovered through the DI container and the rextension global registry, and appear in the components/securitySchemes section of the document.
Implement ResponseExamplesProvider to supply named examples per status code:
func (r *MyRoute) ResponseExamples() map[int]map[string]openapi.ExampleObject { return map[int]map[string]openapi.ExampleObject{ 200: { "success": {Summary: "Successful payment", Value: PaymentResponse{Status: "success"}}, }, } }
Implement RequestBodyExamplesProvider to supply named examples for the request body:
func (r *MyRoute) RequestBodyExamples() map[string]openapi.ExampleObject { return map[string]openapi.ExampleObject{ "usd-payment": {Summary: "USD payment", Value: PaymentRequest{Amount: 1000, Currency: "USD"}}, } }
Implement ResponseDescriptionProvider to supply human-readable descriptions per status code:
func (r *MyRoute) ResponseDescriptions() map[int]string { return map[int]string{ 200: "Payment processed successfully", 400: "Invalid request payload", 401: "Missing or invalid authentication", } }
A route's documented path is the router's BaseURL plus the route's own path.
A route declared as /schemas on a router based at /internal is served at
/internal/schemas, and that is what the document says — the router strips the
prefix before matching, so the route itself declares the bare path.
Which routers contribute routes is configuration, and the default is deliberately narrow: the default router alone. A document that omits a route can be widened; a document that published a private route has already published it.
openapi.NewConfig( openapi.WithIncludeRouters("*"), // every router openapi.WithExcludeRouters("internal"), // except this one )
ExcludeRouters is applied after IncludeRouters, so it wins over "*".
Two routes that resolve to the same method and documented path are a startup error, naming both operations and both routers. They used to overwrite one another, so an operation simply vanished from the document.
One instance of the extension serves one document. An application that wants a
public document on its public listener and a full one on its internal listener
runs two instances — each with its own name, its own router scope, and its
own ServeOnRouter:
rex.New( // Public: the default router only, served on the default listener. openapi.WithOpenAPI(openapi.NewConfig( openapi.WithName("public"), openapi.WithTitle("Public API"), )), // Internal: everything, served on the internal listener only. openapi.WithOpenAPI(openapi.NewConfig( openapi.WithName("internal"), openapi.WithTitle("Internal API"), openapi.WithIncludeRouters("*"), openapi.WithServeOnRouter("internal"), )), // The UI beside the internal document. swagger.WithSwagger(swagger.NewConfig( swagger.WithServeOnRouter("internal"), )), )
Name is what tells the two apart in the logs and in startup errors; both
instances would otherwise report identically.
| Field | Type | Default | Description |
|---|---|---|---|
Name |
string |
"openapi" |
Instance name in log lines and startup errors |
Title |
string |
"API" |
API title in the info section |
Version |
string |
"1.0.0" |
API version in the info section |
Description |
string |
"" |
API description in the info section |
ServePath |
string |
"/openapi.json" |
Path to serve the OpenAPI JSON document |
Tags |
[]Tag |
nil |
Top-level tag descriptions |
IncludeRouters |
[]string |
nil |
Routers whose routes appear; empty = default router, "*" = all |
ExcludeRouters |
[]string |
nil |
Routers to omit, applied after IncludeRouters |
ServeOnRouter |
string |
"" |
Router that serves the document; empty = default router |
openapi.WithOpenAPI(openapi.NewConfig( openapi.WithName("public"), openapi.WithTitle("My API"), openapi.WithVersion("2.0.0"), openapi.WithDescription("My awesome API"), openapi.WithServePath("/docs/openapi.json"), ))
| Option | Description |
|---|---|
WithName(s) |
Names the instance in logs and startup errors |
WithTitle(s) |
Sets the API title |
WithVersion(s) |
Sets the API version |
WithDescription(s) |
Sets the API description |
WithServePath(s) |
Sets the path to serve the OpenAPI JSON |
WithTags(...) |
Registers top-level tag descriptions |
WithIncludeRouters(...) |
Routers whose routes appear in the document |
WithExcludeRouters(...) |
Routers to omit, applied after the include list |
WithServeOnRouter(s) |
Router that serves the document |
- Implement OpenAPIRoute on all public routes: This ensures complete API documentation is generated automatically
- Use meaningful operation IDs: Follow a consistent naming convention like
getUser,createOrderfor clear client SDK generation - Leverage struct tags: Use
jsonandvalidatetags on request/response types for accurate schema generation - Provide response examples: Implement
ResponseExamplesProviderto give consumers concrete examples of your API responses - Add response descriptions: Use
ResponseDescriptionProviderto document what each status code means in your domain - Declare per-status-code responses: Return all possible response types from
Responses()for complete documentation - Use tags to organize operations: Group related endpoints under the same tags for better navigation
- Keep the internal API in its own document: run a second instance with
WithIncludeRoutersandWithServeOnRouterrather than widening the public one - Customize the serve path: In production, consider serving the spec at a well-known path like
/openapi.jsonor/docs/openapi.json
v0.2.1 → v0.3.0. MIGRATION.md is the upgrade guide for this
module, written to stand alone — it carries the dependency-ordered go get
sequence, every breaking change here, and what to verify afterwards. Other
modules of the framework each have their own; that file links to them.
The framework is in alpha, and external contributions open at v1.0.0.
Until then pull requests will be closed unmerged — but issues are very welcome.
Bug reports, questions and feature requests all feed into what v1.0.0 looks
like.
See CONTRIBUTING.md for the rules that will apply, and COMMIT-CONVENTIONS.md for the commit format.
This project is licensed under the MIT License - see the LICENSE file for details.
© 2026 Kryovyx