This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

OpenAPI Specification

Learn how to generate OpenAPI specifications from Go code with automatic parameter discovery and schema generation

The Rivaas OpenAPI package provides automatic OpenAPI 3.0.4 and 3.1.2 specification generation from Go code. Uses struct tags and reflection with a clean, type-safe API. Minimal boilerplate required.

Features

  • Clean API - Builder-style API.Generate() method for specification generation
  • Type-Safe Version Selection - V30x and V31x constants with IDE autocomplete
  • Fluent HTTP Method Constructors - GET(), POST(), PUT(), etc. for clean operation definitions
  • Functional Options - Consistent With* pattern for all configuration
  • Type-Safe Warning Diagnostics - diag package for fine-grained warning control
  • Automatic Parameter Discovery - Extracts query, path, header, and cookie parameters from struct tags
  • Schema Generation - Converts Go types to OpenAPI schemas automatically
  • Swagger UI Configuration - Built-in, customizable Swagger UI settings
  • Semantic Operation IDs - Auto-generates operation IDs from HTTP methods and paths
  • Security Schemes - Support for Bearer, API Key, OAuth2, and OpenID Connect
  • Collision-Resistant Naming - Schema names use pkgname.TypeName format to prevent collisions
  • Built-in Validation - Validates generated specs against official OpenAPI meta-schemas
  • Standalone Validator - Validate external OpenAPI specs with pre-compiled schemas

Quick Start

Here’s a 30-second example to get you started:

package main

import (
    "context"
    "fmt"
    "log"

    "rivaas.dev/openapi"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("My API", "1.0.0"),
        openapi.WithInfoDescription("API for managing users"),
        openapi.WithServer("http://localhost:8080", "Local development"),
        openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
            openapi.WithSecurity("bearerAuth"),
        ),
        openapi.POST("/users",
            openapi.WithSummary("Create user"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
        ),
        openapi.DELETE("/users/:id",
            openapi.WithSummary("Delete user"),
            openapi.WithResponse(204, nil),
            openapi.WithSecurity("bearerAuth"),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Check for warnings (optional)
    if len(result.Warnings) > 0 {
        fmt.Printf("Generated with %d warnings\n", len(result.Warnings))
    }

    fmt.Println(string(result.JSON))
}

How It Works

  • API configuration is done through functional options with With* prefix
  • Operations are defined using HTTP method constructors: GET(), POST(), etc.
  • Types are automatically converted to OpenAPI schemas using reflection
  • Parameters are discovered from struct tags: path, query, header, cookie
  • Validation is optional but recommended for production use

Learning Path

Follow these guides to master OpenAPI specification generation with Rivaas:

  1. Installation - Get started with the openapi package
  2. Basic Usage - Learn the fundamentals of generating specifications
  3. Configuration - Configure API info, servers, and version selection
  4. Security - Add authentication and authorization schemes
  5. Operations - Define HTTP operations with methods and options
  6. Auto-Discovery - Use struct tags for automatic parameter discovery
  7. Schema Generation - Understand Go type to OpenAPI schema conversion
  8. Swagger UI - Customize the Swagger UI interface
  9. Validation - Validate generated specifications
  10. Diagnostics - Handle warnings with type-safe diagnostics
  11. Advanced Usage - Extensions, custom operation IDs, and strict mode
  12. Examples - See real-world usage patterns

Next Steps

1 - Installation

Install and set up the OpenAPI package for Go

Get started with the OpenAPI package by adding it to your Go project.

Prerequisites

  • Go 1.25 or higher - The package requires Go 1.25+
  • Go modules - Your project should use Go modules for dependency management

Installation

Install the package using go get:

go get rivaas.dev/openapi

This will download the latest version of the package and add it to your go.mod file.

Verifying Installation

Create a simple test file to verify the installation:

package main

import (
    "context"
    "fmt"
    "log"

    "rivaas.dev/openapi"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("Test API", "1.0.0"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("OpenAPI spec generated successfully!")
    fmt.Printf("JSON spec: %d bytes\n", len(result.JSON))
}

Run the test:

go run main.go

If you see “OpenAPI spec generated successfully!”, the package is installed correctly.

Sub-packages

The OpenAPI package includes two sub-packages that are automatically available when you install the main package:

Diagnostics (diag)

Type-safe warning handling:

import "rivaas.dev/openapi/diag"

Validator (validate)

Standalone specification validator for validating external OpenAPI specs:

import "rivaas.dev/openapi/validate"

Updating

To update to the latest version:

go get -u rivaas.dev/openapi

Next Steps

2 - Basic Usage

Learn the fundamentals of generating OpenAPI specifications

Learn how to generate OpenAPI specifications from Go code using the openapi package.

Creating an API Configuration

The first step is to create an API configuration using New() or MustNew():

import "rivaas.dev/openapi"

// With error handling
api, err := openapi.New(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithInfoDescription("API description"),
)
if err != nil {
    log.Fatal(err)
}

// Without error handling (panics on error)
api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithInfoDescription("API description"),
)

The MustNew() function is convenient for initialization code. Use it where panicking on error is acceptable.

Generating Specifications

Use api.Generate() with a context and variadic operation arguments:

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithResponse(200, []User{}),
    ),
    openapi.GET("/users/:id",
        openapi.WithSummary("Get user"),
        openapi.WithResponse(200, User{}),
    ),
    openapi.POST("/users",
        openapi.WithSummary("Create user"),
        openapi.WithRequest(CreateUserRequest{}),
        openapi.WithResponse(201, User{}),
    ),
)
if err != nil {
    log.Fatal(err)
}

The Result Object

The Generate() method returns a Result object containing:

  • JSON - The OpenAPI specification as JSON bytes.
  • YAML - The OpenAPI specification as YAML bytes.
  • Warnings - Any generation warnings. See Diagnostics for details.
// Use the JSON specification
fmt.Println(string(result.JSON))

// Or use the YAML specification
fmt.Println(string(result.YAML))

// Check for warnings
if len(result.Warnings) > 0 {
    fmt.Printf("Generated with %d warnings\n", len(result.Warnings))
}

Defining Operations

Operations are defined using HTTP method constructors:

openapi.GET("/path", options...)
openapi.POST("/path", options...)
openapi.PUT("/path", options...)
openapi.PATCH("/path", options...)
openapi.DELETE("/path", options...)
openapi.HEAD("/path", options...)
openapi.OPTIONS("/path", options...)
openapi.TRACE("/path", options...)

Each constructor takes a path and optional operation options.

Path Parameters

Use colon syntax for path parameters:

openapi.GET("/users/:id",
    openapi.WithSummary("Get user by ID"),
    openapi.WithResponse(200, User{}),
)

openapi.GET("/orgs/:orgId/users/:userId",
    openapi.WithSummary("Get user in organization"),
    openapi.WithResponse(200, User{}),
)

Path parameters are automatically discovered and marked as required.

Request and Response Types

Define request and response types using Go structs:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Use in operations
openapi.POST("/users",
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

The package automatically converts Go types to OpenAPI schemas.

Multiple Response Types

Operations can have multiple response types for different status codes:

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
    openapi.WithResponse(404, ErrorResponse{}),
    openapi.WithResponse(500, ErrorResponse{}),
)

Empty Responses

For responses with no body, use nil:

openapi.DELETE("/users/:id",
    openapi.WithSummary("Delete user"),
    openapi.WithResponse(204, nil),
)

Complete Example

Here’s a complete example putting it all together:

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "rivaas.dev/openapi"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("User API", "1.0.0"),
        openapi.WithInfoDescription("API for managing users"),
        openapi.WithServer("http://localhost:8080", "Local development"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users",
            openapi.WithSummary("List users"),
            openapi.WithResponse(200, []User{}),
        ),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
            openapi.WithResponse(404, ErrorResponse{}),
        ),
        openapi.POST("/users",
            openapi.WithSummary("Create user"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
            openapi.WithResponse(400, ErrorResponse{}),
        ),
        openapi.DELETE("/users/:id",
            openapi.WithSummary("Delete user"),
            openapi.WithResponse(204, nil),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Write to file
    if err := os.WriteFile("openapi.json", result.JSON, 0644); err != nil {
        log.Fatal(err)
    }

    fmt.Println("OpenAPI specification written to openapi.json")
}

Next Steps

3 - Configuration

Configure API metadata, version selection, servers, and tags

Learn how to configure your OpenAPI specification with metadata, servers, and version selection.

Basic Configuration

Configuration is done exclusively through functional options with With* prefix:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithInfoDescription("API description"),
    openapi.WithInfoSummary("Short summary"), // 3.1.x only
    openapi.WithTermsOfService("https://example.com/terms"),
)

Required Configuration

Only WithTitle() is required when creating an API configuration:

// Minimal configuration
api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
)

Contact Information

Add contact information for API support:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithContact(
        "API Support",
        "https://example.com/support",
        "support@example.com",
    ),
)

License Information

Specify the API license:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithLicense(
        "Apache 2.0",
        "https://www.apache.org/licenses/LICENSE-2.0.html",
    ),
)

Version Selection

The package supports two OpenAPI version families:

// Target OpenAPI 3.0.x (generates 3.0.4)
api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V30x), // Default
)

// Target OpenAPI 3.1.x (generates 3.1.2)
api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V31x),
)

The constants V30x and V31x represent version families. Internally they map to specific versions. 3.0.4 and 3.1.2 are used in the generated specification.

Version-Specific Features

Some features are only available in OpenAPI 3.1.x:

  • WithInfoSummary() - Short summary for the API
  • WithLicenseIdentifier() - SPDX license identifier
  • Webhooks support
  • Mutual TLS authentication

When using these features with a 3.0.x target, the package will generate warnings (see Diagnostics).

Servers

Add server configurations to specify where the API is available:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithServer("https://api.example.com", "Production"),
    openapi.WithServer("https://staging.example.com", "Staging"),
    openapi.WithServer("http://localhost:8080", "Local development"),
)

Server Variables

Add variables to server URLs for flexible configuration:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithServer("https://{environment}.example.com", "Environment-based"),
    openapi.WithServerVariable("environment", "api", 
        []string{"api", "staging", "dev"},
        "Environment to use",
    ),
)

Multiple variables can be defined for a single server:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithServer("https://{subdomain}.{domain}", "Custom domain"),
    openapi.WithServerVariable("subdomain", "api", 
        []string{"api", "staging"},
        "Subdomain",
    ),
    openapi.WithServerVariable("domain", "example.com", 
        []string{"example.com", "test.com"},
        "Domain",
    ),
)

