OpenAPI

go.putnami.dev/openapi generates OpenAPI 3.0.3 specifications from Go struct types and endpoint metadata.

Plugin setup

import (
    "go.putnami.dev/app"
    fhttp "go.putnami.dev/http"
    "go.putnami.dev/openapi"
)

server := fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000})
oapiPlugin := openapi.NewPlugin(openapi.PluginOptions{})

// Register the plugin — serves the spec at /_/openapi.json
oapiPlugin.RegisterOn(server)

a := app.New("my-service")
a.Module.Use(server)
a.Module.Use(oapiPlugin)

The spec is served at GET /_/openapi.json.

Documenting endpoints

Register endpoints with the plugin using the endpoint builder:

import "reflect"

type CreateUserBody struct {
    Name  string `json:"name" validate:"required,minlen=2" description:"User's display name"`
    Email string `json:"email" validate:"required,email" description:"Email address"`
}

type UserResponse struct {
    ID    string `json:"id" description:"User ID"`
    Name  string `json:"name" description:"User's display name"`
    Email string `json:"email" description:"Email address"`
}

type UserParams struct {
    ID string `json:"id" validate:"required,uuid" description:"User ID"`
}

// Register endpoint with OpenAPI metadata
oapiPlugin.AddRoute(openapi.DiscoveredRoute{
    Method:      "POST",
    Path:        "/users",
    Description: "Create a new user",
    Tags:        []string{"Users"},
    Body:        reflect.TypeOf(CreateUserBody{}),
    Returns:     reflect.TypeOf(UserResponse{}),
    Throws: []openapi.ThrowsMeta{
        {Status: 409, Description: "Email already exists"},
        {Status: 422, Description: "Validation failed"},
    },
})

oapiPlugin.AddRoute(openapi.DiscoveredRoute{
    Method:      "GET",
    Path:        "/users/{id}",
    Description: "Get a user by ID",
    Tags:        []string{"Users"},
    Params:      reflect.TypeOf(UserParams{}),
    Returns:     reflect.TypeOf(UserResponse{}),
    Throws: []openapi.ThrowsMeta{
        {Status: 404, Description: "User not found"},
    },
})

DiscoveredRoute fields

Field Type Description
Method string HTTP method
Path string Route path (supports {param} and [param])
Description string Operation description
Tags []string Grouping tags
Params reflect.Type Path parameters struct type
Query reflect.Type Query parameters struct type
Body reflect.Type Request body struct type
Returns reflect.Type Success response struct type
Throws []ThrowsMeta Possible error responses
Security *SecurityMeta Security requirements

Schema generation

The plugin generates OpenAPI schemas by introspecting Go struct types using reflection and struct tags.

Type mapping

Go type OpenAPI type Format
string string
int, int64 integer int64
float32 number float
float64 number double
bool boolean
[]T array
struct object

Struct tags for OpenAPI

Tag OpenAPI mapping Example
json:"name" Property name json:"email"
description:"text" Field description description:"User email"
validate:"required" Added to required array validate:"required"
validate:"uuid" format: "uuid" validate:"uuid"
validate:"email" format: "email" validate:"email"

Example schema

type Product struct {
    ID          string   `json:"id" validate:"required,uuid" description:"Product ID"`
    Name        string   `json:"name" validate:"required" description:"Product name"`
    Price       float64  `json:"price" validate:"required,min=0" description:"Price in USD"`
    Tags        []string `json:"tags" description:"Product tags"`
    Description string   `json:"description" description:"Product description"`
}

Generates:

{
  "type": "object",
  "required": ["id", "name", "price"],
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "description": "Product ID"
    },
    "name": {
      "type": "string",
      "description": "Product name"
    },
    "price": {
      "type": "number",
      "format": "double",
      "minimum": 0,
      "description": "Price in USD"
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Product tags"
    },
    "description": {
      "type": "string",
      "description": "Product description"
    }
  }
}

Programmatic access

Generate the spec without the plugin:

doc := openapi.GenerateSpec(routes, openapi.Options{
    Title:   "My API",
    Version: "1.0.0",
})

specJSON, err := doc.JSON()

Security metadata

Document security requirements:

oapiPlugin.AddRoute(openapi.DiscoveredRoute{
    Method:      "DELETE",
    Path:        "/users/{id}",
    Description: "Delete a user",
    Security: &openapi.SecurityMeta{
        Roles:  []string{"admin"},
        Scopes: []string{"write"},
    },
})