openapi

package
v0.23.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package openapi generates a valid OpenAPI 3.1 document for a lagodev application using only the standard library and reflection.

It has three layers:

  • Struct -> JSON Schema: SchemaOf[T] / SchemaFor reflect over a Go type, honouring json and validate struct tags (see schema.go, validate.go).
  • Spec builder: New(title, version) returns a *Spec onto which operations are declared with Operation / a fluent OperationBuilder. Request and response bodies are described by Go types and registered as reusable component schemas referenced by $ref.
  • Serve: Spec.JSON renders the document; Spec.Handler and Spec.DocsHandler return net/http handlers serving the JSON and a Swagger-UI page.

Example

type CreateUser struct {
    Name  string `json:"name"  validate:"required,max=64"`
    Email string `json:"email" validate:"required,email"`
    Role  string `json:"role"  validate:"required,in=admin|user"`
}
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

spec := openapi.New("Users API", "1.0.0")
spec.AddServer("https://api.example.com", "production")
spec.Operation(http.MethodPost, "/users", openapi.OperationConfig{
    Summary:     "Create a user",
    Tags:        []string{"users"},
    RequestBody: openapi.BodyOf[CreateUser](),
    Responses: map[int]openapi.Response{
        201: openapi.JSONResponse[User]("created"),
        422: openapi.TextResponse("validation failed"),
    },
})
json, _ := spec.JSON()
http.Handle("/openapi.json", spec.Handler())
http.Handle("/docs", spec.DocsHandler())

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NormalizePath

func NormalizePath(path string) string

NormalizePath rewrites a route path into OpenAPI's brace form. Colon params ("/users/:id") become brace params ("/users/{id}"); brace paths pass through.

func PathParams

func PathParams(path string) []string

PathParams extracts the parameter names from a route path, accepting both the OpenAPI/stdlib brace form ("/users/{id}") and the gin/colon form ("/users/:id"). It returns the names without delimiters, in order.

func Register

func Register[T any](r *Registry) string

Register adds T to the registry, returning its component name. It is the exported way to pre-register a response/request DTO.

func SwaggerHTML

func SwaggerHTML(title, specURL string) string

SwaggerHTML returns a minimal, self-contained Swagger-UI page (CDN-backed) titled with title and loading the spec from specURL.

Types

type Body

type Body struct {
	Description string
	Required    bool
	MediaType   string // default application/json
	// contains filtered or unexported fields
}

Body describes a request body: the schema (usually via BodyOf[T]) plus its media type (defaults to application/json) and required flag.

func BodyOf

func BodyOf[T any]() *Body

BodyOf builds a request Body from a Go type T. The type is registered as a component schema and referenced by $ref.

func RawBody

func RawBody(schema *Schema) *Body

RawBody builds a request Body from an explicit schema (no Go type).

type OperationConfig

type OperationConfig struct {
	Summary     string
	Description string
	Tags        []string
	OperationID string

	// PathParams documents path parameters. Any {id}/:id segment in the path
	// that is not listed here is auto-added as a required string parameter.
	PathParams []Param
	Query      []Param

	RequestBody *Body
	Responses   map[int]Response
}

OperationConfig is the declarative form passed to Spec.Operation.

type Param

type Param struct {
	Name        string
	Description string
	Required    bool
	Schema      *Schema // defaults to {type:string} when nil
}

Param describes a single path or query parameter.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry collects named component schemas so struct types are emitted once under components/schemas and referenced elsewhere by $ref. It de-duplicates by the Go type's name, recursing into nested struct fields.

func (*Registry) Schemas

func (r *Registry) Schemas() map[string]*Schema

Schemas returns the registered component schemas keyed by name. The map is a fresh copy safe for the caller to mutate.

type Response

type Response struct {
	Description string
	MediaType   string // default application/json when a schema is present
	// contains filtered or unexported fields
}

Response describes one HTTP response: a human description plus an optional body schema and media type.

func JSONResponse

func JSONResponse[T any](description string) Response

JSONResponse builds a JSON response whose body is described by Go type T.

func ListResponse

func ListResponse[T any](description string) Response

ListResponse builds a JSON response whose body is an array of T.

