OpenAPI
Auto-generate OpenAPI 3.1 specifications from your routes
Forge automatically generates an OpenAPI 3.1.0 specification from your route definitions. Request and response schemas are derived from Go types, struct tags drive parameter classification, and the spec is served at /_/openapi.
How It Works
When you define routes with schema options, Forge inspects your Go types at startup and builds a complete OpenAPI specification. No code generation step is required -- the spec is always in sync with your code.
router.POST("/users", createUser,
forge.WithRequestSchema(&CreateUserRequest{}),
forge.WithResponseSchema(201, "User created", &User{}),
forge.WithTags("users"),
forge.WithSummary("Create a new user"),
)Access the generated spec at:
GET /_/openapiRequest Schemas with Unified Struct Tags
WithRequestSchema is the recommended approach. Forge automatically classifies struct fields based on their tags:
| Tag | Classification | Example |
|---|---|---|
path:"name" | Path parameter | path:"userId" |
query:"name" | Query parameter | query:"page" |
header:"name" | Header parameter | header:"X-API-Key" |
json:"name" or body:"" | Request body field | json:"email" |
type CreateUserRequest struct {
// Path parameter
TenantID string `path:"tenantId" description:"Tenant identifier" format:"uuid"`
// Query parameter
DryRun bool `query:"dryRun" description:"Preview mode without persisting"`
// Header parameter
APIKey string `header:"X-API-Key" description:"API authentication key"`
// Body fields (json tag = body)
Name string `json:"name" description:"Full name" minLength:"1" maxLength:"100"`
Email string `json:"email" description:"Email address" format:"email"`
Age int `json:"age" description:"User age" minimum:"0" maximum:"150"`
}
router.POST("/tenants/:tenantId/users", createUser,
forge.WithRequestSchema(&CreateUserRequest{}),
)If a struct has no path, query, or header tags, the entire struct is treated as a JSON request body for backward compatibility.
Validation Tags
Struct tags also drive OpenAPI validation constraints:
type ProductRequest struct {
Name string `json:"name" minLength:"1" maxLength:"200"`
Price float64 `json:"price" minimum:"0.01" maximum:"999999.99"`
Quantity int `json:"quantity" minimum:"0"`
SKU string `json:"sku" pattern:"^[A-Z]{2}-[0-9]{6}$"`
Tags []string `json:"tags" minItems:"1" maxItems:"10"`
}Struct Tag Reference
Forge processes struct tags across request binding, schema generation, and validation. This is the complete reference of all supported tags.
Binding Tags
These tags control where request data is bound from:
| Tag | Source | Example |
|---|---|---|
path:"name" | URL path parameter | path:"userId" |
query:"name" | URL query string | query:"page" |
header:"name" | HTTP request header | header:"X-API-Key" |
json:"name" | JSON request body | json:"email" |
body:"name" | Explicit body field | body:"data" |
form:"name" | Multipart form field | form:"avatar" |
All binding tags except path support the ,omitempty option (e.g., query:"page,omitempty") which marks the field as optional.
Field Requirement Tags
These tags control whether a field is required or optional in both request binding and OpenAPI schema generation.
| Tag | Effect |
|---|---|
optional:"true" | Field is optional (highest precedence) |
required:"true" | Field is required |
default:"value" | Sets default value; field is implicitly optional |
Precedence order (first match wins):
optional:"true"-- explicitly optional (highest priority)required:"true"-- explicitly requireddefault:"..."-- fields with defaults are implicitly optional,omitemptyin binding tag -- optional- Pointer type (
*string,*int) -- optional - Non-pointer without above markers -- required (default)
type ListUsersRequest struct {
// Required (non-pointer, no optional/default/omitempty)
TenantID string `path:"tenantId"`
// Optional via default tag (no need for optional:"true")
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"10"`
Sort string `query:"sort" default:"created_at"`
// Optional via explicit tag
Search string `query:"search" optional:"true"`
// Optional via omitempty
Filter string `query:"filter,omitempty"`
// Optional via pointer type
Cursor *string `query:"cursor"`
// Required takes precedence over default
Status string `query:"status" default:"active" required:"true"`
}Fields with a default:"..." tag are implicitly treated as optional. The default value is applied during binding when the field is not provided. You do not need to add optional:"true" alongside default.
Schema Documentation Tags
Add metadata that appears in the generated OpenAPI spec:
| Tag | Purpose | Example |
|---|---|---|
description:"text" | Field description | description:"User's full name" |
title:"text" | Field title | title:"Full Name" |
example:"value" | Example value | example:"alice@example.com" |
deprecated:"true" | Mark as deprecated | deprecated:"true" |
type User struct {
Name string `json:"name" description:"User's full name" example:"Alice Smith"`
Email string `json:"email" title:"Email Address" format:"email"`
OldID string `json:"old_id" deprecated:"true" description:"Use 'id' instead"`
}Schema Type Tags
Control OpenAPI type and format information:
| Tag | Purpose | Example Values |
|---|---|---|
format:"fmt" | OpenAPI format | binary, byte, date-time, uuid, ulid, xid, email |
enum:"a,b,c" | Allowed values | enum:"admin,user,viewer" |
const:"value" | Constant value | const:"v1" |
nullable:"true" | Field can be null | nullable:"true" |
readOnly:"true" | Response only | readOnly:"true" |
writeOnly:"true" | Request only | writeOnly:"true" |
type CreateUserRequest struct {
Role string `json:"role" enum:"admin,user,viewer"`
Email string `json:"email" format:"email"`
Password string `json:"password" writeOnly:"true"`
}
type UserResponse struct {
ID string `json:"id" readOnly:"true" format:"uuid"`
Role string `json:"role" enum:"admin,user,viewer"`
CreatedAt string `json:"created_at" format:"date-time" readOnly:"true"`
}Schema Validation Tags
Drive OpenAPI validation constraints in the generated spec:
String constraints:
| Tag | Purpose | Example |
|---|---|---|
minLength:"n" | Minimum string length | minLength:"1" |
maxLength:"n" | Maximum string length | maxLength:"100" |
pattern:"regex" | Regex pattern | pattern:"^[A-Z]{2}-[0-9]+$" |
Numeric constraints:
| Tag | Purpose | Example |
|---|---|---|
minimum:"n" | Minimum value | minimum:"0" |
maximum:"n" | Maximum value | maximum:"100" |
multipleOf:"n" | Must be multiple of | multipleOf:"0.01" |
exclusiveMinimum:"true" | Exclude min boundary | exclusiveMinimum:"true" |
exclusiveMaximum:"true" | Exclude max boundary | exclusiveMaximum:"true" |
Array constraints:
| Tag | Purpose | Example |
|---|---|---|
minItems:"n" | Minimum array length | minItems:"1" |
maxItems:"n" | Maximum array length | maxItems:"100" |
uniqueItems:"true" | Items must be unique | uniqueItems:"true" |
Object constraints:
| Tag | Purpose | Example |
|---|---|---|
minProperties:"n" | Minimum properties | minProperties:"1" |
maxProperties:"n" | Maximum properties | maxProperties:"50" |
Runtime Validation Tag
Use the validate tag for runtime request validation (powered by go-playground/validator):
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
URL string `json:"url" validate:"omitempty,url"`
}The validate tag controls runtime validation during request binding. The schema tags (minLength, minimum, etc.) control what appears in the OpenAPI spec. Use both for full coverage.
Sensitive Data Tag
Control how sensitive fields are handled in logs and debug output:
| Tag Value | Behavior |
|---|---|
sensitive:"true" or sensitive:"1" | Field is set to its zero value |
sensitive:"redact" | Replaced with [REDACTED] |
sensitive:"mask:***" | Replaced with custom mask string |
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password" sensitive:"redact"`
SSN string `json:"ssn" sensitive:"mask:***-**-****"`
Token string `json:"token" sensitive:"true"`
}Response Schemas
Define response schemas for each status code using WithResponseSchema.
router.GET("/users/:id", getUser,
forge.WithResponseSchema(200, "User found", &User{}),
forge.WithResponseSchema(404, "User not found", &ErrorResponse{}),
)Response Helpers
Forge provides convenience functions for common response patterns.
// Generates: { data: T[], total, page, pageSize, totalPages }
router.GET("/users", listUsers,
forge.WithPaginatedResponse(&User{}, 200),
)// Adds 200, 201, 204, 400, 401, 403, 404, 500
router.POST("/users", createUser,
forge.WithStandardRESTResponses(&User{}),
)// Generates: { jobId, status, statusUrl }
router.POST("/exports", startExport,
forge.WithAcceptedResponse(),
)// Generates: { successful, failed, totalProcessed, successCount, failureCount }
router.POST("/users/batch", batchCreate,
forge.WithBatchResponse(&User{}, 200),
)Full Response Helpers Reference
| Helper | Status | Description |
|---|---|---|
WithResponseSchema(code, desc, type) | Any | Custom response schema |
WithPaginatedResponse(itemType, code) | Any | Paginated list with metadata |
WithStandardRESTResponses(type) | 200/201/204 + errors | Full CRUD response set |
WithErrorResponses() | 400/401/403/404/500 | Standard error responses |
WithCreatedResponse(type) | 201 | Resource created |
WithNoContentResponse() | 204 | No content (deletes) |
WithAcceptedResponse() | 202 | Async operation accepted |
WithListResponse(itemType, code) | Any | Simple array response |
WithBatchResponse(itemType, code) | Any | Batch operation result |
WithFileUploadResponse(code) | Any | File upload success |
WithValidationErrorResponse() | 422 | Validation error details |
Request and Response Examples
Provide example values that appear in the OpenAPI spec and generated documentation.
router.POST("/users", createUser,
forge.WithRequestSchema(&CreateUserRequest{}),
forge.WithRequestExample("basic", CreateUserRequest{
Name: "Alice Smith",
Email: "alice@example.com",
Age: 30,
}),
forge.WithResponseSchema(201, "Created", &User{}),
forge.WithResponseExample(201, "success", User{
ID: "usr_abc123",
Name: "Alice Smith",
Email: "alice@example.com",
}),
)Discriminators for Polymorphic Types
Support polymorphic request/response types using OpenAPI discriminators with oneOf.
type Notification struct {
Type string `json:"type"` // "email", "sms", "push"
}
type EmailNotification struct {
Notification
Subject string `json:"subject"`
Body string `json:"body"`
}
type SMSNotification struct {
Notification
PhoneNumber string `json:"phone_number"`
Message string `json:"message"`
}
router.POST("/notifications", sendNotification,
forge.WithRequestSchema(&Notification{}),
forge.WithDiscriminator(forge.DiscriminatorConfig{
PropertyName: "type",
Mapping: map[string]any{
"email": &EmailNotification{},
"sms": &SMSNotification{},
},
}),
)Content Types
Specify custom content types for request and response bodies.
router.POST("/upload", uploadHandler,
forge.WithRequestContentTypes("multipart/form-data"),
forge.WithResponseContentTypes("application/json", "application/xml"),
)Schema References
Register reusable schemas in the OpenAPI components section.
router.GET("/users/:id", getUser,
forge.WithSchemaRef("User", &User{}),
forge.WithSchemaRef("Error", &ErrorResponse{}),
forge.WithResponseSchema(200, "Success", &User{}),
)Route Metadata
Add OpenAPI metadata to routes for better documentation.
router.GET("/users/:id", getUser,
forge.WithName("getUser"),
forge.WithOperationID("getUserById"),
forge.WithSummary("Get a user by ID"),
forge.WithDescription("Retrieves a user record by their unique identifier."),
forge.WithTags("users", "admin"),
forge.WithDeprecated(),
forge.WithExternalDocs("API Guide", "https://docs.example.com/users"),
forge.WithSecurity("bearerAuth"),
)Excluding Routes from OpenAPI
Prevent specific routes or groups from appearing in the OpenAPI spec.
// Exclude a single route
router.GET("/internal/debug", debugHandler,
forge.WithOpenAPIExclude(),
)
// Exclude all schemas (OpenAPI, AsyncAPI, oRPC)
router.GET("/internal/status", statusHandler,
forge.WithSchemaExclude(),
)
// Exclude an entire group
adminGroup := router.Group("/admin",
forge.WithGroupSchemaExclude(),
)OpenAPI Configuration
Configure the OpenAPI spec globally through OpenAPIConfig on the router.
app := forge.New(
forge.WithAppRouterOptions(
forge.WithOpenAPI(forge.OpenAPIConfig{
Title: "My API",
Version: "2.0.0",
Description: "Production API for My Service",
Servers: []forge.OpenAPIServer{
{URL: "https://api.example.com", Description: "Production"},
{URL: "https://staging-api.example.com", Description: "Staging"},
},
Contact: &forge.Contact{
Name: "API Support",
Email: "api@example.com",
URL: "https://example.com/support",
},
License: &forge.License{
Name: "MIT",
URL: "https://opensource.org/licenses/MIT",
},
SecuritySchemes: map[string]forge.SecurityScheme{
"bearerAuth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
},
"apiKey": {
Type: "apiKey",
In: "header",
Name: "X-API-Key",
},
},
Tags: []forge.OpenAPITag{
{Name: "users", Description: "User management operations"},
{Name: "orders", Description: "Order processing"},
},
}),
),
)Complete Example
package main
import "github.com/xraph/forge"
type CreateUserRequest struct {
TenantID string `path:"tenantId" format:"uuid"`
Name string `json:"name" minLength:"1" maxLength:"100"`
Email string `json:"email" format:"email"`
Role string `json:"role" enum:"admin,user,viewer"`
}
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
func main() {
app := forge.New(
forge.WithAppName("user-api"),
)
r := app.Router()
r.POST("/tenants/:tenantId/users", createUser,
forge.WithSummary("Create user"),
forge.WithDescription("Creates a new user in the specified tenant."),
forge.WithTags("users"),
forge.WithRequestSchema(&CreateUserRequest{}),
forge.WithCreatedResponse(&User{}),
forge.WithErrorResponses(),
forge.WithValidationErrorResponse(),
forge.WithRequestExample("basic", CreateUserRequest{
Name: "Alice",
Email: "alice@example.com",
Role: "user",
}),
)
app.Run()
}
func createUser(ctx forge.Context) error {
var req CreateUserRequest
if err := ctx.BindJSON(&req); err != nil {
return forge.BadRequest("invalid request body")
}
req.TenantID = ctx.Param("tenantId")
// ... create user logic
return ctx.JSON(201, User{
ID: "usr_new",
Name: req.Name,
Email: req.Email,
Role: req.Role,
})
}The generated OpenAPI spec is available at GET /_/openapi and can be used with Swagger UI, Redoc, or any OpenAPI-compatible tool.
How is this guide?