Tags

Tags help organize operations in the documentation:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithTag("users", "User management operations"),
    openapi.WithTag("posts", "Post management operations"),
    openapi.WithTag("auth", "Authentication operations"),
)

Tags are then referenced in operations:

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithTags("users"),
        openapi.WithResponse(200, []User{}),
    ),
    openapi.GET("/posts",
        openapi.WithSummary("List posts"),
        openapi.WithTags("posts"),
        openapi.WithResponse(200, []Post{}),
    ),
)

External Documentation

Link to external documentation:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithExternalDocs(
        "https://docs.example.com",
        "Full API Documentation",
    ),
)

Complete Configuration Example

Here’s a complete example with all common configuration options:

package main

import (
    "context"
    "log"

    "rivaas.dev/openapi"
)

func main() {
    api := openapi.MustNew(
        // Basic info
        openapi.WithTitle("User Management API", "2.1.0"),
        openapi.WithInfoDescription("Comprehensive API for managing users and their profiles"),
        openapi.WithTermsOfService("https://example.com/terms"),
        
        // Contact
        openapi.WithContact(
            "API Support Team",
            "https://example.com/support",
            "api-support@example.com",
        ),
        
        // License
        openapi.WithLicense(
            "Apache 2.0",
            "https://www.apache.org/licenses/LICENSE-2.0.html",
        ),
        
        // Version selection
        openapi.WithVersion(openapi.V31x),
        
        // Servers
        openapi.WithServer("https://api.example.com", "Production"),
        openapi.WithServer("https://staging-api.example.com", "Staging"),
        openapi.WithServer("http://localhost:8080", "Local development"),
        
        // Tags
        openapi.WithTag("users", "User management operations"),
        openapi.WithTag("profiles", "User profile operations"),
        openapi.WithTag("auth", "Authentication and authorization"),
        
        // External docs
        openapi.WithExternalDocs(
            "https://docs.example.com/api",
            "Complete API Documentation",
        ),
        
        // Security schemes (covered in detail in Security guide)
        openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
    )

    result, err := api.Generate(context.Background(),
        // ... operations here
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

Next Steps

4 - Security

Add authentication and authorization schemes to your OpenAPI specification

Learn how to add security schemes to your OpenAPI specification for authentication and authorization.

Security Scheme Types

The package supports four types of security schemes:

  1. Bearer Authentication - JWT or token-based authentication.
  2. API Key - API keys in headers, query parameters, or cookies.
  3. OAuth2 - OAuth 2.0 authorization flows.
  4. OpenID Connect - OpenID Connect authentication.

Bearer Authentication

Bearer authentication is commonly used for JWT tokens:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
)

Using Bearer Authentication in Operations

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithSecurity("bearerAuth"),
        openapi.WithResponse(200, []User{}),
    ),
)

The generated specification will expect an Authorization: Bearer <token> header.

API Key Authentication

API keys can be placed in headers, query parameters, or cookies:

Header-Based API Key

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithAPIKey(
        "apiKey",
        "X-API-Key",
        openapi.InHeader,
        "API key for authentication",
    ),
)

Query Parameter API Key

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithAPIKey(
        "apiKey",
        "api_key",
        openapi.InQuery,
        "API key for authentication",
    ),
)
api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithAPIKey(
        "apiKey",
        "api_key",
        openapi.InCookie,
        "API key for authentication",
    ),
)

Using API Key in Operations

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithSecurity("apiKey"),
        openapi.WithResponse(200, []User{}),
    ),
)

OAuth2

OAuth2 supports multiple flows: authorization code, implicit, password, and client credentials.

Authorization Code Flow

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithOAuth2(
        "oauth2",
        "OAuth2 authentication",
        openapi.OAuth2Flow{
            Type:             openapi.FlowAuthorizationCode,
            AuthorizationURL: "https://example.com/oauth/authorize",
            TokenURL:         "https://example.com/oauth/token",
            Scopes: map[string]string{
                "read":  "Read access to resources",
                "write": "Write access to resources",
                "admin": "Administrative access",
            },
        },
    ),
)

Implicit Flow

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithOAuth2(
        "oauth2",
        "OAuth2 authentication",
        openapi.OAuth2Flow{
            Type:             openapi.FlowImplicit,
            AuthorizationURL: "https://example.com/oauth/authorize",
            Scopes: map[string]string{
                "read":  "Read access",
                "write": "Write access",
            },
        },
    ),
)

Password Flow

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithOAuth2(
        "oauth2",
        "OAuth2 authentication",
        openapi.OAuth2Flow{
            Type:     openapi.FlowPassword,
            TokenURL: "https://example.com/oauth/token",
            Scopes: map[string]string{
                "read":  "Read access",
                "write": "Write access",
            },
        },
    ),
)

Client Credentials Flow

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithOAuth2(
        "oauth2",
        "OAuth2 authentication",
        openapi.OAuth2Flow{
            Type:     openapi.FlowClientCredentials,
            TokenURL: "https://example.com/oauth/token",
            Scopes: map[string]string{
                "api": "API access",
            },
        },
    ),
)

Using OAuth2 in Operations

Specify which scopes are required for an operation:

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithSecurity("oauth2", "read"),
        openapi.WithResponse(200, []User{}),
    ),
    openapi.POST("/users",
        openapi.WithSummary("Create user"),
        openapi.WithSecurity("oauth2", "read", "write"),
        openapi.WithRequest(CreateUserRequest{}),
        openapi.WithResponse(201, User{}),
    ),
)

OpenID Connect

OpenID Connect provides authentication on top of OAuth2:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithOpenIDConnect(
        "openId",
        "https://example.com/.well-known/openid-configuration",
        "OpenID Connect authentication",
    ),
)

Using OpenID Connect in Operations

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithSecurity("openId"),
        openapi.WithResponse(200, []User{}),
    ),
)

Multiple Security Schemes

You can define multiple security schemes:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
    openapi.WithAPIKey(
        "apiKey",
        "X-API-Key",
        openapi.InHeader,
        "API key authentication",
    ),
)

Alternative Security Requirements (OR)

Allow multiple authentication methods for a single operation:

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithSecurity("bearerAuth"),  // Can use bearer auth
        openapi.WithSecurity("apiKey"),      // OR can use API key
        openapi.WithResponse(200, []User{}),
    ),
)

This means the client can authenticate using either bearer auth or an API key.

Optional vs Required Security

Required Security

Apply security at the operation level:

openapi.GET("/users",
    openapi.WithSecurity("bearerAuth"),
    openapi.WithResponse(200, []User{}),
)

Optional Security (Public Endpoint)

Omit the WithSecurity() option:

openapi.GET("/public/status",
    openapi.WithSummary("Public status endpoint"),
    openapi.WithResponse(200, StatusResponse{}),
)

Complete Security Example

Here’s a complete example with multiple security schemes:

package main