func TextResponse

func TextResponse(description string) Response

TextResponse builds a bodyless response carrying only a description (e.g. for 204 or error statuses).

type RouteInfo

type RouteInfo struct {
	Method     string
	Path       string
	PathParams []string
}

RouteInfo is one harvested route: its HTTP method and OpenAPI-normalised path plus the derived path-parameter names. It is the skeleton a caller enriches with request/response types via Spec.Operation.

func Harvest

func Harvest(app *web.App) []RouteInfo

Harvest reads the routes registered on a *web.App (or any value exposing Routes() []web.Route) and returns their method/path/params skeleton. web does not carry request/response type metadata, so Harvest fills only what the router knows; callers supply bodies and responses when declaring operations.

for _, r := range openapi.Harvest(app) {
    spec.Operation(r.Method, r.Path, openapi.OperationConfig{...})
}

func HarvestRouter

func HarvestRouter(routes []web.Route) []RouteInfo

HarvestRouter is Harvest for a *web.Router (e.g. a sub-group) or anything else returning []web.Route.

type Schema

type Schema struct {
	Ref string `json:"$ref,omitempty"`

	Type   typeField `json:"type,omitempty"`
	Format string    `json:"format,omitempty"`

	// Object.
	Properties           map[string]*Schema `json:"properties,omitempty"`
	Required             []string           `json:"required,omitempty"`
	AdditionalProperties *Schema            `json:"additionalProperties,omitempty"`

	// Array.
	Items    *Schema `json:"items,omitempty"`
	MinItems *int    `json:"minItems,omitempty"`
	MaxItems *int    `json:"maxItems,omitempty"`

	// String bounds / numeric bounds (pointers so 0 is distinguishable from
	// "unset").
	MinLength *int     `json:"minLength,omitempty"`
	MaxLength *int     `json:"maxLength,omitempty"`
	Pattern   string   `json:"pattern,omitempty"`
	Minimum   *float64 `json:"minimum,omitempty"`
	Maximum   *float64 `json:"maximum,omitempty"`

	// OpenAPI 3.1 / JSON Schema Draft 2020-12 model exclusive bounds as
	// numbers, but lagodev emits them as booleans paired with minimum/maximum
	// for readability; both forms validate equivalently for our generated docs.
	ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"`
	ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"`

	Enum []any `json:"enum,omitempty"`

	Description string `json:"description,omitempty"`
}

Schema is an OpenAPI 3.1 / JSON Schema object. It is a thin, JSON-faithful representation: only the fields lagodev emits are modelled, and everything marshals with the canonical OpenAPI key names. Unset fields are omitted so a generated document stays minimal.

OpenAPI 3.1 aligns JSON Schema's "nullable" with a type union, so a nullable value is expressed as Type being a []string such as ["string","null"] rather than the 3.0-era `nullable: true`. The Type field therefore marshals as a bare string when single and as an array when it carries "null".

func SchemaFor

func SchemaFor(t reflect.Type) *Schema

SchemaFor builds a JSON Schema for an arbitrary Go type. It honours json tags (names, omitempty, "-"), validate tags (required + format/bounds/enum), and recurses through nested structs, slices, maps and pointers. time.Time is emitted as {type:string, format:date-time}. Pointer and omitempty fields become nullable (the 3.1 ["T","null"] union).

SchemaFor builds inline schemas only; use a Registry / Spec.Components to produce $ref-able named component schemas.

func SchemaOf

func SchemaOf[T any]() *Schema

SchemaOf returns the JSON Schema for T using struct reflection. It is the generic entry point; SchemaFor is the reflect.Type equivalent.

schema := openapi.SchemaOf[CreateUser]()

type Spec

type Spec struct {
	Title       string
	Version     string
	Description string
	// contains filtered or unexported fields
}

Spec is an in-progress OpenAPI 3.1 document. Build it with New, attach operations, then render with JSON or serve with Handler.

Example

ExampleSpec demonstrates building an OpenAPI 3.1 document from Go types and serving it. Request/response bodies are described by structs whose json and validate tags drive the generated JSON Schema.

package main

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

	"github.com/devituz/lagodev/openapi"
)

