How to Generate Swagger/OpenAPI Docs for a Go API

Web
Generate Swagger/OpenAPI docs for a Go API by installing the swag CLI and running swag init to scan your code comments.

The contract drift problem

You just finished a REST endpoint. It accepts a JSON body with a user object, validates the email format, saves the record, and returns a 201 status with the created user. You push the code to the repository. An hour later, a message appears from the frontend team: "What fields does the create user endpoint expect? Is email required? What does the error response look like if the email is taken?"

You open your browser, hit the endpoint with a dummy request, copy the JSON response, and paste it into a Slack message. Two days later, you add a phone field to the struct. You forget to update Slack. The frontend breaks because it's not handling the new field, or worse, the backend starts rejecting requests because the frontend stopped sending a field that is now required. This cycle is the tax of manual documentation. The docs live in a separate place from the code, so they drift the moment the code changes. Swagger and OpenAPI exist to automate the contract so the documentation stays in sync with the implementation.

OpenAPI as code

OpenAPI is a specification. It defines a standard way to describe REST APIs using JSON or YAML. Swagger is the brand name that stuck; the ecosystem often uses "Swagger" to refer to the spec, the editor, and the documentation UI. In Go, the language does not generate this automatically. Go values explicitness. You write comments in your source code that describe the endpoints, parameters, and responses. A tool scans those comments and generates the OpenAPI file.

The result is a machine-readable description. You can use it to power a documentation website, generate client SDKs in other languages, or validate incoming requests. You write the docs as code. The tool compiles them into the spec. If the code changes, you update the comments and regenerate. The docs are always derived from the source of truth.

Comments are the source of truth. If the comment is wrong, the contract is wrong.

Minimal example

Here is the simplest setup. You annotate a handler function with special comments that describe the route, the response type, and the status codes. The swag tool reads these annotations and builds the spec.

First, install the tool. This is a command-line utility, not a library dependency.

# Install the swag CLI tool globally
go install github.com/swaggo/swag/cmd/swag@latest

Create a Go file with a handler. The comments must appear immediately before the function. The @Router tag defines the path and HTTP method. The @Success tag defines the response for a successful request.

package main

import (
    "net/http"
)

// GetGreeting handles the /greeting endpoint.
// @Summary Get a greeting message
// @Description Returns a simple JSON greeting
// @Produce json
// @Success 200 {object} GreetingResponse
// @Router /greeting [get]
func GetGreeting(w http.ResponseWriter, r *http.Request) {
    // Set content type header for JSON responses
    w.Header().Set("Content-Type", "application/json")
    // Write the response body
    w.Write([]byte(`{"message": "Hello"}`))
}

// GreetingResponse defines the structure of the response.
type GreetingResponse struct {
    // The greeting text returned to the client
    Message string `json:"message"`
}

func main() {
    // Register the handler function with the default mux
    http.HandleFunc("/greeting", GetGreeting)
    // Listen on port 8080 and block
    http.ListenAndServe(":8080", nil)
}

Run the scanner. It starts at main.go, finds the @Router tag, resolves the GreetingResponse type, and writes the output files.

# Scan main.go and generate docs in the docs directory
swag init -g main.go -o docs

The tool creates three files in the docs directory. swagger.json and swagger.yaml contain the OpenAPI specification. doc.go is a Go file that embeds the JSON and YAML so you can serve them from your binary.

The scanner only sees what you show it. Import the docs package or the UI stays empty.

How the scanner works

The swag tool is a static analyzer. It does not run your code. It reads the Abstract Syntax Tree of your Go files. When you run swag init, it starts at the entry point you specify. It walks the dependency graph of your code. It finds functions with @Router tags. It extracts the summary, description, parameters, and response schemas.

The tool resolves Go structs to JSON schemas. It uses the json struct tags to determine field names. If a field has json:"-", the tool excludes it from the spec. If a field has json:"name,omitempty", the tool marks it as optional. The scanner respects Go visibility rules. Unexported fields (lowercase) are ignored. Exported fields (uppercase) are included unless tagged to omit.

The generated doc.go file is crucial. It registers the swagger files with the swag library. You must import this package in your main.go to make the docs available. The convention is to use a blank identifier import. This runs the package initialization without referencing any symbols.

import _ "yourproject/docs"

The blank import runs side effects. In this case, it registers the embedded swagger files so the UI handler can find them.

Realistic handler

Real APIs have request bodies, error responses, and context. The comments need to reflect that. The @Param tag describes input parameters. The @Failure tag describes error responses. The tool ignores context.Context parameters automatically, which aligns with Go conventions.