import (
    "context"
    "log"

    "rivaas.dev/openapi"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type CreateUserRequest struct {
    Name string `json:"name"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("Secure API", "1.0.0"),
        
        // Define multiple security schemes
        openapi.WithBearerAuth("bearerAuth", "JWT token authentication"),
        openapi.WithAPIKey(
            "apiKey",
            "X-API-Key",
            openapi.InHeader,
            "API key authentication",
        ),
        openapi.WithOAuth2(
            "oauth2",
            "OAuth2 authentication",
            openapi.OAuth2Flow{
                Type:             openapi.FlowAuthorizationCode,
                AuthorizationURL: "https://example.com/oauth/authorize",
                TokenURL:         "https://example.com/oauth/token",
                Scopes: map[string]string{
                    "read":  "Read access",
                    "write": "Write access",
                },
            },
        ),
    )

    result, err := api.Generate(context.Background(),
        // Public endpoint (no security)
        openapi.GET("/health",
            openapi.WithSummary("Health check"),
            openapi.WithResponse(200, nil),
        ),
        
        // Bearer auth only
        openapi.GET("/users",
            openapi.WithSummary("List users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithResponse(200, []User{}),
        ),
        
        // API key or bearer auth (alternative)
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithSecurity("apiKey"),
            openapi.WithResponse(200, User{}),
        ),
        
        // OAuth2 with specific scopes
        openapi.POST("/users",
            openapi.WithSummary("Create user"),
            openapi.WithSecurity("oauth2", "read", "write"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

Next Steps

5 - Operations

Define HTTP operations with methods, options, and composable configurations

Learn how to define HTTP operations using method constructors and operation options.

HTTP Method Constructors

The package provides HTTP method constructors for defining operations:

openapi.GET("/users/:id", opts...)
openapi.POST("/users", opts...)
openapi.PUT("/users/:id", opts...)
openapi.PATCH("/users/:id", opts...)
openapi.DELETE("/users/:id", opts...)
openapi.HEAD("/users/:id", opts...)
openapi.OPTIONS("/users", opts...)
openapi.TRACE("/debug", opts...)

Each constructor takes a path and optional operation options.

Operation Options

All operation options follow the With* naming convention:

FunctionDescription
WithSummary(s)Set operation summary
WithDescription(s)Set operation description
WithOperationID(id)Set custom operation ID
WithRequest(type, examples...)Set request body type
WithResponse(status, type, examples...)Set response type for status code
WithTags(tags...)Add tags to operation
WithSecurity(scheme, scopes...)Add security requirement
WithDeprecated()Mark operation as deprecated
WithConsumes(types...)Set accepted content types
WithProduces(types...)Set returned content types
WithOperationExtension(key, value)Add operation extension

Basic Operation Definition

Define a simple GET operation:

result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        openapi.WithSummary("Get user by ID"),
        openapi.WithDescription("Retrieves a user by their unique identifier"),
        openapi.WithResponse(200, User{}),
    ),
)

Request Bodies

Use WithRequest() to specify the request body type:

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

Request with Examples

Provide example request bodies:

exampleUser := CreateUserRequest{
    Name:  "John Doe",
    Email: "john@example.com",
}

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}, exampleUser),
    openapi.WithResponse(201, User{}),
)

Response Types

Define multiple response types for different status codes:

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
    openapi.WithResponse(404, ErrorResponse{}),
    openapi.WithResponse(500, ErrorResponse{}),
)

Response with Examples

Provide example responses:

exampleUser := User{
    ID:    123,
    Name:  "John Doe",
    Email: "john@example.com",
}

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}, exampleUser),
)

Tags

Organize operations with tags:

result, err := api.Generate(context.Background(),
    openapi.GET("/users",
        openapi.WithSummary("List users"),
        openapi.WithTags("users"),
        openapi.WithResponse(200, []User{}),
    ),
    openapi.GET("/posts",
        openapi.WithSummary("List posts"),
        openapi.WithTags("posts"),
        openapi.WithResponse(200, []Post{}),
    ),
)

Multiple tags per operation:

openapi.GET("/users/:id/posts",
    openapi.WithSummary("Get user's posts"),
    openapi.WithTags("users", "posts"),
    openapi.WithResponse(200, []Post{}),
)

Security Requirements

Apply security to operations:

// Single security scheme
openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithSecurity("bearerAuth"),
    openapi.WithResponse(200, User{}),
)

// OAuth2 with scopes
openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithSecurity("oauth2", "read", "write"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

// Multiple security schemes (OR)
openapi.DELETE("/users/:id",
    openapi.WithSummary("Delete user"),
    openapi.WithSecurity("bearerAuth"),
    openapi.WithSecurity("apiKey"),
    openapi.WithResponse(204, nil),
)

Deprecated Operations

Mark operations as deprecated:

openapi.GET("/users/legacy",
    openapi.WithSummary("Legacy user list"),
    openapi.WithDescription("This endpoint is deprecated. Use /users instead."),
    openapi.WithDeprecated(),
    openapi.WithResponse(200, []User{}),
)

Content Types

Specify content types for requests and responses:

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithConsumes("application/json", "application/xml"),
    openapi.WithProduces("application/json", "application/xml"),
    openapi.WithResponse(201, User{}),
)

Operation Extensions

Add custom x-* extensions to operations:

openapi.GET("/users",
    openapi.WithSummary("List users"),
    openapi.WithOperationExtension("x-rate-limit", 100),
    openapi.WithOperationExtension("x-internal-only", false),
    openapi.WithResponse(200, []User{}),
)

Complete Operation Example

Here’s a complete example with all options:

openapi.PUT("/users/:id",
    openapi.WithSummary("Update user"),
    openapi.WithDescription("Updates an existing user's information"),
    openapi.WithOperationID("updateUser"),
    openapi.WithRequest(UpdateUserRequest{}),
    openapi.WithResponse(200, User{}),
    openapi.WithResponse(400, ErrorResponse{}),
    openapi.WithResponse(404, ErrorResponse{}),
    openapi.WithResponse(500, ErrorResponse{}),
    openapi.WithTags("users"),
    openapi.WithSecurity("bearerAuth"),
    openapi.WithConsumes("application/json"),
    openapi.WithProduces("application/json"),
    openapi.WithOperationExtension("x-rate-limit", 50),
)

Composable Operation Options

Use WithOptions() to create reusable option sets:

// Define reusable option sets
var (
    CommonErrors = openapi.WithOptions(
        openapi.WithResponse(400, ErrorResponse{}),
        openapi.WithResponse(401, ErrorResponse{}),
        openapi.WithResponse(500, ErrorResponse{}),
    )
    
    UserEndpoint = openapi.WithOptions(
        openapi.WithTags("users"),
        openapi.WithSecurity("bearerAuth"),
        CommonErrors,
    )
    
    JSONContent = openapi.WithOptions(
        openapi.WithConsumes("application/json"),
        openapi.WithProduces("application/json"),
    )
)

// Apply to operations
result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        UserEndpoint,
        JSONContent,
        openapi.WithSummary("Get user"),
        openapi.WithResponse(200, User{}),
    ),
    
    openapi.POST("/users",
        UserEndpoint,
        JSONContent,
        openapi.WithSummary("Create user"),
        openapi.WithRequest(CreateUserRequest{}),
        openapi.WithResponse(201, User{}),
    ),
    
    openapi.PUT("/users/:id",
        UserEndpoint,
        JSONContent,
        openapi.WithSummary("Update user"),
        openapi.WithRequest(UpdateUserRequest{}),
        openapi.WithResponse(200, User{}),
    ),
)

Nested Composable Options

Option sets can be nested:

var (
    ErrorResponses = openapi.WithOptions(
        openapi.WithResponse(400, ErrorResponse{}),
        openapi.WithResponse(500, ErrorResponse{}),
    )
    
    AuthRequired = openapi.WithOptions(
        openapi.WithSecurity("bearerAuth"),
        openapi.WithResponse(401, ErrorResponse{}),
        ErrorResponses,
    )
    
    UserAPI = openapi.WithOptions(
        openapi.WithTags("users"),
        AuthRequired,
    )
)

Custom Operation IDs

By default, operation IDs are auto-generated from the HTTP method and path. Override with WithOperationID():

openapi.GET("/users/:id",
    openapi.WithOperationID("getUserById"),
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

Without WithOperationID(), the operation ID would be auto-generated as getUsers_id.

Next Steps

6 - Auto-Discovery

Use struct tags for automatic parameter discovery

Learn how the package automatically discovers API parameters from struct tags.

Overview

The package automatically discovers parameters from struct tags. This eliminates the need to manually define parameters in the OpenAPI specification.

Supported Parameter Types

The package supports four parameter locations:

  • path - Path parameters. Always required.
  • query - Query parameters.
  • header - Header parameters.
  • cookie - Cookie parameters.

Basic Parameter Discovery

Define parameters using struct tags:

type GetUserRequest struct {
    ID int `path:"id" doc:"User ID" example:"123"`
}

result, err := api.Generate(context.Background(),
    openapi.GET("/users/:id",
        openapi.WithSummary("Get user"),
        openapi.WithResponse(200, User{}),
    ),
)

The package automatically discovers the id path parameter from the struct tag.

Path Parameters

Path parameters are always required and are extracted from the URL path:

type GetUserPostRequest struct {
    UserID int `path:"user_id" doc:"User ID" example:"123"`
    PostID int `path:"post_id" doc:"Post ID" example:"456"`
}

openapi.GET("/users/:user_id/posts/:post_id",
    openapi.WithSummary("Get user's post"),
    openapi.WithResponse(200, Post{}),
)

Query Parameters

Query parameters are extracted from the URL query string:

type ListUsersRequest struct {
    Page     int      `query:"page" doc:"Page number" example:"1" validate:"min=1"`
    PerPage  int      `query:"per_page" doc:"Items per page" example:"20" validate:"min=1,max=100"`
    Sort     string   `query:"sort" doc:"Sort field" enum:"name,created_at"`
    Tags     []string `query:"tags" doc:"Filter by tags"`
    Verified *bool    `query:"verified" doc:"Filter by verification status"`
}

openapi.GET("/users",
    openapi.WithSummary("List users"),
    openapi.WithResponse(200, []User{}),
)

Header Parameters

Header parameters are extracted from HTTP headers:

type GetUserRequest struct {
    ID            int    `path:"id"`
    Accept        string `header:"Accept" doc:"Content type" enum:"application/json,application/xml"`
    IfNoneMatch   string `header:"If-None-Match" doc:"ETag for caching"`
    XRequestID    string `header:"X-Request-ID" doc:"Request correlation ID"`
}

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

Cookie parameters are extracted from HTTP cookies:

type GetUserRequest struct {
    ID        int    `path:"id"`
    SessionID string `cookie:"session_id" doc:"Session identifier"`
    Theme     string `cookie:"theme" doc:"UI theme preference" enum:"light,dark"`
}

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

Request Body Fields

Fields in the request body use the json tag:

type CreateUserRequest struct {
    Name  string `json:"name" doc:"User's full name" example:"John Doe" validate:"required"`
    Email string `json:"email" doc:"User's email address" example:"john@example.com" validate:"required,email"`
    Age   *int   `json:"age,omitempty" doc:"User's age" example:"30" validate:"min=0,max=150"`
}

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

Additional Tags

Use these tags to enhance parameter documentation:

doc Tag

Add descriptions to parameters:

type ListUsersRequest struct {
    Page int `query:"page" doc:"Page number for pagination, starting at 1"`
}

example Tag

Provide example values:

type GetUserRequest struct {
    ID int `path:"id" doc:"User ID" example:"123"`
}

enum Tag

Specify allowed values (comma-separated):

type ListUsersRequest struct {
    Sort   string `query:"sort" doc:"Sort field" enum:"name,email,created_at"`
    Format string `query:"format" doc:"Response format" enum:"json,xml"`
}

validate Tag

Mark parameters as required or add validation constraints:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=0,max=150"`
}

The required validation affects the required field in the OpenAPI spec.

Complete Struct Tag Example

Here’s a comprehensive example using all tag types:

type CreateOrderRequest struct {
    // Path parameter (always required)
    UserID int `path:"user_id" doc:"User ID" example:"123"`
    
    // Query parameters
    Coupon    string `query:"coupon" doc:"Coupon code for discount" example:"SAVE20"`
    SendEmail *bool  `query:"send_email" doc:"Send confirmation email" example:"true"`
    
    // Header parameters
    IdempotencyKey string `header:"Idempotency-Key" doc:"Idempotency key for request" example:"550e8400-e29b-41d4-a716-446655440000"`
    
    // Cookie parameters
    SessionID string `cookie:"session_id" doc:"Session identifier"`
    
    // Request body fields
    Items []OrderItem `json:"items" doc:"Order items" validate:"required,min=1"`
    Total float64     `json:"total" doc:"Order total" example:"99.99" validate:"required,min=0"`
    Notes string      `json:"notes,omitempty" doc:"Additional notes" example:"Please gift wrap"`
}

type OrderItem struct {
    ProductID int     `json:"product_id" validate:"required"`
    Quantity  int     `json:"quantity" validate:"required,min=1"`
    Price     float64 `json:"price" validate:"required,min=0"`
}

Parameter Discovery Rules

Required vs Optional

  • Path parameters: Always required
  • Query/Header/Cookie parameters:
    • Required if validate:"required" tag is present
    • Optional otherwise
  • Request body fields:
    • Required if validate:"required" tag is present
    • Optional if pointer type or omitempty JSON tag

Type Conversion

The package automatically converts Go types to OpenAPI types:

type Parameters struct {
    // String types
    Name   string `query:"name"`    // type: string
    
    // Integer types
    Count  int    `query:"count"`   // type: integer, format: int32
    BigNum int64  `query:"big"`     // type: integer, format: int64
    
    // Floating-point types
    Price  float64 `query:"price"`  // type: number, format: double
    Rate   float32 `query:"rate"`   // type: number, format: float
    
    // Boolean types
    Active bool `query:"active"`    // type: boolean
    
    // Array types
    Tags []string `query:"tags"`    // type: array, items: string
    IDs  []int    `query:"ids"`     // type: array, items: integer
    
    // Pointer types (optional)
    Size *int `query:"size"`        // type: integer, optional
}

Combining Parameters with Request Bodies

A single struct can contain both parameters and request body fields:

type UpdateUserRequest struct {
    // Path parameter
    ID int `path:"id" doc:"User ID"`
    
    // Query parameter
    Notify bool `query:"notify" doc:"Send notification"`
    
    // Request body fields
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

openapi.PUT("/users/:id",
    openapi.WithSummary("Update user"),
    openapi.WithRequest(UpdateUserRequest{}),
    openapi.WithResponse(200, User{}),
)

Nested Structures

Parameters can be in nested structures:

type GetUserRequest struct {
    ID     int             `path:"id"`
    Filter UserListFilter  `query:",inline"`
}

type UserListFilter struct {
    Active   *bool  `query:"active" doc:"Filter by active status"`
    Role     string `query:"role" doc:"Filter by role" enum:"admin,user,guest"`
    Since    string `query:"since" doc:"Filter by creation date"`
}

Next Steps

7 - Schema Generation

Understand how Go types are converted to OpenAPI schemas

Learn how the package automatically converts Go types to OpenAPI schemas.

Overview

The package uses reflection to convert Go types into OpenAPI schemas. This eliminates the need to manually define schemas in the specification.

Supported Go Types

Primitive Types

Go TypeOpenAPI TypeOpenAPI Format
stringstring-
boolboolean-
int, int32integerint32
int64integerint64
uint, uint32integerint32
uint64integerint64
float32numberfloat
float64numberdouble
bytestringbyte

String Types

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

Generates:

type: object
properties:
  name:
    type: string
  email:
    type: string

Integer Types

type Product struct {
    ID       int   `json:"id"`
    Quantity int32 `json:"quantity"`
    Stock    int64 `json:"stock"`
}

Generates:

type: object
properties:
  id:
    type: integer
    format: int32
  quantity:
    type: integer
    format: int32
  stock:
    type: integer
    format: int64

Floating-Point Types

type Product struct {
    Price   float64 `json:"price"`
    Weight  float32 `json:"weight"`
}

Generates:

type: object
properties:
  price:
    type: number
    format: double
  weight:
    type: number
    format: float

Boolean Types

type User struct {
    Active   bool `json:"active"`
    Verified bool `json:"verified"`
}

Generates:

type: object
properties:
  active:
    type: boolean
  verified:
    type: boolean

Pointer Types

Pointer types are nullable and optional:

type User struct {
    Name  string `json:"name"`
    Age   *int   `json:"age,omitempty"`
    Email *string `json:"email,omitempty"`
}

In OpenAPI 3.1.x, pointers generate nullable: true. In OpenAPI 3.0.x, they’re optional fields.

Slices and Arrays

Slices become OpenAPI arrays:

type User struct {
    Tags   []string `json:"tags"`
    Scores []int    `json:"scores"`
    Posts  []Post   `json:"posts"`
}

Generates:

type: object
properties:
  tags:
    type: array
    items:
      type: string
  scores:
    type: array
    items:
      type: integer
  posts:
    type: array
    items:
      $ref: '#/components/schemas/Post'

Maps

Maps become OpenAPI objects with additionalProperties:

type User struct {
    Metadata map[string]string `json:"metadata"`
    Scores   map[string]int    `json:"scores"`
}

Generates:

type: object
properties:
  metadata:
    type: object
    additionalProperties:
      type: string
  scores:
    type: object
    additionalProperties:
      type: integer

Nested Structs

Nested structs are converted to nested schemas or references:

type User struct {
    ID      int     `json:"id"`
    Name    string  `json:"name"`
    Address Address `json:"address"`
}

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    ZipCode string `json:"zip_code"`
}

Generates component schemas with references:

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        address:
          $ref: '#/components/schemas/Address'
    Address:
      type: object
      properties:
        street:
          type: string
        city:
          type: string
        zip_code:
          type: string

Embedded Structs

Embedded struct fields are flattened into the parent:

type Timestamps struct {
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Timestamps
}

Generates:

type: object
properties:
  id:
    type: integer
  name:
    type: string
  created_at:
    type: string
    format: date-time
  updated_at:
    type: string
    format: date-time

Time Types

time.Time becomes a string with date-time format:

type User struct {
    CreatedAt time.Time  `json:"created_at"`
    UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

Generates:

type: object
properties:
  created_at:
    type: string
    format: date-time
  updated_at:
    type: string
    format: date-time

JSON Tags

The package respects json struct tags:

type User struct {
    ID        int    `json:"id"`
    FirstName string `json:"first_name"`
    LastName  string `json:"last_name"`
    Internal  string `json:"-"`           // Ignored
    Optional  string `json:"opt,omitempty"` // Optional
}
  • json:"name" - Sets the property name
  • json:"-" - Field is ignored
  • json:",omitempty" - Field is optional (not required)

Schema Naming

Component schema names use the format pkgname.TypeName to prevent collisions:

// In package "api"
type User struct { ... }  // Becomes "api.User"

// In package "models"
type User struct { ... }  // Becomes "models.User"

This prevents naming collisions when the same type name exists in different packages.

Custom Schema Names

If you need custom schema names, use the openapi struct tag:

type User struct {
    ID   int    `json:"id" openapi:"name=CustomUser"`
    Name string `json:"name"`
}

Validation Tags

Validation tags affect the OpenAPI schema:

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"min=0,max=150"`
}
  • required - Adds field to required array
  • min/max - Sets minimum/maximum for numbers
  • email - Sets format: email for strings

Documentation Tags

Use doc and example tags to enhance schemas:

type User struct {
    ID    int    `json:"id" doc:"Unique user identifier" example:"123"`
    Name  string `json:"name" doc:"User's full name" example:"John Doe"`
    Email string `json:"email" doc:"User's email address" example:"john@example.com"`
}

Generates:

type: object
properties:
  id:
    type: integer
    description: Unique user identifier
    example: 123
  name:
    type: string
    description: User's full name
    example: John Doe
  email:
    type: string
    description: User's email address
    example: john@example.com

Enum Types

Use enum tag to specify allowed values:

type User struct {
    Role   string `json:"role" enum:"admin,user,guest"`
    Status string `json:"status" enum:"active,inactive,pending"`
}

Generates:

type: object
properties:
  role:
    type: string
    enum: [admin, user, guest]
  status:
    type: string
    enum: [active, inactive, pending]

Complete Schema Example

Here’s a comprehensive example using all features:

package main

import (
    "time"
)

type User struct {
    // Basic types
    ID       int    `json:"id" doc:"Unique user identifier" example:"123"`
    Name     string `json:"name" doc:"User's full name" example:"John Doe" validate:"required"`
    Email    string `json:"email" doc:"Email address" example:"john@example.com" validate:"required,email"`
    
    // Optional field
    Bio *string `json:"bio,omitempty" doc:"User biography"`
    
    // Numeric types
    Age    int     `json:"age" doc:"User's age" validate:"min=0,max=150"`
    Score  float64 `json:"score" doc:"User score" example:"95.5"`
    
    // Boolean
    Active bool `json:"active" doc:"Whether user is active" example:"true"`
    
    // Enum
    Role string `json:"role" doc:"User role" enum:"admin,user,guest"`
    
    // Arrays
    Tags   []string `json:"tags" doc:"User tags"`
    Scores []int    `json:"scores" doc:"Test scores"`
    
    // Map
    Metadata map[string]string `json:"metadata" doc:"Additional metadata"`
    
    // Nested struct
    Address Address `json:"address" doc:"User address"`
    
    // Time
    CreatedAt time.Time  `json:"created_at" doc:"Creation timestamp"`
    UpdatedAt *time.Time `json:"updated_at,omitempty" doc:"Last update timestamp"`
    
    // Ignored field
    Internal string `json:"-"`
}

type Address struct {
    Street  string `json:"street" validate:"required"`
    City    string `json:"city" validate:"required"`
    State   string `json:"state"`
    ZipCode string `json:"zip_code" validate:"required"`
    Country string `json:"country" validate:"required"`
}

Next Steps

  • Learn about Operations to use your schemas in API endpoints
  • Explore Validation to validate generated specifications
  • See Examples for complete schema patterns

8 - Swagger UI

Customize the Swagger UI interface for API documentation

Learn how to configure and customize the Swagger UI interface for your OpenAPI specification.

Overview

The package includes built-in Swagger UI support with extensive customization options. Swagger UI provides an interactive interface for exploring and testing your API.

Basic Configuration

Enable Swagger UI by specifying the path:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithSwaggerUI("/docs"),
)

This serves Swagger UI at /docs with default settings.

Disabling Swagger UI

To disable Swagger UI completely:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithoutSwaggerUI(),
)

Display Options

Document Expansion

Control how documentation is initially displayed:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIExpansion(openapi.DocExpansionList),
)

Available options:

  • DocExpansionList - Show endpoints, hide details. This is the default.
  • DocExpansionFull - Show endpoints and details.
  • DocExpansionNone - Hide everything.

Model Rendering

Control how models/schemas are rendered:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIDefaultModelRendering(openapi.ModelRenderingExample),
)

Options:

  • ModelRenderingExample - Show example values. This is the default.
  • ModelRenderingModel - Show schema structure.

Model Expand Depth

Control how deeply nested models are expanded:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIModelExpandDepth(1),      // How deep to expand a single model
    openapi.WithUIModelsExpandDepth(1),     // How deep to expand models section
)

Set to -1 to disable expansion, 1 for shallow, higher numbers for deeper.

Display Operation IDs

Show operation IDs alongside summaries:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIDisplayOperationID(true),
)

Try It Out Features

Enable Try It Out

Allow users to test API endpoints directly:

openapi.WithSwaggerUI("/docs",
    openapi.WithUITryItOut(true),
)

Request Snippets

Show code snippets for making requests:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIRequestSnippets(true,
        openapi.SnippetCurlBash,
        openapi.SnippetCurlPowerShell,
        openapi.SnippetCurlCmd,
    ),
)

Available snippet types:

  • SnippetCurlBash - curl for bash/sh shells
  • SnippetCurlPowerShell - curl for PowerShell
  • SnippetCurlCmd - curl for Windows CMD

Request Snippets Expanded

Expand request snippets by default:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIRequestSnippets(true, openapi.SnippetCurlBash),
    openapi.WithUIRequestSnippetsExpanded(true),
)

Display Request Duration

Show how long requests take:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIDisplayRequestDuration(true),
)

Filtering and Sorting

Filter

Enable a filter/search box:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIFilter(true),
)

Max Displayed Tags

Limit the number of tags displayed:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIMaxDisplayedTags(10),
)

Operations Sorting

Sort operations alphabetically or by HTTP method:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIOperationsSorter(openapi.OperationsSorterAlpha),
)

Options:

  • OperationsSorterAlpha - Sort alphabetically
  • OperationsSorterMethod - Sort by HTTP method
  • Leave unset for default order

Tags Sorting

Sort tags alphabetically:

openapi.WithSwaggerUI("/docs",
    openapi.WithUITagsSorter(openapi.TagsSorterAlpha),
)

Syntax Highlighting

Enable/Disable Syntax Highlighting

openapi.WithSwaggerUI("/docs",
    openapi.WithUISyntaxHighlight(true),
)

Syntax Theme

Choose a color theme for code highlighting:

openapi.WithSwaggerUI("/docs",
    openapi.WithUISyntaxTheme(openapi.SyntaxThemeMonokai),
)

Available themes:

  • SyntaxThemeAgate - Dark theme with blue accents
  • SyntaxThemeArta - Dark theme with orange accents
  • SyntaxThemeMonokai - Dark theme with vibrant colors
  • SyntaxThemeNord - Dark theme with cool blue tones
  • SyntaxThemeObsidian - Dark theme with green accents
  • SyntaxThemeTomorrowNight - Dark theme with muted colors
  • SyntaxThemeIdea - Light theme similar to IntelliJ IDEA

Authentication

Persist Authentication

Keep auth credentials across browser refreshes:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIPersistAuth(true),
)

Send Credentials

Include credentials in requests:

openapi.WithSwaggerUI("/docs",
    openapi.WithUIWithCredentials(true),
)

Validation

Control OpenAPI specification validation:

// Use local validation (recommended)
openapi.WithSwaggerUI("/docs",
    openapi.WithUIValidator(openapi.ValidatorLocal),
)

// Use external validator
openapi.WithSwaggerUI("/docs",
    openapi.WithUIValidator("https://validator.swagger.io/validator"),
)

// Disable validation
openapi.WithSwaggerUI("/docs",
    openapi.WithUIValidator(openapi.ValidatorNone),
)

Complete Swagger UI Example

Here’s a comprehensive example with all common options:

package main

import (
    "rivaas.dev/openapi"
)

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("My API", "1.0.0"),
        openapi.WithSwaggerUI("/docs",
            // Document expansion
            openapi.WithUIExpansion(openapi.DocExpansionList),
            openapi.WithUIModelsExpandDepth(1),
            openapi.WithUIModelExpandDepth(1),
            
            // Display options
            openapi.WithUIDisplayOperationID(true),
            openapi.WithUIDefaultModelRendering(openapi.ModelRenderingExample),
            
            // Try it out
            openapi.WithUITryItOut(true),
            openapi.WithUIRequestSnippets(true,
                openapi.SnippetCurlBash,
                openapi.SnippetCurlPowerShell,
                openapi.SnippetCurlCmd,
            ),
            openapi.WithUIRequestSnippetsExpanded(true),
            openapi.WithUIDisplayRequestDuration(true),
            
            // Filtering and sorting
            openapi.WithUIFilter(true),
            openapi.WithUIMaxDisplayedTags(10),
            openapi.WithUIOperationsSorter(openapi.OperationsSorterAlpha),
            openapi.WithUITagsSorter(openapi.TagsSorterAlpha),
            
            // Syntax highlighting
            openapi.WithUISyntaxHighlight(true),
            openapi.WithUISyntaxTheme(openapi.SyntaxThemeMonokai),
            
            // Authentication
            openapi.WithUIPersistAuth(true),
            openapi.WithUIWithCredentials(true),
            
            // Validation
            openapi.WithUIValidator(openapi.ValidatorLocal),
        ),
    )

    // Generate specification...
}

Swagger UI Path

The Swagger UI path can be any valid URL path:

openapi.WithSwaggerUI("/api-docs")
openapi.WithSwaggerUI("/swagger")
openapi.WithSwaggerUI("/docs/api")

Integration with Web Frameworks

The package generates the OpenAPI specification, but you need to integrate it with your web framework to serve Swagger UI. The typical pattern is:

// Generate the spec
result, err := api.Generate(context.Background(), operations...)
if err != nil {
    log.Fatal(err)
}

// Serve the spec at /openapi.json
http.HandleFunc("/openapi.json", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write(result.JSON)
})

// Serve Swagger UI at /docs
// (Framework-specific implementation)

Next Steps

9 - Validation

Validate OpenAPI specifications against official meta-schemas

Learn how to validate OpenAPI specifications using built-in validation against official meta-schemas.

Overview

The package provides built-in validation against official OpenAPI meta-schemas for both 3.0.x and 3.1.x specifications.

Enabling Validation

Validation is disabled by default for performance. Enable it during development or in CI/CD pipelines:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithValidation(true), // Enable validation
)

result, err := api.Generate(context.Background(), operations...)
if err != nil {
    log.Fatal(err) // Will fail if spec is invalid
}

Why Validation is Disabled by Default

Validation has a performance cost:

  • Schema compilation on first use.
  • JSON schema validation for every generation.
  • Not necessary for production spec generation.

When to enable:

  • During development.
  • In CI/CD pipelines.
  • When debugging specification issues.
  • When accepting external specifications.

When to disable:

  • Production spec generation.
  • When performance is critical.
  • After spec validation is confirmed.

Validation Errors

When validation fails, you’ll receive a detailed error:

result, err := api.Generate(context.Background(), operations...)
if err != nil {
    // Error contains validation details
    fmt.Printf("Validation failed: %v\n", err)
}

Common validation errors:

  • Missing required fields like info, openapi, paths.
  • Invalid field types.
  • Invalid format values.
  • Schema constraint violations.
  • Invalid references.

Validating External Specifications

The package includes a standalone validator for external OpenAPI specifications:

import "rivaas.dev/openapi/validate"

// Read external spec
specJSON, err := os.ReadFile("external-api.json")
if err != nil {
    log.Fatal(err)
}

// Create validator
validator := validate.New()

// Validate against OpenAPI 3.0.x
err = validator.Validate(context.Background(), specJSON, validate.V30)
if err != nil {
    log.Printf("Validation failed: %v\n", err)
}

// Or validate against OpenAPI 3.1.x
err = validator.Validate(context.Background(), specJSON, validate.V31)
if err != nil {
    log.Printf("Validation failed: %v\n", err)
}

Auto-Detection

The validator can auto-detect the OpenAPI version:

validator := validate.New()

// Auto-detects version from the spec
err := validator.ValidateAuto(context.Background(), specJSON)
if err != nil {
    log.Printf("Validation failed: %v\n", err)
}

Swagger UI Validation

Configure validation in Swagger UI:

Use the built-in validator (no external calls):

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithSwaggerUI("/docs",
        openapi.WithUIValidator(openapi.ValidatorLocal),
    ),
)

External Validator

Use an external validation service:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithSwaggerUI("/docs",
        openapi.WithUIValidator("https://validator.swagger.io/validator"),
    ),
)

Disable Validation

Disable validation in Swagger UI:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithSwaggerUI("/docs",
        openapi.WithUIValidator(openapi.ValidatorNone),
    ),
)

Validation in CI/CD

Add validation to your CI/CD pipeline:

# generate-openapi.go
package main

import (
    "context"
    "log"
    "os"
    
    "rivaas.dev/openapi"
)

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("My API", "1.0.0"),
        openapi.WithValidation(true), // Enable for CI/CD
    )
    
    result, err := api.Generate(context.Background(),
        // ... operations
    )
    if err != nil {
        log.Fatalf("Validation failed: %v", err)
    }
    
    // Write to file
    if err := os.WriteFile("openapi.json", result.JSON, 0644); err != nil {
        log.Fatal(err)
    }
    
    log.Println("Valid OpenAPI specification generated")
}

In your CI pipeline:

# .github/workflows/validate-openapi.yml
name: Validate OpenAPI

on: [push, pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.25'
      - run: go run generate-openapi.go

Validation Performance

Validation performance characteristics:

  • First validation: ~10-20ms (schema compilation)
  • Subsequent validations: ~1-5ms (using cached schema)
  • External spec validation: Depends on spec size

For high-performance scenarios, consider:

  • Validate once during build/deployment
  • Cache validated specifications
  • Disable validation in production spec generation

Complete Validation Example

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    
    "rivaas.dev/openapi"
    "rivaas.dev/openapi/validate"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Generate with validation enabled
    api := openapi.MustNew(
        openapi.WithTitle("User API", "1.0.0"),
        openapi.WithValidation(true),
    )
    
    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
        ),
    )
    if err != nil {
        log.Fatalf("Generation/validation failed: %v", err)
    }
    
    fmt.Println("Generated valid OpenAPI 3.0.4 specification")
    
    // Write to file
    if err := os.WriteFile("openapi.json", result.JSON, 0644); err != nil {
        log.Fatal(err)
    }
    
    // Validate external spec (e.g., from a file)
    externalSpec, err := os.ReadFile("external-api.json")
    if err != nil {
        log.Fatal(err)
    }
    
    validator := validate.New()
    if err := validator.ValidateAuto(context.Background(), externalSpec); err != nil {
        log.Printf("External spec validation failed: %v\n", err)
    } else {
        fmt.Println("External spec is valid")
    }
}

Validation vs Warnings

It’s important to distinguish between validation errors and warnings:

  • Validation errors: The specification violates OpenAPI schema requirements
  • Warnings: The specification is valid but uses version-specific features (see Diagnostics)
api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V30x),
    openapi.WithInfoSummary("Summary"), // 3.1-only feature
    openapi.WithValidation(true),
)

result, err := api.Generate(context.Background(), ops...)
// err is nil (spec is valid)
// result.Warnings contains warning about info.summary being dropped

Next Steps

10 - Diagnostics

Handle warnings with type-safe diagnostics

Learn how to work with warnings using the type-safe diagnostics package.

Overview

The package generates warnings when using version-specific features. For example, using OpenAPI 3.1 features with a 3.0 target generates warnings instead of errors.

Working with Warnings

Check for warnings in the generation result:

result, err := api.Generate(context.Background(), operations...)
if err != nil {
    log.Fatal(err)
}

// Basic warning check
if len(result.Warnings) > 0 {
    fmt.Printf("Generated with %d warnings\n", len(result.Warnings))
}

// Iterate through warnings
for _, warn := range result.Warnings {
    fmt.Printf("[%s] %s\n", warn.Code(), warn.Message())
}

The diag Package

Import the diag package for type-safe warning handling:

import "rivaas.dev/openapi/diag"

Warning Interface

Each warning implements the Warning interface:

type Warning interface {
    Code() WarningCode        // Unique warning code
    Message() string          // Human-readable message
    Path() string            // Location in spec (e.g., "info.summary")
    Category() WarningCategory // Warning category
}

Type-Safe Warning Checks

Check for specific warnings using type-safe constants:

import "rivaas.dev/openapi/diag"

result, err := api.Generate(context.Background(), ops...)
if err != nil {
    log.Fatal(err)
}

// Check for specific warning
if result.Warnings.Has(diag.WarnDownlevelWebhooks) {
    log.Warn("webhooks not supported in OpenAPI 3.0")
}

// Check for any of multiple codes
if result.Warnings.HasAny(
    diag.WarnDownlevelMutualTLS,
    diag.WarnDownlevelWebhooks,
) {
    log.Warn("Some 3.1 security features were dropped")
}

Warning Categories

Warnings are organized into categories:

// Filter by category
downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)
fmt.Printf("Downlevel warnings: %d\n", len(downlevelWarnings))

deprecationWarnings := result.Warnings.FilterCategory(diag.CategoryDeprecation)
fmt.Printf("Deprecation warnings: %d\n", len(deprecationWarnings))

Available categories:

  • CategoryDownlevel - 3.1 to 3.0 conversion feature losses
  • CategoryDeprecation - Deprecated feature usage warnings
  • CategoryUnknown - Unrecognized warning codes

Warning Codes

Common warning codes:

Downlevel Warnings

These occur when using 3.1 features with a 3.0 target:

  • WarnDownlevelWebhooks - Webhooks dropped
  • WarnDownlevelInfoSummary - info.summary dropped
  • WarnDownlevelLicenseIdentifier - license.identifier dropped
  • WarnDownlevelMutualTLS - mutualTLS security scheme dropped
  • WarnDownlevelConstToEnum - JSON Schema const converted to enum
  • WarnDownlevelMultipleExamples - Multiple examples collapsed to one
  • WarnDownlevelPatternProperties - patternProperties dropped
  • WarnDownlevelUnevaluatedProperties - unevaluatedProperties dropped
  • WarnDownlevelContentEncoding - contentEncoding dropped
  • WarnDownlevelContentMediaType - contentMediaType dropped

Deprecation Warnings

These occur when using deprecated features:

  • WarnDeprecationExampleSingular - Using deprecated singular example field

Filtering Warnings

Filter Specific Warnings

Get only specific warning types:

licenseWarnings := result.Warnings.Filter(diag.WarnDownlevelLicenseIdentifier)
for _, warn := range licenseWarnings {
    fmt.Printf("%s: %s\n", warn.Path(), warn.Message())
}

Exclude Expected Warnings

Exclude warnings you expect and want to ignore:

unexpected := result.Warnings.Exclude(
    diag.WarnDownlevelInfoSummary,
    diag.WarnDownlevelLicenseIdentifier,
)

if len(unexpected) > 0 {
    fmt.Printf("Unexpected warnings: %d\n", len(unexpected))
    for _, warn := range unexpected {
        fmt.Printf("[%s] %s\n", warn.Code(), warn.Message())
    }
}

Complete Diagnostics Example

package main

import (
    "context"
    "fmt"
    "log"
    
    "rivaas.dev/openapi"
    "rivaas.dev/openapi/diag"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    // Create API with 3.0 target but use 3.1 features
    api := openapi.MustNew(
        openapi.WithTitle("My API", "1.0.0"),
        openapi.WithVersion(openapi.V30x),
        openapi.WithInfoSummary("Short summary"), // 3.1-only feature
    )
    
    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // Check for specific warning
    if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
        fmt.Println("info.summary was dropped (3.1 feature with 3.0 target)")
    }
    
    // Filter by category
    downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)
    if len(downlevelWarnings) > 0 {
        fmt.Printf("\nDownlevel warnings (%d):\n", len(downlevelWarnings))
        for _, warn := range downlevelWarnings {
            fmt.Printf("  [%s] %s at %s\n", 
                warn.Code(), 
                warn.Message(), 
                warn.Path(),
            )
        }
    }
    
    // Check for unexpected warnings
    expected := []diag.WarningCode{
        diag.WarnDownlevelInfoSummary,
    }
    unexpected := result.Warnings.Exclude(expected...)
    
    if len(unexpected) > 0 {
        fmt.Printf("\nUnexpected warnings (%d):\n", len(unexpected))
        for _, warn := range unexpected {
            fmt.Printf("  [%s] %s\n", warn.Code(), warn.Message())
        }
    }
    
    fmt.Printf("\nGenerated %d byte specification with %d warnings\n",
        len(result.JSON), 
        len(result.Warnings),
    )
}

Warning vs Error

The package distinguishes between warnings and errors:

  • Warnings: The specification is valid but features were dropped or converted
  • Errors: The specification is invalid or generation failed
result, err := api.Generate(context.Background(), ops...)
if err != nil {
    // Hard error - generation failed
    log.Fatal(err)
}

if len(result.Warnings) > 0 {
    // Soft warnings - generation succeeded with caveats
    for _, warn := range result.Warnings {
        log.Printf("Warning: %s\n", warn.Message())
    }
}

Strict Downlevel Mode

To treat downlevel warnings as errors, enable strict mode (see Advanced Usage):

api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V30x),
    openapi.WithStrictDownlevel(true), // Error on 3.1 features
    openapi.WithInfoSummary("Summary"), // This will cause an error
)

_, err := api.Generate(context.Background(), ops...)
// err will be non-nil due to strict mode violation

Warning Suppression

Currently, the package does not support per-warning suppression. To handle expected warnings:

  1. Filter them out after generation
  2. Use strict mode to error on any warnings
  3. Log and ignore specific warning codes
// Filter out expected warnings
expected := []diag.WarningCode{
    diag.WarnDownlevelInfoSummary,
    diag.WarnDownlevelLicenseIdentifier,
}

unexpected := result.Warnings.Exclude(expected...)
if len(unexpected) > 0 {
    log.Fatalf("Unexpected warnings: %d", len(unexpected))
}

Next Steps

11 - Advanced Usage

Custom operation IDs, extensions, and strict downlevel mode

Learn about advanced features including custom operation IDs, extensions, and strict downlevel mode.

Custom Operation IDs

By default, operation IDs are auto-generated from the HTTP method and path. You can override this behavior.

Auto-Generated Operation IDs

openapi.GET("/users/:id",
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)
// Generated operation ID: "getUsers_id"

openapi.POST("/users",
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)
// Generated operation ID: "postUsers"

Custom Operation IDs

Override with WithOperationID():

openapi.GET("/users/:id",
    openapi.WithOperationID("getUserById"),
    openapi.WithSummary("Get user"),
    openapi.WithResponse(200, User{}),
)

openapi.POST("/users",
    openapi.WithOperationID("createNewUser"),
    openapi.WithSummary("Create user"),
    openapi.WithRequest(CreateUserRequest{}),
    openapi.WithResponse(201, User{}),
)

Operation ID Best Practices

  • Use camelCase - Consistent with most API conventions.
  • Be descriptive - getUserById rather than getUser1.
  • Avoid conflicts - Ensure unique IDs across all operations.
  • Consider generation - Some tools generate client code from operation IDs.

Extensions

OpenAPI allows custom x-* extensions for vendor-specific metadata.

Root-Level Extensions

Add extensions to the root of the specification:

api := openapi.MustNew(
    openapi.WithTitle("My API", "1.0.0"),
    openapi.WithExtension("x-api-version", "v2"),
    openapi.WithExtension("x-custom-feature", true),
    openapi.WithExtension("x-rate-limit-config", map[string]interface{}{
        "requests": 100,
        "period": "1m",
    }),
)

Operation Extensions

Add extensions to specific operations.

openapi.GET("/users",
    openapi.WithSummary("List users"),
    openapi.WithOperationExtension("x-rate-limit", 100),
    openapi.WithOperationExtension("x-cache-ttl", 300),
    openapi.WithOperationExtension("x-internal-only", false),
    openapi.WithResponse(200, []User{}),
)

Extension Naming Rules

  • Must start with x- - Required by OpenAPI specification
  • Reserved prefixes - x-oai- and x-oas- are reserved in 3.1.x
  • Case-sensitive - x-Custom and x-custom are different

Extension Validation

Extensions are validated:

// Valid
openapi.WithExtension("x-custom", "value")

// Invalid - doesn't start with x-
openapi.WithExtension("custom", "value") // Error

// Invalid - reserved prefix in 3.1.x
openapi.WithExtension("x-oai-custom", "value") // Filtered out in 3.1.x

Common Extension Use Cases

// API versioning
openapi.WithExtension("x-api-version", "2.0")

// Rate limiting
openapi.WithOperationExtension("x-rate-limit", map[string]interface{}{
    "requests": 100,
    "window": "1m",
})

// Caching
openapi.WithOperationExtension("x-cache", map[string]interface{}{
    "ttl": 300,
    "vary": []string{"Authorization", "Accept-Language"},
})

// Internal metadata
openapi.WithOperationExtension("x-internal", map[string]interface{}{
    "team": "platform",
    "cost": "low",
})

// Feature flags
openapi.WithOperationExtension("x-feature-flag", "new-user-flow")

// Code generation hints
openapi.WithOperationExtension("x-codegen", map[string]interface{}{
    "methodName": "customMethodName",
    "packageName": "users",
})

Strict Downlevel Mode

By default, using 3.1 features with a 3.0 target generates warnings. Enable strict mode to error instead:

Default Behavior (Warnings)

api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V30x),
    openapi.WithInfoSummary("Summary"), // 3.1-only feature
)

result, err := api.Generate(context.Background(), ops...)
// err is nil (generation succeeds)
// result.Warnings contains warning about info.summary being dropped

Strict Mode (Errors)

api := openapi.MustNew(
    openapi.WithTitle("API", "1.0.0"),
    openapi.WithVersion(openapi.V30x),
    openapi.WithStrictDownlevel(true), // Enable strict mode
    openapi.WithInfoSummary("Summary"), // This will cause an error
)

result, err := api.Generate(context.Background(), ops...)
// err is non-nil (generation fails)

When to Use Strict Mode

Use strict mode when:

  • Enforcing version compliance - Prevent accidental 3.1 feature usage
  • CI/CD validation - Fail builds on version violations
  • Team standards - Ensure consistent OpenAPI version usage
  • Client compatibility - Target clients require strict 3.0 compliance

Don’t use strict mode when:

  • Graceful degradation - You’re okay with features being dropped
  • Development - Exploring features without hard errors
  • Flexible deployments - Different environments support different versions

Features Affected by Strict Mode

3.1-only features that trigger strict mode:

  • WithInfoSummary() - Short API summary
  • WithLicenseIdentifier() - SPDX license identifier
  • Webhooks - Webhook definitions
  • Mutual TLS - mutualTLS security scheme
  • const in schemas - JSON Schema const keyword
  • Multiple examples - Multiple schema examples

Complete Advanced Example

package main

import (
    "context"
    "fmt"
    "log"
    
    "rivaas.dev/openapi"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type CreateUserRequest struct {
    Name string `json:"name" validate:"required"`
}

func main() {
    api := openapi.MustNew(
        // Basic configuration
        openapi.WithTitle("Advanced API", "1.0.0"),
        openapi.WithVersion(openapi.V30x),
        
        // Root-level extensions
        openapi.WithExtension("x-api-version", "v2"),
        openapi.WithExtension("x-environment", "production"),
        openapi.WithExtension("x-service-info", map[string]interface{}{
            "team": "platform",
            "repository": "github.com/example/api",
        }),
        
        // Strict mode (optional)
        openapi.WithStrictDownlevel(false), // Allow graceful degradation
    )
    
    result, err := api.Generate(context.Background(),
        // Custom operation IDs
        openapi.GET("/users/:id",
            openapi.WithOperationID("getUserById"),
            openapi.WithSummary("Get user by ID"),
            
            // Operation extensions
            openapi.WithOperationExtension("x-rate-limit", 100),
            openapi.WithOperationExtension("x-cache-ttl", 300),
            openapi.WithOperationExtension("x-internal-team", "users"),
            
            openapi.WithResponse(200, User{}),
        ),
        
        openapi.POST("/users",
            openapi.WithOperationID("createUser"),
            openapi.WithSummary("Create a new user"),
            
            // Different extensions per operation
            openapi.WithOperationExtension("x-rate-limit", 10),
            openapi.WithOperationExtension("x-feature-flag", "new-user-flow"),
            openapi.WithOperationExtension("x-mutation", true),
            
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
        ),
        
        openapi.PUT("/users/:id",
            openapi.WithOperationID("updateUser"),
            openapi.WithSummary("Update user"),
            
            openapi.WithOperationExtension("x-rate-limit", 50),
            openapi.WithOperationExtension("x-mutation", true),
            
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(200, User{}),
        ),
        
        openapi.DELETE("/users/:id",
            openapi.WithOperationID("deleteUser"),
            openapi.WithSummary("Delete user"),
            
            openapi.WithOperationExtension("x-rate-limit", 10),
            openapi.WithOperationExtension("x-mutation", true),
            openapi.WithOperationExtension("x-dangerous", true),
            
            openapi.WithResponse(204, nil),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // Check for warnings
    if len(result.Warnings) > 0 {
        fmt.Printf("Generated with %d warnings:\n", len(result.Warnings))
        for _, warn := range result.Warnings {
            fmt.Printf("  - %s\n", warn.Message())
        }
    }
    
    fmt.Printf("Generated %d byte specification\n", len(result.JSON))
}

Best Practices

Operation IDs

  1. Be consistent - Use the same naming convention across all operations
  2. Make them unique - Avoid duplicate operation IDs
  3. Consider clients - Generated client libraries use these names
  4. Document the convention - Help team members follow the pattern

Extensions

  1. Use sparingly - Only add extensions when necessary
  2. Document them - Explain what custom extensions mean
  3. Validate format - Ensure extensions follow your schema
  4. Version them - Consider versioning extension formats
  5. Tool compatibility - Check if tools support your extensions

Strict Mode

  1. Enable in CI/CD - Catch version issues early
  2. Document the choice - Explain why strict mode is enabled/disabled
  3. Test both modes - Ensure graceful degradation works if disabled
  4. Communicate clearly - Make version requirements explicit

Next Steps

12 - Examples

Complete examples and real-world usage patterns

Complete examples demonstrating real-world usage patterns for the OpenAPI package.

Basic CRUD API

A simple CRUD API with all HTTP methods.

package main

import (
    "context"
    "log"
    "os"
    "time"

    "rivaas.dev/openapi"
)

type User struct {
    ID        int       `json:"id" doc:"User ID" example:"123"`
    Name      string    `json:"name" doc:"User's full name" example:"John Doe"`
    Email     string    `json:"email" doc:"Email address" example:"john@example.com"`
    CreatedAt time.Time `json:"created_at" doc:"Creation timestamp"`
}

type CreateUserRequest struct {
    Name  string `json:"name" doc:"User's full name" validate:"required"`
    Email string `json:"email" doc:"Email address" validate:"required,email"`
}

type ErrorResponse struct {
    Code    int    `json:"code" doc:"Error code"`
    Message string `json:"message" doc:"Error message"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("User API", "1.0.0"),
        openapi.WithInfoDescription("Simple CRUD API for user management"),
        openapi.WithServer("http://localhost:8080", "Local development"),
        openapi.WithServer("https://api.example.com", "Production"),
        openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
        openapi.WithTag("users", "User management operations"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users",
            openapi.WithSummary("List users"),
            openapi.WithDescription("Retrieve a list of all users"),
            openapi.WithTags("users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithResponse(200, []User{}),
            openapi.WithResponse(401, ErrorResponse{}),
        ),
        
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithDescription("Retrieve a specific user by ID"),
            openapi.WithTags("users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithResponse(200, User{}),
            openapi.WithResponse(404, ErrorResponse{}),
            openapi.WithResponse(401, ErrorResponse{}),
        ),
        
        openapi.POST("/users",
            openapi.WithSummary("Create user"),
            openapi.WithDescription("Create a new user"),
            openapi.WithTags("users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
            openapi.WithResponse(400, ErrorResponse{}),
            openapi.WithResponse(401, ErrorResponse{}),
        ),
        
        openapi.PUT("/users/:id",
            openapi.WithSummary("Update user"),
            openapi.WithDescription("Update an existing user"),
            openapi.WithTags("users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(200, User{}),
            openapi.WithResponse(400, ErrorResponse{}),
            openapi.WithResponse(404, ErrorResponse{}),
            openapi.WithResponse(401, ErrorResponse{}),
        ),
        
        openapi.DELETE("/users/:id",
            openapi.WithSummary("Delete user"),
            openapi.WithDescription("Delete a user"),
            openapi.WithTags("users"),
            openapi.WithSecurity("bearerAuth"),
            openapi.WithResponse(204, nil),
            openapi.WithResponse(404, ErrorResponse{}),
            openapi.WithResponse(401, ErrorResponse{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    if err := os.WriteFile("openapi.json", result.JSON, 0644); err != nil {
        log.Fatal(err)
    }

    log.Println("OpenAPI specification generated: openapi.json")
}

API with Query Parameters and Pagination

package main

import (
    "context"
    "log"
    
    "rivaas.dev/openapi"
)

type ListUsersRequest struct {
    Page    int      `query:"page" doc:"Page number" example:"1" validate:"min=1"`
    PerPage int      `query:"per_page" doc:"Items per page" example:"20" validate:"min=1,max=100"`
    Sort    string   `query:"sort" doc:"Sort field" enum:"name,email,created_at"`
    Order   string   `query:"order" doc:"Sort order" enum:"asc,desc"`
    Tags    []string `query:"tags" doc:"Filter by tags"`
    Active  *bool    `query:"active" doc:"Filter by active status"`
}

type User struct {
    ID     int      `json:"id"`
    Name   string   `json:"name"`
    Email  string   `json:"email"`
    Active bool     `json:"active"`
    Tags   []string `json:"tags"`
}

type PaginatedResponse struct {
    Data       []User `json:"data"`
    Page       int    `json:"page"`
    PerPage    int    `json:"per_page"`
    TotalPages int    `json:"total_pages"`
    TotalItems int    `json:"total_items"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("Paginated API", "1.0.0"),
        openapi.WithInfoDescription("API with pagination and filtering"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users",
            openapi.WithSummary("List users with pagination"),
            openapi.WithDescription("Retrieve paginated list of users with filtering"),
            openapi.WithResponse(200, PaginatedResponse{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

Multi-Source Parameters

package main

import (
    "context"
    "log"
    
    "rivaas.dev/openapi"
)

type CreateOrderRequest struct {
    // Path parameter
    UserID int `path:"user_id" doc:"User ID" example:"123"`
    
    // Query parameters
    Coupon    string `query:"coupon" doc:"Coupon code" example:"SAVE20"`
    SendEmail *bool  `query:"send_email" doc:"Send confirmation email"`
    
    // Header parameters
    IdempotencyKey string `header:"Idempotency-Key" doc:"Idempotency key"`
    
    // Request body
    Items []OrderItem `json:"items" validate:"required,min=1"`
    Total float64     `json:"total" validate:"required,min=0"`
    Notes string      `json:"notes,omitempty"`
}

type OrderItem struct {
    ProductID int     `json:"product_id" validate:"required"`
    Quantity  int     `json:"quantity" validate:"required,min=1"`
    Price     float64 `json:"price" validate:"required,min=0"`
}

type Order struct {
    ID     int         `json:"id"`
    UserID int         `json:"user_id"`
    Items  []OrderItem `json:"items"`
    Total  float64     `json:"total"`
    Status string      `json:"status" enum:"pending,processing,completed,cancelled"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("E-commerce API", "1.0.0"),
    )

    result, err := api.Generate(context.Background(),
        openapi.POST("/users/:user_id/orders",
            openapi.WithSummary("Create order"),
            openapi.WithDescription("Create a new order for a user"),
            openapi.WithRequest(CreateOrderRequest{}),
            openapi.WithResponse(201, Order{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

Composable Options Pattern

package main

import (
    "context"
    "log"
    
    "rivaas.dev/openapi"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type ErrorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

// Define reusable option sets
var (
    // Common error responses
    CommonErrors = openapi.WithOptions(
        openapi.WithResponse(400, ErrorResponse{}),
        openapi.WithResponse(401, ErrorResponse{}),
        openapi.WithResponse(500, ErrorResponse{}),
    )
    
    // Authenticated user endpoints
    UserEndpoint = openapi.WithOptions(
        openapi.WithTags("users"),
        openapi.WithSecurity("bearerAuth"),
        CommonErrors,
    )
    
    // JSON content type
    JSONContent = openapi.WithOptions(
        openapi.WithConsumes("application/json"),
        openapi.WithProduces("application/json"),
    )
    
    // Read operations
    ReadOperation = openapi.WithOptions(
        UserEndpoint,
        JSONContent,
    )
    
    // Write operations
    WriteOperation = openapi.WithOptions(
        UserEndpoint,
        JSONContent,
        openapi.WithResponse(404, ErrorResponse{}),
    )
)

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("Composable API", "1.0.0"),
        openapi.WithBearerAuth("bearerAuth", "JWT authentication"),
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            ReadOperation,
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
        ),
        
        openapi.POST("/users",
            WriteOperation,
            openapi.WithSummary("Create user"),
            openapi.WithRequest(User{}),
            openapi.WithResponse(201, User{}),
        ),
        
        openapi.PUT("/users/:id",
            WriteOperation,
            openapi.WithSummary("Update user"),
            openapi.WithRequest(User{}),
            openapi.WithResponse(200, User{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

OAuth2 with Multiple Flows

package main

import (
    "context"
    "log"
    
    "rivaas.dev/openapi"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("OAuth2 API", "1.0.0"),
        
        // Authorization code flow (for web apps)
        openapi.WithOAuth2(
            "oauth2AuthCode",
            "OAuth2 authorization code flow",
            openapi.OAuth2Flow{
                Type:             openapi.FlowAuthorizationCode,
                AuthorizationURL: "https://auth.example.com/authorize",
                TokenURL:         "https://auth.example.com/token",
                Scopes: map[string]string{
                    "read":  "Read access",
                    "write": "Write access",
                    "admin": "Admin access",
                },
            },
        ),
        
        // Client credentials flow (for service-to-service)
        openapi.WithOAuth2(
            "oauth2ClientCreds",
            "OAuth2 client credentials flow",
            openapi.OAuth2Flow{
                Type:     openapi.FlowClientCredentials,
                TokenURL: "https://auth.example.com/token",
                Scopes: map[string]string{
                    "api": "API access",
                },
            },
        ),
    )

    result, err := api.Generate(context.Background(),
        // Public endpoint
        openapi.GET("/health",
            openapi.WithSummary("Health check"),
            openapi.WithResponse(200, nil),
        ),
        
        // User-facing endpoint (auth code flow)
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithSecurity("oauth2AuthCode", "read"),
            openapi.WithResponse(200, User{}),
        ),
        
        // Service endpoint (client credentials flow)
        openapi.POST("/users/sync",
            openapi.WithSummary("Sync users"),
            openapi.WithSecurity("oauth2ClientCreds", "api"),
            openapi.WithResponse(200, nil),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Use result...
}

Version-Aware API with Diagnostics

package main

import (
    "context"
    "fmt"
    "log"
    
    "rivaas.dev/openapi"
    "rivaas.dev/openapi/diag"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    api := openapi.MustNew(
        openapi.WithTitle("Version-Aware API", "1.0.0"),
        openapi.WithVersion(openapi.V30x), // Target 3.0.x
        openapi.WithInfoSummary("API with 3.1 features"), // 3.1-only feature
    )

    result, err := api.Generate(context.Background(),
        openapi.GET("/users/:id",
            openapi.WithSummary("Get user"),
            openapi.WithResponse(200, User{}),
        ),
    )
    if err != nil {
        log.Fatal(err)
    }

    // Handle warnings
    if result.Warnings.Has(diag.WarnDownlevelInfoSummary) {
        fmt.Println("Note: info.summary was dropped (3.1 feature with 3.0 target)")
    }

    // Filter by category
    downlevelWarnings := result.Warnings.FilterCategory(diag.CategoryDownlevel)
    if len(downlevelWarnings) > 0 {
        fmt.Printf("Downlevel warnings: %d\n", len(downlevelWarnings))
        for _, warn := range downlevelWarnings {
            fmt.Printf("  [%s] %s\n", warn.Code(), warn.Message())
        }
    }

    // Fail on unexpected warnings
    expected := []diag.WarningCode{
        diag.WarnDownlevelInfoSummary,
    }
    unexpected := result.Warnings.Exclude(expected...)
    if len(unexpected) > 0 {
        log.Fatalf("Unexpected warnings: %d", len(unexpected))
    }

    fmt.Println("Specification generated successfully")
}

Complete Production Example

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "rivaas.dev/openapi"
    "rivaas.dev/openapi/diag"
)

// Domain models
type User struct {
    ID        int       `json:"id" doc:"User ID"`
    Name      string    `json:"name" doc:"User's full name"`
    Email     string    `json:"email" doc:"Email address"`
    Role      string    `json:"role" doc:"User role" enum:"admin,user,guest"`
    Active    bool      `json:"active" doc:"Whether user is active"`
    CreatedAt time.Time `json:"created_at" doc:"Creation timestamp"`
    UpdatedAt time.Time `json:"updated_at" doc:"Last update timestamp"`
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
    Role  string `json:"role" validate:"required" enum:"admin,user,guest"`
}

type ErrorResponse struct {
    Code      int       `json:"code"`
    Message   string    `json:"message"`
    Details   string    `json:"details,omitempty"`
    Timestamp time.Time `json:"timestamp"`
}

// Reusable option sets
var (
    CommonErrors = openapi.WithOptions(
        openapi.WithResponse(400, ErrorResponse{}),
        openapi.WithResponse(401, ErrorResponse{}),
        openapi.WithResponse(500, ErrorResponse{}),
    )
    
    UserEndpoint = openapi.WithOptions(
        openapi.WithTags("users"),
        openapi.WithSecurity("bearerAuth"),
        CommonErrors,
    )
)

func main() {
    api := openapi.MustNew(
        // Basic info
        openapi.WithTitle("User Management API", "2.1.0"),
        openapi.WithInfoDescription("Production-ready API for managing users and permissions"),
        openapi.WithTermsOfService("https://example.com/terms"),
        
        // Contact
        openapi.WithContact(
            "API Support",
            "https://example.com/support",
            "api-support@example.com",
        ),
        
        // License
        openapi.WithLicense("Apache 2.0", "https://www.apache.org/licenses/LICENSE-2.0.html"),
        
        // Version
        openapi.WithVersion(openapi.V31x),
        
        // Servers
        openapi.WithServer("https://api.example.com/v2", "Production"),
        openapi.WithServer("https://staging-api.example.com/v2", "Staging"),
        openapi.WithServer("http://localhost:8080/v2", "Development"),
        
        // Security
        openapi.WithBearerAuth("bearerAuth", "JWT token authentication"),
        
        // Tags
        openapi.WithTag("users", "User management operations"),
        
        // Extensions
        openapi.WithExtension("x-api-version", "2.1"),
        openapi.WithExtension("x-environment", os.Getenv("ENVIRONMENT")),
        
        // Enable validation
        openapi.WithValidation(true),
    )

    result, err := api.Generate(context.Background(),
        // Public endpoints
        openapi.GET("/health",
            openapi.WithSummary("Health check"),
            openapi.WithDescription("Check API health status"),
            openapi.WithResponse(200, map[string]string{"status": "ok"}),
        ),
        
        // User CRUD operations
        openapi.GET("/users",
            UserEndpoint,
            openapi.WithSummary("List users"),
            openapi.WithDescription("Retrieve paginated list of users"),
            openapi.WithResponse(200, []User{}),
        ),
        
        openapi.GET("/users/:id",
            UserEndpoint,
            openapi.WithSummary("Get user"),
            openapi.WithDescription("Retrieve a specific user by ID"),
            openapi.WithResponse(200, User{}),
            openapi.WithResponse(404, ErrorResponse{}),
        ),
        
        openapi.POST("/users",
            UserEndpoint,
            openapi.WithSummary("Create user"),
            openapi.WithDescription("Create a new user"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(201, User{}),
        ),
        
        openapi.PUT("/users/:id",
            UserEndpoint,
            openapi.WithSummary("Update user"),
            openapi.WithDescription("Update an existing user"),
            openapi.WithRequest(CreateUserRequest{}),
            openapi.WithResponse(200, User{}),
            openapi.WithResponse(404, ErrorResponse{}),
        ),
        
        openapi.DELETE("/users/:id",
            UserEndpoint,
            openapi.WithSummary("Delete user"),
            openapi.WithDescription("Delete a user"),
            openapi.WithResponse(204, nil),
            openapi.WithResponse(404, ErrorResponse{}),
        ),
    )
    if err != nil {
        log.Fatalf("Generation failed: %v", err)
    }

    // Handle warnings
    if len(result.Warnings) > 0 {
        fmt.Printf("Generated with %d warnings:\n", len(result.Warnings))
        for _, warn := range result.Warnings {
            fmt.Printf("  [%s] %s at %s\n", 
                warn.Code(), 
                warn.Message(),
                warn.Path(),
            )
        }
    }

    // Write specification files
    if err := os.WriteFile("openapi.json", result.JSON, 0644); err != nil {
        log.Fatal(err)
    }
    
    if err := os.WriteFile("openapi.yaml", result.YAML, 0644); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("✓ Generated OpenAPI %s specification\n", api.Version())
    fmt.Printf("✓ JSON: openapi.json (%d bytes)\n", len(result.JSON))
    fmt.Printf("✓ YAML: openapi.yaml (%d bytes)\n", len(result.YAML))
}

Next Steps