func main() {
	type CreateUser struct {
		Name  string `json:"name"  validate:"required,max=64"`
		Email string `json:"email" validate:"required,email"`
		Role  string `json:"role"  validate:"required,in=admin|user"`
	}
	type User struct {
		ID    int    `json:"id"`
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	spec := openapi.New("Users API", "1.0.0")
	spec.AddServer("https://api.example.com", "production")

	spec.Operation(http.MethodPost, "/users", openapi.OperationConfig{
		Summary:     "Create a user",
		Tags:        []string{"users"},
		RequestBody: openapi.BodyOf[CreateUser](),
		Responses: map[int]openapi.Response{
			201: openapi.JSONResponse[User]("created"),
			422: openapi.TextResponse("validation failed"),
		},
	})
	spec.Operation(http.MethodGet, "/users/{id}", openapi.OperationConfig{
		Summary: "Get a user",
		Tags:    []string{"users"},
		Responses: map[int]openapi.Response{
			200: openapi.JSONResponse[User]("the user"),
			404: openapi.TextResponse("not found"),
		},
	})

	// Mount the JSON document and a Swagger-UI docs page.
	mux := http.NewServeMux()
	mux.Handle("/openapi.json", spec.Handler())
	mux.Handle("/docs", spec.DocsHandler("/openapi.json"))

	b, _ := spec.JSON()
	var doc map[string]any
	_ = json.Unmarshal(b, &doc)
	fmt.Println("openapi:", doc["openapi"])
	comps := doc["components"].(map[string]any)["schemas"].(map[string]any)
	_, hasUser := comps["User"]
	_, hasCreate := comps["CreateUser"]
	fmt.Println("has User component:", hasUser)
	fmt.Println("has CreateUser component:", hasCreate)

}
Output:
openapi: 3.1.0
has User component: true
has CreateUser component: true

func FromApp

func FromApp(app *web.App, title, version string) *Spec

FromApp builds a Spec pre-populated with a bare operation for every route on app: method, path and path parameters are filled, each with a single default 200 response. It is the fastest path to a serveable document; callers can then refine individual operations by calling Spec.Operation again (a repeat method+path replaces the prior operation).

func New

func New(title, version string) *Spec

New returns an empty Spec for the given API title and version.

func (*Spec) AddServer

func (s *Spec) AddServer(url, description string) *Spec

AddServer appends a server URL (with optional description) to the document.

func (*Spec) DocsHandler

func (s *Spec) DocsHandler(specURL string) http.Handler

DocsHandler returns an http.Handler that serves a Swagger-UI HTML page pointing at specURL (the path where Handler is mounted, e.g. "/openapi.json"). The page loads Swagger UI assets from a public CDN; for offline use, host the assets yourself and adapt SwaggerHTML.

http.Handle("/openapi.json", spec.Handler())
http.Handle("/docs", spec.DocsHandler("/openapi.json"))

func (*Spec) Handler

func (s *Spec) Handler() http.Handler

Handler returns an http.Handler that serves the OpenAPI document as JSON. It renders the spec on first request and caches the bytes; mount it at a path such as /openapi.json. Only GET/HEAD are answered (405 otherwise).

http.Handle("/openapi.json", spec.Handler())

func (*Spec) JSON

func (s *Spec) JSON() ([]byte, error)

JSON renders the document as indented JSON bytes (valid OpenAPI 3.1).

func (*Spec) Map

func (s *Spec) Map() (map[string]any, error)

Map returns the document as a generic map (via a JSON round-trip), useful for callers that want to merge or post-process before serving.

func (*Spec) Operation

func (s *Spec) Operation(method, path string, cfg OperationConfig) *Spec

Operation declares an operation on the spec. Path params are auto-derived from {id}/:id segments and merged with explicit PathParams docs. Request and response bodies are resolved to component $refs via the registry.

func (*Spec) Registry

func (s *Spec) Registry() *Registry

Registry exposes the component-schema registry so callers can pre-register types or inspect generated schemas.

func (*Spec) Tags

func (s *Spec) Tags() []string

Tags returns the distinct operation tags across the spec, sorted. Useful for building a tags section or a docs index.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL