The most surprising thing about generating OpenAPI specifications from code is that the code itself becomes the single source of truth for your API’s contract, and the spec generation is a side effect of your development, not a separate, manual process.

Let’s see this in action with a simple Go example. Imagine we have a User struct and a handler function to get a user by ID.

package main

import (
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/swaggest/swgui/v5"
	"github.com/swaggest/swgui/v5/options"
)

// User represents a user in the system.
// @Description User model
// @Description with basic information
type User struct {
	ID   int    `json:"id" example:"123"`
	Name string `json:"name" example:"John Doe"`
}

// @Summary Get user by ID
// @Description Retrieve a specific user using their unique identifier.
// @Tags users
// @Accept  json
// @Produce  json
// @Param   id path int true "User ID"
// @Success 200 {object} User
// @Failure 400 {object} string "Invalid ID supplied"
// @Failure 404 {object} string "User not found"
// @Router /users/{id} [get]
func getUserByID(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, "Invalid ID supplied")
		return
	}

	// In a real app, you'd fetch from a database.
	// For this example, we'll simulate a user.
	if id == 123 {
		c.JSON(http.StatusOK, User{ID: id, Name: "John Doe"})
	} else {
		c.JSON(http.StatusNotFound, "User not found")
	}
}

func main() {
	r := gin.Default()

	// Register the handler
	r.GET("/users/:id", getUserByID)

	// Swagger UI setup
	// This is where the magic happens. The annotations are parsed here.
	// The options.OpenAPIPath is crucial as it tells the UI where to find the spec.
	// We're serving the spec from the root path, which is generated by the library.
	r.GET("/swagger/doc.json", func(c *gin.Context) {
		// In a real application, you might generate this spec dynamically
		// or load it from a file. The swaggest library can often do this
		// implicitly by scanning your code.
		// For demonstration, we'll use a placeholder.
		// The actual spec generation happens when the UI is initialized.
		// The library will scan the code for annotations.
		c.JSON(http.StatusOK, map[string]string{"message": "Swagger spec endpoint"})
	})

	// Serve Swagger UI
	// The swgui.Handler sets up the UI and points it to the doc.json endpoint.
	// The options.Title sets the title displayed in the UI.
	// The options.OpenAPIPath is key here – it tells the UI where to fetch the OpenAPI spec.
	// The library automatically generates the spec from annotations when this handler is accessed.
	url := "/swagger/index.html"
	r.GET(url, swgui.Handler(options.OpenAPIPath(url)))

	r.Run(":8080")
}

In this example, the go-swagger/swag library (and its UI component swaggest/swgui) scans your Go source code for special comments starting with // @. These are your OpenAPI annotations.

The // @Description, // @Summary, // @Tags, // @Accept, // @Produce, // @Param, // @Success, // @Failure, and // @Router directives tell the library how to describe your API endpoints, parameters, responses, and models.

When you run your application and navigate to /swagger/index.html (or whatever path you configure), the swgui.Handler intercepts the request. It then triggers the spec generation process by parsing all the annotations found in your code. This generated spec, typically in JSON or YAML format, is then served to the Swagger UI, which renders an interactive documentation page.

The User struct also has annotations (// @Description, // @Description, json:"id" example:"123", json:"name" example:"John Doe"). These are used to define the schema for your User model within the OpenAPI specification. The example tags are particularly useful for providing concrete examples in the documentation.

The core problem this solves is the drift between your running API and its documentation. Traditionally, you’d write code, then separately write documentation (or generate a spec from a template). This leads to out-of-sync docs. With code-first annotations, the code is the documentation’s source. Any change to your API’s behavior or contract must be reflected in the annotations if you want the generated spec to be accurate.

The // @Param id path int true "User ID" annotation tells Swagger that there’s a parameter named id, it’s located in the path, it’s of type int, it’s required (true), and its description is "User ID". The getUserByID function then uses c.Param("id") to retrieve this value. The library correlates these.

The // @Success 200 {object} User and // @Failure 400 {object} string "Invalid ID supplied" annotations describe the possible responses. The {object} User part tells Swagger that the successful response body will be a JSON object conforming to the User schema. The {object} string indicates a plain string response for the error.

The // @Router /users/{id} [get] annotation maps the HTTP method (get) and the URL path (/users/{id}) to this handler function. The {id} in the path here is directly linked to the // @Param id path int true "User ID" annotation.

The truly powerful part is how the library infers the schema for the User struct. It looks at the struct fields, their Go types, and the json tags. The example tags are then embedded directly into the User schema definition within the OpenAPI spec. This means your examples are generated as part of the spec, not written separately.

The swgui.Handler(options.OpenAPIPath(url)) is where the dynamic generation happens. When the Swagger UI is loaded, it makes a request to the path specified by options.OpenAPIPath (in this case, /swagger/index.html itself, which is a bit of a simplification but demonstrates the concept). The swgui library intercepts this, scans the running application’s code for annotations, generates the OpenAPI spec on the fly, and then serves it to the UI. The UI then uses this spec to render the interactive documentation.

When you add new endpoints or modify existing ones, you just add or update the annotations. The next time you refresh your Swagger UI, the documentation will be updated automatically. This drastically reduces the effort required to keep API documentation current.

The most commonly misunderstood aspect of code-first OpenAPI generation is that it’s not just about documentation; it’s about contract enforcement. If your code changes but your annotations don’t, your generated spec will be wrong. Tools can leverage this spec for generating client SDKs, validating requests/responses, and more. Therefore, treating annotations as part of the API contract itself, rather than just documentation comments, is critical.

The next concept you’ll likely run into is handling more complex request bodies, authentication mechanisms, and versioning strategies within your OpenAPI annotations.

Want structured learning?

Take the full Swagger course →