Specyl
Scan your Go API and generate Swagger/OpenAPI or a Postman Collection — no annotations, no code changes. Supports Gin, Echo, Fiber, Gorilla Mux, and Chi.
go install github.com/devenock/specyl@latest
Why
The standard way to generate OpenAPI docs for a Go API — swaggo/swag — works by parsing comment annotations you write above every handler:
// @Summary Show an account
// @Router /accounts/{id} [get]
// @Param id path int true "Account ID"
// @Success 200 {object} model.Account
func (c *Controller) ShowAccount(ctx *gin.Context) {
That's a real, well-established tool, but the annotations are a second source of truth you maintain by hand: add an endpoint and forget the comment block, and the docs silently fall behind the code with no error to catch it.
specyl removes that step entirely. It reads your code as-is — route registrations, handler signatures, request/response structs, binding calls — via Go's own AST parser, the same way go vet or gofmt do, and builds the OpenAPI/Postman spec from what's actually there. No comments to write, no existing code to touch.
One consequence of that: because there's nothing to keep in sync, docs stay accurate as you build incrementally. Register one handler, generate, ship; add the next one whenever it's ready, generate again — each run reflects exactly what's in the code at that moment, with zero extra steps. (To be precise: every run is a full, fresh scan of the project, not a cached diff of what changed — "incremental" describes the workflow this enables, not an incremental-build engine under the hood.)
"No annotations" only helps if the scanner is actually reliable, so the analyzer is built to degrade honestly rather than guess silently: it resolves route groups, auth middleware, embedded struct fields, query/path parameters, and response bodies across real-world patterns in Gin, Echo, Fiber, Gorilla Mux, and Chi; falls back to generic net/http-style detection for anything else; and surfaces ambiguity instead of hiding it — a .go file that fails to parse, a framework it can't confidently pick between, or zero endpoints found are all reported explicitly (-v for detail) rather than producing a spec that looks complete but silently isn't.
Install
Requires Go 1.26.8+. The binary is installed to $(go env GOPATH)/bin — usually ~/go/bin. Make sure that directory is on your PATH.
# Add to ~/.zshrc or ~/.bashrc if needed
export PATH="$PATH:$(go env GOPATH)/bin"
Verify:
specyl --version
Generate docs
Run from your Go project root — the directory that contains go.mod and your route definitions.
# Interactive wizard — choose type, output dir, title, etc.
specyl generate
# Or non-interactive — skip all prompts
specyl generate --no-interactive --type swagger -o ./docs
specyl generate --no-interactive --type postman -o ./docs
Point at a project in a different directory:
specyl generate /path/to/your-api --no-interactive --type swagger -o /path/to/your-api/docs
specyl init inside your project to generate a .specyl.yaml config file.
Commit it so the whole team shares the same defaults.
Output
| Type | Files |
|---|---|
swagger |
openapi.json · openapi.yaml · index.html (Swagger UI) |
postman |
collection.json (Postman Collection v2.1) |
Swagger — after generating, a local preview server starts automatically (bound to 127.0.0.1 only — never exposed to your network) and opens index.html in your browser. Pass -q/--quiet in CI or scripts to skip this. The Swagger UI assets are vendored into the binary and written alongside your docs, so the page works fully offline — no CDN calls, nothing to break if you view it later without a network connection. To reopen it later, open ./docs/index.html directly in your browser.
Postman — open Postman, click Import in the sidebar, and drag collection.json onto the dialog. Endpoints are organized into folders by tag, with example request bodies and bearer auth pre-wired.
Coverage
What the analyzer actually detects and emits, checked against the code rather than aspirational.
Supported
- Paths, operations, path and query parameters
- Request bodies — JSON binding calls (
ShouldBindJSON,BodyParser,Decode, and common project-specific wrapper names matched by hint) - Response bodies — JSON responses across Gin/Echo/Fiber/Gorilla Mux/Chi/net/http response patterns
- Bearer auth detection — marks a route
security: [BearerAuth]when its middleware looks like auth (or matches your configuredauth_middlewarelist) - Struct tags —
json(name,omitempty,-to exclude),binding:"required"/validate:"required" - Embedded struct fields promoted into the parent schema
- Tags for Swagger UI grouping, derived from the path
- OpenAPI 3.0.3 and Postman Collection v2.1 output
- Common external types —
time.Time,time.Duration,uuid.UUID(google/uuid), anddatabase/sql'sNull*types resolve to their real, verified JSON shape (checked againstencoding/json's actual output, not assumed —sql.NullStringlooks like it should marshal as a plain string, but it has no customMarshalJSON, so it's really{"String":...,"Valid":...})
Not yet supported
- Header parameters — not extracted
- Non-JSON content types — request/response bodies are always modeled as
application/json; no multipart/form-data, XML, or plain text - Auth schemes other than Bearer — no API Key, Basic Auth, or OAuth2 security scheme detection
- Enums, example values, response headers — the schema model has fields for all three (so output stays forward-compatible), but nothing in the analyzer populates them yet
- Any other type from outside the scanned project (a third-party library type not in the supported list above, a shared internal module, etc.) resolves to an empty
objectwith no fields - File uploads (
multipart/form-data) — not detected
Found a pattern that isn't covered? Open an issue — new patterns belong in the analyzer, not worked around.
Applying it to your project
Your project does not need any changes — the tool reads your existing route definitions.
Recommended layout
my-go-api/
├── go.mod
├── main.go
├── handlers/
├── .specyl.yaml ← optional config (see below)
└── docs/ ← generated output
Optional Makefile target
So the whole team runs the same command:
docs:
specyl generate --no-interactive --type swagger -o ./docs
Optional config file
Run specyl init inside your project to create .specyl.yaml. Commit it so everyone shares the same defaults (output dir, title, framework, etc.). In interactive mode, doc type and framework are skipped entirely once set in the file; title, version, base path, and output directory are still prompted but pre-filled with the file's value — press Enter to accept it. Use --no-interactive to skip all prompts and use the file (plus flags/env) as-is.
Flags
Core
| Flag | Description |
|---|---|
-t, --type | Output type: swagger or postman |
-o, --output | Output directory (default: ./docs) |
-f, --framework | Force framework: gin · echo · fiber · gorilla · chi |
-y, --no-interactive | Disable all prompts — required in CI |
--exclude | Directory names to skip (repeatable). Replaces the default list entirely rather than adding to it — if you use this flag, re-list vendor, node_modules, .git, test, tests alongside your own if you still want them skipped |
--tags | Filter endpoints by tag: plain name to include, !name to exclude (comma-separated, mixable) |
--required-by-default | Mark every struct field required unless it has json:",omitempty" (default: only binding/validate:"required" tags count) |
--title | API title (default: auto-detected from your go.mod module name) |
--version | API version string (default: 1.0.0) |
--base-path | Prefix prepended to every path |
--description | API description shown in the generated spec |
--dry-run | Show what would be generated, write nothing |
--show-config | Print effective config and exit |
--serve | Serve the Swagger UI (on the app's own detected port when found, else 8765) and open it in your browser after generating (swagger only, default true); pass --serve=false to skip |
--write-annotations | Write swag-style // @... comments above same-file handler functions |
--skip-build-check | Skip the go vet ./... pre-flight check against the target project (see Build check) |
Global (all commands)
| Flag | Description |
|---|---|
--config | Path to config file (default: .specyl.yaml in the current directory) |
-v, --verbose | Show detected framework, endpoint count, matched middleware, etc. |
-q, --quiet | Suppress progress output and skip the auto-opened browser preview (errors still print to stderr) |
Full list: specyl generate --help
Config file
Create .specyl.yaml in your project root to set persistent defaults. Run specyl init to generate one interactively.
# .specyl.yaml
type: swagger
output: ./docs
framework: "" # leave blank to auto-detect from go.mod
title: My API
version: 1.0.0
Precedence
Configuration is merged from lowest to highest precedence — flags always win:
- Config file —
.specyl.yamlin the current directory (or the path set by--config) - Env vars —
SPECYL_<KEY>(e.g.SPECYL_TYPE,SPECYL_OUTPUT) - Flags — e.g.
--type swagger,-o ./docs
Flag · env · config key mapping
| Flag | Env var | Config key | Default |
|---|---|---|---|
--output, -o | SPECYL_OUTPUT | output | ./docs |
--type, -t | SPECYL_TYPE | type | — |
--framework, -f | SPECYL_FRAMEWORK | framework | auto-detect |
--base-path | SPECYL_BASE_PATH | base_path | "" |
--title | SPECYL_TITLE | title | auto-detected from go.mod's module name (falls back to API Documentation) |
--version | SPECYL_VERSION | version | 1.0.0 |
--description | SPECYL_DESCRIPTION | description | "" |
--exclude | SPECYL_EXCLUDE | exclude | vendor, node_modules, .git, test, tests |
--no-interactive, -y | — | — | false |
--verbose, -v | SPECYL_VERBOSE | verbose | false |
--quiet, -q | SPECYL_QUIET | quiet | false |
--dry-run | — | — | false |
--show-config | — | — | false |
--write-annotations | — | — | false |
--skip-build-check | — | — | false |
--required-by-default | — | — | false |
--tags | — | — | none — keeps everything |
--serve (swagger) | — | — | true |
[path] (positional) | — | — | . |
Default exclude dirs are matched by exact directory name (basename), not substring.
Auth middleware detection
Config-file only (no flag/env equivalent). By default, a route group is marked authenticated (security: [BearerAuth] in the generated spec) when its middleware's name contains auth or jwt (case-insensitive), plus a small set of common exact names. Set auth_middleware in .specyl.yaml to override that heuristic entirely with an exact (case-insensitive) list of your own middleware names:
auth_middleware:
- requireSession
- JWTAuth
Run with -v to see exactly what was matched, either way.
Inspect effective config
specyl generate --show-config
Prints the merged values and exits without generating or prompting.
Full reference: CONFIGURATION.md
Build check
Before analyzing, generate runs go vet ./... against the target project as a pre-flight check. It's purely advisory — a project that fails it still gets docs generated from whatever the AST parser can recover, but you get a warning up front that the result may be incomplete, rather than silently missing routes with no explanation:
⚠️ This project does not currently pass `go vet ./...` —
generated docs may be incomplete or incorrect.
Run with -v for details, or pass --skip-build-check to suppress this check.
Pass --skip-build-check to disable it (e.g. running against a branch mid-refactor in CI). It also skips itself automatically wherever the go toolchain isn't on PATH — including this project's own Docker runtime image, which ships as a slim alpine base with just the compiled binary and no Go toolchain.
Postman
--type postman generates collection.json with your endpoints organized into folders by tag, example request bodies pre-filled, and Bearer auth wired up. Specyl does not upload it anywhere or talk to the Postman API — that would need an API key, and holding Postman credentials just to skip a five-second drag-and-drop isn't worth the complexity. Import it yourself:
- Open Postman
- Click Import (top-left of the sidebar)
- Choose Upload Files and select
collection.json
Or drag the file directly into the Postman sidebar.
specyl generate --no-interactive --type postman -o ./docs
Docker
No Go installation needed. Pull the published image:
docker run --rm \
-v "$(pwd)":/workspace \
-w /workspace \
ghcr.io/devenock/specyl:latest generate --no-interactive --type swagger -o ./docs
Or build it yourself from source:
docker build -t specyl .
docker run --rm \
-v "$(pwd)":/workspace \
-w /workspace \
specyl generate --no-interactive --type swagger -o ./docs
Replace $(pwd) with the path to your API project if you're not running from its root. The container runs as a non-root user — if generated files come out owned by a UID your host user can't write to, add --user "$(id -u):$(id -g)" to the docker run command.
CI & scripts
Always pass -y and --type so the CLI never waits for input.
# GitHub Actions example
- name: Generate API docs
run: |
go install github.com/devenock/specyl@latest
specyl generate -y --type swagger -o ./docs --title "My API"
Exit codes: 0 success · 1 validation error · 2 runtime error.
Security
specyl only reads your project's source to extract routes and types via static AST parsing — it never executes your code. A few specific hardening notes, since this runs against real codebases:
- Confined file access — every file read is scoped to your project directory via Go's
os.Root, so a symlink (accidental or crafted) pointing outside the project tree is never followed, even if it's swapped in mid-scan. - Output stays put — the output directory is validated to stay within your working directory unless you pass
--output/-oexplicitly yourself, so a.specyl.yamlfrom a repo you don't control can't silently redirect where files get written. - Local-only preview — the Swagger UI preview server binds to
127.0.0.1, never your network, and generated docs are fully offline (no CDN dependency). - No credentials, ever — Specyl never talks to any external API (Postman included); it only reads your project and writes files locally, so there's no credential-handling surface at all.
Found a security issue? See SECURITY.md for how to report it.
Troubleshooting
Command not found after go install
Go's bin directory is not on your PATH. Add it and reload your shell:
export PATH="$PATH:$(go env GOPATH)/bin"
source ~/.zshrc # or ~/.bashrc
No endpoints found
Usually one of:
- The framework wasn't auto-detected — pass
--frameworkexplicitly (e.g.--framework gin). - Routes are registered in a file inside an excluded directory (
vendor,test,tests, etc.) — check--excludeand the defaults. - Routes are registered through an indirection the analyzer doesn't follow (a wrapper function several layers removed from the route-registration call).
- Run with
-vto see exactly what the analyzer read and which framework it picked.
"Multiple frameworks detected in go.mod"
Your go.mod imports more than one supported framework (e.g. migrating from Gin to Echo). Pass --framework explicitly to disambiguate.
"This project does not currently pass go vet ./..."
Advisory only — docs are still generated from whatever the AST parser can recover. Run with -v for details on what failed, or pass --skip-build-check to suppress the warning (see Build check).
"N file(s) could not be parsed"
One or more .go files have a syntax error the Go parser can't recover from. Endpoints and types in that file are skipped; everything else still generates. Run with -v to see which files failed and why.
Invalid configuration
Two specific error strings to watch for: an unrecognized type value (only swagger and postman are valid), or an output path that resolves outside your project directory when it wasn't explicitly passed via --output/-o (a deliberate safety check — see Security).
Config file not being read
Ensure .specyl.yaml is in the directory you're running from, not a subdirectory (or point --config at it explicitly). Inspect the merged result with specyl generate --show-config.
Prompts appearing in CI
Always pass -y (--no-interactive) and --type. Without these the CLI waits for interactive input. Exit codes in non-interactive mode: 0 success · 1 validation error · 2 runtime error.
Quiet and verbose modes
-q/--quiet suppresses progress output and skips the auto-opened browser preview; errors still print to stderr. -v/--verbose shows the detected framework, endpoint count, matched auth middleware, and per-file parse results.
Full guide: TROUBLESHOOTING.md