OpenAPI
The openapi package auto-generates an OpenAPI 3.0.3 spec from your routes and Go types. It lives in a separate package - zero cost when not imported, zero per-request overhead (spec is generated once at startup via sync.Once).
Setup
Create an openapi.Config and pass it to openapi.New:
import "github.com/Pavan-Silva/go-zen/openapi"
doc := openapi.New(openapi.Config{
Title: "Users API",
Version: "1.0.0",
Description: "A simple user management API",
})
Routes are registered explicitly via doc.GET, doc.POST, etc. - no auto-discovery needed.
Registering Metadata
Enrich routes with openapi.RI() - a builder that only needs the openapi. prefix once:
doc.GET("/api/v1/users", openapi.RI().
Summary("List all users").
Desc("Returns a paginated list of users").
Tags("Users").
Resp(200, &ListUsersResponse{}),
)
doc.POST("/api/v1/users", openapi.RI().
Summary("Create a user").
Tags("Users").
Body(&CreateUserRequest{}).
Resp(201, &UserResponse{}).
Resp(400, &ErrorResponse{}),
)
Method helpers (doc.GET, doc.POST, doc.PUT, doc.DELETE, doc.PATCH, doc.HEAD, doc.OPTIONS) match each route to the right HTTP method.
The path must match the full path registered on the engine (including group prefixes).
Builder Methods
| Method | Description |
|---|---|
Summary(s) | Short route summary |
Desc(s) | Longer route description |
Tags(t ...string) | Tags for grouping in the UI |
Resp(code, model) | Response type for a status code (pass nil for no body, e.g. 204) |
Body(model) | Request body type |
Security(name, scopes...) | Security requirement for this route (pass "" for no auth) |
Deprecated() | Marks the route as deprecated |
Resp and Body accept any Go type. The package uses reflection to convert structs (with json tags) to OpenAPI Schema Objects at spec-generation time - not per request.
Separate Registration
If you prefer, use Register for full control:
doc.Register("GET", "/api/v1/users", openapi.RI().
Summary("List users").
Resp(200, &ListUsersResponse{}),
)
Serving the Spec and UI
Use RegisterRoutes to serve the spec JSON and docs UI - it bypasses middleware via HandleRaw:
doc.RegisterRoutes(r)
By default:
GET /openapi.json- the generated OpenAPI specGET /docs- Swagger UI documentation
Customize paths in Config:
doc := openapi.New(openapi.Config{
SpecPath: "/api/openapi.json",
DocPath: "/api/docs",
})
Disable the UI entirely:
doc := openapi.New(openapi.Config{
DisableUI: true,
})
SwaggerUI Configuration
Pass additional SwaggerUI init options via SwaggerUIOptions. Any attribute
accepted by the SwaggerUIBundle constructor may be specified - values are
serialized as JSON literals, so strings, booleans, numbers, arrays, and
nested objects all work:
doc := openapi.New(openapi.Config{
Title: "Users API",
Version: "1.0.0",
SwaggerUIOptions: map[string]any{
"persistAuthorizations": true,
"docExpansion": "list",
"filter": true,
"tryItOutEnabled": true,
},
})
Alternate Handlers
For manual mounting, use SpecHandler and DocHandler directly:
r.GET("/openapi.json", doc.SpecHandler())
r.GET("/docs", doc.DocHandler())
Security Schemes
Define authentication schemes in Config.SecuritySchemes and apply them with default or per-route security requirements.
Defining Schemes
doc := openapi.New(openapi.Config{
SecuritySchemes: map[string]openapi.SecurityScheme{
"BearerAuth": openapi.BearerSecurity("JWT"),
"ApiKeyAuth": openapi.APIKeySecurity("X-API-Key"),
"BasicAuth": openapi.BasicSecurity(),
},
DefaultSecurity: []map[string][]string{
{"BearerAuth": {}},
},
})
Helper constructors:
APIKeySecurity(headerName)- apiKey in headerAPIKeyQuerySecurity(paramName)- apiKey in query paramBearerSecurity(format)- HTTP bearer (JWT, etc.)BasicSecurity()- HTTP basic
Per-Route Security
Override the default security on individual routes:
// Public route - no auth required
doc.GET("/api/v1/health", openapi.RI().
Summary("Health check").
Security(""),
)
// Route requiring specific OAuth2 scope
doc.GET("/api/v1/admin", openapi.RI().
Summary("Admin panel").
Security("BearerAuth", "admin"),
)
The security schemes map directly to your zen auth middleware - the OpenAPI spec documents what your middleware enforces.
OAuth2 Flows
For OAuth2 schemes, configure flows:
openapi.SecurityScheme{
Type: openapi.SecurityOAuth2,
Flows: &openapi.OAuthFlows{
AuthorizationCode: &openapi.OAuthFlow{
AuthorizationURL: "https://auth.example.com/authorize",
TokenURL: "https://auth.example.com/token",
Scopes: map[string]string{"admin": "Admin access", "user": "User access"},
},
},
}
Schema Generation
The package converts Go types to OpenAPI Schema Objects automatically:
- Structs become
objectschemas withjsontag-based property names validate:"required"tags map to therequiredarray- Embedded structs are merged inline
time.Timemaps tostringwithformat: date-time- Slices/Arrays become
arraytypes - Maps become
objectwithadditionalProperties
Responses and request bodies are registered as named schemas and referenced via $ref.
Full Example
package main
import (
"github.com/Pavan-Silva/go-zen"
"github.com/Pavan-Silva/go-zen/middleware"
"github.com/Pavan-Silva/go-zen/openapi"
)
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
type ErrorResponse struct {
Error string `json:"error"`
}
type ListUsersResponse struct {
Users []UserResponse `json:"users"`
Total int `json:"total"`
}
func main() {
r := zen.New(":8080")
r.Use(middleware.Recover)
r.Use(middleware.Logger)
doc := openapi.New(openapi.Config{
Title: "Users API",
Version: "1.0.0",
Description: "A simple user management API",
SecuritySchemes: map[string]openapi.SecurityScheme{
"BearerAuth": openapi.BearerSecurity("JWT"),
},
DefaultSecurity: []map[string][]string{
{"BearerAuth": {}},
},
})
api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/users", listUsers)
doc.GET("/api/v1/users", openapi.RI().
Summary("List all users").
Tags("Users").
Resp(200, &ListUsersResponse{}),
)
v1.POST("/users", createUser)
doc.POST("/api/v1/users", openapi.RI().
Summary("Create a user").
Tags("Users").
Body(&CreateUserRequest{}).
Resp(201, &UserResponse{}).
Resp(400, &ErrorResponse{}),
)
v1.GET("/health", healthCheck)
doc.GET("/api/v1/health", openapi.RI().
Summary("Health check").
Security(""),
)
doc.RegisterRoutes(r)
r.Run()
}
func listUsers(c *zen.Ctx) {
c.JSON(200, ListUsersResponse{Users: []UserResponse{}, Total: 0})
}
func createUser(c *zen.Ctx) {
var req CreateUserRequest
if err := c.BindJSON(&req); err != nil {
c.JSON(400, ErrorResponse{Error: err.Error()})
return
}
c.JSON(201, UserResponse{ID: 1, Name: req.Name, Email: req.Email})
}
func healthCheck(c *zen.Ctx) {
c.JSON(200, map[string]string{"status": "ok"})
}