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 /_/openapi

Request Schemas with Unified Struct Tags

WithRequestSchema is the recommended approach. Forge automatically classifies struct fields based on their tags:

TagClassificationExample
path:"name"Path parameterpath:"userId"
query:"name"Query parameterquery:"page"
header:"name"Header parameterheader:"X-API-Key"
json:"name" or body:""Request body fieldjson:"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:

TagSourceExample
path:"name"URL path parameterpath:"userId"
query:"name"URL query stringquery:"page"
header:"name"HTTP request headerheader:"X-API-Key"
json:"name"JSON request bodyjson:"email"
body:"name"Explicit body fieldbody:"data"
form:"name"Multipart form fieldform:"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.

TagEffect
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):

  1. optional:"true" -- explicitly optional (highest priority)
  2. required:"true" -- explicitly required
  3. default:"..." -- fields with defaults are implicitly optional
  4. ,omitempty in binding tag -- optional
  5. Pointer type (*string, *int) -- optional
  6. 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:

TagPurposeExample
description:"text"Field descriptiondescription:"User's full name"
title:"text"Field titletitle:"Full Name"
example:"value"Example valueexample:"alice@example.com"
deprecated:"true"Mark as deprecateddeprecated:"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:

TagPurposeExample Values
format:"fmt"OpenAPI formatbinary, byte, date-time, uuid, ulid, xid, email
enum:"a,b,c"Allowed valuesenum:"admin,user,viewer"
const:"value"Constant valueconst:"v1"
nullable:"true"Field can be nullnullable:"true"
readOnly:"true"Response onlyreadOnly:"true"
writeOnly:"true"Request onlywriteOnly:"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:

TagPurposeExample
minLength:"n"Minimum string lengthminLength:"1"
maxLength:"n"Maximum string lengthmaxLength:"100"
pattern:"regex"Regex patternpattern:"^[A-Z]{2}-[0-9]+$"

Numeric constraints:

TagPurposeExample
minimum:"n"Minimum valueminimum:"0"
maximum:"n"Maximum valuemaximum:"100"
multipleOf:"n"Must be multiple ofmultipleOf:"0.01"
exclusiveMinimum:"true"Exclude min boundaryexclusiveMinimum:"true"
exclusiveMaximum:"true"Exclude max boundaryexclusiveMaximum:"true"

Array constraints:

TagPurposeExample
minItems:"n"Minimum array lengthminItems:"1"
maxItems:"n"Maximum array lengthmaxItems:"100"
uniqueItems:"true"Items must be uniqueuniqueItems:"true"

Object constraints:

TagPurposeExample
minProperties:"n"Minimum propertiesminProperties:"1"
maxProperties:"n"Maximum propertiesmaxProperties:"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 ValueBehavior
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

HelperStatusDescription
WithResponseSchema(code, desc, type)AnyCustom response schema
WithPaginatedResponse(itemType, code)AnyPaginated list with metadata
WithStandardRESTResponses(type)200/201/204 + errorsFull CRUD response set
WithErrorResponses()400/401/403/404/500Standard error responses
WithCreatedResponse(type)201Resource created
WithNoContentResponse()204No content (deletes)
WithAcceptedResponse()202Async operation accepted
WithListResponse(itemType, code)AnySimple array response
WithBatchResponse(itemType, code)AnyBatch operation result
WithFileUploadResponse(code)AnyFile upload success
WithValidationErrorResponse()422Validation 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?

On this page