Skip to main content

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

MethodDescription
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 spec
  • GET /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 header
  • APIKeyQuerySecurity(paramName) - apiKey in query param
  • BearerSecurity(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 object schemas with json tag-based property names
  • validate:"required" tags map to the required array
  • Embedded structs are merged inline
  • time.Time maps to string with format: date-time
  • Slices/Arrays become array types
  • Maps become object with additionalProperties

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"})
}