package main

import (
    "context"
    "net/http"
)

// CreateTodo handles creating a new todo item.
// @Summary Create a new todo
// @Description Adds a todo item to the list
// @Accept json
// @Produce json
// @Param todo body TodoInput true "Todo input"
// @Success 201 {object} TodoResponse
// @Failure 400 {object} ErrorResponse "Invalid input"
// @Failure 500 {object} ErrorResponse "Internal error"
// @Router /todos [post]
func CreateTodo(w http.ResponseWriter, r *http.Request) {
    // Extract context for cancellation and deadlines
    ctx := r.Context()
    // Parse request body into struct
    var input TodoInput
    // ... validation and save logic ...
    // Return success with created resource
    w.WriteHeader(http.StatusCreated)
}

// TodoInput represents the request payload.
type TodoInput struct {
    // The title of the todo item
    Title string `json:"title" binding:"required"`
    // The description of the todo item
    Description string `json:"description"`
}

// TodoResponse represents the created todo.
type TodoResponse struct {
    // Unique identifier assigned by the server
    ID int `json:"id"`
    // The title of the todo
    Title string `json:"title"`
}

// ErrorResponse represents an error response.
type ErrorResponse struct {
    // Human-readable error message
    Message string `json:"message"`
}

The @Param tag syntax is name type in required description. Here, todo is the name, body is the type, and TodoInput is the Go struct. The true flag marks it as required. The @Failure tags document error cases. This helps frontend developers handle errors gracefully. The tool maps the binding:"required" tag to the OpenAPI required array in some configurations, but it is safer to rely on the @Param tag for the contract.

Errors matter in the spec. Document the failure modes so clients can recover.

Serving the UI

The generated files are not enough. You need a way to view the documentation. The swaggo/http-swagger package provides a handler that serves the Swagger UI. Install it as a dependency.

go get github.com/swaggo/http-swagger

Register the handler in your main.go. The WrapHandler function returns an http.Handler that serves the UI and proxies requests to the swagger JSON.

import (
    "net/http"
    _ "yourproject/docs"
    "github.com/swaggo/http-swagger"
)

func main() {
    // Register the API handler
    http.HandleFunc("/todos", CreateTodo)
    // Serve the Swagger UI at /swagger/*
    http.Handle("/swagger/", httpSwagger.WrapHandler)
    // Start the server
    http.ListenAndServe(":8080", nil)
}

Navigate to http://localhost:8080/swagger/index.html. You see the interactive documentation. You can try out the endpoints, see the request/response schemas, and copy curl commands. The UI is generated from the swagger.json file embedded in your binary.

The UI is just a viewer. The value is the contract. Use the spec to generate clients or validate requests.

Pitfalls and errors

The swag tool is strict. If you write @Router /todos [post] but the function signature does not match, or if you reference a type that does not exist, the generation fails. You might see parse error: cannot find type definition for "User" if you reference a struct that the scanner cannot reach. This happens when the type is in a different package and you did not import it, or when the type is defined in a file that swag did not scan.

Circular references are a common issue. If struct A contains struct B, and struct B contains struct A, swag might loop or fail to resolve the schema. The tool warns with circular reference detected. You need to break the cycle. Use pointers to create a tree structure instead of a cycle, or flatten the structure. OpenAPI supports $ref for recursive schemas, but the scanner needs help to detect them safely.

Another issue is internal packages. By default, swag only scans exported types in external packages. If you have types in your own package that are not exported, the scanner ignores them. Use the --parseInternal flag to include internal types.

# Scan internal types as well
swag init --parseInternal

The tool also requires comments to be attached to the function. If you put a blank line between the comment block and the function, the scanner ignores the comments. gofmt helps keep comments attached, but it does not enforce the swag syntax. Run swag fmt to format the comments.

# Format swag comments in the project
swag fmt

Circular references break the scanner. Flatten the structure or use pointers to define the graph.

When to use Swagger

Use Swagger/OpenAPI with swag when you need a standard contract for a REST API and want to generate client SDKs or a documentation UI. Use swag when your team values keeping documentation close to the code and wants to catch drift during the build process. Use a manual OpenAPI YAML file when your API is simple and changes infrequently, or when you prefer to design the contract before writing code. Use protobuf with gRPC when you are building internal microservices and need strict typing and high performance over HTTP/JSON. Use plain Markdown or a wiki when the API is internal, experimental, or the overhead of maintaining annotations outweighs the benefits.

Swagger is a contract. Treat it like code, not like a blog post.

Where to go next