go-openapi codescan

Documentation set for latest master. Latest release v0.36.4. Requires Go 1.26.0 or later. Built 2026-09-04.

github.com/go-openapi/codescan is a Go source code scanner that produces Swagger 2.0 (OpenAPI 2.0) specifications.

It reads specially formatted comments (annotations) in Go source files and extracts API metadata β€” routes, parameters, responses, schemas and more β€” to build a complete spec.Swagger document. It supports Go modules (since go1.11).

The scanner works entirely at the AST / go/types level: it never compiles or executes the code it scans. It only reads the source and its annotation comments.

Status

Fork me Stable API. Actively maintained.

The only exposed API is Run() and Options.

Getting started

To use codescan in your go program:

go get github.com/go-openapi/codescan

Point the scanner at one or more packages and get back a *spec.Swagger:

import "github.com/go-openapi/codescan"

swaggerSpec, err := codescan.Run(&codescan.Options{
    Packages: []string{"./..."},
})

Or as a command, to run in a build or a pipeline:

go install github.com/go-openapi/codescan/cmd/genspec@latest

Or as a terminal front-end, to watch a spec take shape as you annotate:

go install github.com/go-openapi/codescan/cmd/genspec-tui@latest

Try it out now from your browser in our Playground.

Relationship to go-swagger

go-swagger is a CLI tool that consumes the codescan library. It works exactly on the same set of annotations.

The main differences with the newer genspec CLI shipped by this project are:

  • release cadence (expect slightly less frequent updates on go-swagger, which has more dependencies and constraints)
  • package distribution: at this moment, the codescan CLI tools do not ship as distro packages or docker images
  • exposed CLI knobs and default settings (defaults need to be backward-compatible for go-swagger users)

genspec and genspec-tui are intended for users who want tools leaner than go-swagger, or who want to experiment with the latest features.

Where to go next

  • What codescan is, why you would scan source to produce a spec.

    How does it relate to the go-openapi & go-swagger toolkits.

  • Install codescan and choose how to drive it.

    As a Go library from your own program, as a command in a build, or interactively from the terminal UI.

  • Scan Go source in your browser.

    Edit annotations, watch the specification change, and follow a spec node back to the code that produced it.

  • All the knobs codescan takes. What they are, and how they relate.

    There are three spellings: a Go field, a command-line flag, and a key in a configuration file.

  • Repo-level information for github.com/go-openapi/codescan.

    Contributing guidelines & maintainers documentation.

  • Learn codescan by spec concept: model definitions, routes and operations, validations, examples, and document metadata.

    Each demonstrated as annotated Go next to the Swagger it produces.

  • How-to guides for the knobs that change how the same Go source renders into the spec.

    Grouped by what they shape: scope & discovery, names & $refs, titles & descriptions, field types & formats, and response bodies.

  • Every swagger:* annotation at a glance.

    What it produces and where it attaches β€” linked to both its worked example and its full reference.

  • The complete, normative reference for the codescan annotation language.

    Every annotation, every keyword, the embedded sub-languages, the formal grammar the parser implements, and how the commands are put together.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of go-openapi codescan

About

codescan is a code-first OpenAPI engine: it reads specially formatted comments (annotations) in your Go source and produces a Swagger 2.0 specification. It works entirely at the AST / go/types level β€” it never compiles or runs the code it scans.

Two ways to build an API

APIs and their documentation tend to evolve along one of two paths. The go-openapi / go-swagger toolkit supports both.

  • Design-first (contract-first) β€” you write the OpenAPI document first and treat it as the contract, then generate servers and clients from it.

If this is your workflow, you should reach for go-swagger (swagger generate server / swagger generate client).

  • Code-first β€” you write annotated Go and scan it to produce the spec.

This keeps the document in sync with the code as it changes, and lets you produce a valid specification for a service that already exists.

codescan is the engine for the code-first path.

Relationship to go-swagger

codescan began life as a single package inside go-swagger and was spun out into its own go-openapi repository. It is the scanner behind the go-swagger command:

swagger generate spec ./...

For now, go-swagger remains the main command-line consumer of this library.

This site documents the scanner library itself β€” the layer beneath swagger generate spec β€” so it sits upstream of go-swagger’s “generate spec” documentation.

If you arrived here from go-swagger: the annotations are exactly the same, and you can either keep using the swagger CLI or call codescan.Run directly from your own program (see Getting started).

Info

codescan now releases standalone CLI tools that ship independently from go-swagger: genspec, genspec-wasi and genspec-tui.

These are supplementary tools, not a replacement, and go-swagger will continue shipping updates from this library.

Why scan from source

  • One source of truth. The spec is derived from the code, so it cannot silently drift from what the service actually exposes.
  • Fast iteration. Add a field, add its annotation, regenerate β€” no separate document to keep in step by hand.
  • Document what exists. Produce a standards-compliant spec for an API server that is already deployed, so it becomes interoperable with new clients and tooling.

When document-level metadata (info, security, servers) is more naturally hand-authored, you do not have to push it into the code: scan the code for the operations and models, and overlay the result onto a hand-written base document (see Shaping the output β†’ Overlaying a spec).

The go-openapi toolkit

go-openapi and go-swagger are community-driven, open-source building blocks meant to be assembled and customized β€” there are too many ways to approach APIs to cover them all.

Fork, reuse, and adapt what you find useful. See the go-swagger project’s “About” page for the wider toolkit story.

Where to go next

  • Getting started

    Install codescan and produce your first spec.

    β†’ getting-started

  • Tutorials

    Learn by spec concept, with annotated Go beside the spec it produces.

    β†’ tutorials

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Getting started

Install

As a library, to call from your own program:

go get github.com/go-openapi/codescan

codescan exposes a deliberately small surface: a single Run function and an Options struct.

func Run(opts *Options) (*spec.Swagger, error)

Or as a command, to run in a build or a pipeline:

go install github.com/go-openapi/codescan/cmd/genspec@latest

Or as a terminal front-end, to watch a spec take shape as you annotate:

go install github.com/go-openapi/codescan/cmd/genspec-tui@latest

If you just want to experiment, learn or reproduce an issue you’re currently having, the easiest way is to try our Playground in your browser.

Ways to use codescan

  • Import codescan, annotate a package.

    Produce a Swagger 2.0 specification from your Go program.

  • Point genspec at annotated Go source and get a Swagger 2.0 document β€” on standard output, in a file, checked against the schema, or as a machine-readable envelope from a sandbox with no Go toolchain in it.
  • Drive codescan interactively: browse an annotated source tree, watch the spec it produces.

    It re-renders on every save, and follow any node back to the code that made it.

All three drive the same scanner over the same annotations, and every knob is spelled the same way in each, so the terminal UI shows exactly the document your build will produce.

See Setting an option.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Getting started

Usage as a library

The most direct way to use codescan from your own Go program is to import it and call Run.

This supports various use-cases such as a generator, a go:generate step, or a test that keeps your spec in sync with the source.

Install

go get github.com/go-openapi/codescan

codescan exposes a deliberately small surface: a single Run function and an Options struct.

func Run(opts *Options) (*spec.Swagger, error)

Annotate your source

Annotations are special comments following the go-swagger convention (swagger:meta, swagger:route, swagger:model, swagger:parameters, swagger:response, …).

A package-level swagger:meta block carries the top-level metadata of the spec:

// Package petstore Petstore API
//
// A tiny pet store, used to demonstrate codescan annotations: the package
// comment is a `swagger:meta` block carrying the top-level metadata of the
// generated specification (title, description, version, base path, …).
//
//	Schemes: https
//	Version: 1.0.0
//	BasePath: /v1
//
//	Consumes:
//	- application/json
//
//	Produces:
//	- application/json
//
// swagger:meta

Full source: docs/examples/petstore/doc.go

A swagger:model annotation turns a Go struct into a definition; field-level comments become validations and descriptions:

// Pet is a single pet in the store.
//
// swagger:model Pet
type Pet struct {
	// The id of the pet.
	//
	// required: true
	// minimum: 1
	ID int64 `json:"id"`

	// The name of the pet.
	//
	// required: true
	// min length: 1
	Name string `json:"name"`

	// The tags associated with this pet.
	Tags []string `json:"tags,omitempty"`
}

Full source: docs/examples/petstore/pet.go

Run the scanner

Point codescan at the package(s) to scan. Patterns are relative go list-style patterns, resolved against WorkDir:

opts := &codescan.Options{
	WorkDir:    workDir,                // module root to resolve patterns from
	Packages:   []string{"./petstore"}, // relative package pattern
	ScanModels: true,                   // also emit definitions for swagger:model types
}

doc, err := codescan.Run(opts)
if err != nil {
	return nil, err
}

Full source: docs/examples/basic/scan.go

The returned *spec.Swagger is the standard github.com/go-openapi/spec document β€” marshal it to JSON or YAML, feed it to a validator, or merge it onto an existing spec via Options.InputSpec.

Options worth knowing

FieldEffect
PackagesRelative go list patterns to scan (e.g. ./...).
WorkDirDirectory the patterns resolve against.
ScanModelsAlso emit definitions for swagger:model types.
PruneUnusedModelsWith ScanModels, drop what nothing references β€” see Pruning unused models.
InputSpecOverlay: merge discoveries on top of an existing spec.
BuildTags, Include/ExcludeScope control over what gets scanned.
OnDiagnosticWhere everything the scan observed goes. codescan writes to no stream of its own, so without this the observations are lost.

Those are the ones a first call tends to need. Everything else β€” alias handling, $ref siblings, naming, doc-comment cleanup, the loader, the go environment β€” is in the Options reference, which gives each field with its command-line and configuration-file spellings beside it. The godoc is the normative source.

Dependencies

Code loading relies by default on the go toolchain and this requires go to be installed.

To alleviate this constraint, you may want to use the pure-go ToolchainFreeLoader loader in your options, so programs that build on top of codescan don’t shell out a go list command.

Next

  • Scan a package β€” the whole of the above as one runnable, test-covered example.
  • Usage as a headless CLI β€” the same scan without writing a program, for a build or a pipeline.
  • Tutorials β€” the worked, by-concept version of the above, each with the spec it produces.
  • Annotation index β€” every annotation at a glance, linked to its example and its full reference.
  • Maintainers reference β€” the complete annotation vocabulary, keywords, and grammar.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Usage as a headless CLI

genspec is the ordinary way to run codescan without writing a program: point it at annotated Go source, get a Swagger 2.0 document. Everything the library can be told is a flag, the document goes to standard output or to -output, and what the scan observed goes to standard error.

Install

go install github.com/go-openapi/codescan/cmd/genspec@latest
# scan the module in the current directory, document to standard output
genspec

# somewhere else, narrowed, to a file, and checked
genspec -workdir ../my-api -output swagger.yaml -validate ./internal/api/...

The packages are Go patterns, resolved against -workdir. Naming none scans ./....

Note

genspec does the job of go-swagger’s swagger generate spec, but is released independently of it β€” so fixes and new options reach it at codescan’s pace rather than go-swagger’s. go-swagger has a much larger scope, and the dependencies that go with it.

Two streams, never mixed

The document goes to standard output; diagnostics go to standard error, colored when that is a terminal. Nothing is written to standard error when there is nothing to say. So the obvious pipeline is safe:

genspec > swagger.json
genspec | jq '.definitions | keys'

This is the command’s doing, not the library’s: codescan itself never writes to either stream. Every observation it makes β€” a dropped construct, a rename, a prune β€” is passed to the command through a callback, and genspec renders those callbacks as lines you can read.

Choosing the output

FlagEffect
-outputthe file to write, or - for standard output (the default)
-formatjson, yaml, or auto β€” which reads the extension of -output, and writes JSON when that says nothing
-compactJSON with no indentation
-inputmerge the scan’s discoveries into an existing document

auto is why the common cases need no -format at all:

genspec -output swagger.yaml     # YAML, because of the name
genspec -output swagger.json     # JSON
genspec > swagger.json           # JSON
genspec -compact                 # JSON with no indentation

YAML is derived from the JSON rendering, which costs key order: the document comes out alphabetical rather than in the order the spec types declare. Same information, a different diff against a hand-written file.

-input is the place for everything a scanner cannot know β€” the host, the security definitions, a hand-written path the annotations do not describe. The scan is merged on top of it; see Overlaying a spec.

Diagnostics

FlagEffect
-quietsay nothing at all
-verbosealso report hints, which are muted by default β€” and say which configuration file was read
-colorauto (a terminal), always, never. auto honours NO_COLOR and TERM=dumb
-validatecheck the document against the Swagger 2.0 schema and report what is wrong with it
-fail-onexit non-zero when something reaches this severity: error, warning, or never

-fail-on covers what -validate found as well as what the scan observed: they reach the reader as one stream, so a threshold that saw only half of it would be a trap rather than a policy. It defaults to never, because a scan that emits warnings is the ordinary case, and a command that failed the build over one would mostly teach people to stop reading them.

A scan that reports nothing is not a promise that the document it produced is valid. The scanner diagnoses what is wrong with your annotations; whether the result is a legal Swagger 2.0 document is a separate question. Pass -validate to answer it:

genspec -validate -output swagger.yaml ./internal/api/...

It checks the rendered JSON rather than the document in memory, so what is validated is exactly what was written β€” including whatever the round trip through JSON did to it. The check is go-openapi/validate, the same one behind swagger validate.

Findings arrive through the same stream as the scan’s own, because to a reader they are the same kind of news about the same document. Each is located by the JSON pointer the validator recorded as it walked the spec, so it names the node rather than a line:

ERROR | paths./orders.post.parameters.1.in in body is required at=/paths/~1orders/post/parameters/1

A finding about something the document lacks β€” no info block, no paths β€” has an empty pointer, which is a location in RFC 6901 and not the absence of one. It is reported as the whole document rather than printed as nothing:

ERROR | info in body is required at=(the whole document)
genspec: the specification is not valid: 1 finding(s)

Warnings and errors both come out. Only errors make the document invalid: a warning β€” an unused definition, say β€” is worth saying and is not a verdict, and whether it fails the command is -fail-on’s business rather than the validator’s.

An invalid document exits 4, and that outranks -fail-on’s 3: it is the more specific answer. The document is still written either way, so you can look at what was rejected.

Exit status

A specification is written whenever one could be produced, so a non-zero status says what was wrong with the document rather than meaning nothing came out.

StatusMeaning
0the scan produced a document, and nothing asked for more
1the scan failed
2the command line does not make sense
3what was reported reached the severity -fail-on names
4-validate found the document invalid

An invalid document outranks -fail-on: it is the more specific answer.

Configuring it once

Anything that can be a flag can be preset in a .codescan.yaml, found by searching upwards from wherever you are β€” so a project configures itself once and the command is run bare:

scan:
  exclude-tags: [internal]

document:
  format: yaml
  compact: true

diagnostics:
  validate: true
  fail-on: warning

The options naming a path are not among them β€” -workdir, -output, -input β€” because a file found by searching upwards belongs to the tree being scanned, and that tree must not choose where the command reads or writes. They are typed:

genspec -workdir ./api -output swagger.yaml ./...

Anything typed on the command line wins over it. The file’s full contract β€” where it is looked for, what sections exist, -config and --no-config β€” is in Setting an option.

The scan itself

genspec -h lists every flag. They fall into the four families a configuration file uses as its sections:

  • which code (scan) β€” -workdir, the positional patterns, -build-tags, -include / -exclude, -include-tags / -exclude-tags, -exclude-deps
  • what it is built as (go) β€” -goos, -goarch, -goflags, -gowork, -goexperiment. Each decides what compiles, and so what the document says; each is a flag rather than inherited state, so a scan is reproducible
  • how it is read (load) β€” -loader, -stub-stdlib, -compiled-dependencies
  • what is emitted (emit) β€” -scan-models, -prune-unused-models, the alias and allOf knobs, -skip-extensions, the naming and doc-comment knobs

They are the library’s own options under their own names: a flag is the kebab-case of the field it writes, without exception. The Options reference gives each one with its field, its default and what it does.

Without a Go toolchain

genspec-wasi is the same scan with nothing behind it: it depends on nothing beyond the library, runs no subprocess, and cross-compiles to wasip1/wasm. Use it where genspec cannot go β€” a sandbox, a CI image with no Go in it, a WebAssembly runtime β€” and it is the engine behind the Playground.

go install github.com/go-openapi/codescan/cmd/genspec-wasi@latest
genspec-wasi -workdir ../my-api ./internal/api/...

It carries the same scan flags, but drops genspec’s document and diagnostics surface: no -validate, no -fail-on, no -color. In their place it offers an envelope.

An envelope for a program

Document on stdout and prose on stderr suits a person or a pipeline. It is not enough for a program: prose carries a position only in the sense that one is printed in it, and provenance β€” which Go construct produced this spec node β€” has nowhere to go at all. -format=json writes one object instead:

{
  "spec": { "swagger": "2.0", "definitions": { "doc": {} } },
  "diagnostics": [
    {"severity": "hint", "code": "validate.dropped-ref-sibling",
     "message": "field \"for\": description dropped …",
     "file": "models/m.go", "line": 12, "col": 5}
  ],
  "provenance": [
    {"pointer": "/definitions/doc", "file": "models/m.go", "line": 8, "col": 6}
  ],
  "runtime": { "sys": 390070272, "heapAlloc": 369098752, "collections": 2 }
}

Positions are relative to -workdir, so a caller holds models/pet.go and need not know what the guest called it; a position outside the module stays absolute, which is how a consumer tells the two apart. Provenance covers anchors β€” type declarations, fields, values, route and meta blocks β€” sorted by pointer, so the same scan produces the same bytes. Under -format=json standard error stays clean, so the envelope is safe to read from a pipe.

Experimental, inheriting the status of Options.OnProvenance.

Under a WASI runtime

GOOS=wasip1 GOARCH=wasm go build -o genspec-wasi.wasm ./cmd/genspec-wasi

# wasmtime β€” <host>::<guest>
wasmtime run --dir "$PWD::$PWD" genspec-wasi.wasm -workdir "$PWD" ./...

# wazero β€” <host>:<guest>, no separator
wazero run -mount="$PWD:$PWD" genspec-wasi.wasm -workdir "$PWD" ./...

Two things a guest cannot work out for itself:

  • -goos / -goarch must be passed. Left alone they default to the platform the scanner is running on, which inside a guest is wasip1 β€” which silently drops every _linux.go file and produces a different document than the same scan run natively.
  • GOROOT and the module cache are found by path. Nothing in a WASI environment can ask the go command where they live, so if the scan needs them they must be mounted and named through the environment.

How much of the host to expose

The real choice is how much of the host the guest may see. Measured on the petstore fixture under wasmtime:

mountedmodetimepeak RSSresult
GOROOT + module cachedefault7.3 s681 MBidentical to a go list scan
module cache-export-data1.0 s138 MBidentical
module cache-stub-stdlib1.0 s147 MBdegraded
project tree only-stub-stdlib0.1 s123 MBdegraded

Memory is usually the binding constraint rather than time β€” 681 MB for a fixture this small is more than a browser tab can host.

-export-data reads dependencies from the export data the compiler already produced instead of parsing and type-checking them. It costs a fraction of the time with no loss of fidelity, the types being the compiler’s own; it is valid only for the toolchain that produced it. -stub-stdlib fabricates standard-library types from the names the code selects, needs no GOROOT at all, and is the one option here that is not failsafe: recognition by identity survives (time.Time is still a date-time) but structure does not, so json.RawMessage stops rendering as a byte array and nothing is seen to implement encoding.TextMarshaler. Every synthesized import raises a scan.synthesized-import diagnostic, so the loss is never silent.

Full detail, including a build that embeds its own export data and needs nothing mounted but the project, is in the genspec-wasi README.

Next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Usage as a terminal UI

genspec-tui is an interactive terminal front-end for codescan.

It puts the Go source on the left, the Swagger document that source produces on the right, and the scanner’s diagnostics underneath β€” all regenerated every time you save.

Its reason to exist is that loop: change an annotation, save, see the spec change.

Predicting what an annotation will produce is the slow part of writing one, and reading a golden file after a build is a poor substitute for watching the node appear.

Beyond the loop, it links the two sides together. You can ask “which Go declaration produced this node?” or “what did this field turn into?” and get an answer by position β€” not by matching names by eye.

An open Go file on the left, the spec it produces on the right, and the scan’s diagnostics underneath An open Go file on the left, the spec it produces on the right, and the scan’s diagnostics underneath

Note

The TUI is a separate Go module inside the codescan repository, so bubbletea and its dependency tree never reach the library β€” installing it pulls none of that into your own project. Why the commands are split that way is in The commands.

Install and run

go install github.com/go-openapi/codescan/cmd/genspec-tui@latest
# scan the module in the current directory
genspec-tui

# or point it somewhere, and narrow the scope
genspec-tui -workdir ../my-api ./internal/models/... ./internal/api/...

The selected packages are a positional argument, resolved against -workdir; naming none scans ./....

-packages, taking one comma-separated list, is the older spelling and still works

The TUI registers the same flags as the other commands β€” one per field of codescan.Options β€” and reads the same .codescan.yaml, so a session starts where your build leaves off. The flags decide the first scan; almost all of them are also live toggles, below.

The ones worth knowing at the point of starting a session:

FlagDefaultMeaning
-workdir.module directory the scan runs in (WorkDir)
-scan-modelstruealso emit definitions for swagger:model types
-build-tagsβ€”comma-separated build tags to apply while loading
-include / -excludeβ€”patterns selecting which packages are scanned
-include-tags / -exclude-tagsβ€”swagger tags selecting which operations are emitted
-name-from-tagsjsonordered struct tags a field’s name derives from, e.g. form,json for gin. Pass -name-from-tags= (empty) to use the Go field name
-name-concat-budget0.65readability cutoff when deconflicting colliding definition names

Note that -scan-models defaults to on here, where the library’s ScanModels defaults to off. A spec you are browsing in order to see what your types became has little to show without them.

A second group settles what gets built, and how it is loaded β€” the environment go list would read, plus codescan’s own loader:

FlagDefaultMeaning
-goos / -goarchthis machine’sthe platform the scanned code is built for, so build-tagged files are selected the way that platform selects them
-goflagsβ€”default go command flags, as GOFLAGS β€” -build-tags wins over a -tags given here
-goworksearch upwardsworkspace selection, as GOWORK: off to ignore a go.work, or the path to one
-goexperimentβ€”toolchain experiments, as GOEXPERIMENT
-loaderautoauto runs go list, as every native build does, and picks own only where no subprocess can be started; go always runs go list; own always uses codescan’s own loader, which needs no toolchain (experimental)
-stub-stdlibfalsesynthesize the standard library instead of reading GOROOT (needs -loader=own)
-compiled-dependenciesfalsetake dependency types from the compiler’s export data instead of reading every dependency from source (needs -loader=go). Worth having here more than anywhere: a session rescans on every save, so the build cache is warm after the first one

Everything the flags set is also a live toggle. Press o for the options popup, space to flip a row, Esc to apply β€” the spec re-renders on close, which makes the popup the fastest way to find out what a knob such as EmitRefSiblings actually changes. Rows that only bite in combination say so: PruneUnusedModels reads (needs ScanModels) until that one is on.

Being in both places is the point: you can start a session one way and change your mind without restarting. The one option with no route in at all is InputSpec (overlay mode).

Scanning: the edit-save-see loop

The left pane starts as the source tree; Enter opens a file into the viewer, which is read-only and navigable. i turns it into an editor, Ctrl-S saves, a file watcher notices the write, and the spec re-renders. Editing outside the TUI works just as well β€” disk is the source of truth, and F5 re-reads the open file (asking first, if you have unsaved edits to lose).

A rescan keeps you where you were. The spec cursor is restored to the same node, not the same line number, so a definition that appears above what you are reading does not slide you somewhere else. If the node is gone β€” you deleted the type β€” the cursor falls back to its nearest surviving ancestor.

Both panes are syntax-highlighted by the same palette, and in the source viewer a comment gets three classes rather than one, because in a spec generator a comment is not uniformly commentary:

Looks likeReads asWhy
// swagger:model ordera spec keythe annotation declares the thing; it is the input that produced the pane opposite
// required: truea keywordgrammar the parser acts on, in the class Go’s own type and func get
// the id of the orderdimmed prosefreeform description

What lights up as a keyword comes from the parser’s own table, so aliases (min β†’ minimum) and letter case are free β€” and what is highlighted is what the parser will actually act on.

Tracking: from a spec node back to the code

Follow mode: a spec node highlighted beside the source line that produced it, with the badge naming the resolved target Follow mode: a spec node highlighted beside the source line that produced it, with the badge naming the resolved target

f turns on a persistent link between panes. The pane you pressed it in is the driver and keeps focus; the others mirror it on every cursor move, centring and highlighting the linked line. A SPEC β–Έ SOURCE badge names the direction and the target it resolved to.

It works in three directions β€” spec β†’ source, source β†’ spec, and a diagnostic to both. Esc, a second f, changing focus, or starting to edit all leave it.

Two indexes, rebuilt on every render, meet at a JSON pointer: one maps each rendered spec line to the pointer of the node on it, the other maps pointers back to Go source positions through codescan’s OnProvenance callback. That is why the answer is exact rather than a name match.

The gutter marks which lines actually lead somewhere, so you can see what is navigable without probing for it:

MarkerIn the spec paneIn the source viewer
β€’this node has a source position of its ownthis line produced a spec node
β†’a followable $ref; Enter goes to its definitionβ€”

Only exact anchors are marked. Nearly every line resolves to something through its nearest anchored ancestor, so marking those would dot the whole document and tell you nothing.

F3 steps through the places the node under the cursor is referenced, wrapping; shift+F3 goes back; Enter follows a $ref to its definition.

Diagnostics: what the scanner made of your annotations

The diagnostics pane under a severity tally, with the tokens the findings name underlined in the source viewer The diagnostics pane under a severity tally, with the tokens the findings name underlined in the source viewer

Whether your annotations were understood is a different question from whether the document they produced is well formed, and the pane at the bottom answers the first one. Under a one-line severity tally it lists everything the scan observed, in source order, each row carrying its severity’s colour so the pane can be read for red at a glance. Enter jumps to the source line a finding names and focuses it; f makes the selection drive both other panes at once.

Findings are also drawn at the site: the token a diagnostic names is underlined in the severity’s colour, in the source viewer itself. The pane tells you what and where; the underline tells you which token, without leaving the line you are reading.

Marks are re-derived on every rescan, so they never outlive the finding that produced them.

The validation tab of the diagnostics pane, listing what go-openapi/validate found, each by its path The validation tab of the diagnostics pane, listing what go-openapi/validate found, each by its path

v runs the generated document through go-openapi/validate and lists what it finds in a validation tab of the diagnostics pane. V switches between that tab and the scan’s own findings.

This is the second of the two questions above, and answering both is why they are tabs rather than one list: a scan can be perfectly clean and still produce something a consumer rejects.

They also track different things. A scan diagnostic carries a source position, so it drives the source pane. A validation finding carries only a JSON pointer, so Enter and f there drive the spec pane and nothing else.

The tab exists only once you have pressed v, and a rescan retires it: those findings judged a document that has just been replaced, and a list of complaints about a spec that no longer exists invites navigating to nodes that may have moved or gone. Press v again.

Where a finding lands

A finding carries the JSON pointer the validator recorded as it walked, so navigation is exact. Indexed paths included β€” /paths/~1pets/get/parameters/0/type lands on that parameter, not on the list β€” and so are faults reached through a $ref, which are reported against the shared definition that actually holds them.

A finding about something the document lacks is reported on the value that should hold it: a response missing its description lands on the response, and a document missing its info block lands on the document, which Enter takes you to the top of.

What a scan cost: m

m opens a card describing the run that just finished β€” how long it took, what it allocated, what it left live, and how much the process holds from the OS.

The run-cost card: a split line recapping time and memory as ratios, then elapsed, allocated, retained, live objects, GC cycles and memory held from the OS β€” each split between scanning and rendering The run-cost card: a split line recapping time and memory as ratios, then elapsed, allocated, retained, live objects, GC cycles and memory held from the OS β€” each split between scanning and rendering

Time and memory are split between scanning and rendering the document, and recapped as ratios on the split line, because the question a reader arrives with is usually which phase is this? The two can disagree sharply, which is why both are there.

Allocated counts everything the run churned through, garbage included; retained counts what it left behind. A scan that allocates half a gigabyte and retains a few megabytes is not the same problem as one that keeps what it takes, and one number could not tell you which you have.

Two things the card says about itself, worth repeating: the window is process-wide, so the redraw loop and the file watcher are in the figures; and a rescan holds two documents at once, so the retained figure reads high by about one. That is arithmetic, not a leak.

A scalar cannot say who spent it, which is what -profile is for. Start the session with it and each scan is profiled as well as timed, so the card also reports where the CPU went and what allocated it, by function and per phase:

genspec-tui -profile -workdir ../my-api

# every allocation counted rather than sampled every 512 KiB β€” accurate, and slow
genspec-tui -profile -mem-profile-rate=1 -workdir ../my-api

The profiled card: the same figures plus a cpu ratio, then a “where the CPU went” table charging each sample to the call of ours that led there, and a “what allocated it” table by function The profiled card: the same figures plus a cpu ratio, then a “where the CPU went” table charging each sample to the call of ours that led there, and a “what allocated it” table by function

CPU is charged to the call of ours that led there, not to the leaf frame. A leaf answers “what was executing”, which for this program is mostly the allocator and the collector β€” true, and nothing anyone can act on. Charged our way, a row names the boundary where codescan hands the work to somebody else’s code, and covers everything under it.

Profiling is not free, and the card says so rather than letting you compare across runs by accident: under -profile the scalars at the top carry the profiler’s own overhead and its collections, while the tables below exclude them. The card scrolls when it outgrows the terminal (↑↓/jk, PgUp/PgDn, Home/End).

It names the .pprof files it wrote and the go tool pprof commands that open them. All three flags are addressed in the profile section of a .codescan.yaml.

Looking up an annotation

The annotation reference popup: the annotation, what it does, its syntax, and the keywords its body accepts The annotation reference popup: the annotation, what it does, its syntax, and the keywords its body accepts

With the viewer’s navigation line on a comment carrying a swagger: directive, K shows what that annotation does: its syntax, a one-line summary, and what may be written in its body. Clicking the directive does the same.

K because it is vim’s “look up what is under the cursor”, and what LSP clients bind hover to. It reads the buffer, so it works on an annotation you are still typing β€” which is when you want it.

The popup covers all twenty swagger: annotations, and each entry names the family of keywords its body accepts rather than listing them: there are far too many individual keywords for a popup to be the right place for them. For those, and for worked examples of every annotation, the reference on this site is the long form:

  • Annotations β€” one page per annotation, with grammar and live examples
  • Keyword reference β€” every keyword, grouped by the family it belongs to

Keys worth knowing

The binding surface is context-dependent β€” f follows from three different panes, Enter opens a file in the tree but follows a $ref in the spec β€” so the header carries a standing h: help banner.

KeyAction
h / ?the full keymap, grouped by pane
Tab / clickfocus a pane (the wheel scrolls whichever pane is under the pointer)
ctrl+arrowsmove either divider, in the arrow’s own direction
ffollow mode
Kwhat the swagger: annotation on this line means
v / Vvalidate the spec / switch diagnostics tab
oscanner options
mwhat the last scan cost
r / F5rescan now / re-read the open file from disk
ccopy the focused pane to the clipboard
ctrl+qquit

Everything else β€” the per-pane bindings, the editor, the popups β€” is in the h overlay and in the module README, which documents the internals as well.

Worth knowing before you rely on it

These limits are deliberate; the TUI reports them rather than guessing.

The editor rewrites whitespace on save

bubbles/textarea expands tabs to four spaces when a file is loaded and treats a lone CR as a line break, and neither has an exported knob. Everything the TUI shows you agrees with itself because it all reads the same normalised text β€” but Ctrl-S writes the buffer, so saving re-indents a tab-indented file with spaces and rewrites CRLF endings as LF. Edit and save here only when you are content with that; otherwise edit in your own editor and let the watcher pick the change up.

  • Not every node has source. codescan anchors code-detail nodes β€” type declarations, fields, values, route and meta blocks β€” and finer nodes resolve to their nearest anchored ancestor. A node with no anchored ancestor at all was not produced from code (an InputSpec overlay node, say); the follower holds position and says so instead of jumping somewhere plausible.
  • Positions are as of the last scan. With unsaved edits in the buffer every anchor below the edit has shifted, so follow shows a STALE badge. Saving triggers a rescan and clears it.
  • $ref resolution is a site index, not a resolver. Local #/… refs are followable; a ref into another file or a URL is reported as external rather than chased.
  • Some keys are terminal-dependent. shift+F3 and ctrl+arrow rely on sequences most modern emulators send and a few (notably inside a default tmux) do not. Where they are missing nothing misfires β€” there is simply no previous-reference key, and no resize keys.
  • Split sizes last for the session only. They survive rescans and terminal resizes, but not a restart. The TUI does read a .codescan.yaml β€” but what a file can set is flags, and where the splits sit is not one of them.

The module README carries the full list.

What’s next

  • Usage as a library β€” drive the same scanner from your own program, a go:generate step, or a test.
  • Usage as a headless CLI β€” the same scan in a build, with the settings you converged on here.
  • Options reference β€” every knob the o popup toggles, and what it does to the document.
  • Tutorials β€” annotate a package from meta to definitions, with the TUI open beside you.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Playground

Everything below runs in this tab. There is no server: the scanner is codescan itself, compiled to WebAssembly.

The source you open never leaves your browser.

Experimental

Offered for demonstration. It follows genspec-tui closely, and its interface is checked by hand rather than by tests.

Room to work:

What to try

The Examples menu carries five modules, each one whole and each scanning as it stands:

shows
Modelsa struct becoming a definition: validations, an enum, an example, a $ref
Routesswagger:route with its parameters and responses
Operationswagger:operation, where you write the OpenAPI directly in YAML
Enumsa Go constant set becoming an enum, typed from the declaration
Polymorphisma discriminated base and its subtypes under swagger:allOf

Edit anything on the left and it rescans after a pause.

Track joins the two panes. Put the cursor on a Go line and the specification highlights what that line produced; put it on a spec line and the source highlights what produced it; click a diagnostic and both light up. It answers by position rather than by matching names, which is why it survives a rename.

One direction is exact and the other is not. The scanner records where a field starts and not where it ends, so a cursor sitting in a doc comment is attributed to the nearest anchor, ties resolving downwards because Go documentation sits above what it documents.

Press / in the specification to search it, n and N to step through matches. The Swagger UI tab renders the document as a reader of your API would see it.

Scanning your own code

Use Open module…. Three things are worth knowing first, and the second is the one nobody guesses:

  1. It has to be a module. go mod init example.com/api if there is no go.mod. Import paths resolve against the module, and without one there is nothing to resolve them against.

  2. Vendor the dependencies β€” go mod vendor. There is no module cache in a browser and nothing is downloaded, so a dependency’s types can only arrive as source. That matters more than it looks: a library that declares things in its comments, as strfmt does with swagger:strfmt, cannot be understood any other way. Open a module without vendoring and the playground says so before it scans.

  3. Pick the folder, not the files. The whole directory goes in at once. Test files are skipped, vendor/ is kept, and the tree is re-rooted on the outermost go.mod β€” so it does not matter whether you pick the module, its parent, or a directory inside it.

The status line reports what each scan cost, in time and in memory. Hovering it breaks the time into fetching the scanner, compiling it, preparing the filesystem and scanning, which is how to tell a slow first load from a slow scan. The scanner is fetched once and then cached, so later scans start immediately.

The same thing without a browser

genspec-wasi is the command this page is built around. It writes a specification to standard output, and -format=json wraps it with the diagnostics and cross-references that drive the two panes above.

go install github.com/go-openapi/codescan/cmd/genspec-wasi@latest
genspec-wasi -workdir ./my-api ./...

For an interactive scan in a terminal, with the same tracking and the same diagnostics, use genspec-tui.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Advanced Usage

codescan is a go library that ships with a few utility commands. These commands are thin wrappers around the library’s scanner.

The library’s behavior is tuned by fields on codescan.Options. The commands β€” genspec, genspec-tui and genspec-wasi β€” register one flag per field over that same struct.

So every available knob has three spellings for one meaning:

SpellingLooks like
A Go fieldopts.NameFromTags = []string{"form", "json"}
A command-line flaggenspec -name-from-tags form,json
A configuration keyemit:
Β Β name-from-tags: [form, json]

The mapping is mechanical: a flag is the kebab-case of the field it writes, without exception, and a configuration key is the flag, spelled exactly as on the command line, under the section that flag belongs to.

That is why genspec -h doubles as the reference for the file β€” and why the one reference below serves all three.

Note

The commands do not each declare that surface: it is written once, and a guard fails the build when an option lands with no flag β€” so a knob added to the library is reachable from all of them at the same moment. How that is arranged is in The commands.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Advanced Usage

Setting options

There are three ways to set the same knob β€” a Go field, a command-line flag, a key in a .codescan.yaml β€” where the file is looked for, what may be in it, and which spelling wins when they disagree.

As a Go field

Calling the library, an option is a field on codescan.Options, passed to Run. The zero value is a valid configuration β€” every boolean defaults to false, every slice to nil, every numeric tunable to its built-in default β€” so you set only what you need:

opts := &codescan.Options{
	WorkDir:    workDir,                // module root to resolve patterns from
	Packages:   []string{"./petstore"}, // relative package pattern
	ScanModels: true,                   // also emit definitions for swagger:model types
}

doc, err := codescan.Run(opts)
if err != nil {
	return nil, err
}

Full source: docs/examples/basic/scan.go

This is the only spelling that reaches everything: a few options are not values a command line can carry β€” a filesystem to read, a document to merge into, a callback to receive diagnostics. See What has no flag.

As a flag

This is available for every CLI tool shipped: genspec, genspec-tui, genspec-wasi. The library itself is not bound to command line flags.

Every value-typed option is a flag, on every command:

genspec -name-from-tags form,json -prune-unused-models ./internal/api/...

A flag is the kebab-case of the field it writes, without exception. SkipJSONifyInterfaceMethods is -skip-jsonify-interface-methods; no shorter spelling is offered, because guessing one is how a caller ends up guessing wrong. genspec -h lists them all, and the Options reference gives the flag beside the field for each.

Two shapes do not follow from a field name:

  • the packages to scan are positional arguments β€” genspec ./api/... β€” so that the command reads like the go commands it resolves patterns for. Naming none scans ./...;
  • -loader takes go, own or auto where the field (ToolchainFreeLoader) is a boolean: the useful default is the third answer, “whichever one can run here”. On a native build that is go list, so a stock run loads exactly as it always did; auto picks own only where no subprocess can be started. That is what lets the same source build for a WASI guest, which has no process model and so can never run go list.

Commands may add some extra flags that do not affect the scanning itself.

  • genspec adds its own -output, -validate or -fail-on (these are not library options)
  • genspec-tui adds a unique -profile flag and related flags to store profiling data
  • genspec-wasi adds a unique -export-data flag to preload a compiled go standard library

Those are documented with the command that owns them.

As a configuration key

Anything that can be a flag can be preset in a .codescan.yaml, so a project configures itself once and the command is run bare.

This is available for genspec and genspec-tui β€” not, for now, for genspec-wasi, which reads its whole configuration from the arguments its host hands it. The library itself is not bound to config files.

scan:
  exclude-tags: [internal]

emit:
  scan-models: true
  name-from-tags: [form, json]

document:
  format: yaml
  compact: true

diagnostics:
  validate: true
  fail-on: warning

Keys are grouped into sections, and inside a section a key is the flag it sets, spelled exactly as on the command line. There is no second vocabulary to learn and no mapping table to keep in step: genspec -h is the reference for the file.

What a file may not set

The options naming a path are settable on the command line only: -workdir, genspec’s -output and -input, and genspec-tui’s -profile-dir.

A file is found by searching upwards, so running a command inside a repository reads that repository’s file β€” and a tool whose job is reading somebody else’s code must not let the code decide where it reads or writes. Everything else a file sets shapes the document, which is what one is for. Naming a file with -config does not lift the restriction: the rule belongs to the option, so there is nothing to remember at the point of use.

The sections

The library’s four are the questions a scan answers, in the order it answers them. Each command adds its own for the flags that are its business:

SectionThe questionDeclared by
scanwhich code is looked atthe library β€” every command
gowhat it is built as: the go environment that decides what compilesthe library β€” every command
loadhow the packages are readthe library β€” every command
emitwhat the specification ends up sayingthe library β€” every command
documenthow the specification is rendered: format, compactgenspec
diagnosticshow loud it is about what it saw: color, quiet, verbose, validate, fail-ongenspec
profilewhether a run is profiled: profile, mem-profile-rategenspec-tui

Where the file is found

The search walks upwards from where the command was run, so running it from anywhere inside a project finds the project’s file. It stops at the first hit rather than merging what it passed: a file half-overridden by another three directories up is not something anyone can read off the page.

.codescan.yaml, .codescan.yml and .codescan.json are looked for in that order. JSON is a subset of YAML, so it needs no parser of its own β€” and a generated file is as likely to be JSON.

FlagEffect
-config <path>, -c <path>read this file, which must exist β€” a caller who named one meant that file
--no-configread none, whatever is lying around, for a run that has to be reproducible

Asking for both at once is an error rather than a coin toss. So is -config and -c naming different files.

Note

The search starts from the current directory, not from -workdir. A file found through the very directory it was meant to describe would be reasoning in a circle β€” which is one of the reasons -workdir is not something a file sets.

Which spelling wins

Flags win over config. That holds for a flag typed with the value it already had:

# false, even where the file says scan-models: true
genspec -scan-models=false

It is decided by asking the flag set which flags were actually seen, not by comparing values against defaults β€” which is what makes the rule statable in one sentence instead of one per flag.

Everything the file sets lands through the same path a command-line argument takes, so a value is parsed and validated exactly once, and a file cannot express anything an argument could not. A malformed value is refused the same way, naming the file.

Between the two, the ordinary defaults apply β€” with one deliberate divergence: the commands default -scan-models to true where the library field is false. A command asked to produce a specification and handed a package of annotated models should produce their definitions.

One file, several commands

There is one file name for the whole family, not one per command: a project configuring a scan has configured it for all of them. What tells them apart is the sections.

  • A section a command does not recognize is skipped, not rejected β€” that is how genspec-tui’s profile: settings sit beside genspec’s document: in the same file.
  • A key inside a section it does know must name one of its flags. That is what makes a typo an error rather than a setting that quietly never applied.

Run genspec -verbose to see which file was read and which keys were skipped.

What has no flag

Four options are not values a command line can carry.

The command owns them instead β€” and each of the commands makes its own choice about how, which is why they are not in the shared table:

OptionReached by
InputSpecgenspec -input <file> (document.input) β€” the command loads the document
FSno flag: a filesystem is a Go value. The commands read the real one; the Playground hands the browser’s
ExportDatagenspec-wasi -export-data <dir|zip> β€” the command opens the path. This option is specific to the wasi CLI for now
OnDiagnostic, OnProvenanceno flag: the commands wire the sinks. genspec prints diagnostics to standard error; genspec-wasi -format=json puts both in its envelope. Custom hooks are only available to the library.

Two more fields carry no flag because nothing should be reaching for them: DescWithRef and Debug are deprecated.

Next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Options reference

codescan.Options is the single configuration struct passed to codescan.Run.

The zero value is a valid configuration β€” every flag defaults to false, every slice/map to nil, every numeric tunable to its built-in default. You set only what you need.

This page is the field-by-field catalogue, and serves the commands as much as the library: Flag gives the text to type, and Section gives the .codescan.yaml section addressing it β€” the key inside that section is the flag without its dash. See Setting an option for the rules those two columns follow, and for the handful of options that are nobody’s flag.

The godoc is the normative source; each field here links to the how-to guide that shows it on real input where one exists.

Note

Default is the library’s. The commands agree with it everywhere but one: -scan-models defaults to true, because a command asked for a specification and handed a package of annotated models should produce their definitions.

Note

Config Section names the .codescan.yaml section addressing the option. A dash means it has none: either the option is not a value a file can carry (a callback, a positional argument), or it names a path, which is settable on the command line only. See what a file may not set.

Note

codescan never writes to stdout or stderr. Every scan-time observation β€” a dropped construct, a rename, a prune β€” flows through the OnDiagnostic callback. See Diagnostics & observability below.

Inputs & scope

What gets loaded and which packages and types are in play. See Scope & discovery.

OptionTypeDefaultFlagConfig SectionEffect
Packages[]stringnil(positional)β€”Package patterns to scan (e.g. ./...), resolved relative to WorkDir.
WorkDirstring"" (cwd)-workdirβ€”Working directory the package patterns and module resolution are rooted at. Command line only: see what a file may not set.
BuildTagsstring""-build-tagsscanGo build tags to activate while loading, so tag-guarded source is scanned. See Build tags.
Include[]stringnil-includescanAllow-list of package path patterns; when non-empty only matching packages are scanned. See Scoping the scan.
Exclude[]stringnil-excludescanDeny-list of package path patterns, applied after Include. See Scoping the scan.
IncludeTags[]stringnil-include-tagsscanAllow-list filtering routes/operations by their swagger tags.
ExcludeTags[]stringnil-exclude-tagsscanDeny-list filtering routes/operations by their swagger tags.
ExcludeDepsboolfalse-exclude-depsscanSkip types reached through module dependencies, keeping the scan to first-party packages.
ScanModelsboolfalse-scan-modelsemitAlso emit a definition for every swagger:model type, not just route-reachable ones. See When the scanner emits a type.
PruneUnusedModelsboolfalse-prune-unused-modelsemitWith ScanModels, drop discovered definitions not transitively reachable from a path, shared response/parameter, or InputSpec root. Runs before name reduction; InputSpec definitions are pinned. No-op without ScanModels. See Pruning unused models.
InputSpec*spec.Swaggernil-input (genspec)documentBase document to overlay scanned discoveries onto; its definitions are pinned and seed pruning roots. See Overlaying a spec.

Loading & the go environment

Where the package graph comes from, and which platform it is built for. These change the emitted spec the way BuildTags does β€” by deciding which files each package is made of β€” or change what the scan needs in order to run at all.

They are options rather than inherited process state so that a scan is reproducible: a value picked up from whatever shell started it is easy to apply on one code path and forget on another.

Which loader, and why

Three ways to get a package graph. The table below catalogues the fields; this is how to choose between them.

Standard loader β€” the default. Loads your code with the Go toolchain, through golang.org/x/tools/go/packages. Maintained by the Go team, and the reference for how patterns and imports resolve: where either of the others disagrees with it, the other one is wrong. Requires Go installed, and uses the build cache.

Pure-Go loader (ToolchainFreeLoader) β€” loads your code with codescan’s own reimplementation. Cuts memory by roughly 45%, and needs no go command and no subprocess. It still reads GOROOT/src for the standard library, so it wants a Go installation β€” just not a runnable toolchain. Modules only. It uses no build cache, so cold costs what warm costs: about level with the standard loader on a warm cache, roughly 30% faster on a cold one, and the only choice whose cost does not depend on cache state. Usually the right pick for CI.

Compiled dependencies (CompiledDependencies) β€” the standard loader taking dependency types from the compiler’s export data instead of reading their source. It produces the same document either way β€” whatever the spec needs out of a dependency is read at the moment it is needed β€” and on a warm build cache it is the fastest by a wide margin, and several times smaller.

It must compile the dependency closure rather than type-check it, so on a cold cache it is an order of magnitude slower and writes a large build cache. Reach for it where the cache is warm by construction β€” your own machine, a watch loop, a pipeline that restores its cache β€” and leave it off where a clean checkout is the norm, as a CI runner usually is. Code that does not compile is not a reason to avoid it: such a load is retried from source automatically.

Two further options drop the GOROOT requirement altogether, for environments with no Go installation at all β€” a WASI guest, a browser. StubStdlib synthesizes the standard library, and pays for the reach in fidelity: a fabricated type has the right name and no structure. ExportData serves dependencies from a blob you prepare in advance, and pays in preparation instead β€” the types are the compiler’s own, but the blob is only valid for the toolchain that produced it, and a package it does not cover falls back to source and then to synthesis.

Note

The percentages are indicative, not a promise: the balance moves with the size of the tree being scanned, and on a small one the pure-Go loader is slower warm than the standard loader. The figures, the corpora they were taken on and the method are in internal/benchmarks, whose harness also takes an extra corpus of your own to measure alongside them.

OptionTypeDefaultFlagSectionEffect
GOOS / GOARCHstring"" (this machine)-goos / -goarchgoThe platform the scanned code is built for. //go:build lines and _linux.go / _amd64.go filename suffixes resolve against them, so they select which files a package is made of.
GOFLAGSstring"" (process env)-goflagsgoDefault go command flags, e.g. -tags=integration. Flags given through BuildTags win, as they do for the go command.
GOWORKstring"" (search upwards)-goworkgoWorkspace selection: off disables it, a path names a go.work. Inside a workspace a sibling module resolves to the copy being worked on rather than to the module cache β€” miss that and its types are read stale, or synthesized empty.
GOEXPERIMENTstring"" (process env)-goexperimentgoToolchain experiments, e.g. jsonv2; each contributes a goexperiment.<name> build tag.
ToolchainFreeLoaderboolfalse-loader=ownloadResolve the package graph with codescan’s own loader instead of golang.org/x/tools/go/packages. Same job and, across the fixture corpus, the same spec; it differs in needing no installed toolchain and no subprocess, since it never runs go list. Experimental.
FSfs.FSnilβ€”β€”Read source through a virtual filesystem β€” an in-memory tree, an uploaded archive, an embed.FS β€” instead of the real one. Implies ToolchainFreeLoader, since go list can only read the real filesystem. FS is the whole world the scan can read: dependencies and GOROOT come through it too, absolute paths map by dropping the leading separator, and anything unreachable is synthesized β€” a valid but quietly thinner spec, announced by scan.synthesized-import and scan.degraded-load. Experimental.
StubStdlibboolfalse-stub-stdlibloadSynthesize the standard library from the names the code selects, rather than reading GOROOT. Toolchain-free loader only. Identity recognition still works (time.Time, json.RawMessage are matched on package and name), but a synthesized type has no fields and no method set β€” so json.RawMessage stops rendering as a byte array and nothing is seen to implement encoding.TextMarshaler. Trades fidelity for reach, quietly; prefer a full graph where GOROOT is available. Experimental.
ExportDatafs.FSnil-export-data (genspec-wasi)β€”Serve dependencies from pre-computed export data (one <import path>.export file per package) under the toolchain-free loader. Unlike StubStdlib this costs no fidelity, the types being the ones the compiler computed β€” but it is valid only for the toolchain that produced it, and an uncovered package falls back to source, then to synthesis. The module under scan is never read this way, and neither is a dependency whose source carries annotations or one the spec later needs a declaration from. Experimental.
CompiledDependenciesboolfalse-compiled-dependenciesloadTake dependency types from the compiler’s export data instead of reading every dependency from source, under the go/packages loader. It costs no meaning: a dependency whose source carries annotations is read back after the load, and one that merely declares a type the spec carries is read at the lookup that wants it β€” so a swagger:strfmt written in a library still counts, and a model declared in an unannotated dependency keeps its doc comment and its fields. Set it for cost alone, and only where the build cache is warm: it is markedly faster warm and markedly slower cold, since go list -export compiles the closure before it can read it. A closure that does not compile is handled either way β€” the load falls back to source and raises scan.compiled-dependencies.
Note

The virtual-filesystem and export-data options exist to make a scan possible where no Go toolchain is present: they let codescan run compiled to WebAssembly. See the Playground.

Names & references

How definitions are named and how $refs render. See Names & $refs.

OptionTypeDefaultFlagSectionEffect
NameFromTags[]stringnil (β‡’ ["json"])-name-from-tagsemitOrdered struct-tag types a property/parameter/header name is derived from; first that supplies a name wins. Explicit empty slice β‡’ Go field name. Only the name β€” json encoding directives (-, ,omitempty, ,string) always come from json. See Naming from struct tags.
SkipJSONifyInterfaceMethodsboolfalse-skip-jsonify-interface-methodsemitEmit interface-method property names verbatim (ID, CreatedAt) instead of auto-jsonifying them (id, createdAt). Only affects interface methods; struct fields and swagger:name overrides are unchanged. See Interface-method property names.
RefAliasesboolfalse-ref-aliasesemitRender Go type aliases as a first-class $ref (via swagger:model) instead of expanding them inline. See Alias rendering.
TransparentAliasesboolfalse-transparent-aliasesemitMake aliases fully transparent β€” never creating a definition. See Alias rendering.
DefaultAllOfForEmbedsboolfalse-default-all-of-for-embedsemitRender a plain (untagged, unnamed) struct embed as an allOf member β€” a $ref for a model embed, an inline member otherwise β€” with the embedding struct’s own fields in a sibling member, instead of inlining promoted properties. json-named embeds, swagger:allOf embeds, and interface embeds are unaffected. See Composing embeds with allOf.
NameConcatBudgetfloat640 (β‡’ 0.65)-name-concat-budgetemitReadability cutoff [0,1] for the package-segment concatenation that deconflicts colliding definition names; lower scores are more readable. A group whose best concat scores above the budget is a candidate for the hierarchical fallback. See Resolving $ref name conflicts.
EmitHierarchicalNamesboolfalse-emit-hierarchical-namesemitFor the rare collision group whose best flat concat exceeds NameConcatBudget, emit nested container definitions (#/definitions/<pkg>/<Name>) instead of a long flat concat, with an explanatory diagnostic. The always-correct flat concat is the default. See Resolving $ref name conflicts.
EmitRefSiblingsboolfalse-emit-ref-siblingsemitEmit a $ref’d field’s description and vendor extensions as direct $ref siblings ({$ref, description, x-*}) instead of an allOf wrap. Validations/externalDocs still force a compound. See Descriptions beside a $ref.
SkipAllOfCompoundingboolfalse-skip-all-of-compoundingemitNever emit an allOf compound for a $ref’d field. Validations/externalDocs are dropped (description/extensions too, unless EmitRefSiblings keeps them as siblings); each drop raises a diagnostic. required is unaffected. See Descriptions beside a $ref.
DescWithRefboolfalseβ€”β€”Deprecated β€” prefer EmitRefSiblings. In the description-only case, wrap the $ref in a single-arm allOf to preserve the description (strict draft-4 shape). No-op when EmitRefSiblings is set. See Descriptions beside a $ref.

Titles & descriptions

The human-readable text the spec carries. See Titles & descriptions.

OptionTypeDefaultFlagSectionEffect
SingleLineCommentAsDescriptionboolfalse-single-line-comment-as-descriptionemitRoute every single-line doc comment to description, never to title/summary (the first-sentence convention otherwise applies). Multi-line comments keep the title/description split. See Single-line comments as descriptions.
AfterDeclCommentsboolfalse-after-decl-commentsemitLet swagger annotations live inside a struct body or as a trailing comment, in addition to the doc comment above the declaration, so the godoc stays clean. v0.36 scope: type declarations (struct inside-body + alias trailing comment). See Keeping annotations out of the godoc.
CleanGoDocboolfalse-clean-go-docemitStrip godoc doc-link brackets from generated title/description (humanizing unresolved ones, dropping reference-definition lines, recomposing resolved links to each schema’s exposed name). Applies only to godoc-derived prose; overrides are untouched. See Cleaning godoc doc-links.

Field types, formats & extensions

How an individual property renders. See Field types & formats.

OptionTypeDefaultFlagSectionEffect
SetXNullableForPointersboolfalse-set-x-nullable-for-pointersemitEmit x-nullable: true on pointer-typed fields. See Nullable pointers.
SkipExtensionsboolfalse-skip-extensionsemitSuppress all x-go-* vendor extensions in the output. See Vendor extensions.
EmitXGoTypeboolfalse-emit-x-go-typeemitStamp an x-go-type extension (fully-qualified originating Go type) on every emitted definition, for round-tripping a spec back to its Go types. Suppressed under SkipExtensions. See Vendor extensions.
SkipEnumDescriptionsboolfalse-skip-enum-descriptionsemitKeep the per-enum-value const-name mapping (from swagger:enum) out of the description, exposing it only via the x-go-enum-desc extension. Suppressed entirely under SkipExtensions.

Diagnostics & observability

Channels for what the scan observed; these do not change the output spec.

OptionTypeDefaultFlagSectionEffect
OnDiagnosticfunc(Diagnostic)nilβ€”β€”Invoked once per diagnostic in source order (parser warnings, validation failures, prunes, renames). Diagnostics never block the build β€” invalid constructs are dropped from the spec while their explanation flows here. The only output channel. Experimental while LSP integration matures.
OnProvenancefunc(Provenance)nilβ€”β€”Invoked once per anchor node in the produced spec, carrying its JSON pointer and the source position of the Go construct that produced it. Never blocks the build. Experimental while LSP/TUI integration matures.
Debugboolfalseβ€”β€”Deprecated, ignored. The legacy stderr debug logger was retired; wire OnDiagnostic instead. Retained for API compatibility.

The two callbacks are how the commands report: genspec renders diagnostics on standard error, and genspec-wasi -format=json carries both in its envelope for a program to read.

See also

  • Setting an option β€” the flag and key rules the two middle columns follow, and which spelling wins.
  • Annotations β€” the swagger:* vocabulary the scanner reads from comments.
  • Keyword reference β€” the keyword: value forms inside annotation bodies.
  • Shaping the output β€” task-oriented how-tos that put these options to work on real input.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Project

Everything else on this site is about using codescan. This section is about the repository it comes from β€” who publishes it, under what licence, and where to send a patch.

  • README β€” repo overview and announcements
  • License β€” Apache-2.0

codescan follows the conventions of every go-openapi repository rather than inventing its own, so how to contribute to it is documented once, for all of them, in the shared doc-site:

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Project

README

codescan

A Go source code scanner that produces Swagger 2.0 (OpenAPI 2.0) specifications from annotated Go source files.

Supports Go modules (since go1.11).

Announcements

  • 2025-04-19: large package layout reshuffle
    • the entire project is being refactored to restore a reasonable level of maintainability
    • the only exposed API is Run() and Options.

Status

API is stable.

Import this library in your project

go get github.com/go-openapi/codescan

Basic usage

import (
  "github.com/go-openapi/codescan"
)

swaggerSpec, err := codescan.Run(&codescan.Options{
  Packages: []string{"./..."},
})

See getting started for a worked example.

Change log

See https://github.com/go-openapi/codescan/releases.

Licensing

This library ships under the Apache-2.0 license.

See the license NOTICE, which recalls the licensing terms of all the pieces of software on top of which it has been built.

Other documentation

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

LICENSE

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Tutorials

These tutorials teach codescan by spec concept, not annotation by annotation.

Each page takes one thing you want in your OpenAPI document β€” a model definition, a route, a validated field β€” and shows the Go annotation that produces it next to the resulting JSON, side by side.

Every Go snippet on these pages comes from the test-covered docs/examples module, and every JSON pane is a golden file a test regenerates β€” so what you see published here has been tested.

Reading the panes

The example panes put the annotation in on the left and the spec concept out on the right:

Annotated Go
// Pet is a single pet in the store.
//
// swagger:model Pet
type Pet struct {
	// The id of the pet.
	//
	// required: true
	// minimum: 1
	ID int64 `json:"id"`

	// The name of the pet.
	//
	// required: true
	// min length: 1
	Name string `json:"name"`

	// The tags associated with this pet.
	Tags []string `json:"tags,omitempty"`
}

Full source: docs/examples/petstore/pet.go

Generated spec
{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A tiny pet store, used to demonstrate codescan annotations: the package\ncomment is a `swagger:meta` block carrying the top-level metadata of the\ngenerated specification (title, description, version, base path, …).",
    "title": "Petstore API",
    "version": "1.0.0"
  },
  "basePath": "/v1",
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          "pets"
        ],
        "summary": "Lists all the pets in the store.",
        "operationId": "listPets",
        "responses": {
          "200": {
            "$ref": "#/responses/petsResponse"
          }
        }
      }
    }
  },
  "definitions": {
    "Pet": {
      "type": "object",
      "title": "Pet is a single pet in the store.",
      "required": [
        "id",
        "name"
      ],
      "properties": {
        "id": {
          "description": "The id of the pet.",
          "type": "integer",
          "format": "int64",
          "minimum": 1,
          "x-go-name": "ID"
        },
        "name": {
          "description": "The name of the pet.",
          "type": "string",
          "minLength": 1,
          "x-go-name": "Name"
        },
        "tags": {
          "description": "The tags associated with this pet.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "x-go-name": "Tags"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/petstore"
    }
  },
  "responses": {
    "petsResponse": {
      "description": "petsResponse is the list of pets returned by listPets.",
      "schema": {
        "type": "array",
        "items": {
          "$ref": "#/definitions/Pet"
        }
      }
    }
  }
}

Full source: docs/examples/basic/testdata/swagger.json

The concepts

  • The smallest end-to-end use of codescan: annotate a package, scan it, and get back a Swagger 2.0 document.
  • Turn Go types into spec definitions β€” structs, string formats, enums, allOf composition, and the per-type overrides.
  • Publish a Go const block as a spec enum β€” any constant expression, the type and format taken from the declaration, and the same members inline on parameters and headers.
  • How Go maps render as objects, which key types survive, and how to control a schema’s open/closed/typed extra keys with additionalProperties and patternProperties.
  • Model a Swagger 2.0 type hierarchy β€” a base type with a discriminator and subtypes that compose it with swagger:allOf.
  • Publish paths and operations β€” swagger:route and swagger:operation β€” with their parameters and responses.
  • Declare a parameter or response once and reuse it across operations through the spec-level shared namespace, with the wildcard swagger:parameters and swagger:response forms.
  • Drive JSON-Schema validations from field doc comments β€” numeric ranges, length and array bounds, patterns, formats, and enums β€” and understand the reduced surface on parameters and headers.
  • Attach example values and defaults to properties β€” and understand the narrow swagger:default hint.
  • Set the top-level spec fields β€” title, version, host, basePath, schemes, consumes/produces, license and contact β€” from the package doc comment.
  • Declare security schemes in swagger:meta, require them per route, or keep security out of your code entirely and overlay it onto the spec.
  • The smallest end-to-end use of codescan: annotate a package, scan it, and get back a Swagger 2.0 document.

When you want the exhaustive rule rather than an example, every page links into the Maintainers reference; the Annotation index maps every annotation to both.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Tutorials

Scan a package

This tutorial scans a tiny annotated “petstore” package and produces a Swagger 2.0 spec. It is the worked version of Usage as a library β€” start here, then take one concept at a time from the pages that follow.

The annotated API

A package-level swagger:meta block sets the top-level metadata:

// Package petstore Petstore API
//
// A tiny pet store, used to demonstrate codescan annotations: the package
// comment is a `swagger:meta` block carrying the top-level metadata of the
// generated specification (title, description, version, base path, …).
//
//	Schemes: https
//	Version: 1.0.0
//	BasePath: /v1
//
//	Consumes:
//	- application/json
//
//	Produces:
//	- application/json
//
// swagger:meta

Full source: docs/examples/petstore/doc.go

A swagger:route registers a path and ties it to a response:

// swagger:route GET /pets pets listPets
//
// Lists all the pets in the store.
//
// responses:
//
//	200: petsResponse

Full source: docs/examples/petstore/pet.go

A swagger:model struct becomes a definition, with field comments driving validations:

// Pet is a single pet in the store.
//
// swagger:model Pet
type Pet struct {
	// The id of the pet.
	//
	// required: true
	// minimum: 1
	ID int64 `json:"id"`

	// The name of the pet.
	//
	// required: true
	// min length: 1
	Name string `json:"name"`

	// The tags associated with this pet.
	Tags []string `json:"tags,omitempty"`
}

Full source: docs/examples/petstore/pet.go

Running the scan

ScanPetstore builds the Options and calls codescan.Run:

opts := &codescan.Options{
	WorkDir:    workDir,                // module root to resolve patterns from
	Packages:   []string{"./petstore"}, // relative package pattern
	ScanModels: true,                   // also emit definitions for swagger:model types
}

doc, err := codescan.Run(opts)
if err != nil {
	return nil, err
}

Full source: docs/examples/basic/scan.go

The generated spec

Marshalling the returned *spec.Swagger to JSON yields the document below β€” the meta block became the top-level info / basePath, the swagger:route became the /pets path, and the swagger:model became the Pet definition:

{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A tiny pet store, used to demonstrate codescan annotations: the package\ncomment is a `swagger:meta` block carrying the top-level metadata of the\ngenerated specification (title, description, version, base path, …).",
    "title": "Petstore API",
    "version": "1.0.0"
  },
  "basePath": "/v1",
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          "pets"
        ],
        "summary": "Lists all the pets in the store.",
        "operationId": "listPets",
        "responses": {
          "200": {
            "$ref": "#/responses/petsResponse"
          }
        }
      }
    }
  },
  "definitions": {
    "Pet": {
      "type": "object",
      "title": "Pet is a single pet in the store.",
      "required": [
        "id",
        "name"
      ],
      "properties": {
        "id": {
          "description": "The id of the pet.",
          "type": "integer",
          "format": "int64",
          "minimum": 1,
          "x-go-name": "ID"
        },
        "name": {
          "description": "The name of the pet.",
          "type": "string",
          "minLength": 1,
          "x-go-name": "Name"
        },
        "tags": {
          "description": "The tags associated with this pet.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "x-go-name": "Tags"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/petstore"
    }
  },
  "responses": {
    "petsResponse": {
      "description": "petsResponse is the list of pets returned by listPets.",
      "schema": {
        "type": "array",
        "items": {
          "$ref": "#/definitions/Pet"
        }
      }
    }
  }
}

Full source: docs/examples/basic/testdata/swagger.json

This JSON is not hand-written: it is a golden file the example’s test regenerates and compares on every run (UPDATE_GOLDEN=1 go test ./...). Because the example is ordinary, test-covered Go, go test ./docs/examples/... keeps the page honest β€” if the scanner’s output changes, CI fails before the documentation can go stale.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Model definitions

A definitions entry is the most common thing you ask codescan to produce. This page walks the annotations that create and shape one, from the plain swagger:model struct to the per-type overrides. Each pane pairs the annotated Go (left) with the exact fragment the scanner emits (right) β€” both come from the test-covered docs/examples/concepts/models package.

For the exhaustive rule on any annotation below, follow its link to the Maintainers reference.

swagger:model

swagger:model publishes a Go struct as a definition. Field doc comments become property descriptions; json tags drive the property names; the Go type drives the JSON-Schema type / format. Well-known standard-library types resolve automatically β€” a time.Time field, for instance, is published as {type: string, format: date-time}.

UUIDs are recognised two different ways, and both publish {type: string, format: uuid}:

  • by type identity β€” the standard-library uuid.UUID introduced in Go 1.27, matched on its import path and name, so nothing else can be mistaken for it;
  • by type name β€” any other type named UUID (case-insensitively) that marshals as text, which covers github.com/google/uuid, gofrs/uuid, strfmt.UUID and the like.

The name-based rule is a heuristic and stays available whichever Go version you build with. Either way an explicit swagger:strfmt on the type wins, so you can always overrule the recognition.

Annotated Go
// Pet is a single pet in the store.
//
// swagger:model
type Pet struct {
	// ID is the unique identifier.
	ID int64 `json:"id"`

	// Name is the pet's display name.
	Name string `json:"name"`

	// Tags categorise the pet.
	Tags []string `json:"tags,omitempty"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Pet
{
  "type": "object",
  "title": "Pet is a single pet in the store.",
  "properties": {
    "id": {
      "description": "ID is the unique identifier.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "ID"
    },
    "name": {
      "description": "Name is the pet's display name.",
      "type": "string",
      "x-go-name": "Name"
    },
    "tags": {
      "description": "Tags categorise the pet.",
      "type": "array",
      "items": {
        "type": "string"
      },
      "x-go-name": "Tags"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/model.json

A swagger:model that nothing else references appears in definitions only when you scan with Options.ScanModels (the -m flag). See When the scanner emits a type.

swagger:strfmt

swagger:strfmt <name> marks a named string type as a custom format. The type does not become its own definition β€” instead, every field typed by it renders as {type: string, format: <name>}.

Annotated Go
// MAC is a hardware address rendered as a colon-separated hex string.
//
// swagger:strfmt mac
type MAC string

func (m MAC) MarshalText() ([]byte, error)  { return []byte(m), nil }
func (m *MAC) UnmarshalText(b []byte) error { *m = MAC(b); return nil }

// Device exposes a strfmt-typed field: wherever MAC appears it renders inline
// as {type: string, format: mac}.
//
// swagger:model
type Device struct {
	// Addr is the hardware address.
	Addr MAC `json:"addr"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Device
{
  "description": "Device exposes a strfmt-typed field: wherever MAC appears it renders inline\nas {type: string, format: mac}.",
  "type": "object",
  "properties": {
    "addr": {
      "description": "Addr is the hardware address.",
      "type": "string",
      "format": "mac",
      "x-go-name": "Addr"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/strfmt.json

Add swagger:model to the strfmt type to opt it into a first-class definition ({type: string, format: <name>}) that fields $ref instead of inlining β€” the general swagger:model β‡’ definition + $ref rule.

swagger:enum

swagger:enum <name> collects the type’s const values. When a model field references the type, the property carries the enum array and an x-go-enum-desc extension built from the per-value doc comments. (The enum type is reachable, and so emitted, only because a model field points at it.) A bare swagger:enum on the type declaration works too β€” the name is inferred.

Annotated Go
// Priority is the urgency level on a task.
//
// swagger:enum Priority
type Priority string

const (
	// PriorityLow is for tasks that can wait.
	PriorityLow Priority = "low"
	// PriorityMedium is the default.
	PriorityMedium Priority = "medium"
	// PriorityHigh is for tasks that must run soon.
	PriorityHigh Priority = "high"
)

// Task is a unit of work carrying an enum-typed field. Referencing Priority
// from a model is what makes the enum reachable, and so emitted.
//
// swagger:model
type Task struct {
	// Priority is the task's urgency.
	Priority Priority `json:"priority"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Task
{
  "description": "Task is a unit of work carrying an enum-typed field. Referencing Priority\nfrom a model is what makes the enum reachable, and so emitted.",
  "type": "object",
  "properties": {
    "priority": {
      "description": "Priority is the task's urgency.\nlow PriorityLow is for tasks that can wait.\nmedium PriorityMedium is the default.\nhigh PriorityHigh is for tasks that must run soon.",
      "type": "string",
      "enum": [
        "low",
        "medium",
        "high"
      ],
      "x-go-enum-desc": "low PriorityLow is for tasks that can wait.\nmedium PriorityMedium is the default.\nhigh PriorityHigh is for tasks that must run soon.",
      "x-go-name": "Priority"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/enum.json

Here the values inline on the referencing field. Add swagger:model to the enum type to make it a first-class definition (carrying the enum array) that fields $ref β€” again the swagger:model β‡’ definition + $ref rule.

Enums have more to them than a string const block: iota and computed members, what decides the emitted type / format, and the inline form parameters and headers take. They get their own page β€” Enumerations.

swagger:allOf

Embedding base types under swagger:allOf composes a schema. Each embedded base becomes a $ref arm of the allOf; the struct’s own (non-embedded) fields form a final inline arm. That last arm is inline rather than a $ref because those fields are new to this type β€” they belong to no existing definition to point at. Here Dog embeds two bases (Animal, Tagged) and adds breed, producing three arms: two $refs and one inline object.

Annotated Go
// Animal is one abstract base.
//
// swagger:model
type Animal struct {
	// Kind discriminates the animal.
	Kind string `json:"kind"`
}

// Tagged is a second reusable base.
//
// swagger:model
type Tagged struct {
	// Tags label the resource.
	Tags []string `json:"tags"`
}

// Dog composes two base models plus its own fields: each embedded base becomes
// a $ref arm of the allOf, and the struct's own (non-embedded) fields β€” which
// are new and cannot be a $ref β€” form the final inline arm.
//
// swagger:model
type Dog struct {
	// swagger:allOf
	Animal

	// swagger:allOf
	Tagged

	// Breed is the dog's breed.
	Breed string `json:"breed"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Dog
{
  "description": "Dog composes two base models plus its own fields: each embedded base becomes\na $ref arm of the allOf, and the struct's own (non-embedded) fields β€” which\nare new and cannot be a $ref β€” form the final inline arm.",
  "allOf": [
    {
      "$ref": "#/definitions/Animal"
    },
    {
      "$ref": "#/definitions/Tagged"
    },
    {
      "type": "object",
      "properties": {
        "breed": {
          "description": "Breed is the dog's breed.",
          "type": "string",
          "x-go-name": "Breed"
        }
      }
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/allof.json

When the base also declares a discriminator, this composition becomes a Swagger 2.0 type hierarchy β€” see Polymorphic models.

swagger:type

swagger:type <type> overrides the type codescan would infer. Here a [16]byte field is published as a string.

Annotated Go
// ULID is a 128-bit identifier stored as bytes but rendered as a string.
//
// swagger:type string
type ULID [16]byte

// Token carries a field whose inferred type is overridden, inline.
//
// swagger:model
type Token struct {
	// ID renders as a string despite its [16]byte Go type.
	ID ULID `json:"id"`
}


// RawID is a custom 16-byte identifier β€” an array under the hood, so left to
// itself a field of this type would render as an array of integers.
type RawID [16]byte

// Coupon overrides the type of a single field directly on the field doc β€” no
// wrapper-type annotation. Code publishes as a bare string while RawID is left
// untouched everywhere else it appears.
//
// swagger:model
type Coupon struct {
	// Code is an opaque identifier published as a string.
	//
	// swagger:type string
	Code RawID `json:"code"`

	// Amount is the discount in cents.
	Amount int64 `json:"amount"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Token
{
  "type": "object",
  "title": "Token carries a field whose inferred type is overridden, inline.",
  "properties": {
    "id": {
      "description": "ID renders as a string despite its [16]byte Go type.",
      "type": "string",
      "x-go-name": "ID"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/type.json

swagger:type is an inline directive: it renders the chosen type in place and never emits a $ref. The value is a scalar type (string, integer, number, boolean, object, or a Go builtin like int64), []T for an array of an inlined type, inline to expand the field’s own Go type, or a known type name to inline that type. array is deprecated in favour of inline / []T, and file is rejected (use swagger:file). When combined with swagger:strfmt, the type wins and the format is kept only if compatible β€” see the reference.

The override also works on an individual field doc β€” no wrapper-type annotation. Here Code is published as a string while its RawID type is left untouched everywhere else:

Annotated Go
// RawID is a custom 16-byte identifier β€” an array under the hood, so left to
// itself a field of this type would render as an array of integers.
type RawID [16]byte

// Coupon overrides the type of a single field directly on the field doc β€” no
// wrapper-type annotation. Code publishes as a bare string while RawID is left
// untouched everywhere else it appears.
//
// swagger:model
type Coupon struct {
	// Code is an opaque identifier published as a string.
	//
	// swagger:type string
	Code RawID `json:"code"`

	// Amount is the discount in cents.
	Amount int64 `json:"amount"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Coupon
{
  "description": "Coupon overrides the type of a single field directly on the field doc β€” no\nwrapper-type annotation. Code publishes as a bare string while RawID is left\nuntouched everywhere else it appears.",
  "type": "object",
  "properties": {
    "amount": {
      "description": "Amount is the discount in cents.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Amount"
    },
    "code": {
      "description": "Code is an opaque identifier published as a string.",
      "type": "string",
      "x-go-name": "Code"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/typefield.json

swagger:name

swagger:name <name> overrides the JSON property name a field or method renders as. It works on a struct field (overriding the json: tag / Go field name) and on an interface method. It is most useful on interface methods, which publish one property per nullary method and cannot carry a json tag: below, StructType() would default to structType, and the annotation publishes it as jsonClass instead.

Note

swagger:name is the legacy annotation form. The name: keyword is the canonical, universal equivalent β€” it renames a property here exactly the same way, and is the only form that also works on parameters and response headers. Precedence: name: > swagger:name > json: tag > Go field name.

Annotated Go
// Car is exposed as a schema via its method set. Interface methods cannot carry
// a json tag, so by default each property takes the camelCased method name;
// swagger:name overrides that where the default is not what you want.
//
// swagger:model
type Car interface {
	// Maker is the manufacturer. With no override the property is the
	// camelCased method name, "maker".
	Maker() string

	// StructType is the polymorphic class. Without the override the property
	// would be "structType"; swagger:name publishes it as "jsonClass".
	//
	// swagger:name jsonClass
	StructType() string
}


// Account shows the universal name: keyword renaming model struct fields. The
// same keyword used on parameters and response headers also sets a property key
// here, winning over a json tag, the legacy swagger:name annotation, and the Go
// field name.
//
// swagger:model
type Account struct {
	// Bal has no json tag; the keyword sets the property key directly.
	//
	// name: balance
	Bal float64

	// Currency carries both naming forms; the keyword wins over the
	// legacy annotation and the json tag.
	//
	// name: currencyCode
	// swagger:name legacyCurrency
	Currency string `json:"currency"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Car
{
  "description": "Car is exposed as a schema via its method set. Interface methods cannot carry\na json tag, so by default each property takes the camelCased method name;",
  "type": "object",
  "properties": {
    "jsonClass": {
      "description": "StructType is the polymorphic class. Without the override the property\nwould be \"structType\"; swagger:name publishes it as \"jsonClass\".",
      "type": "string",
      "x-go-name": "StructType"
    },
    "maker": {
      "description": "Maker is the manufacturer. With no override the property is the\ncamelCased method name, \"maker\".",
      "type": "string",
      "x-go-name": "Maker"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/name.json

The name: keyword does the same on a model property β€” and, being the universal form, with the same syntax you would use on a parameter or header. Here it names a tag-less field directly, and on a field that also carries a json: tag and a legacy swagger:name, the keyword wins (name: > swagger:name > json: tag > Go field name):

Annotated Go
// Account shows the universal name: keyword renaming model struct fields. The
// same keyword used on parameters and response headers also sets a property key
// here, winning over a json tag, the legacy swagger:name annotation, and the Go
// field name.
//
// swagger:model
type Account struct {
	// Bal has no json tag; the keyword sets the property key directly.
	//
	// name: balance
	Bal float64

	// Currency carries both naming forms; the keyword wins over the
	// legacy annotation and the json tag.
	//
	// name: currencyCode
	// swagger:name legacyCurrency
	Currency string `json:"currency"`
}

Full source: docs/examples/concepts/models/models.go

#/definitions/Account
{
  "description": "Account shows the universal name: keyword renaming model struct fields. The\nsame keyword used on parameters and response headers also sets a property key\nhere, winning over a json tag, the legacy swagger:name annotation, and the Go\nfield name.",
  "type": "object",
  "properties": {
    "balance": {
      "description": "Bal has no json tag; the keyword sets the property key directly.",
      "type": "number",
      "format": "double",
      "x-go-name": "Bal"
    },
    "currencyCode": {
      "description": "Currency carries both naming forms; the keyword wins over the\nlegacy annotation and the json tag.",
      "type": "string",
      "x-go-name": "Currency"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/namekeyword.json

swagger:ignore

swagger:ignore drops a declaration from the output. The scanner sees Secret, classifies it, then excludes it β€” so it never reaches the definitions (a fact the example’s TestIgnoreOmitsType asserts). It also works on a single struct field β€” placed on the field’s doc comment, it drops just that property from the model.

// Secret never reaches the spec.
//
// swagger:ignore
type Secret struct {
	// Token is internal.
	Token string `json:"token"`
}

Full source: docs/examples/concepts/models/models.go

Decorating a $ref

When a field’s type resolves to a named model, the property is a $ref β€” and a bare $ref cannot carry sibling keywords (a JSON Schema draft-4 rule). So a field that is both a reference and decorated (a description, a vendor extension, a default/example, a validation override) would lose its decorations.

codescan avoids that by wrapping the $ref as a member of an allOf. The property is then an ordinary schema object, free to carry the decorations alongside the reference:

  • the description and any x-* extensions sit on the property itself;
  • a value override (default, example) rides a second allOf member;
  • required rides the parent model’s required list.
Annotated Go
// Address is a referenced model.
//
// swagger:model
type Address struct {
	// Street is the street line.
	Street string `json:"street"`
}

// Person references Address through a field that also carries a description and
// a vendor extension. A bare $ref cannot hold sibling keywords, so codescan
// wraps the reference as a member of an allOf β€” the property is then a normal
// schema that keeps the description and the x-* extension, and required goes to
// the parent. Nothing is dropped.
//
// swagger:model
type Person struct {
	// Home is where the person lives.
	//
	// required: true
	// extensions:
	//   x-ui-order: 3
	Home Address `json:"home"`
}

Full source: docs/examples/concepts/refoverride/refoverride.go

#/definitions/Person
{
  "description": "Person references Address through a field that also carries a description and\na vendor extension. A bare $ref cannot hold sibling keywords, so codescan\nwraps the reference as a member of an allOf β€” the property is then a normal\nschema that keeps the description and the x-* extension, and required goes to\nthe parent. Nothing is dropped.",
  "type": "object",
  "required": [
    "home"
  ],
  "properties": {
    "home": {
      "description": "Home is where the person lives.",
      "allOf": [
        {
          "$ref": "#/definitions/Address"
        }
      ],
      "x-go-name": "Home",
      "x-ui-order": 3
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/refoverride"
}

Full source: docs/examples/concepts/refoverride/testdata/refoverride.json

Nothing is dropped. The description-only case is the exception β€” it is governed by the DescWithRef option (see Descriptions beside a $ref) β€” and a default/example on a $ref’d field is shown in Examples & defaults.

Info

Same-name collisions. Two structs that share the same short name in different packages stay distinct: codescan keys each by a compiler-unique identity and qualifies the colliding ones with a package segment (billing.Account / identity.Account β†’ BillingAccount / IdentityAccount), deterministically. Pin the names in your public contract with an explicit swagger:model <Name>, and let auto-resolution handle the rest β€” see Resolving $ref name conflicts.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Enumerations

An enum in Go is a named type plus a block of constants. swagger:enum turns that pair into an enum array on every schema, parameter and header the type reaches. This page covers what the scanner accepts on the value side, what decides the emitted type / format, and the two shapes that do not work.

swagger:enum and enum: are two different things

They produce the same spec keyword from opposite directions, and the names are close enough to trip over:

swagger:enum β€” an annotationenum: β€” a keyword
Whereon the type declarationinside any annotation block, on a field, parameter, header or declaration
Members come fromthe Go const block of that type, read from the type-checkerthe literal list you write after the colon
Type / formatthe declared Go typethe schema the keyword sits on
Use it whenthe values already exist as Go constantsthere is no const block, or the members are not Go values at all
// swagger:enum Kind        ← annotation: members are collected from the consts
type Kind string
const (
	KindA Kind = "a"
	KindB Kind = "b"
)

type Filter struct {
	// enum: asc, desc       ← keyword: members are taken verbatim
	Order string `json:"order"`
}

The annotation is the better tool whenever the constants exist: it stays in sync with the code, carries each member’s doc comment into x-go-enum-desc, and cannot drift from the Go values. The keyword is the escape hatch for everything else.

Every Go snippet below comes from the test-covered docs/examples/concepts/enums package, and every JSON pane is a golden file a test regenerates.

swagger:enum

swagger:enum <name> collects the const values declared with that type. A bare swagger:enum on the type declaration works too β€” the name is inferred from the declaration it sits on.

The enum type is emitted because something points at it: a model field, a parameter, a header. On its own it is unreachable, and unreachable types are not published. Add swagger:model to the enum type to make it a first-class definition (carrying the enum array) that fields $ref instead β€” the general swagger:model β‡’ definition + $ref rule.

Each member’s doc comment becomes a line of the x-go-enum-desc extension, and is appended to the property description. Set SkipEnumDescriptions to keep the mapping on the extension only.

Any constant expression, not just literals

The values come from the Go type-checker, which has already evaluated the const block. So the members do not have to be written as literals β€” anything the compiler can fold is collected.

iota is the case that matters most, because after the first line there is nothing left in the source to read: the following specs carry neither a type nor a value, and inherit both implicitly.

Annotated Go
// Weekday is an iota enum: only the first spec carries a type and a value, and
// every following one inherits both implicitly.
//
// swagger:enum Weekday
type Weekday int

const (
	// Sunday is the first day.
	Sunday Weekday = iota
	// Monday is the second day.
	Monday
	// Tuesday is the third day.
	Tuesday
)

// Schedule carries the enum, which is what makes it reachable and so emitted.
//
// swagger:model
type Schedule struct {
	// Day the job runs on.
	Day Weekday `json:"day"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Schedule
{
  "type": "object",
  "title": "Schedule carries the enum, which is what makes it reachable and so emitted.",
  "properties": {
    "day": {
      "description": "Day the job runs on.\n0 Sunday is the first day.\n1 Monday is the second day.\n2 Tuesday is the third day.",
      "type": "integer",
      "format": "int64",
      "enum": [
        0,
        1,
        2
      ],
      "x-go-enum-desc": "0 Sunday is the first day.\n1 Monday is the second day.\n2 Tuesday is the third day.",
      "x-go-name": "Day"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/iota.json

Constant expressions and references to earlier members are collected the same way.

Annotated Go
// Level is built from a constant expression and from a reference to an earlier
// member β€” neither of which is a literal.
//
// swagger:enum Level
type Level int

const (
	// LevelLow is the floor.
	LevelLow Level = 1 << 3
	// LevelHigh doubles it.
	LevelHigh Level = LevelLow * 2
)

// Threshold carries the computed enum.
//
// swagger:model
type Threshold struct {
	// Level to alert at.
	Level Level `json:"level"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Threshold
{
  "type": "object",
  "title": "Threshold carries the computed enum.",
  "properties": {
    "level": {
      "description": "Level to alert at.\n8 LevelLow is the floor.\n16 LevelHigh doubles it.",
      "type": "integer",
      "format": "int64",
      "enum": [
        8,
        16
      ],
      "x-go-enum-desc": "8 LevelLow is the floor.\n16 LevelHigh doubles it.",
      "x-go-name": "Level"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/expressions.json

The same goes for every other constant form Go offers: negative values, the non-decimal bases (0x2a, 0b101010, 0o52) and digit separators, values above MaxInt64 in an unsigned enum, true / false, rune literals, and both the raw and the escaped string forms.

Negative members are worth a pane of their own, since a signed constant is not a literal in the Go grammar β€” it is an expression wrapping one:

Annotated Go
// PanDirection straddles zero. A signed constant is not a literal in the Go
// grammar β€” it is an expression wrapping one β€” so these are the members that
// used to go missing.
//
// swagger:enum PanDirection
type PanDirection int8

const (
	// PanLeft pans to the left.
	PanLeft PanDirection = -1
	// NoPan holds the current position.
	NoPan PanDirection = 0
	// PanRight pans to the right.
	PanRight PanDirection = 1
)

// Camera carries the signed enum, declared int8.
//
// swagger:model
type Camera struct {
	// Pan direction of the camera.
	Pan PanDirection `json:"pan"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Camera
{
  "type": "object",
  "title": "Camera carries the signed enum, declared int8.",
  "properties": {
    "pan": {
      "description": "Pan direction of the camera.\n-1 PanLeft pans to the left.\n0 NoPan holds the current position.\n1 PanRight pans to the right.",
      "type": "integer",
      "format": "int8",
      "enum": [
        -1,
        0,
        1
      ],
      "x-go-enum-desc": "-1 PanLeft pans to the left.\n0 NoPan holds the current position.\n1 PanRight pans to the right.",
      "x-go-name": "Pan"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/signed.json

The type comes from the declaration

type and format are read from the Go type you declared, never from the members. PanDirection above is an int8, so the property is {integer, int8} β€” even though every member would fit in a smaller or larger box.

This is also what makes the const block safe to reorder. Zoom is a float32 whose first member is written 0, an integer literal; the schema is a number enum regardless of which member comes first:

Annotated Go
// Zoom is a float32 enum whose FIRST member is written as an integer literal.
// The schema type follows the declared type, so the block can be reordered
// freely.
//
// swagger:enum Zoom
type Zoom float32

const (
	// ZoomNone is the neutral step.
	ZoomNone Zoom = 0
	// ZoomOut steps back.
	ZoomOut Zoom = -1.5
	// ZoomIn steps in.
	ZoomIn Zoom = 1.5
)

// Lens carries the float enum.
//
// swagger:model
type Lens struct {
	// Zoom step of the lens.
	Zoom Zoom `json:"zoom"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Lens
{
  "type": "object",
  "title": "Lens carries the float enum.",
  "properties": {
    "zoom": {
      "description": "Zoom step of the lens.\n0 ZoomNone is the neutral step.\n-1.5 ZoomOut steps back.\n1.5 ZoomIn steps in.",
      "type": "number",
      "format": "float",
      "enum": [
        0,
        -1.5,
        1.5
      ],
      "x-go-enum-desc": "0 ZoomNone is the neutral step.\n-1.5 ZoomOut steps back.\n1.5 ZoomIn steps in.",
      "x-go-name": "Zoom"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/width.json

A type declared over another named type keeps what that type contributed. An enum written over a string format is still that format:

Annotated Go
// Kind is an enum written over a string format rather than over a plain string:
// the format of the type it is declared over comes with it.
//
// swagger:enum Kind
type Kind strfmt.UUID

const (
	// KindPrimary is the primary kind.
	KindPrimary Kind = "0a8bcf1e-0000-0000-0000-000000000000"
	// KindSecondary is the secondary kind.
	KindSecondary Kind = "0a8bcf1e-1111-1111-1111-111111111111"
)

// Label carries the formatted enum.
//
// swagger:model
type Label struct {
	// Kind of the label.
	Kind Kind `json:"kind"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Label
{
  "type": "object",
  "title": "Label carries the formatted enum.",
  "properties": {
    "kind": {
      "description": "Kind of the label.\n0a8bcf1e-0000-0000-0000-000000000000 KindPrimary is the primary kind.\n0a8bcf1e-1111-1111-1111-111111111111 KindSecondary is the secondary kind.",
      "type": "string",
      "format": "uuid",
      "enum": [
        "0a8bcf1e-0000-0000-0000-000000000000",
        "0a8bcf1e-1111-1111-1111-111111111111"
      ],
      "x-go-enum-desc": "0a8bcf1e-0000-0000-0000-000000000000 KindPrimary is the primary kind.\n0a8bcf1e-1111-1111-1111-111111111111 KindSecondary is the secondary kind.",
      "x-go-name": "Kind"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/strfmt.json

Parameters and headers

OpenAPI 2.0 forbids a $ref on a non-body parameter or a response header, so there the members and the format are written inline. Nothing changes on the annotation side β€” the same enum type reaches in: query, path, header and formData, and the items of an array-typed one:

Annotated Go
// SearchParams consumes an enum from a non-body parameter, where OpenAPI 2.0
// forbids a $ref: the members and the format ship inline.
//
// swagger:parameters search
type SearchParams struct {
	// Direction to pan while searching.
	//
	// in: query
	Pan PanDirection `json:"pan"`

	// Directions the client accepts.
	//
	// in: query
	Accepted []PanDirection `json:"accepted"`
}

Full source: docs/examples/concepts/enums/enums.go

[
  {
    "enum": [
      -1,
      0,
      1
    ],
    "type": "integer",
    "format": "int8",
    "x-go-enum-desc": "-1 PanLeft pans to the left.\n0 NoPan holds the current position.\n1 PanRight pans to the right.",
    "x-go-name": "Pan",
    "description": "Direction to pan while searching.\n-1 PanLeft pans to the left.\n0 NoPan holds the current position.\n1 PanRight pans to the right.",
    "name": "pan",
    "in": "query"
  },
  {
    "type": "array",
    "items": {
      "enum": [
        -1,
        0,
        1
      ],
      "type": "integer",
      "format": "int8"
    },
    "x-go-name": "Accepted",
    "description": "Directions the client accepts.",
    "name": "accepted",
    "in": "query"
  }
]

Full source: docs/examples/concepts/enums/testdata/params.json

Two shapes that do not work

A rune or byte enum emits integers. It is collected like any other, and 'a' reaches the spec as 97:

Annotated Go
// Letter is a rune enum. A rune is an int32, on the wire as much as in Go, so
// the members are code points β€” 'a' is 97.
//
// swagger:enum Letter
type Letter rune

const (
	// LetterA is the first letter.
	LetterA Letter = 'a'
	// LetterB is the second letter.
	LetterB Letter = 'b'
)

// Glyph carries the rune enum.
//
// swagger:model
type Glyph struct {
	// Letter of the glyph.
	Letter Letter `json:"letter"`
}

Full source: docs/examples/concepts/enums/enums.go

#/definitions/Glyph
{
  "type": "object",
  "title": "Glyph carries the rune enum.",
  "properties": {
    "letter": {
      "description": "Letter of the glyph.\n97 LetterA is the first letter.\n98 LetterB is the second letter.",
      "type": "integer",
      "format": "int32",
      "enum": [
        97,
        98
      ],
      "x-go-enum-desc": "97 LetterA is the first letter.\n98 LetterB is the second letter.",
      "x-go-name": "Letter"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/enums"
}

Full source: docs/examples/concepts/enums/testdata/runes.json

That is unlikely to be what you pictured, and it is the only faithful answer: a scalar rune is an int32 on the wire as much as in Go, so json.Marshal writes 97, and encoding/json refuses to unmarshal "a" back into the field. A string-typed schema would describe a payload your own server rejects. If you meant characters, declare the type over string (type Letter string, LetterA Letter = "a") β€” that changes the wire, and the schema follows.

An alias to a basic type cannot host an enum.

type Unsigned = uint64          // an alias, not a new type

// swagger:enum Unsigned        // ← collects nothing
const Zero Unsigned = 0

The Go type-checker erases the alias, so Zero is indistinguishable from any other uint64 constant and there is no set of members to collect. Declare a real type instead (type Unsigned uint64). An alias to a named enum type is fine β€” the named type survives.

Where to go next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Maps & free-form objects

Not every object has a fixed set of named fields. A Go map models an object with dynamic keys; a struct can be marked open (extra keys allowed), closed (extra keys forbidden), or given a typed value schema for its extras. codescan expresses all of this with additionalProperties and patternProperties. The panes below are rendered from the test-covered docs/examples/concepts/maps package.

Maps become objects

A map field renders as {type: object} whose values all share one schema, carried as additionalProperties β€” the value schema is derived from the Go map’s element type:

Annotated Go
// Inventory shows a plain Go map. A map renders as an object whose values all
// share one schema, carried as additionalProperties.
//
// swagger:model
type Inventory struct {
	// Counts maps each SKU to its on-hand quantity.
	Counts map[string]int `json:"counts"`
}

Full source: docs/examples/concepts/maps/maps.go

#/definitions/Inventory
{
  "description": "Inventory shows a plain Go map. A map renders as an object whose values all\nshare one schema, carried as additionalProperties.",
  "type": "object",
  "properties": {
    "counts": {
      "description": "Counts maps each SKU to its on-hand quantity.",
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "format": "int64"
      },
      "x-go-name": "Counts"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/naturalmap.json

Which map keys work

A map key has to become a JSON object key β€” a string. codescan accepts every key type encoding/json can stringify: string kinds, every integer / unsigned kind (so map[int]V, map[uint8]V, …), and any type implementing encoding.TextMarshaler. An integer-keyed map is therefore still a valid object:

Annotated Go
// Lookups shows which Go map keys survive. A key is usable when encoding/json
// can stringify it: string kinds, every integer/unsigned kind, and types
// implementing encoding.TextMarshaler. Other keys (float, bool, struct without
// TextMarshaler) are dropped with a diagnostic.
//
// swagger:model
type Lookups struct {
	// ByName is keyed by a plain string.
	ByName map[string]int `json:"byName"`

	// ByCode is keyed by an integer β€” JSON stringifies it, so the map is still
	// an object with additionalProperties.
	ByCode map[int]string `json:"byCode"`
}

Full source: docs/examples/concepts/maps/maps.go

#/definitions/Lookups
{
  "description": "Lookups shows which Go map keys survive. A key is usable when encoding/json\ncan stringify it: string kinds, every integer/unsigned kind, and types\nimplementing encoding.TextMarshaler. Other keys (float, bool, struct without\nTextMarshaler) are dropped with a diagnostic.",
  "type": "object",
  "properties": {
    "byCode": {
      "description": "ByCode is keyed by an integer β€” JSON stringifies it, so the map is still\nan object with additionalProperties.",
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "x-go-name": "ByCode"
    },
    "byName": {
      "description": "ByName is keyed by a plain string.",
      "type": "object",
      "additionalProperties": {
        "type": "integer",
        "format": "int64"
      },
      "x-go-name": "ByName"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/keytypes.json

Info

A key JSON cannot stringify β€” a float, bool, a struct without TextMarshaler, or an interface β€” is not silently dropped: the map’s additionalProperties is omitted and a CodeUnsupportedType diagnostic (“additionalProperties dropped”) is raised. A json:"-" map field is muted before this check, so it never warns.

Open & closed objects

A struct normally renders with just its named properties and says nothing about extra keys. The decl-level swagger:additionalProperties <spec> marker decides the policy, where <spec> is true, false, or a value type:

// ClosedObject forbids any key beyond its named properties: the marker false
// closes the object.
//
// swagger:model
// swagger:additionalProperties false
type ClosedObject struct {
	A string `json:"a"`
	B int    `json:"b"`
}

// OpenObject keeps its named property and also allows arbitrary extra keys.
//
// swagger:model
// swagger:additionalProperties true
type OpenObject struct {
	A string `json:"a"`
}

// TypedObject complements its named property with typed (integer) extra values.
//
// swagger:model
// swagger:additionalProperties integer
type TypedObject struct {
	A string `json:"a"`
}

// RefObject references a model as the schema of its extra values.
//
// swagger:model
// swagger:additionalProperties Thing
type RefObject struct {
	A string `json:"a"`
}

Full source: docs/examples/concepts/maps/maps.go

false closes the object (only the named properties are valid); true opens it (any extra key is allowed):

false β€” closed object
{
  "description": "ClosedObject forbids any key beyond its named properties: the marker false\ncloses the object.",
  "type": "object",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    },
    "b": {
      "type": "integer",
      "format": "int64",
      "x-go-name": "B"
    }
  },
  "additionalProperties": false,
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/closed.json

true β€” open object
{
  "type": "object",
  "title": "OpenObject keeps its named property and also allows arbitrary extra keys.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": true,
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/open.json

A type spec instead of a bool gives the extra values a schema β€” a primitive (or []T), or a model name that becomes a $ref (the same value-type grammar as swagger:type, except a type name resolves to a $ref):

integer β€” typed values
{
  "type": "object",
  "title": "TypedObject complements its named property with typed (integer) extra values.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": {
    "type": "integer"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/typed.json

Thing β€” model values
{
  "type": "object",
  "title": "RefObject references a model as the schema of its extra values.",
  "properties": {
    "a": {
      "type": "string",
      "x-go-name": "A"
    }
  },
  "additionalProperties": {
    "$ref": "#/definitions/Thing"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/ref.json

On a map type the marker overrides the element-derived value schema; on a struct it complements the named properties (as above). It composes with the object validations maxProperties / minProperties / patternProperties.

Info

The model name is a bare leaf: codescan resolves it in the annotating type’s own package first, then uniquely across the scanned model set, so a value type declared in another package resolves to a $ref by name. A leaf that matches a model in several packages is ambiguous β€” it is dropped with a validate.ambiguous-type-name diagnostic. See Resolving $ref name conflicts.

Per-field control

The same <spec> is available as a field keyword, additionalProperties: <spec>, decorating one struct field. On a map field it overrides the value schema; on a $ref’d field the value rides an allOf sibling so the reference is preserved:

Annotated Go
// Holder decorates individual fields with the additionalProperties: keyword.
//
// swagger:model
type Holder struct {
	// OverriddenMap keeps its map shape but overrides the value schema from the
	// Go element type (string) to integer.
	//
	// additionalProperties: integer
	OverriddenMap map[string]string `json:"overriddenMap"`

	// RefMap points the map values at a model.
	//
	// additionalProperties: Thing
	RefMap map[string]string `json:"refMap"`

	// ClosedRef references a model and forbids extra keys. Because the field is
	// a $ref, the value rides an allOf sibling so the reference is preserved.
	//
	// additionalProperties: false
	ClosedRef Thing `json:"closedRef"`
}

Full source: docs/examples/concepts/maps/maps.go

#/definitions/Holder
{
  "type": "object",
  "title": "Holder decorates individual fields with the additionalProperties: keyword.",
  "properties": {
    "closedRef": {
      "description": "ClosedRef references a model and forbids extra keys. Because the field is\na $ref, the value rides an allOf sibling so the reference is preserved.",
      "allOf": [
        {
          "$ref": "#/definitions/Thing"
        },
        {
          "additionalProperties": false
        }
      ],
      "x-go-name": "ClosedRef"
    },
    "overriddenMap": {
      "description": "OverriddenMap keeps its map shape but overrides the value schema from the\nGo element type (string) to integer.",
      "type": "object",
      "additionalProperties": {
        "type": "integer"
      },
      "x-go-name": "OverriddenMap"
    },
    "refMap": {
      "description": "RefMap points the map values at a model.",
      "type": "object",
      "additionalProperties": {
        "$ref": "#/definitions/Thing"
      },
      "x-go-name": "RefMap"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/fieldkeyword.json

Pattern properties

patternProperties constrains extra keys by a name regex rather than allowing all of them. The regex-only patternProperties: keyword maps a pattern to an empty (any-value) schema. The decl-level swagger:patternProperties "<re>": <spec>, … marker is the typed counterpart β€” each quoted regex pairs with a value spec (a primitive, or a model name that becomes a $ref):

Annotated Go
// TypedPatterns maps property-name regexes to typed value schemas. Each quoted
// regex pairs with a value spec β€” a primitive or a model name (which becomes a
// $ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0
// subset; codescan emits it ungated.
//
// swagger:model
// swagger:patternProperties "^x-": string, "^\d+$": integer, "^item-": Thing
type TypedPatterns struct {
	Known string `json:"known"`
}

Full source: docs/examples/concepts/maps/maps.go

#/definitions/TypedPatterns
{
  "description": "TypedPatterns maps property-name regexes to typed value schemas. Each quoted\nregex pairs with a value spec β€” a primitive or a model name (which becomes a\n$ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0\nsubset; codescan emits it ungated.",
  "type": "object",
  "properties": {
    "known": {
      "type": "string",
      "x-go-name": "Known"
    }
  },
  "patternProperties": {
    "^\\d+$": {
      "type": "integer"
    },
    "^item-": {
      "$ref": "#/definitions/Thing"
    },
    "^x-": {
      "type": "string"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/patterntyped.json

Note

Beyond Swagger 2.0. patternProperties is a JSON-Schema (draft-4) keyword, not part of the Swagger 2.0 Schema Object subset. codescan emits it ungated, consistent with go-openapi’s JSON-Schema-first stance β€” your downstream tooling must understand it. Each regex is RE2-hygiene-checked: one that does not compile raises a CodeInvalidAnnotation warning but is preserved on the schema.

Info

additionalProperties and patternProperties are object-schema keywords β€” they only ride on an object. They are the lowest-priority annotations: if a prior rule already fixed a non-object type (a swagger:type scalar, a swagger:strfmt, a special type), the marker is dropped with a CodeShapeMismatch diagnostic. Object schemas also have no OAS-2 SimpleSchema form, so neither keyword applies on a non-body parameter or a response header (it is dropped there with a diagnostic).

What’s next

  • Validations β€” maxProperties / minProperties and the regex-only patternProperties: keyword.
  • Model definitions β€” the $ref mechanics the typed value schemas rely on.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Polymorphic models

Swagger 2.0 expresses polymorphism with three ingredients:

  1. a base type that declares a discriminator β€” the property whose value says which concrete subtype a payload is;
  2. subtypes that include the base via allOf and add their own fields;
  3. a discriminator value per subtype (here, the subtype’s definition name).

The panes below are backed by the test-covered docs/examples/concepts/polymorphism package.

The base type

Mark one property discriminator: true. codescan writes that property’s name onto the schema’s discriminator. A discriminator property must also be required β€” a consumer cannot pick a subtype from a value that may be absent.

Annotated Go
// Pet is the polymorphic base type. The field marked `discriminator: true` names
// the property whose value tells a consumer which concrete subtype a payload is:
// codescan writes that property's name onto the schema's `discriminator`, and a
// discriminator property must be `required`.
//
// swagger:model
type Pet struct {
	// PetType selects the concrete subtype β€” its value is the subtype's
	// definition name (e.g. "Cat" or "Dog").
	//
	// discriminator: true
	// required: true
	PetType string `json:"petType"`

	// Name is common to every pet.
	//
	// required: true
	Name string `json:"name"`
}

Full source: docs/examples/concepts/polymorphism/polymorphism.go

#/definitions/Pet
{
  "description": "Pet is the polymorphic base type. The field marked `discriminator: true` names\nthe property whose value tells a consumer which concrete subtype a payload is:\ncodescan writes that property's name onto the schema's `discriminator`, and a\ndiscriminator property must be `required`.",
  "type": "object",
  "required": [
    "petType",
    "name"
  ],
  "properties": {
    "name": {
      "description": "Name is common to every pet.",
      "type": "string",
      "x-go-name": "Name"
    },
    "petType": {
      "description": "PetType selects the concrete subtype β€” its value is the subtype's\ndefinition name (e.g. \"Cat\" or \"Dog\").",
      "type": "string",
      "x-go-name": "PetType"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism",
  "discriminator": "petType"
}

Full source: docs/examples/concepts/polymorphism/testdata/base.json

The subtypes

Each subtype embeds the base as an anonymous field annotated swagger:allOf. The result is allOf: [ {$ref to the base}, {the subtype's own fields} ] β€” the same composition covered in Model definitions, now given meaning by the base’s discriminator.

Annotated Go
// Cat is a Pet subtype. `swagger:allOf` composes it as
// `allOf: [ $ref Pet, {its own fields} ]` β€” the composition Swagger 2.0
// polymorphism builds on.
//
// swagger:model
type Cat struct {
	// swagger:allOf
	Pet

	// HuntingSkill is how the cat hunts.
	HuntingSkill string `json:"huntingSkill"`
}

// Dog is a second Pet subtype.
//
// swagger:model
type Dog struct {
	// swagger:allOf
	Pet

	// PackSize is the size of the dog's pack.
	PackSize int32 `json:"packSize"`
}

Full source: docs/examples/concepts/polymorphism/polymorphism.go

#/definitions/Cat
{
  "description": "Cat is a Pet subtype. `swagger:allOf` composes it as\n`allOf: [ $ref Pet, {its own fields} ]` β€” the composition Swagger 2.0\npolymorphism builds on.",
  "allOf": [
    {
      "$ref": "#/definitions/Pet"
    },
    {
      "type": "object",
      "properties": {
        "huntingSkill": {
          "description": "HuntingSkill is how the cat hunts.",
          "type": "string",
          "x-go-name": "HuntingSkill"
        }
      }
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
}

Full source: docs/examples/concepts/polymorphism/testdata/subtype.json

Dog follows the identical shape. A payload is then recognised as a Cat or a Dog by its petType value.

How subtypes are discovered

A family has an awkward property: the references all point upwards. A subtype $refs its base, and nothing ever $refs a subtype. So an API that returns the base β€” the whole point of polymorphism β€” names only the base, and the ordinary reachability rule would stop right there, leaving a discriminator with nothing to discriminate between.

codescan therefore looks the relation up backwards. When a definition that declares a discriminator enters the spec, every swagger:model that composes it under swagger:allOf is pulled in with it β€” wherever those subtypes are declared, including other packages. No ScanModels needed. The route below references only Pet:

Annotated Go
// PetResponse returns the polymorphic BASE β€” nothing in the API surface names
// Cat or Dog.
//
// swagger:response petResponse
type PetResponse struct {
	// in: body
	Body Pet `json:"body"`
}

// swagger:route GET /pets pets listPets
//
// Lists pets.
//
// responses:
//
//	200: petResponse

Full source: docs/examples/concepts/polymorphism/polymorphism.go

Whole spec, scanned WITHOUT ScanModels
{
  "swagger": "2.0",
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          "pets"
        ],
        "summary": "Lists pets.",
        "operationId": "listPets",
        "responses": {
          "200": {
            "$ref": "#/responses/petResponse"
          }
        }
      }
    }
  },
  "definitions": {
    "Cat": {
      "description": "Cat is a Pet subtype. `swagger:allOf` composes it as\n`allOf: [ $ref Pet, {its own fields} ]` β€” the composition Swagger 2.0\npolymorphism builds on.",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "huntingSkill": {
              "description": "HuntingSkill is how the cat hunts.",
              "type": "string",
              "x-go-name": "HuntingSkill"
            }
          }
        }
      ],
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
    },
    "Dog": {
      "title": "Dog is a second Pet subtype.",
      "allOf": [
        {
          "$ref": "#/definitions/Pet"
        },
        {
          "type": "object",
          "properties": {
            "packSize": {
              "description": "PackSize is the size of the dog's pack.",
              "type": "integer",
              "format": "int32",
              "x-go-name": "PackSize"
            }
          }
        }
      ],
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism"
    },
    "Pet": {
      "description": "Pet is the polymorphic base type. The field marked `discriminator: true` names\nthe property whose value tells a consumer which concrete subtype a payload is:\ncodescan writes that property's name onto the schema's `discriminator`, and a\ndiscriminator property must be `required`.",
      "type": "object",
      "required": [
        "petType",
        "name"
      ],
      "properties": {
        "name": {
          "description": "Name is common to every pet.",
          "type": "string",
          "x-go-name": "Name"
        },
        "petType": {
          "description": "PetType selects the concrete subtype β€” its value is the subtype's\ndefinition name (e.g. \"Cat\" or \"Dog\").",
          "type": "string",
          "x-go-name": "PetType"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism",
      "discriminator": "petType"
    }
  },
  "responses": {
    "petResponse": {
      "description": "PetResponse returns the polymorphic BASE β€” nothing in the API surface names\nCat or Dog.",
      "schema": {
        "$ref": "#/definitions/Pet"
      }
    }
  }
}

Full source: docs/examples/concepts/polymorphism/testdata/spec-reachable-only.json

Cat and Dog are in there, and each pull is announced on the OnDiagnostic sink, so a definition you did not ask for by name is never a mystery:

scan.discovered-subtype: definition "Cat" discovered as a subtype of discriminated base "Pet"
scan.discovered-subtype: definition "Dog" discovered as a subtype of discriminated base "Pet"

Full source: docs/examples/concepts/polymorphism/testdata/hints.txt

Three consequences worth knowing:

  • Reachability, not existence. The trigger is the base entering the spec, not merely existing in the scanned source. A discriminated base that nothing references still emits nothing β€” bases are not roots, or every hierarchy in a shared library would land in every spec.
  • The family travels as a unit. With PruneUnusedModels a reachable discriminated base keeps its subtypes, even though no $ref reaches them; and an unreachable base is dropped together with its subtypes. You never get a base whose subtypes have vanished.
  • Only swagger:allOf counts. A plain embed inlines the base’s properties instead of composing them, so it is not a subtype relation. The DefaultAllOfForEmbeds option changes how embeds render, deliberately not which definitions exist.

Multi-level hierarchies

A subtype can be a base in its own right. Write the intermediate level as an interface β€” only an interface can be embedded by the concrete structs beneath it β€” composing its parent with swagger:allOf and declaring a discriminator of its own:

Annotated Go
// Shape is the root of the hierarchy: a base written as an interface, whose
// `discriminator: true` member names the property a consumer switches on.
//
// swagger:model
type Shape interface {
	// ShapeType selects the concrete subtype.
	//
	// discriminator: true
	// required: true
	// swagger:name shapeType
	ShapeType() string

	// swagger:name area
	Area() float64
}

// Polygon is a subtype of Shape AND a base of its own: it composes Shape as an
// allOf member and declares a second discriminator. An intermediate level is
// written as an interface, because only an interface can be embedded by the
// concrete structs below it.
//
// swagger:model
type Polygon interface {
	// swagger:allOf
	Shape

	// PolygonType selects the concrete polygon.
	//
	// discriminator: true
	// required: true
	// swagger:name polygonType
	PolygonType() string
}

// Square is a leaf, two levels down. It composes the INTERMEDIATE type, so it
// inherits Shape transitively.
//
// swagger:model
type Square struct {
	// swagger:allOf
	Polygon

	// Side is the length of a side.
	Side float64 `json:"side"`
}

Full source: docs/examples/concepts/polymorphism-nested/nested.go

#/definitions/Polygon β€” subtype AND base
{
  "description": "Polygon is a subtype of Shape AND a base of its own: it composes Shape as an\nallOf member and declares a second discriminator. An intermediate level is\nwritten as an interface, because only an interface can be embedded by the\nconcrete structs below it.",
  "allOf": [
    {
      "$ref": "#/definitions/Shape"
    },
    {
      "type": "object",
      "required": [
        "polygonType"
      ],
      "properties": {
        "polygonType": {
          "description": "PolygonType selects the concrete polygon.",
          "type": "string",
          "x-go-name": "PolygonType"
        }
      },
      "discriminator": "polygonType"
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/polymorphism-nested"
}

Full source: docs/examples/concepts/polymorphism-nested/testdata/intermediate.json

Discovery cascades: the route references Shape, Shape pulls Polygon, and Polygon β€” itself only just discovered β€” pulls Square. Note where each level’s discriminator lands, because the two differ:

levelshapediscriminator
root (Shape)a plain objectat the top level
intermediate (Polygon)allOf: [ $ref Shape, {own} ]inside its own allOf member
leaf (Square)allOf: [ $ref Polygon, {own} ]none of its own

A leaf never inherits its base’s discriminator as its own: it points at a discriminated base, which is what makes it a subtype, not a base.

Info

The discriminator value for each subtype is its definition name (Cat, Dog) β€” so petType must carry exactly "Cat" or "Dog". codescan does not implement a custom-value annotation (swagger:discriminatorValue), so the subtype name is the value. Keep the discriminator a plain string and required on the base; it is inherited by every subtype through the $ref.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Routes & operations

Routes and operations turn an annotation into an entry in the spec’s paths map, wired to the parameters it accepts and the responses it returns. This page covers the two operation annotations and the companion structs they reference. Each pane pairs the annotated Go (left) with the exact fragment the scanner emits (right), from the test-covered docs/examples/concepts/routes package.

For the exhaustive rule on any annotation below, follow its link to the Maintainers reference; the Parameters: / Responses: body grammars are covered in Sub-languages.

swagger:route

swagger:route <METHOD> <path> [tags] <operationID> declares a path and its operation in one annotation. The body’s responses: block ties status codes to named responses ($ref into the spec’s responses). It lives in a plain comment block β€” no Go declaration required.

Annotated Go
// swagger:route GET /pets pets listPets
//
// Lists pets in the store, optionally filtered by tag.
//
// responses:
//
//	200: petsResponse
//	default: errorResponse

Full source: docs/examples/concepts/routes/routes.go

{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Lists pets in the store, optionally filtered by tag.",
    "operationId": "listPets",
    "parameters": [
      {
        "type": "string",
        "x-go-name": "Tag",
        "description": "Tag filters pets by tag.",
        "name": "tag",
        "in": "query"
      },
      {
        "maximum": 100,
        "minimum": 1,
        "type": "integer",
        "format": "int32",
        "x-go-name": "Limit",
        "description": "Limit caps the number of results.",
        "name": "limit",
        "in": "query"
      }
    ],
    "responses": {
      "200": {
        "$ref": "#/responses/petsResponse"
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/route.json

The body can also carry an indented Parameters: block to declare simple parameters (path / query / header) inline β€” no swagger:parameters struct needed. For body parameters or parameter sets shared across operations, use swagger:parameters instead. The block syntax is covered in sub-languages.

swagger:operation

swagger:operation carries the same header but spells the operation out as a YAML document after a --- fence β€” useful when you want to author the operation object directly (here a path parameter and an inline $ref response schema).

Annotated Go
// swagger:operation GET /pets/{id} pets getPet
//
// ---
// summary: Get a pet by ID.
// parameters:
//   - name: id
//     in: path
//     required: true
//     type: integer
//     format: int64
// responses:
//   '200':
//     description: the requested pet
//     schema:
//       $ref: '#/definitions/Pet'
//   default:
//     $ref: '#/responses/errorResponse'

Full source: docs/examples/concepts/routes/routes.go

{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Get a pet by ID.",
    "operationId": "getPet",
    "parameters": [
      {
        "type": "integer",
        "format": "int64",
        "name": "id",
        "in": "path",
        "required": true
      }
    ],
    "responses": {
      "200": {
        "description": "the requested pet",
        "schema": {
          "$ref": "#/definitions/Pet"
        }
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/operation.json

swagger:parameters

swagger:parameters <operationID>… declares a struct whose fields become the parameters of the named operation(s). Field doc comments carry in:, the validations, and the description; the parameters attach to every operation ID listed.

Annotated Go
// ListPetsParams is the parameter set for the listPets operation. Each field
// becomes one parameter; the operation IDs after swagger:parameters name the
// operations the set applies to.
//
// swagger:parameters listPets
type ListPetsParams struct {
	// Tag filters pets by tag.
	//
	// in: query
	Tag string `json:"tag"`

	// Limit caps the number of results.
	//
	// in: query
	// minimum: 1
	// maximum: 100
	Limit int32 `json:"limit"`
}

Full source: docs/examples/concepts/routes/routes.go

parameters on listPets
[
  {
    "type": "string",
    "x-go-name": "Tag",
    "description": "Tag filters pets by tag.",
    "name": "tag",
    "in": "query"
  },
  {
    "maximum": 100,
    "minimum": 1,
    "type": "integer",
    "format": "int32",
    "x-go-name": "Limit",
    "description": "Limit caps the number of results.",
    "name": "limit",
    "in": "query"
  }
]

Full source: docs/examples/concepts/routes/testdata/parameters.json

A field marked in: body makes its Go type the request body schema β€” the usual shape for a POST or PUT payload:

Annotated Go
// CreatePetParams is the request parameter set for createPet. A field marked
// `in: body` makes its Go type the request body schema β€” the usual shape for a
// POST or PUT payload.
//
// swagger:parameters createPet
type CreatePetParams struct {
	// Body is the pet to create.
	//
	// in: body
	// required: true
	Body Pet `json:"body"`
}

// swagger:route POST /pets/import pets createPet
//
// responses:
//
//	200: petsResponse

Full source: docs/examples/concepts/routes/routes.go

parameters on createPet
[
  {
    "x-go-name": "Body",
    "description": "Body is the pet to create.",
    "name": "body",
    "in": "body",
    "required": true,
    "schema": {
      "$ref": "#/definitions/Pet"
    }
  }
]

Full source: docs/examples/concepts/routes/testdata/bodyparam.json

When a parameter field’s Go type is a struct (or any type that has no simple Swagger representation), it cannot be a query/path/header parameter on its own. A swagger:type override collapses it to a simple parameter β€” a scalar, or a []-wrapped scalar for an array parameter:

Annotated Go
// Cursor is an opaque pagination token. As a struct it cannot be a query
// parameter on its own; swagger:type publishes it as a simple parameter.
type Cursor struct {
	Page  int
	Token string
}

// FilterPetsParams shows swagger:type on parameter fields: a struct-typed field
// is collapsed to a simple parameter. The override accepts a scalar or a
// []-wrapped scalar β€” the inline / type-name forms are rejected here (a
// non-body parameter has no schema to inline into).
//
// swagger:parameters filterPets
type FilterPetsParams struct {
	// After is an opaque cursor carried as a plain string query parameter.
	//
	// in: query
	// swagger:type string
	After Cursor `json:"after"`

	// Sort is a list of sort keys carried as an array-of-string query parameter.
	//
	// in: query
	// swagger:type []string
	Sort []Cursor `json:"sort"`
}

// swagger:route GET /pets/filter pets filterPets
//
// Filter pets with cursor pagination.
//
// responses:
//
//	200: description: matched pets

Full source: docs/examples/concepts/routes/routes.go

parameters on filterPets
[
  {
    "type": "string",
    "x-go-name": "After",
    "description": "After is an opaque cursor carried as a plain string query parameter.",
    "name": "after",
    "in": "query"
  },
  {
    "type": "array",
    "items": {
      "type": "string"
    },
    "x-go-name": "Sort",
    "description": "Sort is a list of sort keys carried as an array-of-string query parameter.",
    "name": "sort",
    "in": "query"
  }
]

Full source: docs/examples/concepts/routes/testdata/paramtype.json

swagger:response

swagger:response <name> declares a struct as a named entry in the spec’s top-level responses. A Body field (or in: body) becomes the response schema; routes reference it by name. Here the body is a []Pet, so the schema is an array of $refs.

Annotated Go
// PetsResponse is the list returned by listPets.
//
// swagger:response petsResponse
type PetsResponse struct {
	// in: body
	Body []Pet
}

// ErrorResponse is the default error payload.
//
// swagger:response errorResponse
type ErrorResponse struct {
	// in: body
	Body struct {
		// Message is a human-readable error message.
		Message string `json:"message"`
	}
}

Full source: docs/examples/concepts/routes/routes.go

responses[petsResponse]
{
  "description": "PetsResponse is the list returned by listPets.",
  "schema": {
    "type": "array",
    "items": {
      "$ref": "#/definitions/Pet"
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/response.json

swagger:file

swagger:file on a parameter field marks it as a binary upload β€” the parameter emits as {type: file}. It belongs on a formData field of a swagger:parameters struct.

Annotated Go
// swagger:route POST /pets/{id}/photo pets uploadPetPhoto
//
// responses:
//
//	200: petsResponse

// UploadParams is the multipart upload for the uploadPetPhoto operation.
//
// swagger:parameters uploadPetPhoto
type UploadParams struct {
	// Photo is the image to upload.
	//
	// in: formData
	// swagger:file
	Photo io.ReadCloser `json:"photo"`
}

Full source: docs/examples/concepts/routes/routes.go

parameters on uploadPetPhoto
[
  {
    "type": "file",
    "x-go-name": "Photo",
    "description": "Photo is the image to upload.",
    "name": "photo",
    "in": "formData"
  }
]

Full source: docs/examples/concepts/routes/testdata/file.json

externalDocs

An externalDocs: block (description + url) links an object out to external documentation. It rides an operation (in a swagger:route or swagger:operation body) and a full schema (a swagger:model). It is a full-Schema-only keyword: on a simple-schema parameter (anything but in: body) it is dropped with a diagnostic. The same ExternalDocs: block on a swagger:meta package populates the spec’s top-level externalDocs (see Document metadata).

Annotated Go
// swagger:route GET /pets/search pets searchPets
//
// Searches pets. The operation links out to external documentation.
//
// externalDocs:
//   description: Search guide
//   url: https://example.com/docs/search
//
// responses:
//
//	200: petsResponse

// CatalogEntry carries externalDocs at the schema level (the link rides the
// definition) and on its fields. (On a simple-schema parameter externalDocs is
// dropped with a diagnostic: it is a full-Schema-only keyword.)
//
// externalDocs: {description: "Catalog schema reference", url: "https://example.com/docs/catalog"}
//
// swagger:model
type CatalogEntry struct {
	// SKU is the catalog identifier.
	SKU string `json:"sku"`

	// Vendor is a plain field: externalDocs attaches directly to the property.
	//
	// externalDocs: {description: "Vendor field docs", url: "https://example.com/docs/vendor"}
	Vendor string `json:"vendor"`

	// Supplier is a $ref'd field: its sibling externalDocs lifts onto the
	// field's allOf compound (a bare $ref cannot carry sibling keywords).
	//
	// externalDocs: {description: "Supplier docs", url: "https://example.com/docs/supplier"}
	Supplier Supplier `json:"supplier"`
}

// Supplier is referenced by CatalogEntry.Supplier.
//
// swagger:model
type Supplier struct {
	// Name is the supplier name.
	Name string `json:"name"`
}

Full source: docs/examples/concepts/routes/routes.go

operation externalDocs
{
  "description": "Search guide",
  "url": "https://example.com/docs/search"
}

Full source: docs/examples/concepts/routes/testdata/externaldocs.json

On a model the link rides the definition, and it also rides individual struct fields: on a plain field it attaches to the property directly; on a $ref’d field it is lifted onto the field’s allOf compound (a bare $ref cannot carry sibling keywords). The value can be written as the indented block above or as an equivalent inline { description: …, url: … } mapping β€” which reads better on a single-line doc comment:

Annotated Go
// swagger:route GET /pets/search pets searchPets
//
// Searches pets. The operation links out to external documentation.
//
// externalDocs:
//   description: Search guide
//   url: https://example.com/docs/search
//
// responses:
//
//	200: petsResponse

// CatalogEntry carries externalDocs at the schema level (the link rides the
// definition) and on its fields. (On a simple-schema parameter externalDocs is
// dropped with a diagnostic: it is a full-Schema-only keyword.)
//
// externalDocs: {description: "Catalog schema reference", url: "https://example.com/docs/catalog"}
//
// swagger:model
type CatalogEntry struct {
	// SKU is the catalog identifier.
	SKU string `json:"sku"`

	// Vendor is a plain field: externalDocs attaches directly to the property.
	//
	// externalDocs: {description: "Vendor field docs", url: "https://example.com/docs/vendor"}
	Vendor string `json:"vendor"`

	// Supplier is a $ref'd field: its sibling externalDocs lifts onto the
	// field's allOf compound (a bare $ref cannot carry sibling keywords).
	//
	// externalDocs: {description: "Supplier docs", url: "https://example.com/docs/supplier"}
	Supplier Supplier `json:"supplier"`
}

// Supplier is referenced by CatalogEntry.Supplier.
//
// swagger:model
type Supplier struct {
	// Name is the supplier name.
	Name string `json:"name"`
}

Full source: docs/examples/concepts/routes/routes.go

#/definitions/CatalogEntry
{
  "description": "CatalogEntry carries externalDocs at the schema level (the link rides the\ndefinition) and on its fields. (On a simple-schema parameter externalDocs is\ndropped with a diagnostic: it is a full-Schema-only keyword.)",
  "type": "object",
  "properties": {
    "sku": {
      "description": "SKU is the catalog identifier.",
      "type": "string",
      "x-go-name": "SKU"
    },
    "supplier": {
      "description": "Supplier is a $ref'd field: its sibling externalDocs lifts onto the\nfield's allOf compound (a bare $ref cannot carry sibling keywords).",
      "allOf": [
        {
          "$ref": "#/definitions/Supplier"
        }
      ],
      "x-go-name": "Supplier",
      "externalDocs": {
        "description": "Supplier docs",
        "url": "https://example.com/docs/supplier"
      }
    },
    "vendor": {
      "description": "Vendor is a plain field: externalDocs attaches directly to the property.",
      "type": "string",
      "x-go-name": "Vendor",
      "externalDocs": {
        "description": "Vendor field docs",
        "url": "https://example.com/docs/vendor"
      }
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/routes",
  "externalDocs": {
    "description": "Catalog schema reference",
    "url": "https://example.com/docs/catalog"
  }
}

Full source: docs/examples/concepts/routes/testdata/externaldocs_schema.json

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Sharing parameters & responses

When the same header, query parameter or error response appears on many operations, you don’t have to repeat it. codescan can publish a parameter or response once into the spec’s top-level parameters / responses maps, then reference it from each operation as a $ref. This is the OpenAPI 2.0 shared namespace (#/parameters/{name}, #/responses/{name}).

Each pane below pairs the annotated Go (left) with the exact fragment the scanner emits (right), from the test-covered docs/examples/concepts/sharedparams package. For the per-operation basics this builds on β€” the plain swagger:parameters <operationID> and swagger:response <name> forms β€” see Routes & operations.

Declaring a shared parameter

swagger:parameters * declares a struct whose fields are registered at the spec top level, #/parameters/{name}, keyed by each parameter’s resolved name. The bare * is register-only: it publishes the parameter but does not, by itself, attach it to any operation.

The convenience form swagger:parameters * <operationID>… does both at once β€” it registers the parameter and $refs it into the listed operations, which is handy for a small spec.

// CommonHeaders registers a reusable header parameter at the spec top
// level, #/parameters/X-Request-ID. The bare `*` target is register-only:
// it publishes the parameter but does not, by itself, attach it to any
// operation.
//
// swagger:parameters *
type CommonHeaders struct {
	// RequestID correlates a request across services.
	//
	// in: header
	RequestID string `json:"X-Request-ID"`
}

// AuthHeader registers #/parameters/X-API-Key and, in the same breath,
// $ref's it into the createPet operation β€” the convenient `* <opid>`
// form for a small spec.
//
// swagger:parameters * createPet
type AuthHeader struct {
	// APIKey authorises access.
	//
	// in: header
	// required: true
	APIKey string `json:"X-API-Key"`
}


// ErrorResponse is the common error envelope returned by every operation.
//
// swagger:response *
type ErrorResponse struct {
	// in: body
	Body struct {
		// Code is a machine-readable error code.
		Code int `json:"code"`
		// Message is a human-readable error message.
		Message string `json:"message"`
	} `json:"body"`
}

Full source: docs/examples/concepts/sharedparams/sharedparams.go

Both structs land in the top-level parameters map, each keyed by its parameter name (the json: tag, or a name: / swagger:name override):

{
  "X-API-Key": {
    "type": "string",
    "x-go-name": "APIKey",
    "description": "APIKey authorises access.",
    "name": "X-API-Key",
    "in": "header",
    "required": true
  },
  "X-Request-ID": {
    "type": "string",
    "x-go-name": "RequestID",
    "description": "RequestID correlates a request across services.",
    "name": "X-Request-ID",
    "in": "header"
  }
}

Full source: docs/examples/concepts/sharedparams/testdata/parameters.json

Referencing a shared parameter

Once a parameter is registered, an operation can pull it in by name. There are two reference channels:

  • swagger:parameters * <operationID> on the declaring struct β€” the convenience form above. AuthHeader uses it to inject X-API-Key into createPet.
  • swagger:parameters <operationID> <name>… as a standalone marker on the operation’s function β€” the scaling channel: the shared struct need not enumerate every operation that wants the parameter; instead each operation opts in next to its own swagger:route.
// ListPets lists pets.
//
// The standalone reference marker on the next line pulls the shared
// X-Request-ID parameter into this operation as a $ref. This is the
// scaling channel: the shared struct need not enumerate every operation
// that wants the parameter.
//
// swagger:route GET /pets pets listPets
// swagger:parameters listPets X-Request-ID
// Responses:
//
//	default: ErrorResponse
func ListPets() {}

// CreatePet creates a pet. Its X-API-Key comes from AuthHeader
// (#/parameters/X-API-Key, $ref'd via `* createPet`); its body comes from
// the inlined CreatePetParams.
//
// swagger:route POST /pets pets createPet
// Responses:
//
//	default: ErrorResponse
func CreatePet() {}

Full source: docs/examples/concepts/sharedparams/sharedparams.go

listPets opts in through the standalone marker, so its only parameter is a $ref to the shared X-Request-ID:

{
  "tags": [
    "pets"
  ],
  "operationId": "listPets",
  "parameters": [
    {
      "$ref": "#/parameters/X-Request-ID"
    }
  ],
  "responses": {
    "default": {
      "$ref": "#/responses/ErrorResponse"
    }
  }
}

Full source: docs/examples/concepts/sharedparams/testdata/listpets.json

createPet receives the $ref’d X-API-Key (from * createPet) alongside its own inlined body parameter β€” references and inline parameters coexist:

{
  "tags": [
    "pets"
  ],
  "operationId": "createPet",
  "parameters": [
    {
      "x-go-name": "Body",
      "name": "body",
      "in": "body",
      "required": true,
      "schema": {
        "$ref": "#/definitions/Pet"
      }
    },
    {
      "$ref": "#/parameters/X-API-Key"
    }
  ],
  "responses": {
    "default": {
      "$ref": "#/responses/ErrorResponse"
    }
  }
}

Full source: docs/examples/concepts/sharedparams/testdata/createpet.json

Path-item parameters

A parameter can also attach to a whole path rather than a single operation. swagger:parameters /path inlines a struct’s fields into the path-item’s parameters array, so every operation under that path inherits them.

// TenantHeader inlines a required header into the /pets/{id} path-item
// itself, so every operation under that exact path inherits it. The
// target is a literal path, and matching is exact β€” OAS2 has no path
// hierarchy, so this does NOT apply to /pets.
//
// swagger:parameters /pets/{id}
type TenantHeader struct {
	// Tenant scopes the request to a customer.
	//
	// in: header
	// required: true
	Tenant string `json:"X-Tenant"`
}

// GetPet fetches one pet. It declares no header of its own; X-Tenant
// reaches it through the /pets/{id} path-item parameter above.
//
// swagger:route GET /pets/{id} pets getPet
// Responses:
//
//	default: ErrorResponse
func GetPet() {}

Full source: docs/examples/concepts/sharedparams/sharedparams.go

X-Tenant now rides the /pets/{id} path-item; getPet inherits it without declaring a header of its own:

{
  "get": {
    "tags": [
      "pets"
    ],
    "operationId": "getPet",
    "responses": {
      "default": {
        "$ref": "#/responses/ErrorResponse"
      }
    }
  },
  "parameters": [
    {
      "type": "string",
      "x-go-name": "Tenant",
      "description": "Tenant scopes the request to a customer.",
      "name": "X-Tenant",
      "in": "header",
      "required": true
    }
  ]
}

Full source: docs/examples/concepts/sharedparams/testdata/pathitem.json

Warning

Exact path, no hierarchy. OpenAPI 2.0 has no path nesting, so the target is matched literally: swagger:parameters /pets/{id} applies to /pets/{id} only β€” not to /pets. Path-item parameters also co-exist with operation-level ones rather than replacing them; if an operation declares a parameter with the same (name, in), the operation’s wins at resolution time per the OAS2 rule.

To $ref an already-registered shared parameter into a path-item (instead of inlining a new one), use the reference form with a path target: swagger:parameters /path <name>… β€” the path-item analogue of the per-operation marker above.

Shared responses

Responses share the same way. swagger:response * registers a struct at #/responses/{name} (keyed by the Go type name). The * is a synonym for the bare/named swagger:response form β€” its job is to mark the response as a shared one. Operations then name it in their Responses: block and it is emitted as a $ref.

// ErrorResponse is the common error envelope returned by every operation.
//
// swagger:response *
type ErrorResponse struct {
	// in: body
	Body struct {
		// Code is a machine-readable error code.
		Code int `json:"code"`
		// Message is a human-readable error message.
		Message string `json:"message"`
	} `json:"body"`
}

Full source: docs/examples/concepts/sharedparams/sharedparams.go

The shared ErrorResponse lands in the top-level responses map:

{
  "ErrorResponse": {
    "description": "ErrorResponse is the common error envelope returned by every operation.",
    "schema": {
      "type": "object",
      "properties": {
        "code": {
          "description": "Code is a machine-readable error code.",
          "type": "integer",
          "format": "int64",
          "x-go-name": "Code"
        },
        "message": {
          "description": "Message is a human-readable error message.",
          "type": "string",
          "x-go-name": "Message"
        }
      }
    }
  }
}

Full source: docs/examples/concepts/sharedparams/testdata/responses.json

Both routes write default: ErrorResponse, which resolves to a single shared $ref (visible as responses.default.$ref in the operation panes above) β€” one error envelope, defined once, referenced everywhere.

Conflicts, duplicates & dangling references

The shared namespace is referenced only by short name, so codescan cannot silently rename a collision the way it deconflicts model definitions. Instead it applies a deterministic, observable policy and reports every adjustment through Options.OnDiagnostic (the scan never fails on these β€” it keeps a valid spec and warns):

SituationPolicyDiagnostic
Two swagger:parameters * register the same namekeep-first (sorted by package path then position; never renamed) β€” later one droppedscan.shared-parameter-conflict
Two swagger:response * register the same namekeep-first; later one droppedscan.shared-response-conflict
A reference names a parameter no * registeredreference dropped (no dangling $ref emitted)scan.dangling-parameter-ref
An operation names an unregistered shared responsereference droppedscan.dangling-response-ref
A * <opid>… marker repeats an operation idduplicate droppedscan.duplicate-target
A reference repeats a parameter namecollapses to a single $refscan.duplicate-ref
Note

The shared parameters, responses and definitions namespaces are independent: #/parameters/Status, #/responses/Status and #/definitions/Status can all coexist. The resolved (name:-overridden) name is the key, so references must use that name β€” not the Go field name. An InputSpec overlay entry seeds the namespace and wins any keep-first conflict.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Validations

Validations are keyword-driven: you write keyword: value lines in a field’s doc comment and they become minimum, maxLength, pattern, enum, and the rest of the validation surface on that property. Each pane below pairs the annotated Go (left) with the fragment the scanner emits (right), from the test-covered docs/examples/concepts/validations package.

For the per-keyword reference card β€” value shapes, aliases, and legal contexts β€” see Keywords.

On a model field β€” the full surface

A swagger:model field accepts the full JSON-schema validation vocabulary:

  • Numeric β€” minimum, maximum, multipleOf (on Price).
  • Length β€” min length / max length (on Name).
  • Arrays β€” min items / max items / unique (on Tags).
  • Pattern β€” a regular expression (on SKU).
  • Enum β€” a fixed value set (on Grade).
  • Required β€” required: true lifts the property into the schema’s object-level required array (sku).
Annotated Go
// Product is a model whose fields carry the full JSON-schema validation surface.
//
// swagger:model
type Product struct {
	// SKU is the stock code.
	//
	// required: true
	// pattern: ^[A-Z]{3}-[0-9]{4}$
	SKU string `json:"sku"`

	// Price is the price in cents.
	//
	// minimum: 1
	// maximum: 1000000
	// multipleOf: 1
	Price int64 `json:"price"`

	// Name is the display name.
	//
	// min length: 1
	// max length: 120
	Name string `json:"name"`

	// Grade is a quality band.
	//
	// enum: A,B,C
	Grade string `json:"grade"`

	// Tags label the product.
	//
	// min items: 1
	// max items: 10
	// unique: true
	Tags []string `json:"tags"`
}

Full source: docs/examples/concepts/validations/validations.go

{
  "type": "object",
  "title": "Product is a model whose fields carry the full JSON-schema validation surface.",
  "required": [
    "sku"
  ],
  "properties": {
    "grade": {
      "description": "Grade is a quality band.",
      "type": "string",
      "enum": [
        "A",
        "B",
        "C"
      ],
      "x-go-name": "Grade"
    },
    "name": {
      "description": "Name is the display name.",
      "type": "string",
      "maxLength": 120,
      "minLength": 1,
      "x-go-name": "Name"
    },
    "price": {
      "description": "Price is the price in cents.",
      "type": "integer",
      "format": "int64",
      "maximum": 1000000,
      "minimum": 1,
      "multipleOf": 1,
      "x-go-name": "Price"
    },
    "sku": {
      "description": "SKU is the stock code.",
      "type": "string",
      "pattern": "^[A-Z]{3}-[0-9]{4}$",
      "x-go-name": "SKU"
    },
    "tags": {
      "description": "Tags label the product.",
      "type": "array",
      "maxItems": 10,
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "type": "string"
      },
      "x-go-name": "Tags"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/validations"
}

Full source: docs/examples/concepts/validations/testdata/field.json

On parameters β€” the simple-schema surface

Info

Simple schemas have a reduced surface. Parameters other than in: body, and response headers, are simple schemas in OpenAPI 2.0 β€” not full JSON schemas. They accept the validation subset (maximum/minimum/multipleOf, maxLength/minLength/pattern, maxItems/minItems/uniqueItems, enum, plus the simple-schema-only collectionFormat) but not schema-only constructs. A schema-only keyword such as readOnly placed on a query parameter is simply not emitted β€” spec.Parameter has nowhere to carry it.

A Go map field has no simple-schema representation either: on a non-body parameter or a response header it is skipped with a validate.unsupported-in-simple-schema warning. Maps are only representable on a body schema (as object + additionalProperties).

An array element is itself a simple schema, so it may not be a $ref. A named-primitive element ([]Label, underlying string) expands inline to its type; an object element ([]SomeStruct) has no simple-schema form and dissolves to an empty items: {} with the same warning. Use a body schema for an array of objects.

The same numeric and length keywords work on a query parameter; arrays add collectionFormat:

Annotated Go
// SearchParams is the simple-schema parameter set for searchProducts. Query
// parameters accept the reduced OAS 2.0 validation surface.
//
// swagger:parameters searchProducts
type SearchParams struct {
	// Q is the search text.
	//
	// in: query
	// min length: 3
	// max length: 50
	Q string `json:"q"`

	// Limit caps the number of results.
	//
	// in: query
	// minimum: 1
	// maximum: 100
	Limit int32 `json:"limit"`

	// Sort lists the sort fields.
	//
	// in: query
	// collection format: csv
	// unique: true
	Sort []string `json:"sort"`
}

Full source: docs/examples/concepts/validations/validations.go

parameters on searchProducts
[
  {
    "maxLength": 50,
    "minLength": 3,
    "type": "string",
    "x-go-name": "Q",
    "description": "Q is the search text.",
    "name": "q",
    "in": "query"
  },
  {
    "maximum": 100,
    "minimum": 1,
    "type": "integer",
    "format": "int32",
    "x-go-name": "Limit",
    "description": "Limit caps the number of results.",
    "name": "limit",
    "in": "query"
  },
  {
    "uniqueItems": true,
    "type": "array",
    "items": {
      "type": "string"
    },
    "collectionFormat": "csv",
    "x-go-name": "Sort",
    "description": "Sort lists the sort fields.",
    "name": "sort",
    "in": "query"
  }
]

Full source: docs/examples/concepts/validations/testdata/param.json

On response headers

A response header is also a simple schema, so it takes the same reduced validation set (here minimum on an integer header). Note headers carry no required flag.

Annotated Go
// RateLimited is a response carrying a validated header (a simple schema).
//
// swagger:response rateLimited
type RateLimited struct {
	// XRateRemaining is the remaining request budget.
	//
	// minimum: 0
	XRateRemaining int32 `json:"X-Rate-Remaining"`
}

Full source: docs/examples/concepts/validations/validations.go

responses[rateLimited]
{
  "description": "RateLimited is a response carrying a validated header (a simple schema).",
  "headers": {
    "X-Rate-Remaining": {
      "minimum": 0,
      "type": "integer",
      "format": "int32",
      "description": "XRateRemaining is the remaining request budget."
    }
  }
}

Full source: docs/examples/concepts/validations/testdata/header.json

On an object β€” property count and name patterns

The object-validation keywords constrain a free-form object as a whole rather than named fields: minProperties / maxProperties bound the property count, and patternProperties permits properties whose name matches a regex. They are schema-only β€” kept on an object-typed model, stripped (with a diagnostic) on a scalar model or a simple-schema parameter. For dynamic-key objects, the typed value forms, and additionalProperties, see Maps & free-form objects.

Annotated Go
// Attributes is a free-form object constrained by the object-validation
// keywords: it must carry between 1 and 10 properties, and any property whose
// name matches the regex is permitted. Object validations constrain the map of
// (additional) properties rather than named struct fields.
//
// minProperties: 1
// maxProperties: 10
// patternProperties: ^x-
//
// swagger:model Attributes
type Attributes map[string]any

Full source: docs/examples/concepts/validations/validations.go

#/definitions/Attributes
{
  "description": "Attributes is a free-form object constrained by the object-validation\nkeywords: it must carry between 1 and 10 properties, and any property whose\nname matches the regex is permitted. Object validations constrain the map of\n(additional) properties rather than named struct fields.",
  "type": "object",
  "maxProperties": 10,
  "minProperties": 1,
  "additionalProperties": {},
  "patternProperties": {
    "^x-": {}
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/validations"
}

Full source: docs/examples/concepts/validations/testdata/object.json

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Examples & defaults

Example values and defaults are documentation that travels with the schema: an example: shows a caller what a value looks like, a default: declares what the field is when the caller omits it. Both are typed to the field β€” a numeric default on an integer field is a JSON number, not a string. The panes below pair the annotated Go with the fragment the scanner emits, from the test-covered docs/examples/concepts/examples package.

For the exact value shapes these keywords accept, see Keywords.

example

example: <value> attaches an example to the property, coerced to the field’s type β€” Hello, world! stays a string, 3 becomes a number.

Annotated Go
// Greeting carries an example value for documentation.
//
// swagger:model
type Greeting struct {
	// Message is the greeting text.
	//
	// example: Hello, world!
	Message string `json:"message"`

	// Count is how many times to repeat it.
	//
	// example: 3
	Count int32 `json:"count"`
}

Full source: docs/examples/concepts/examples/examples.go

{
  "type": "object",
  "title": "Greeting carries an example value for documentation.",
  "properties": {
    "count": {
      "description": "Count is how many times to repeat it.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Count",
      "example": 3
    },
    "message": {
      "description": "Message is the greeting text.",
      "type": "string",
      "x-go-name": "Message",
      "example": "Hello, world!"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/examples"
}

Full source: docs/examples/concepts/examples/testdata/example.json

The value is not limited to scalars. A JSON literal is parsed into a structured example β€” a { … } object on a map field, a [ … ] array on a slice field. A bare comma-separated list (example: a,b) is not split; it is kept verbatim as a string, so write example: ["a","b"] when you need an array.

On a plain string field a surrounding pair of double quotes is treated as delimiters and stripped β€” so example: "Foo" yields Foo, and example: "" sets an empty string (the same applies to default:). Bare values keep their text as-is.

Annotated Go
// Profile carries structured (non-scalar) example values. A JSON object literal
// on a map field and a JSON array literal on a slice field are parsed into
// structured examples β€” a bare comma-separated list would instead be kept
// verbatim as a string.
//
// swagger:model
type Profile struct {
	// Labels is a set of key/value labels.
	//
	// example: {"env":"prod","tier":"gold"}
	Labels map[string]string `json:"labels"`

	// Roles is the list of assigned roles.
	//
	// example: ["admin","auditor"]
	Roles []string `json:"roles"`
}

Full source: docs/examples/concepts/examples/examples.go

#/definitions/Profile
{
  "description": "Profile carries structured (non-scalar) example values. A JSON object literal\non a map field and a JSON array literal on a slice field are parsed into\nstructured examples β€” a bare comma-separated list would instead be kept\nverbatim as a string.",
  "type": "object",
  "properties": {
    "labels": {
      "description": "Labels is a set of key/value labels.",
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "x-go-name": "Labels",
      "example": {
        "env": "prod",
        "tier": "gold"
      }
    },
    "roles": {
      "description": "Roles is the list of assigned roles.",
      "type": "array",
      "items": {
        "type": "string"
      },
      "x-go-name": "Roles",
      "example": [
        "admin",
        "auditor"
      ]
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/examples"
}

Full source: docs/examples/concepts/examples/testdata/complexexample.json

default

default: <value> sets the property’s default, again typed to the field β€” 8080 is a number, false a boolean, auto a string.

Annotated Go
// Settings carries default values applied when a field is omitted.
//
// swagger:model
type Settings struct {
	// Port is the listen port.
	//
	// default: 8080
	Port int32 `json:"port"`

	// Mode is the run mode.
	//
	// default: auto
	Mode string `json:"mode"`

	// Verbose toggles verbose logging.
	//
	// default: false
	Verbose bool `json:"verbose"`
}

Full source: docs/examples/concepts/examples/examples.go

#/definitions/Settings
{
  "type": "object",
  "title": "Settings carries default values applied when a field is omitted.",
  "properties": {
    "mode": {
      "description": "Mode is the run mode.",
      "type": "string",
      "default": "auto",
      "x-go-name": "Mode"
    },
    "port": {
      "description": "Port is the listen port.",
      "type": "integer",
      "format": "int32",
      "default": 8080,
      "x-go-name": "Port"
    },
    "verbose": {
      "description": "Verbose toggles verbose logging.",
      "type": "boolean",
      "default": false,
      "x-go-name": "Verbose"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/examples"
}

Full source: docs/examples/concepts/examples/testdata/default.json

swagger:default (deprecated)

Warning

swagger:default never emitted a default into the spec. It is now an inert sink that raises a validate.deprecated diagnostic. Use the default: keyword above.

The keyword covers every place OpenAPI 2.0 admits a default value: a model field, a non-body parameter, a header, and array items. The one remaining sense of “default” β€” an operation’s default response β€” is not a value at all; it is written as a response code in a route’s responses: body:

// swagger:route GET /pets pets listPets
//
// Lists pets in the store, optionally filtered by tag.
//
// responses:
//
//	200: petsResponse
//	default: errorResponse

Full source: docs/examples/concepts/routes/routes.go

On a defined-type field

When a field’s type is a named (defined) type, it renders as a $ref to that type’s definition β€” and a $ref cannot carry sibling keywords. An example: or default: on such a field is therefore preserved on the override arm of an allOf compound, so the value still reaches the spec.

Annotated Go
// Currency is a named (defined) string type, so it earns its own definition and
// a field typed Currency renders as a $ref. A $ref cannot carry sibling
// keywords, so an example or default on such a field rides the override arm of
// an allOf compound β€” the value still reaches the spec.
//
// swagger:model
type Currency string

// Price shows example + default on a defined-type field.
//
// swagger:model
type Price struct {
	// Unit is the ISO currency code.
	//
	// default: USD
	// example: EUR
	Unit Currency `json:"unit"`
}

Full source: docs/examples/concepts/examples/examples.go

#/definitions/Price
{
  "type": "object",
  "title": "Price shows example + default on a defined-type field.",
  "properties": {
    "unit": {
      "description": "Unit is the ISO currency code.",
      "allOf": [
        {
          "$ref": "#/definitions/Currency"
        },
        {
          "default": "USD",
          "example": "EUR"
        }
      ],
      "x-go-name": "Unit"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/examples"
}

Full source: docs/examples/concepts/examples/testdata/reffield.json

A JSON object or array literal on a $ref’d field is coerced into a structured value on that override arm β€” exactly as it is on a plain field β€” so the example reads as real JSON, not an escaped string. A bare scalar is the exception: on the override arm the referenced type is unknown, so a scalar stays a string rather than being silently retyped.

Annotated Go
// Coordinates is a defined struct, so a field typed Coordinates renders as a
// $ref.
//
// swagger:model
type Coordinates struct {
	// Lat is the latitude.
	Lat float64 `json:"lat"`

	// Lng is the longitude.
	Lng float64 `json:"lng"`
}

// Place shows a JSON-object example on a $ref'd field. Because the field is a
// $ref, the example rides the override arm of the allOf β€” and a JSON object (or
// array) literal is coerced into a structured value there, exactly as it is on a
// plain field. A bare scalar would instead stay a string, since the referenced
// type is not known on the override arm.
//
// swagger:model
type Place struct {
	// At is the location.
	//
	// example: {"lat":48.85,"lng":2.35}
	At Coordinates `json:"at"`
}

Full source: docs/examples/concepts/examples/examples.go

#/definitions/Place
{
  "description": "Place shows a JSON-object example on a $ref'd field. Because the field is a\n$ref, the example rides the override arm of the allOf β€” and a JSON object (or\narray) literal is coerced into a structured value there, exactly as it is on a\nplain field. A bare scalar would instead stay a string, since the referenced\ntype is not known on the override arm.",
  "type": "object",
  "properties": {
    "at": {
      "description": "At is the location.",
      "allOf": [
        {
          "$ref": "#/definitions/Coordinates"
        },
        {
          "example": {
            "lat": 48.85,
            "lng": 2.35
          }
        }
      ],
      "x-go-name": "At"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/examples"
}

Full source: docs/examples/concepts/examples/testdata/refstructured.json

On a response body

example: is not limited to model fields. On a swagger:response whose body is a top-level array (or other non-struct) type, the example lands on the response body schema:

Annotated Go
// NTPServers is a top-level array response carrying an example. The example
// lands on the response body schema rather than being dropped.
//
// swagger:response ntpServers
// example: ["10.0.0.1","10.0.0.2"]
type NTPServers []string

// swagger:route GET /ntp ntp listNTP
//
// responses:
//
//	200: ntpServers


// Pet is the response payload.
//
// swagger:model Pet
type Pet struct {
	Name string `json:"name"`
}

// PetResponse returns a pet, with one example payload per media type.
//
// The plural `examples:` keyword on a struct swagger:response is a YAML map
// keyed by media type, populating the OpenAPI response `examples` object.
//
// swagger:response petResponse
//
// examples:
//
//	application/json:
//	  name: Fluffy
//	application/xml: "<pet><name>Fluffy</name></pet>"
type PetResponse struct {
	// in: body
	Body Pet `json:"body"`
}

// swagger:route GET /pets pets listPets
//
// responses:
//
//	200: petResponse

Full source: docs/examples/concepts/examples/examples.go

responses[ntpServers]
{
  "description": "NTPServers is a top-level array response carrying an example. The example\nlands on the response body schema rather than being dropped.",
  "schema": {
    "type": "array",
    "items": {
      "type": "string"
    },
    "example": [
      "10.0.0.1",
      "10.0.0.2"
    ]
  }
}

Full source: docs/examples/concepts/examples/testdata/responseexample.json

Response examples by media type

A response can carry an examples: map keyed by media type β€” these populate the OpenAPI response examples object, one example payload per content type. Both annotation styles support it.

In a swagger:operation YAML body, examples: sits under the response code:

// swagger:operation GET /status status getStatus
//
// ---
// responses:
//   '200':
//     description: Success
//     examples:
//       application/json:
//         hello: world

On a struct-based swagger:response, the same examples: block lives in the declaration comment (the plural examples: is the response keyword; the singular example: above is the schema decorator) and produces the same response examples object:

Annotated Go
// Pet is the response payload.
//
// swagger:model Pet
type Pet struct {
	Name string `json:"name"`
}

// PetResponse returns a pet, with one example payload per media type.
//
// The plural `examples:` keyword on a struct swagger:response is a YAML map
// keyed by media type, populating the OpenAPI response `examples` object.
//
// swagger:response petResponse
//
// examples:
//
//	application/json:
//	  name: Fluffy
//	application/xml: "<pet><name>Fluffy</name></pet>"
type PetResponse struct {
	// in: body
	Body Pet `json:"body"`
}

// swagger:route GET /pets pets listPets
//
// responses:
//
//	200: petResponse

Full source: docs/examples/concepts/examples/examples.go

responses[petResponse]
{
  "description": "PetResponse returns a pet, with one example payload per media type.\n\nThe plural `examples:` keyword on a struct swagger:response is a YAML map\nkeyed by media type, populating the OpenAPI response `examples` object.",
  "schema": {
    "$ref": "#/definitions/Pet"
  },
  "examples": {
    "application/json": {
      "name": "Fluffy"
    },
    "application/xml": "\u003cpet\u003e\u003cname\u003eFluffy\u003c/name\u003e\u003c/pet\u003e"
  }
}

Full source: docs/examples/concepts/examples/testdata/responseexamplesbymime.json

Because the example lives on the response, one shared model can carry a different example per response code and per operation β€” a 200 and a 404 that both return the same error model each show their own illustrative payload, with no need for a distinct struct per case.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Other type decorators

Beyond validations, a couple of keyword decorators annotate a property’s or operation’s role. The panes below pair the annotated Go with the fragment the scanner emits, from the test-covered docs/examples/concepts/decorators package.

For the value shapes and legal contexts of each, see the Keyword reference.

readOnly

read only: true on a model field marks the property readOnly β€” the server sets it, clients must not.

Annotated Go
// Token is issued by the server.
//
// swagger:model
type Token struct {
	// ID is assigned by the server and cannot be set by clients.
	//
	// read only: true
	ID string `json:"id"`

	// Value is the token value.
	Value string `json:"value"`
}

Full source: docs/examples/concepts/decorators/decorators.go

{
  "type": "object",
  "title": "Token is issued by the server.",
  "properties": {
    "id": {
      "description": "ID is assigned by the server and cannot be set by clients.",
      "type": "string",
      "x-go-name": "ID",
      "readOnly": true
    },
    "value": {
      "description": "Value is the token value.",
      "type": "string",
      "x-go-name": "Value"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/decorators"
}

Full source: docs/examples/concepts/decorators/testdata/readonly.json

This is the idiomatic way to model server-set fields (an id, a createdAt) that appear in responses but should not be supplied on create β€” one model, marked readOnly, rather than separate request/response structs. (codescan does not hide fields per operation; if you truly need different shapes, declare distinct request and response models.)

deprecated

deprecated: true in a swagger:route / swagger:operation body marks the operation deprecated.

Annotated Go
// swagger:route GET /legacy/ping legacy ping
//
// Ping is the legacy health check.
//
// deprecated: true
//
// responses:
//
//	200: pingResponse


// Gadget is a deprecated model. OpenAPI 2.0 has no native `deprecated` on a
// schema, so codescan emits `x-deprecated: true` β€” here triggered by the
// godoc-style "Deprecated:" paragraph, which is recognised on its own without a
// separate annotation. (The explicit `deprecated: true` annotation, shown on the
// operation above, has the same effect on a model or field.)
//
// Deprecated: superseded by the v2 widget API.
//
// swagger:model
type Gadget struct {
	// SerialNo is the legacy identifier.
	//
	// Deprecated: use the v2 identifier instead.
	SerialNo string `json:"serialNo"`

	// Name is the current display name.
	Name string `json:"name"`
}

Full source: docs/examples/concepts/decorators/decorators.go

paths[/legacy/ping]
{
  "get": {
    "tags": [
      "legacy"
    ],
    "summary": "Ping is the legacy health check.",
    "operationId": "ping",
    "deprecated": true,
    "responses": {
      "200": {
        "$ref": "#/responses/pingResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/decorators/testdata/deprecated.json

Info

On an operation, deprecated: true sets the native OpenAPI 2.0 deprecated field. OpenAPI 2.0 has no native deprecated on the Schema object, so on a model or model field codescan emits the x-deprecated: true vendor extension instead.

A godoc-style Deprecated: paragraph (the pkgsite convention) is an exact synonym for deprecated: true, recognised in any context. On a Go doc comment it is the natural form β€” a bare // deprecated: true line there reads as a malformed deprecation notice to Go linters, whereas the capitalised Deprecated: paragraph is idiomatic. Use deprecated: true in the indented route / operation bodies, and the Deprecated: paragraph on model and field doc comments; either yields the same result. x-deprecated carries semantic intent rather than reflection metadata, so it is emitted even when SkipExtensions is set.

A godoc Deprecated: paragraph marks a model and its fields β€” codescan emits x-deprecated: true on each (the explicit deprecated: true annotation has the same effect):

Annotated Go
// Gadget is a deprecated model. OpenAPI 2.0 has no native `deprecated` on a
// schema, so codescan emits `x-deprecated: true` β€” here triggered by the
// godoc-style "Deprecated:" paragraph, which is recognised on its own without a
// separate annotation. (The explicit `deprecated: true` annotation, shown on the
// operation above, has the same effect on a model or field.)
//
// Deprecated: superseded by the v2 widget API.
//
// swagger:model
type Gadget struct {
	// SerialNo is the legacy identifier.
	//
	// Deprecated: use the v2 identifier instead.
	SerialNo string `json:"serialNo"`

	// Name is the current display name.
	Name string `json:"name"`
}

Full source: docs/examples/concepts/decorators/decorators.go

#/definitions/Gadget
{
  "description": "Deprecated: superseded by the v2 widget API.",
  "type": "object",
  "title": "Gadget is a deprecated model. OpenAPI 2.0 has no native `deprecated` on a\nschema, so codescan emits `x-deprecated: true` β€” here triggered by the\ngodoc-style \"Deprecated:\" paragraph, which is recognised on its own without a\nseparate annotation. (The explicit `deprecated: true` annotation, shown on the\noperation above, has the same effect on a model or field.)",
  "properties": {
    "name": {
      "description": "Name is the current display name.",
      "type": "string",
      "x-go-name": "Name"
    },
    "serialNo": {
      "description": "SerialNo is the legacy identifier.\n\nDeprecated: use the v2 identifier instead.",
      "type": "string",
      "x-deprecated": true,
      "x-go-name": "SerialNo"
    }
  },
  "x-deprecated": true,
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/decorators"
}

Full source: docs/examples/concepts/decorators/testdata/deprecated_model.json

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Document metadata

A single swagger:meta block on a package doc comment carries the document’s top-level metadata: its info (title, description, version, license, contact), the host and basePath, the default schemes, and consumes/produces. The pane pairs the annotated package with the document it produces, from the test-covered docs/examples/concepts/meta package.

swagger:meta

The block lives in the package doc comment. The title comes from the first line with the Package <name> prefix stripped; the following paragraph becomes the description. The indented Key: value lines and list blocks populate the rest β€” License: and Contact: parse into structured objects, and an ExternalDocs: block (description + url) populates the spec’s top-level externalDocs. An InfoExtensions: block adds x-* vendor extensions to the info object β€” this is where an x-logo (rendered by ReDoc / Swagger UI) goes.

Package doc comment
// Package meta Pet Store.
//
// A small API that demonstrates the document-level swagger:meta block: the
// package doc comment carries the spec's top-level metadata.
//
//	Schemes: https
//	Host: api.example.com
//	BasePath: /v1
//	Version: 1.2.0
//	License: Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0.html
//	Contact: API Team <api@example.com> https://example.com/support
//
//	Consumes:
//	  - application/json
//
//	Produces:
//	  - application/json
//
//	ExternalDocs:
//	  description: Full API guide
//	  url: https://example.com/docs
//
//	Tags:
//	- name: pets
//	  description: Everything about your Pets
//	  externalDocs:
//	    description: Find out more
//	    url: https://example.com/docs/pets
//	- name: store
//	  description: Access to Petstore orders
//	  x-display-name: Store
//
//	SecurityDefinitions:
//	  basic_auth:
//	    type: basic
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//
//	Security:
//	  basic_auth:
//
//	InfoExtensions:
//	  x-logo:
//	    url: https://example.com/logo.png
//	    altText: Example
//
// swagger:meta
package meta

Full source: docs/examples/concepts/meta/doc.go

the document
{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A small API that demonstrates the document-level swagger:meta block: the\npackage doc comment carries the spec's top-level metadata.",
    "title": "Pet Store.",
    "contact": {
      "name": "API Team",
      "url": "https://example.com/support",
      "email": "api@example.com"
    },
    "license": {
      "name": "Apache 2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    },
    "version": "1.2.0",
    "x-logo": {
      "altText": "Example",
      "url": "https://example.com/logo.png"
    }
  },
  "host": "api.example.com",
  "basePath": "/v1",
  "paths": {},
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "basic_auth": {
      "type": "basic"
    }
  },
  "security": [
    {
      "basic_auth": []
    }
  ],
  "tags": [
    {
      "description": "Everything about your Pets",
      "name": "pets",
      "externalDocs": {
        "description": "Find out more",
        "url": "https://example.com/docs/pets"
      }
    },
    {
      "description": "Access to Petstore orders",
      "name": "store",
      "x-display-name": "Store"
    }
  ],
  "externalDocs": {
    "description": "Full API guide",
    "url": "https://example.com/docs"
  }
}

Full source: docs/examples/concepts/meta/testdata/meta.json

Tags

A Tags: block declares the spec’s top-level tags β€” a YAML sequence of tag objects, each with a name, an optional description, a nested externalDocs, and any x-* vendor extensions. This is how you attach per-tag descriptions to the tags your routes reference (above, pets and store).

For the full meta keyword surface (security definitions, external docs, extensions, terms of service), see the swagger:meta reference and the meta keywords.

Security

The meta block above also declares SecurityDefinitions: (the auth schemes) and a Security: default β€” authentication is declared, not hand-rolled. Declaring schemes, requiring them per route, and overlaying security from outside the code have their own walkthrough: Security.

A build-time version

Version: is a static literal in source β€” there is no Options field for it. To stamp a version computed at build time, drive codescan as a library and set it on the returned document after Run:

doc, _ := codescan.Run(opts)
doc.Info.Version = buildVersion // e.g. injected via -ldflags "-X main.buildVersion=..."

Alternatively, overlay a base document that already carries the version with Options.InputSpec (see Overlaying a spec).

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Security

OpenAPI 2.0 splits authentication into two parts: security definitions name the schemes (an API key, OAuth2, HTTP Basic), and security requirements reference those schemes β€” document-wide and/or per operation. The panes below are backed by the test-covered docs/examples/concepts/security package.

Declare the schemes

A SecurityDefinitions: block in swagger:meta declares every scheme once; a Security: block sets the document-wide default requirement that applies to operations that do not state their own.

Package doc comment
// Package security Reports API.
//
// The swagger:meta block declares the security schemes once and sets the
// document-wide default requirement.
//
//	Version: 1.0.0
//
//	SecurityDefinitions:
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//	  oauth2:
//	    type: oauth2
//	    flow: accessCode
//	    authorizationUrl: https://example.com/auth
//	    tokenUrl: https://example.com/token
//	    scopes:
//	      read: read reports
//	      write: write reports
//
//	Security:
//	  - api_key: []
//
// swagger:meta
package security

Full source: docs/examples/concepts/security/doc.go

securityDefinitions + security
{
  "security": [
    {
      "api_key": []
    }
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "oauth2": {
      "type": "oauth2",
      "flow": "accessCode",
      "authorizationUrl": "https://example.com/auth",
      "tokenUrl": "https://example.com/token",
      "scopes": {
        "read": "read reports",
        "write": "write reports"
      }
    }
  }
}

Full source: docs/examples/concepts/security/testdata/schemes.json

The scheme type drives the rest: apiKey needs in + name, oauth2 needs a flow (and the URLs/scopes it implies), basic needs nothing more. The full scheme surface is in the securityDefinitions reference.

Require a scheme on a route

A route with no Security: keyword inherits the document-wide default (api_key, above). A route that needs something different states its own Security: requirement β€” here createReport requires oauth2 with the read and write scopes, overriding the default:

swagger:route
// listReports inherits the document-wide default requirement (api_key) β€” no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement β€”
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once β€” two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely β€” an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

[
  {
    "oauth2": [
      "read",
      "write"
    ]
  }
]

Full source: docs/examples/concepts/security/testdata/route.json

A Security: block is plain YAML β€” a sequence of requirement objects. Scopes are a flow list ([read, write]) or a block list; an empty list (api_key: []) is the scheme with no scopes. The combining rule follows OpenAPI 2.0:

  • multiple schemes in one item are ANDed β€” all are required;
  • separate items are ORed β€” satisfying any one grants access.

So requiring both an API key and an OAuth2 scope is two keys under a single sequence item:

swagger:route
// listReports inherits the document-wide default requirement (api_key) β€” no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement β€”
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once β€” two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely β€” an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

security on archiveReport (AND)
[
  {
    "api_key": [],
    "oauth2": [
      "write"
    ]
  }
]

Full source: docs/examples/concepts/security/testdata/and.json

A route’s requirements replace the document default for that operation. To make one operation public β€” opting out of the document-wide default β€” give it an empty Security: []. That emits an explicit empty requirement (distinct from omitting the keyword, which inherits the default):

swagger:route
// listReports inherits the document-wide default requirement (api_key) β€” no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement β€”
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once β€” two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely β€” an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

security on publicReport
{
  "security": []
}

Full source: docs/examples/concepts/security/testdata/public.json

The same works from a swagger:operation YAML body β€” a security: key there sets that operation’s requirement. (The schemes themselves are always global swagger:meta β€” OpenAPI 2.0 has no per-operation securityDefinitions.)

Keep security out of your code

Authentication is often handled by a layer in front of the app β€” a gateway or service mesh β€” and you may not want security details in the annotations at all. In that case, leave the code free of security annotations and overlay the schemes and requirements with Options.InputSpec:

// base carries only the security scheme + default requirement.
var base spec.Swagger
_ = json.Unmarshal(baseSpecJSON, &base)

doc, _ := codescan.Run(&codescan.Options{
    Packages:   []string{"./..."},
    ScanModels: true,
    InputSpec:  &base, // securityDefinitions + security come from here
})

The app package above (concepts/routes) carries no security annotations, yet the merged document is secured β€” the schemes and the default requirement come entirely from the base:

{
  "security": [
    {
      "api_key": []
    }
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    }
  }
}

Full source: docs/examples/concepts/security/testdata/overlay.json

See Overlaying a spec for the full InputSpec merge semantics.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Putting it together

This capstone scans a tiny annotated “petstore” package and produces a Swagger 2.0 spec β€” the concepts from the pages above, assembled into one runnable example. It is the worked version of usage as a library.

The annotated API

A package-level swagger:meta block sets the top-level metadata:

// Package petstore Petstore API
//
// A tiny pet store, used to demonstrate codescan annotations: the package
// comment is a `swagger:meta` block carrying the top-level metadata of the
// generated specification (title, description, version, base path, …).
//
//	Schemes: https
//	Version: 1.0.0
//	BasePath: /v1
//
//	Consumes:
//	- application/json
//
//	Produces:
//	- application/json
//
// swagger:meta

Full source: docs/examples/petstore/doc.go

A swagger:route registers a path and ties it to a response:

// swagger:route GET /pets pets listPets
//
// Lists all the pets in the store.
//
// responses:
//
//	200: petsResponse

Full source: docs/examples/petstore/pet.go

A swagger:model struct becomes a definition, with field comments driving validations:

// Pet is a single pet in the store.
//
// swagger:model Pet
type Pet struct {
	// The id of the pet.
	//
	// required: true
	// minimum: 1
	ID int64 `json:"id"`

	// The name of the pet.
	//
	// required: true
	// min length: 1
	Name string `json:"name"`

	// The tags associated with this pet.
	Tags []string `json:"tags,omitempty"`
}

Full source: docs/examples/petstore/pet.go

Running the scan

ScanPetstore builds the Options and calls codescan.Run:

opts := &codescan.Options{
	WorkDir:    workDir,                // module root to resolve patterns from
	Packages:   []string{"./petstore"}, // relative package pattern
	ScanModels: true,                   // also emit definitions for swagger:model types
}

doc, err := codescan.Run(opts)
if err != nil {
	return nil, err
}

Full source: docs/examples/basic/scan.go

The generated spec

Marshalling the returned *spec.Swagger to JSON yields the document below β€” the meta block became the top-level info / basePath, the swagger:route became the /pets path, and the swagger:model became the Pet definition:

{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A tiny pet store, used to demonstrate codescan annotations: the package\ncomment is a `swagger:meta` block carrying the top-level metadata of the\ngenerated specification (title, description, version, base path, …).",
    "title": "Petstore API",
    "version": "1.0.0"
  },
  "basePath": "/v1",
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          "pets"
        ],
        "summary": "Lists all the pets in the store.",
        "operationId": "listPets",
        "responses": {
          "200": {
            "$ref": "#/responses/petsResponse"
          }
        }
      }
    }
  },
  "definitions": {
    "Pet": {
      "type": "object",
      "title": "Pet is a single pet in the store.",
      "required": [
        "id",
        "name"
      ],
      "properties": {
        "id": {
          "description": "The id of the pet.",
          "type": "integer",
          "format": "int64",
          "minimum": 1,
          "x-go-name": "ID"
        },
        "name": {
          "description": "The name of the pet.",
          "type": "string",
          "minLength": 1,
          "x-go-name": "Name"
        },
        "tags": {
          "description": "The tags associated with this pet.",
          "type": "array",
          "items": {
            "type": "string"
          },
          "x-go-name": "Tags"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/petstore"
    }
  },
  "responses": {
    "petsResponse": {
      "description": "petsResponse is the list of pets returned by listPets.",
      "schema": {
        "type": "array",
        "items": {
          "$ref": "#/definitions/Pet"
        }
      }
    }
  }
}

Full source: docs/examples/basic/testdata/swagger.json

This JSON is not hand-written: it is a golden file the example’s test regenerates and compares on every run (UPDATE_GOLDEN=1 go test ./...). Because the example is ordinary, test-covered Go, go test ./docs/examples/... keeps the page honest β€” if the scanner’s output changes, CI fails before the documentation can go stale.

Seeing it rendered

The same golden spec, rendered as live API documentation by Swagger UI β€” what a consumer of the generated document sees. This closes the loop the capstone is about: annotated Go β†’ the Swagger 2.0 JSON above β†’ the API docs those annotations produce. The widget reads the very same golden file, so the rendered view can’t drift from the JSON either.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Shaping the output

The same annotated Go can render into the spec in more than one shape, and a handful of codescan.Options (plus a few field-level annotations) let you choose.

The guides are grouped by what they shape:

  • Scope & discovery β€” which packages are read and which types become definitions.
  • Names & $refs β€” the names definitions are published under and how references render.
  • Titles & descriptions β€” the human-readable text the spec carries.
  • Field types & formats β€” how an individual property renders.
  • Response bodies β€” describing a payload without a dedicated swagger:response struct.
  • Choose what gets scanned and which definitions land in the spec β€” package patterns and filters, when a type is emitted, pruning unreferenced models, overlaying an existing document, and build constraints.
  • Control the names definitions are published under and how references render β€” deconflicting collisions, deriving member names from struct tags, alias rendering, and a description sitting beside a $ref.
  • Shape the human-readable text β€” override godoc with API-facing title and description, route single-line comments to the description, keep annotations out of the godoc, and clean godoc doc-links out of generated prose.
  • Tune how an individual field renders β€” force a conformant format, mark pointer fields nullable, and control the x-go-* vendor extensions codescan emits.
  • Describe a concrete response payload without a dedicated swagger:response struct β€” declare the body inline on the route, or shadow a generic envelope’s payload with a doc-only struct.

Each guide is task-oriented β€” “I want the output to look like this” β€” and shows the same input rendered both ways, as before/after golden output the example tests verify. For the field-by-field meaning of every option, see the Options reference or the Options godoc.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Shaping the output

Scope & discovery

These knobs decide the inputs and the surface of the scan: which packages codescan reads, which types become definitions, and how that set is trimmed or merged before anything is rendered.

  • Limit what gets scanned β€” package patterns, working directory, include/exclude filters, tag filters, and dependency handling.
  • codescan never invents definitions β€” a type appears only when it is reachable or registered. Understand reachability and swagger:model so nothing goes missing or appears unexpectedly.
  • Scan a shared library with swagger:model discovery, then keep only the definitions actually reachable from your API β€” the middle ground between “only what routes use” and “every model, used or not”.
  • Merge scanned discoveries on top of an existing Swagger document with InputSpec.
  • Scan source guarded by Go build constraints by passing build tags to the scanner.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Scope & discovery

Scoping the scan

Several options narrow what codescan looks at, independent of how individual types render. They decide which packages are loaded and which discovered operations survive into the spec.

Package patterns and WorkDir

Options.Packages takes relative go list-style patterns β€” ./petstore, ./... for a whole tree β€” resolved against Options.WorkDir (the module root). This is the worked form in the Getting started guide:

codescan.Run(&codescan.Options{
    WorkDir:    "/path/to/module",
    Packages:   []string{"./..."},
    ScanModels: true,
})

To produce several specs from one module β€” e.g. one per API version β€” run a scan per package tree (./v1/..., then ./v2/...) and write each result separately. There is no single-run “split by version”; the unit of a scan is the set of packages you pass.

Include / Exclude

Options.Include and Options.Exclude are lists of regular expressions matched against package import paths. Include acts as an allow-list (when non-empty, only matching packages are scanned); Exclude removes matches. Use them to keep internal or generated packages out of the spec:

codescan.Run(&codescan.Options{
    Packages: []string{"./..."},
    Exclude:  []string{"/internal/", "/testdata/"},
})

Tag filters

Options.IncludeTags / Options.ExcludeTags filter operations by their Swagger tags after discovery β€” handy for publishing a public subset of an API while keeping the admin routes in the source:

codescan.Run(&codescan.Options{
    Packages:    []string{"./..."},
    ExcludeTags: []string{"admin", "internal"},
})

ExcludeDeps

By default codescan may follow types into dependency packages to resolve referenced models. Options.ExcludeDeps keeps the scan within your own module, leaving out types pulled in from dependencies.

Build constraints get their own guide β€” see Build tags.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

When the scanner emits a type

codescan does not emit a definition for every type it can see. A named type reaches the spec when either of these holds:

  • it is reachable β€” referenced (directly or transitively) from an operation, parameter, response, or another emitted model; or
  • it is registered β€” annotated swagger:model, which (with Options.ScanModels) publishes it even when nothing references it; or
  • it is a subtype of an emitted discriminated base β€” a swagger:model that composes that base with swagger:allOf. This one runs against the reference direction (a subtype $refs its base, never the reverse), so it is the one case where a definition arrives without anything referencing it. See Polymorphic models.

A type that is neither reachable nor registered is simply absent β€” the scanner never invents it. The package below has one of each case:

// Order is reached only through Cart below. A referenced named type is emitted
// as a $ref target even without swagger:model.
type Order struct {
	// ID is the order identifier.
	ID string `json:"id"`
}

// Cart references Order, so Order gets a definition and the field a $ref.
//
// swagger:model
type Cart struct {
	// Order is the referenced (and therefore emitted) nested model.
	Order Order `json:"order"`
}

// Standalone is never referenced, but swagger:model together with ScanModels
// publishes it anyway.
//
// swagger:model
type Standalone struct {
	// Label is a free-text label.
	Label string `json:"label"`
}

// Orphan is never referenced and carries no swagger:model β€” the scanner does
// not invent it, so it never reaches the spec.
type Orphan struct {
	// Secret is internal.
	Secret string `json:"secret"`
}

Full source: docs/examples/shaping/discovery/discovery.go

Scanned with ScanModels: true, the definitions are:

{
  "Cart": {
    "type": "object",
    "title": "Cart references Order, so Order gets a definition and the field a $ref.",
    "properties": {
      "order": {
        "$ref": "#/definitions/Order"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/discovery"
  },
  "Order": {
    "description": "Order is reached only through Cart below. A referenced named type is emitted\nas a $ref target even without swagger:model.",
    "type": "object",
    "properties": {
      "id": {
        "description": "ID is the order identifier.",
        "type": "string",
        "x-go-name": "ID"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/discovery"
  },
  "Standalone": {
    "description": "Standalone is never referenced, but swagger:model together with ScanModels\npublishes it anyway.",
    "type": "object",
    "properties": {
      "label": {
        "description": "Label is a free-text label.",
        "type": "string",
        "x-go-name": "Label"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/discovery"
  }
}

Full source: docs/examples/shaping/discovery/testdata/definitions.json

  • Cart β€” a swagger:model root.
  • Order β€” has no swagger:model, yet it is emitted (as a $ref target) because Cart references it. You do not need to annotate every nested type.
  • Standalone β€” a swagger:model that nothing references; ScanModels publishes it anyway.
  • Orphan β€” neither referenced nor annotated, so it never appears.
Info

If a model is missing from your spec, it is almost always unreachable: no operation/parameter/response/model leads to it. Either reference it, or annotate it swagger:model and scan with ScanModels. For the opposite problem β€” a ScanModels scan that pulls in models you do not want, like Standalone β€” see Pruning unused models.

Generic and embedded types

codescan resolves types through go/packages type information, so two forms that look tricky still work:

  • Generics. An instantiated generic β€” WrappedRequest[Order], whether annotated swagger:parameters or swagger:model β€” emits the concrete type: the type argument is substituted, so a T-typed field becomes a $ref to the argument’s definition. The generic’s declaration may live in a different file from its instantiation. A free (un-instantiated) type parameter is skipped with a warning.
  • Embedded fields, including those from an external package or a type declared in another source file, are promoted into the embedding type (only exported members, recursively), and a custom field type resolves to its underlying type. An in: / required: annotation written on the embedded field itself applies to all the members it promotes. An embed that carries a json: tag (Base \json:“base”`) is **not** promoted β€” it nests as a single property of that name (a $ref` to the embedded type), matching Go’s own JSON encoding.
  • A type the scanner cannot model β€” e.g. a bare function type β€” is skipped with a warning rather than failing the whole scan; the annotated models around it are still emitted.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Pruning unused models

Options.ScanModels (the -m flag) publishes every swagger:model type it finds, whether or not anything references it β€” see When the scanner emits a type. That is exactly what you want when the annotated package is the contract. It is the wrong default when you point codescan at a large shared model library and only care about the slice your API actually exposes: the spec fills up with definitions no operation, parameter or response ever references.

Options.PruneUnusedModels is the middle ground. It runs swagger:model discovery as usual, then drops every discovered definition that is not reachable from your API surface.

Three emission modes

The same source renders three ways, depending on two options:

ModeOptionsWhat is emitted
Reachable only(default)Only models reachable from an operation, parameter or response β€” discovery-driven. A swagger:model that nothing references is not emitted.
Every modelScanModelsEvery swagger:model type, reachable or not. The library’s whole annotated surface lands in definitions.
Models, then prunedScanModels + PruneUnusedModelsDiscovery runs as in Every model, then the unreachable definitions are pruned away β€” you keep the reachable subset, including models discovered only because swagger:model published them.

PruneUnusedModels is a modifier on ScanModels. Without ScanModels the emitted set is already reachable-only, so the flag has nothing to do: it is a no-op and says so with a single informational diagnostic.

What counts as reachable

A definition survives the prune when it is reachable β€” directly or transitively through any $ref β€” from one of these roots:

  • an operation’s body parameters and response schemas;
  • a top-level shared response or parameter;
  • a definition supplied via InputSpec.

The walk follows references through every schema shape β€” properties, allOf / anyOf / oneOf, array items, additionalProperties, and so on β€” and terminates cleanly on recursive or cyclic models. A model referenced only by another unreferenced model is itself unreachable, so the whole dead subtree is removed, not just its entry point.

One rule does not follow a $ref: a reachable definition that declares a discriminator also keeps its subtypes. They compose the base rather than being referenced by it, so the walk cannot see them and a polymorphic family would otherwise be pruned down to its base alone. The family travels as a unit β€” an unreachable base is still dropped, together with its subtypes. See Polymorphic models.

Those shared response / parameter roots are pruned too. A shared parameter or response that no operation and no path-item references is itself dropped (with a scan.pruned-unused Hint) β€” and because that happens before the definition walk reads its roots from the same #/parameters / #/responses maps, a model kept alive only by a now-pruned shared object becomes prunable in turn. Shared objects supplied through InputSpec are pinned, exactly like definitions.

Info

Definitions you supply through InputSpec are pinned: they are never pruned, and they seed the reachability roots, so anything they $ref survives too. The prune only ever removes definitions codescan discovered, never ones you handed it.

Pruning happens before name resolution

This is the part that makes pruning more than a convenience. codescan keys every definition by a compiler-unique identity while it builds, then a final stage projects each one back to the shortest unique name β€” deconflicting cross-package collisions along the way (billing.Account / identity.Account β†’ BillingAccount / IdentityAccount; see Resolving $ref name conflicts).

PruneUnusedModels runs before that name-resolution stage. So when one half of a colliding pair is unused, it is pruned first β€” and the collision never happens. The surviving model keeps its clean, unqualified name instead of being pushed to a package-qualified one to avoid a twin that is not even in your spec. Pruning a shared library this way removes a whole class of surprising #/definitions/<Pkg><Name> renames that only existed because of models you were not using.

Diagnostics

The prune is never silent. Through the OnDiagnostic sink codescan reports:

  • scan.pruned-unused β€” one informational diagnostic per pruned definition, located at the originating Go type, so you can see exactly what was dropped and why; and the single no-op notice when the flag is set without ScanModels.
  • scan.renamed-definition β€” one per collision the name stage did resolve, located at the Go type, recording the final name it landed under. With pruning on, collisions that vanish produce no such diagnostic at all.

When to use it

  • Reach for PruneUnusedModels when you scan a shared or third-party model package with -m and want only the definitions your API actually exposes β€” the reachable subset, with the noise dropped and the collision churn gone.
  • Stay on plain ScanModels when the annotated package is itself the published contract and every swagger:model is meant to appear.
  • Stay on the default (neither flag) when you only ever want what your routes reference; there is nothing extra to discover or prune.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Overlaying a spec

Options.InputSpec seeds the scan with an existing *spec.Swagger: codescan merges what it discovers on top of it rather than starting from a blank document. Use it to keep hand-authored top-level metadata or a hand-written definition, or to compose a spec across several scans.

The scanned package contributes one model:

// Widget is discovered by the scan and merged onto the input spec.
//
// swagger:model
type Widget struct {
	// ID identifies the widget.
	ID string `json:"id"`
}

Full source: docs/examples/shaping/overlay/overlay.go

Given a base document with metadata and a hand-authored Health definition, the scan preserves all of it and adds the discovered Widget:

InputSpec (base)
{
  "swagger": "2.0",
  "info": {
    "title": "Inventory API",
    "version": "1.0.0"
  },
  "host": "api.example.com",
  "basePath": "/v1",
  "paths": null,
  "definitions": {
    "Health": {
      "type": "object",
      "properties": {
        "ok": {
          "type": "boolean"
        }
      }
    }
  }
}

Full source: docs/examples/shaping/overlay/testdata/base.json

After the scan
{
  "swagger": "2.0",
  "info": {
    "title": "Inventory API",
    "version": "1.0.0"
  },
  "host": "api.example.com",
  "basePath": "/v1",
  "paths": {},
  "definitions": {
    "Health": {
      "type": "object",
      "properties": {
        "ok": {
          "type": "boolean"
        }
      }
    },
    "Widget": {
      "type": "object",
      "title": "Widget is discovered by the scan and merged onto the input spec.",
      "properties": {
        "id": {
          "description": "ID identifies the widget.",
          "type": "string",
          "x-go-name": "ID"
        }
      },
      "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/overlay"
    }
  }
}

Full source: docs/examples/shaping/overlay/testdata/merged.json

var base spec.Swagger
_ = json.Unmarshal(baseSpecJSON, &base)

doc, _ := codescan.Run(&codescan.Options{
    Packages:   []string{"./..."},
    ScanModels: true,
    InputSpec:  &base,
})

The document’s info, host, basePath and the hand-authored Health definition survive untouched; only the discovered definitions are added.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Build tags

Go files can be guarded by //go:build constraints. By default codescan loads a package under the default build configuration, so tag-gated files are skipped. Options.BuildTags passes the tags through to the package loader, so the annotations in those files are scanned too.

The package has an always-present model plus this one, in a file that opens with the constraint //go:build experimental:

// Experimental is only scanned when the "experimental" build tag is set.
//
// swagger:model
type Experimental struct {
	// Beta flags a beta-only feature.
	Beta bool `json:"beta"`
}

Full source: docs/examples/shaping/buildtags/experimental.go

Scanned with no tags and with experimental, the gated Experimental model appears only in the second:

Default
{
  "Stable": {
    "type": "object",
    "title": "Stable is always scanned.",
    "properties": {
      "name": {
        "description": "Name is the feature name.",
        "type": "string",
        "x-go-name": "Name"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/buildtags"
  }
}

Full source: docs/examples/shaping/buildtags/testdata/off.json

BuildTags: experimental
{
  "Experimental": {
    "type": "object",
    "title": "Experimental is only scanned when the \"experimental\" build tag is set.",
    "properties": {
      "beta": {
        "description": "Beta flags a beta-only feature.",
        "type": "boolean",
        "x-go-name": "Beta"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/buildtags"
  },
  "Stable": {
    "type": "object",
    "title": "Stable is always scanned.",
    "properties": {
      "name": {
        "description": "Name is the feature name.",
        "type": "string",
        "x-go-name": "Name"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/buildtags"
  }
}

Full source: docs/examples/shaping/buildtags/testdata/on.json

codescan.Run(&codescan.Options{
    Packages:   []string{"./..."},
    ScanModels: true,
    BuildTags:  "experimental",
})

BuildTags accepts the same comma-separated form as go build -tags.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Names & $refs

Once codescan knows which definitions to emit, these knobs govern how they are named and referenced: the definition names that form your published $ref contract, where member names come from, and the shape a reference takes in the output.

  • When two Go types want the same definition name, codescan keeps them distinct with deterministic, package-qualified names β€” and you stay in control of the $ref names that form your published contract.
  • Derive property, parameter and header names from a struct tag other than json (form, xml, …) via NameFromTags.
  • Emit interface-method property names verbatim (ID, CreatedAt) instead of the auto-jsonified spelling (id, createdAt), with SkipJSONifyInterfaceMethods.
  • Choose how Go type aliases render β€” dissolved to their target, or exposed as a first-class $ref via swagger:model, with RefAliases / TransparentAliases.
  • Render a plain struct embed as an allOf composition β€” a $ref to the embedded model plus a sibling member for the embedding struct’s own fields β€” instead of inlining the promoted properties, with DefaultAllOfForEmbeds.
  • Control how a field’s description and extensions are rendered when its type resolves to a $ref β€” wrapped in an allOf, emitted as direct siblings (EmitRefSiblings), or dropped (SkipAllOfCompounding).
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Names & $refs

Resolving $ref name conflicts

A Swagger definition is keyed by a single short name (#/definitions/Account), but a Go program routinely has several types that would map to that name β€” the same leaf declared in different packages, or a swagger:model Account override applied twice. codescan keys every definition by a compiler-unique identity (<package-path>/<name>) while it builds, then a final reduce stage projects each identity back to the shortest name that is still unique. The result is deterministic regardless of discovery or map-iteration order: no silent overwrite, no lost definition.

The panes below are backed by the test-covered docs/examples/shaping/nameconflicts package tree.

When names collide

Two packages each declare an Account, with entirely different fields:

// Account is the billing view of a customer account.
//
// swagger:model Account
type Account struct {
	// the current balance, in minor units
	Balance int64 `json:"balance"`
	// the ISO-4217 currency code
	Currency string `json:"currency"`
}

Full source: docs/examples/shaping/nameconflicts/billing/account.go

// Account is the identity view of a customer account.
//
// swagger:model Account
type Account struct {
	// the login email
	Email string `json:"email"`
	// whether the email has been verified
	Verified bool `json:"verified"`
}

Full source: docs/examples/shaping/nameconflicts/identity/account.go

A Dashboard model references both, so they are discovered together:

// Dashboard references the same-named Account from two packages plus both
// ledger entries, forcing all of them to be discovered together. The two
// Accounts collide on the short name and are deconflicted by package segment;
// the refs below point at the resolved names, never a bare "Account".
//
// swagger:model Dashboard
type Dashboard struct {
	Billing  billing.Account  `json:"billing"`
	Identity identity.Account `json:"identity"`
	// the ledger entry that kept the name "Entry"
	Primary ledger.Entry `json:"primary"`
	// the duplicate that reverted to its Go name "Reversal"
	Secondary ledger.Reversal `json:"secondary"`
}

Full source: docs/examples/shaping/nameconflicts/doc.go

Before name-identity, the two would have merged onto a single #/definitions/Account β€” a union of fields, last package wins, non-deterministically. Now each keeps its own definition and the references resolve to the deconflicted names:

{
  "description": "Dashboard references the same-named Account from two packages plus both\nledger entries, forcing all of them to be discovered together. The two\nAccounts collide on the short name and are deconflicted by package segment;\nthe refs below point at the resolved names, never a bare \"Account\".",
  "type": "object",
  "properties": {
    "billing": {
      "$ref": "#/definitions/BillingAccount"
    },
    "identity": {
      "$ref": "#/definitions/IdentityAccount"
    },
    "primary": {
      "$ref": "#/definitions/Entry"
    },
    "secondary": {
      "$ref": "#/definitions/Reversal"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts"
}

Full source: docs/examples/shaping/nameconflicts/testdata/dashboard.json

How codescan resolves them automatically

The reduce stage gives every reachable identity the shortest acceptable name:

  • A globally unique leaf is lifted to its bare name β€” byte-identical to the pre-feature output, so the common case sees zero churn.
  • A colliding leaf is qualified with the minimal-depth PascalCase concat of its nearest package segments (billing.Account / identity.Account β†’ BillingAccount / IdentityAccount), deepening one segment at a time until the whole group is unique. A validate.colliding-model-name diagnostic records each rename.

Every emitted definition also carries an x-go-package extension recording the source package, so even identically-shaped collisions stay traceable:

{
  "type": "object",
  "title": "Account is the billing view of a customer account.",
  "properties": {
    "balance": {
      "description": "the current balance, in minor units",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Balance"
    },
    "currency": {
      "description": "the ISO-4217 currency code",
      "type": "string",
      "x-go-name": "Currency"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts/billing"
}

Full source: docs/examples/shaping/nameconflicts/testdata/billingaccount.json

Info

The whole pass is a pure function of the reachable identity set, so the names are stable across runs β€” but they are derived from your package paths. Renaming or moving a package changes the segment used in a qualified name. Pin the names that matter (see below).

Same-package duplicates

A single package cannot own a definition name twice. If two Go types in the same package both claim swagger:model Entry, codescan keeps one (deterministically) and reverts the other to its Go type name, with a validate.duplicate-model-name diagnostic:

// Entry keeps the contested name: the definition is "Entry".
//
// swagger:model Entry
type Entry struct {
	Debit int64 `json:"debit"`
}

// Reversal also asks for "Entry". The name is already taken in this package, so
// it reverts to its Go name, "Reversal", and a diagnostic is raised.
//
// swagger:model Entry
type Reversal struct {
	Credit int64 `json:"credit"`
}

Full source: docs/examples/shaping/nameconflicts/ledger/ledger.go

Here Entry keeps the contested name and Reversal falls back to its Go name β€” the Dashboard refs above point at #/definitions/Entry and #/definitions/Reversal, never a merged Entry. This is a genuine authoring error (one package, one name); the fallback keeps the spec valid rather than silently dropping a model.

Referencing a model by leaf across packages

The type-name keywords β€” swagger:type, swagger:additionalProperties, and swagger:patternProperties β€” accept a bare leaf as their argument. codescan resolves it the same way the reduce stage does: the annotating type’s own package first, then uniquely across the scanned model set. A leaf unique in another package resolves to a $ref:

swagger:additionalProperties Widget
// Bag is an open object whose additional properties are catalog.Widget,
// named by the bare leaf "Widget" β€” resolved cross-package because that leaf is
// unique across the scanned model set.
//
// swagger:model Bag
// swagger:additionalProperties Widget
type Bag struct {
	ID string `json:"id"`
}

Full source: docs/examples/shaping/nameconflicts/doc.go

#/definitions/Bag
{
  "description": "named by the bare leaf \"Widget\" β€” resolved cross-package because that leaf is\nunique across the scanned model set.",
  "type": "object",
  "title": "Bag is an open object whose additional properties are catalog.Widget,",
  "properties": {
    "id": {
      "type": "string",
      "x-go-name": "ID"
    }
  },
  "additionalProperties": {
    "$ref": "#/definitions/Widget"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nameconflicts"
}

Full source: docs/examples/shaping/nameconflicts/testdata/bag.json

If the leaf matches a model in several packages it is ambiguous: the reference is dropped (never guessed) and a validate.ambiguous-type-name diagnostic is raised. Disambiguate with a same-package type or pin the target with a swagger:model <Name> override. The same leaf rule applies to the additionalProperties: / swagger:patternProperties value forms covered in Maps & free-form objects.

Keeping the exposed names under your control

The generated $ref names are part of your published contract, so the author β€” not the resolver β€” should decide the ones that matter:

  • Pin a public name with an explicit swagger:model <Name>. A pinned name is the identity’s leaf, so two pinned names that still collide are deconflicted by package segment exactly like inferred ones β€” pin distinct names for the types in your public surface.
  • Let auto-resolution handle the rest. Incidental or internal collisions get a valid, stable, package-qualified name with no action from you.

Tuning the qualified names

Two scanner options steer the rare, deep collisions:

OptionDefaultEffect
NameConcatBudget0.65Readability cutoff in [0,1] (lower is more readable). A collision group whose best flat concat scores above the budget becomes a candidate for the hierarchical fallback. Raise toward 1.0 to accept longer concats; lower to fall back sooner.
EmitHierarchicalNamesfalseOpt into the fallback: over-budget groups are emitted as nested container definitions (#/definitions/<pkg>/<Name>, each tagged with x-go-package) instead of a long flat concat.
Warning

EmitHierarchicalNames is off by default on purpose. A nested definition is a deep JSON pointer that only ExpandSpec resolves, and a definitions-enumerating consumer (e.g. go-swagger codegen, one model per entry) sees the container nodes rather than the models. The always-correct flat concat stays the default; enable the nested shape only when you prefer it for the over-budget tail.

When to tune vs. let it auto-resolve

  • Pin the names that appear in your public API contract β€” clients, generated SDKs, and hand-written $refs depend on them.
  • Let auto-resolution handle incidental collisions between internal types; the qualified names are valid and stable.
  • Reach for EmitHierarchicalNames only when a few collision groups have package names long enough to make the flat concat unwieldy, and your consumers resolve $ref pointers (rather than enumerating definitions).

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Naming from struct tags

By default codescan derives a field’s spec name from its json: tag (then the Go field name). Options.NameFromTags lets you choose which struct-tag types supply the name, in precedence order β€” handy when your structs are tagged for another binding library (for example gin’s form:). It applies everywhere a name is derived from a field: schema properties, parameters, and response headers. The model below tags every field with both json: and form::

// Filter is a query model whose fields carry both json: and form: tags.
//
// swagger:model
type Filter struct {
	// SortKey selects the sort column.
	SortKey string `form:"sort_key" json:"sortKey"`

	// PageSize bounds the page length.
	PageSize int `form:"page_size" json:"pageSize"`
}

Full source: docs/examples/shaping/naming-from-tags/naming.go

Scanned with the default (["json"]) and with ["form","json"], the property names differ β€” form: wins because it is listed first:

Default (json)
{
  "type": "object",
  "title": "Filter is a query model whose fields carry both json: and form: tags.",
  "properties": {
    "pageSize": {
      "description": "PageSize bounds the page length.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "PageSize"
    },
    "sortKey": {
      "description": "SortKey selects the sort column.",
      "type": "string",
      "x-go-name": "SortKey"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/naming-from-tags"
}

Full source: docs/examples/shaping/naming-from-tags/testdata/default.json

NameFromTags: [form, json]
{
  "type": "object",
  "title": "Filter is a query model whose fields carry both json: and form: tags.",
  "properties": {
    "page_size": {
      "description": "PageSize bounds the page length.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "PageSize"
    },
    "sort_key": {
      "description": "SortKey selects the sort column.",
      "type": "string",
      "x-go-name": "SortKey"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/naming-from-tags"
}

Full source: docs/examples/shaping/naming-from-tags/testdata/form.json

codescan.Run(&codescan.Options{
    Packages:     []string{"./..."},
    ScanModels:   true,
    NameFromTags: []string{"form", "json"},
})

The first listed tag that supplies a usable name wins; a tag that is absent or carries only options (e.g. ,omitempty) is skipped and the next is tried. An explicit empty list (NameFromTags: []string{}) consults no tag and falls back to the Go field name.

Info

Name only. NameFromTags changes only the name. The encoding/json directives β€” json:"-" (exclude), ,omitempty, ,string β€” are always read from the json tag, whatever names the field. Targeted renames (the name: keyword, swagger:name, and swagger:model {name}) still take precedence over any tag-derived name.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Interface-method property names

When a model’s shape is described by an interface, its methods have no natural JSON serialization β€” Go’s encoding/json can’t marshal interface methods, so there’s no struct tag to read a name from. codescan invents one by running its jsonify transform on the Go method name: ID β†’ id, CreatedAt β†’ createdAt. That “one size fits all” convention isn’t always what you want β€” an interface already named for its JSON shape, or a codebase with its own canonical-name discipline, wants the Go name kept as-is.

SkipJSONifyInterfaceMethods opts out of the mangler. With it set, an interface-method property is emitted under the Go method name verbatim. It is an opt-out and defaults to off; with it off, output is unchanged.

What changes

This model is an interface with two default-path methods and one carrying a swagger:name override:

// Account is a read model whose shape is described by interface methods.
//
// swagger:model Account
type Account interface {
	// ID is emitted as "id" by default; verbatim "ID" with the opt-out set.
	ID() string

	// CreatedAt is emitted as "createdAt" by default; verbatim "CreatedAt" with
	// the opt-out set.
	CreatedAt() string

	// swagger:name explicit_name
	//
	// A swagger:name override is taken verbatim either way β€” re-mangling would
	// camelCase it to "explicitName".
	OverriddenField() string
}

Full source: docs/examples/shaping/interfacenames/interfacenames.go

Scanned with the flag off the method names auto-jsonify; on, they ride through verbatim:

Default β€” jsonified
{
  "type": "object",
  "title": "Account is a read model whose shape is described by interface methods.",
  "properties": {
    "createdAt": {
      "description": "CreatedAt is emitted as \"createdAt\" by default; verbatim \"CreatedAt\" with\nthe opt-out set.",
      "type": "string",
      "x-go-name": "CreatedAt"
    },
    "explicit_name": {
      "description": "\nA swagger:name override is taken verbatim either way β€” re-mangling would\ncamelCase it to \"explicitName\".",
      "type": "string",
      "x-go-name": "OverriddenField"
    },
    "id": {
      "description": "ID is emitted as \"id\" by default; verbatim \"ID\" with the opt-out set.",
      "type": "string",
      "x-go-name": "ID"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/interfacenames"
}

Full source: docs/examples/shaping/interfacenames/testdata/account_off.json

SkipJSONifyInterfaceMethods β€” verbatim
{
  "type": "object",
  "title": "Account is a read model whose shape is described by interface methods.",
  "properties": {
    "CreatedAt": {
      "description": "CreatedAt is emitted as \"createdAt\" by default; verbatim \"CreatedAt\" with\nthe opt-out set.",
      "type": "string"
    },
    "ID": {
      "description": "ID is emitted as \"id\" by default; verbatim \"ID\" with the opt-out set.",
      "type": "string"
    },
    "explicit_name": {
      "description": "\nA swagger:name override is taken verbatim either way β€” re-mangling would\ncamelCase it to \"explicitName\".",
      "type": "string",
      "x-go-name": "OverriddenField"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/interfacenames"
}

Full source: docs/examples/shaping/interfacenames/testdata/account_on.json

Reading the two panes:

  • Default-path methods are jsonified. ID() β†’ id, CreatedAt() β†’ createdAt; the original Go name is preserved as the x-go-name extension.
  • With the opt-out, the Go name is the property name. ID and CreatedAt appear verbatim β€” and x-go-name drops, since it would now just repeat the property name.
  • A swagger:name override is verbatim either way. OverriddenField is published as explicit_name in both panes β€” the override already bypasses the mangler, so the flag never touches it (and never re-mangles it to explicitName).
Note

This flag only affects interface methods, which have no JSON serialization to mirror. Struct-field property names are untouched β€” they always reflect what encoding/json actually produces (see Naming from struct tags to source those from a different tag).

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Alias rendering

A Go type alias (type Price = Money) is, to the Go type system, literally the same type as its target. codescan’s default is to treat it that way: at a use site the alias dissolves to its target, producing no definition of its own.

Annotated Go
// Money is the underlying model.
//
// swagger:model
type Money struct {
	// Cents is the amount in cents.
	Cents int64 `json:"cents"`

	// Currency is the ISO currency code.
	Currency string `json:"currency"`
}

// Price is a Go alias of Money. By default an alias is a Go implementation
// detail: at use sites it dissolves to its target, producing no definition of
// its own.
type Price = Money

// Invoice references Price; the field resolves to Money.
//
// swagger:model
type Invoice struct {
	// Total is the invoice total.
	Total Price `json:"total"`
}

Full source: docs/examples/shaping/aliases/aliases.go

#/definitions/Invoice
{
  "type": "object",
  "title": "Invoice references Price; the field resolves to Money.",
  "properties": {
    "total": {
      "$ref": "#/definitions/Money"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases"
}

Full source: docs/examples/shaping/aliases/testdata/invoice.json

Invoice.total is typed Price, but the field resolves straight to #/definitions/Money β€” Price itself never appears.

Exposing an alias as a first-class entity

This is an advanced, rarely-needed case. To keep the alias name in the spec β€” its own definition that other schemas $ref β€” annotate the alias with swagger:model:

// Amount is the underlying model.
//
// swagger:model
type Amount struct {
	// Cents is the amount in cents.
	Cents int64 `json:"cents"`

	// Currency is the ISO currency code.
	Currency string `json:"currency"`
}

// Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name
// in the spec instead of dissolving it to Amount.
//
// swagger:model
type Fee = Amount

// Receipt references the alias, not the target.
//
// swagger:model
type Receipt struct {
	// Charge is the fee charged.
	Charge Fee `json:"charge"`
}

Full source: docs/examples/shaping/aliases-firstclass/firstclass.go

Two top-level options then govern how that first-class alias definition is shaped. The panes below are the same package scanned under each.

Default β€” the alias definition is a copy

Fee is emitted as a structural duplicate of Amount, and Receipt.charge points at the alias:

Default (expand)
{
  "Amount": {
    "type": "object",
    "title": "Amount is the underlying model.",
    "properties": {
      "cents": {
        "description": "Cents is the amount in cents.",
        "type": "integer",
        "format": "int64",
        "x-go-name": "Cents"
      },
      "currency": {
        "description": "Currency is the ISO currency code.",
        "type": "string",
        "x-go-name": "Currency"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  },
  "Fee": {
    "description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
    "type": "object",
    "properties": {
      "cents": {
        "description": "Cents is the amount in cents.",
        "type": "integer",
        "format": "int64",
        "x-go-name": "Cents"
      },
      "currency": {
        "description": "Currency is the ISO currency code.",
        "type": "string",
        "x-go-name": "Currency"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  },
  "Receipt": {
    "type": "object",
    "title": "Receipt references the alias, not the target.",
    "properties": {
      "charge": {
        "$ref": "#/definitions/Fee"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  }
}

Full source: docs/examples/shaping/aliases-firstclass/testdata/expand.json

RefAliases: true
{
  "Amount": {
    "type": "object",
    "title": "Amount is the underlying model.",
    "properties": {
      "cents": {
        "description": "Cents is the amount in cents.",
        "type": "integer",
        "format": "int64",
        "x-go-name": "Cents"
      },
      "currency": {
        "description": "Currency is the ISO currency code.",
        "type": "string",
        "x-go-name": "Currency"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  },
  "Fee": {
    "description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
    "$ref": "#/definitions/Amount"
  },
  "Receipt": {
    "type": "object",
    "title": "Receipt references the alias, not the target.",
    "properties": {
      "charge": {
        "$ref": "#/definitions/Fee"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  }
}

Full source: docs/examples/shaping/aliases-firstclass/testdata/refaliases.json

RefAliases: true β€” the alias definition is a $ref chain

The right pane above: Fee becomes {"$ref": "#/definitions/Amount"}. One shape, two names β€” the alias survives at use sites without duplicating the target’s properties. Prefer this over the default whenever the alias is genuinely a synonym: a copy drifts the moment the target changes.

TransparentAliases: true β€” use sites dissolve

{
  "Amount": {
    "type": "object",
    "title": "Amount is the underlying model.",
    "properties": {
      "cents": {
        "description": "Cents is the amount in cents.",
        "type": "integer",
        "format": "int64",
        "x-go-name": "Cents"
      },
      "currency": {
        "description": "Currency is the ISO currency code.",
        "type": "string",
        "x-go-name": "Currency"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  },
  "Fee": {
    "description": "Fee is a FIRST-CLASS alias: the swagger:model annotation keeps the alias name\nin the spec instead of dissolving it to Amount.",
    "type": "object",
    "properties": {
      "cents": {
        "description": "Cents is the amount in cents.",
        "type": "integer",
        "format": "int64",
        "x-go-name": "Cents"
      },
      "currency": {
        "description": "Currency is the ISO currency code.",
        "type": "string",
        "x-go-name": "Currency"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  },
  "Receipt": {
    "type": "object",
    "title": "Receipt references the alias, not the target.",
    "properties": {
      "charge": {
        "$ref": "#/definitions/Amount"
      }
    },
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/aliases-firstclass"
  }
}

Full source: docs/examples/shaping/aliases-firstclass/testdata/transparent.json

Receipt.charge now points straight at #/definitions/Amount: the alias is gone from the reference graph.

Warning

Note what did not happen: Fee is still emitted. TransparentAliases governs how an alias renders at its use sites, not whether an annotated declaration produces a definition β€” so with ScanModels you get a Fee definition that nothing references. Add PruneUnusedModels to drop it, or simply do not annotate an alias you intend to dissolve.

The three modes at a glance:

Fee definitionReceipt.charge
default (expand)copy of Amount$ref: Fee
RefAliases: true$ref: Amount$ref: Fee
TransparentAliases: truecopy of Amount, unreferenced$ref: Amount

Wider calibration lives in the testdata/enhancements/alias-calibration-embed golden trio.

Note

Most APIs never need first-class aliases β€” prefer naming a real swagger:model type over aliasing one. Reach for RefAliases / TransparentAliases only when you specifically need to control whether an alias name survives in the output.

The swagger:alias annotation is deprecated and has no effect β€” alias rendering is governed by the plain Go alias plus these options, or by swagger:model for a first-class definition.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Composing embeds with allOf

When a struct embeds another struct, Go promotes the embedded fields, and by default codescan mirrors that: the embedded type’s properties are inlined flat into the embedding schema. That is faithful to the Go value, but it loses the “this composes Base relationship β€” every embedding model emits its own flat copy of the embedded fields, and a client generator can’t recover the shared base type.

DefaultAllOfForEmbeds changes that. With the option on, a plain embed (one with no explicit name and no swagger:allOf tag) is rendered as an allOf member β€” exactly as if it carried swagger:allOf β€” so the composition relationship survives in the spec. It is opt-in and defaults to off; with it off, output is byte-identical to before.

What composes

This model embeds a swagger:model type (Base), a non-model type (Mixin), and adds an own field:

// Base is a reusable base model.
//
// swagger:model Base
type Base struct {
	ID   int64  `json:"id"`
	Name string `json:"name"`
}

// Mixin is a non-model embedded type (no swagger:model), reachable only through
// embedding. Under the flag it composes as an inline allOf member, since it has
// no definition of its own to $ref.
type Mixin struct {
	Note string `json:"note"`
}

Full source: docs/examples/shaping/embedallof/embedallof.go

// PlainEmbed embeds a model and a non-model plainly, plus an own field.
//
// swagger:model PlainEmbed
type PlainEmbed struct {
	Base
	Mixin

	Color string `json:"color"`
}

Full source: docs/examples/shaping/embedallof/embedallof.go

Scanned with the flag off the embedded properties inline flat; on, the embed becomes an allOf composition:

Default β€” inlined
{
  "type": "object",
  "title": "PlainEmbed embeds a model and a non-model plainly, plus an own field.",
  "properties": {
    "color": {
      "type": "string",
      "x-go-name": "Color"
    },
    "id": {
      "type": "integer",
      "format": "int64",
      "x-go-name": "ID"
    },
    "name": {
      "type": "string",
      "x-go-name": "Name"
    },
    "note": {
      "type": "string",
      "x-go-name": "Note"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/embedallof"
}

Full source: docs/examples/shaping/embedallof/testdata/plainembed_off.json

DefaultAllOfForEmbeds β€” composed
{
  "title": "PlainEmbed embeds a model and a non-model plainly, plus an own field.",
  "allOf": [
    {
      "$ref": "#/definitions/Base"
    },
    {
      "type": "object",
      "properties": {
        "note": {
          "type": "string",
          "x-go-name": "Note"
        }
      }
    },
    {
      "type": "object",
      "properties": {
        "color": {
          "type": "string",
          "x-go-name": "Color"
        }
      }
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/embedallof"
}

Full source: docs/examples/shaping/embedallof/testdata/plainembed_on.json

Reading the composed pane, each embed takes the path its kind dictates:

  • A model embed becomes a $ref member. Base is a swagger:model, so it has its own definition and composes as {$ref: "#/definitions/Base"} β€” no copy of id / name.
  • A non-model embed becomes an inline member. Mixin carries no swagger:model, so it has no definition to point at; its note property rides an inline allOf member instead.
  • The embedding struct’s own fields move to a sibling member. color is no longer a top-level property β€” it lands in its own allOf arm alongside the composed embeds.

What’s left alone

The flag only changes the untagged, unnamed embed β€” every other embed shape is unaffected:

// PointerEmbed embeds a model through a pointer; the pointer is peeled and
// takes the same $ref path as a value embed.
//
// swagger:model PointerEmbed
type PointerEmbed struct {
	*Base

	Tag string `json:"tag"`
}

// NamedEmbed embeds Base under an explicit json name, so Go does not promote
// it: it stays a single nested property, identical on or off (go-swagger#2038).
//
// swagger:model NamedEmbed
type NamedEmbed struct {
	Base `json:"base"`

	Extra string `json:"extra"`
}

// TaggedEmbed already composes Base via an explicit swagger:allOf tag, so the
// flag does not change its shape.
//
// swagger:model TaggedEmbed
type TaggedEmbed struct {
	// swagger:allOf
	Base

	Field string `json:"field"`
}

Full source: docs/examples/shaping/embedallof/embedallof.go

  • Pointer embeds are peeled first, so *Base composes to the same $ref member as a value embed.
  • A json-named embed is not a promotion. Giving the embed a json tag (Base \json:“base”`) makes it a single nested property named base`, on or off β€” Go doesn’t promote a named embed (go-swagger#2038).
  • An explicit swagger:allOf embed already composes, so the flag is a no-op for it; it only makes allOf the default for untagged embeds.
  • Interface embeds compose via allOf regardless of this flag.
Note

DefaultAllOfForEmbeds is the global default-on switch for the same shape swagger:allOf produces per-embed. Reach for the annotation when only some embeds should compose; reach for the option when composition is your house style for every plain embed.

Composition needs a marshaller you write

An allOf says the JSON document satisfies every member at once β€” one flat object carrying all their properties. Go’s default marshaller only produces that shape by coincidence, and the coincidence holds for exactly one case: a plain struct embed with no marshaller of its own, whose fields Go promotes.

Step outside that case and the default rendering stops matching the spec:

  • a member that is not a struct β€” a map, a slice, a named basic β€” promotes nothing, so Go emits it as one key named after the type instead of merging it;
  • a member with its own MarshalJSON/MarshalText is promoted into your type’s method set, and json.Marshal then consults it before reading any field β€” rendering the whole struct as whatever that method returns.

This is why go-swagger’s generated models never rely on the default. A model with allOf embeds its members and carries a hand-written pair that flattens them, reading every member from the same raw document:

// swagger:model WithAllOf
type WithAllOf struct {
	Notable                             // an allOf member

	AO1 map[string]int32 `json:"-"`     // a map member β€” json:"-" keeps the default out of the way

	WithAllOfAO2P2                      // another member

	Body  string `json:"body,omitempty"`   // the model's own fields
	Title string `json:"title,omitempty"`
}

// UnmarshalJSON reads every member from the SAME document β€” that is what allOf means.
func (m *WithAllOf) UnmarshalJSON(raw []byte) error {
	var aO0 Notable
	if err := jsonutils.ReadJSON(raw, &aO0); err != nil {
		return err
	}
	m.Notable = aO0

	var aO1 map[string]int32
	if err := jsonutils.ReadJSON(raw, &aO1); err != nil {
		return err
	}
	m.AO1 = aO1

	// … one block per member, then the model's own fields
}
Warning

If you hand-write the Go types that codescan scans, swagger:allOf describes your intent; it does not make encoding/json produce that document. Write the marshaller, or generate the model from the spec and let go-swagger write it for you. codescan reads declarations β€” it cannot tell whether the marshalling you need exists, so it will not warn you.

Because of this, codescan reads an embed as composition and never as an instruction about the default marshaller. In particular, a promoted MarshalText/MarshalJSON on an embedded type is not treated as a claim that the whole model is a scalar β€” see Forcing a conformant format if you want a type rendered as one.

Annotate the embedded type, not the embed

A classifier annotation in an embedded field’s doc comment does nothing. swagger:strfmt and swagger:type written there are ignored β€” codescan reports them under scan.ineffective-annotation rather than dropping them quietly:

type Wrong struct {
	// swagger:strfmt uuid   ← ignored, and warned about
	Token
}

An embed contributes the shape of the type it embeds, and what that shape is comes from that type’s own declaration. Put the annotation there and every embed of it composes the same way:

// swagger:strfmt uuid
type Token [16]byte

The catch is that both annotations are honoured on an ordinary field, so the same line means something one field down and nothing on an embed. Only swagger:allOf, swagger:omit, swagger:name, swagger:ignore and a required: inheritance hint act on an embed itself β€” everything else describes the embedded type and belongs with it.

When an override cannot be composed

Composition has one limit worth knowing. Inlining an embed resolves an override β€” a field the enclosing struct re-declares wins, exactly as Go’s depth rule decides it. allOf instead accumulates: members conjoin, and a conjunction can only narrow, never replace. So a re-declaration that replaces is not expressible as composition:

  • re-declaring a promoted field to decorate it (add readOnly, a description, a validation) leaves the property in both members β€” valid, but a generator walking the members sees it twice;
  • re-declaring it with a different type yields {type: integer} and {type: string} for one property β€” a schema nothing can satisfy.

codescan does not guess which declaration you meant: many Go types can be written whose composition has no faithful schema, and inventing one would be deciding your intent. Resolve it yourself with swagger:omit on the embed, which drops the promoted twin so only your re-declaration survives:

type Decorated struct {
	// swagger:omit ID
	Base

	// ID is assigned by the server.
	//
	// read only: true
	ID int64
}

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Descriptions beside a $ref

When a struct field’s Go type resolves to a named model, the field becomes a $ref. Strict JSON Schema draft 4 (the dialect OpenAPI 2.0 is built on) says a $ref replaces its siblings β€” so a description, a validation, or an x-* extension written on that field cannot simply sit next to the $ref.

codescan’s default is to preserve those decorations by wrapping the reference in an allOf compound, which is the draft-4-correct shape. Three options tune this behaviour. The decorations split into two classes:

  • description & extensions β€” siblings-eligible: modern tooling (OpenAPI 3.1 / JSON Schema 2020-12, most Swagger-UI renderers) reads them directly beside a $ref.
  • validations & externalDocs β€” compound-only: they have no valid bare-$ref form, so they can only ride an allOf compound.
// Address is a referenced model.
//
// swagger:model
type Address struct {
	// Street is the street line.
	Street string `json:"street"`
}

// Person references Address through a field decorated with a description and a
// vendor extension β€” both can, in principle, sit beside the $ref. How they are
// rendered depends on the options.
//
// swagger:model
type Person struct {
	// Home is where the person lives.
	//
	// extensions:
	//   x-ui-order: 3
	Home Address `json:"home"`
}

Full source: docs/examples/shaping/refsiblings/refsiblings.go

The default β€” an allOf wrapper

With no options set, the field’s description and extension are preserved by wrapping the $ref as the single member of an allOf; the decorations ride the outer schema:

{
  "description": "Home is where the person lives.",
  "allOf": [
    {
      "$ref": "#/definitions/Address"
    }
  ],
  "x-go-name": "Home",
  "x-ui-order": 3
}

Full source: docs/examples/shaping/refsiblings/testdata/default.json

This is the always-correct shape and needs no configuration β€” see also Decorating a $ref in the Model definitions tutorial.

Emit siblings directly β€” EmitRefSiblings

Set Options.EmitRefSiblings to render the description and extensions as direct siblings of the $ref, with no allOf wrapper β€” the leaner shape modern tools expect:

codescan.Run(&codescan.Options{
    Packages:        []string{"./..."},
    ScanModels:      true,
    EmitRefSiblings: true,
})
Default β€” allOf wrapper
{
  "description": "Home is where the person lives.",
  "allOf": [
    {
      "$ref": "#/definitions/Address"
    }
  ],
  "x-go-name": "Home",
  "x-ui-order": 3
}

Full source: docs/examples/shaping/refsiblings/testdata/default.json

EmitRefSiblings: true
{
  "description": "Home is where the person lives.",
  "x-ui-order": 3,
  "$ref": "#/definitions/Address"
}

Full source: docs/examples/shaping/refsiblings/testdata/siblings.json

Info

EmitRefSiblings only changes the cases where nothing else forces a compound. When the field also carries a validation or externalDocs (which cannot live beside a bare $ref), the allOf wrapper is still emitted and the description / extensions ride its outer schema.

Drop the compound entirely β€” SkipAllOfCompounding

Some downstream consumers β€” notably go-swagger’s code generator β€” expect a field that points at a model to be a bare $ref and do not handle the allOf-compounded shape. Set Options.SkipAllOfCompounding to never emit an allOf compound:

codescan.Run(&codescan.Options{
    Packages:             []string{"./..."},
    ScanModels:           true,
    SkipAllOfCompounding: true,
})

No compound is produced, so validations and externalDocs are dropped, and the description and extension go with them β€” leaving a bare $ref:

{
  "$ref": "#/definitions/Address"
}

Full source: docs/examples/shaping/refsiblings/testdata/skip.json

Every dropped decoration is reported through Options.OnDiagnostic (code validate.dropped-ref-sibling), so the loss is never silent. Combine it with EmitRefSiblings to keep the description and extensions as siblings while still dropping the compound-only validations:

codescan.Run(&codescan.Options{
    Packages:             []string{"./..."},
    ScanModels:           true,
    EmitRefSiblings:      true, // keep description / x-* as $ref siblings
    SkipAllOfCompounding: true, // drop validations / externalDocs, no allOf
})
Note

required: is never affected by any of these options. It is a property of the parent object (it lands in the parent’s required list), not a sibling of the $ref, so it is always preserved.

DescWithRef (deprecated)

Options.DescWithRef predates EmitRefSiblings and covers only the narrow description-only case: a $ref’d field whose sole decoration is a description. By default that description is dropped; DescWithRef preserves it by wrapping the $ref in a single-arm allOf.

Default β€” description dropped
{
  "description": "Person references Address through a field whose only decoration is a\ndescription.",
  "type": "object",
  "properties": {
    "home": {
      "$ref": "#/definitions/Address"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/descref"
}

Full source: docs/examples/shaping/descref/testdata/off.json

DescWithRef: true
{
  "description": "Person references Address through a field whose only decoration is a\ndescription.",
  "type": "object",
  "properties": {
    "home": {
      "description": "Home is where the person lives.",
      "allOf": [
        {
          "$ref": "#/definitions/Address"
        }
      ],
      "x-go-name": "Home"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/descref"
}

Full source: docs/examples/shaping/descref/testdata/on.json

Warning

DescWithRef is deprecated β€” prefer EmitRefSiblings, which preserves both descriptions and extensions (as direct siblings). DescWithRef keeps its original behaviour for compatibility and is a no-op when EmitRefSiblings is set.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Titles & descriptions

The same Go doc comments feed both pkg.go.dev and your API documentation, and the two audiences rarely want the exact same words. These knobs let you keep a concise godoc while curating the title / description text the spec carries.

  • Replace the godoc-derived title and description with API-facing text using swagger:title and swagger:description β€” on models, fields, $ref’d fields and responses.
  • Carry a verbatim markdown body β€” tables, blank lines, indentation and all β€” into a description with the swagger:description | literal block-scalar marker, instead of letting Option B fold it.
  • Let swagger annotations live inside a struct body or as trailing comments so the godoc above each declaration stays clean β€” the AfterDeclComments opt-in.
  • Strip godoc doc-link brackets from generated descriptions and recompose resolvable links to each schema’s exposed name β€” the CleanGoDoc opt-in.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Titles & descriptions

Overriding titles & descriptions

A Go doc comment is written for Go readers. The same prose is not always what you want in the published API β€” a comment may explain internal usage, reference Go types, or simply read awkwardly to an API consumer. swagger:title and swagger:description let the spec text diverge from the godoc: the annotation replaces the prose-derived value, leaving the Go comment free to say whatever Go developers need.

This is the explicit counterpart to Single-line comments, which controls how a plain comment is implicitly routed to title vs description. Each pane below pairs the annotated Go (left) with the exact fragment the scanner emits (right), from the test-covered docs/examples/shaping/overrides package.

Overriding a model and its fields

swagger:title <text> sets the title; swagger:description <text> sets the description. Both sit in the comment block beside swagger:model (on a type) or beside a field’s other keywords. The model’s Go-facing godoc here is replaced wholesale by the two overrides:

// Widget is the Go-facing widget doc, written for Go readers.
//
// It explains internal Go usage that should not leak into the API spec.
//
// swagger:model
// swagger:title A Public Widget
// swagger:description A widget exposed via the public API.
type Widget struct {
	// ID explains the Go field for Go readers.
	//
	// swagger:description The unique widget identifier.
	ID string `json:"id"`

	// Label is the Go-facing field doc. Fields carry no title by default;
	// the override is the only way a property gets one.
	//
	// swagger:title Display Label
	// swagger:description Human-readable label shown to API consumers.
	Label string `json:"label"`

	// Plain keeps its godoc description because it carries no override.
	Plain string `json:"plain"`

	// Capacity combines a description override with an inline validation
	// keyword on the same field: the override applies AND maximum is kept,
	// because the override annotations dispatch through the schema family.
	//
	// swagger:description The maximum capacity, in liters.
	// maximum: 1000
	Capacity int64 `json:"capacity"`

	// Suppressed has a godoc that a bare swagger:description suppresses: the
	// empty value is applied (description omitted) and scan.empty-override is
	// raised, in case the bare marker was left behind by mistake.
	//
	// swagger:description
	Suppressed string `json:"suppressed"`

	// Notes carries a multi-line description override: the lines following the
	// annotation fold into the description until the blank line, joined with
	// newlines.
	//
	// swagger:description Free-form notes about the widget.
	// They may span several lines, all folded into one description.
	//
	// The blank line above terminates the override body; this paragraph is
	// ordinary godoc and is discarded (the override won).
	Notes string `json:"notes"`

	// Gadget is a $ref field carrying title + description overrides. They are
	// symmetric $ref siblings: kept under EmitRefSiblings, dropped to a bare
	// $ref under the default flags β€” the same rule a prose description follows.
	//
	// swagger:title Gadget Ref
	// swagger:description The attached gadget, described for API consumers.
	Gadget Gadget `json:"gadget"`
}

// Gadget is a plain referenced model.
//
// swagger:model
type Gadget struct {
	Serial string `json:"serial"`
}

Full source: docs/examples/shaping/overrides/overrides.go

{
  "description": "A widget exposed via the public API.",
  "type": "object",
  "title": "A Public Widget",
  "properties": {
    "capacity": {
      "description": "The maximum capacity, in liters.",
      "type": "integer",
      "format": "int64",
      "maximum": 1000,
      "x-go-name": "Capacity"
    },
    "gadget": {
      "$ref": "#/definitions/Gadget"
    },
    "id": {
      "description": "The unique widget identifier.",
      "type": "string",
      "x-go-name": "ID"
    },
    "label": {
      "description": "Human-readable label shown to API consumers.",
      "type": "string",
      "title": "Display Label",
      "x-go-name": "Label"
    },
    "notes": {
      "description": "Free-form notes about the widget.\nThey may span several lines, all folded into one description.",
      "type": "string",
      "x-go-name": "Notes"
    },
    "plain": {
      "description": "Plain keeps its godoc description because it carries no override.",
      "type": "string",
      "x-go-name": "Plain"
    },
    "suppressed": {
      "type": "string",
      "x-go-name": "Suppressed"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/overrides"
}

Full source: docs/examples/shaping/overrides/testdata/widget.json

A few things to read out of that pane:

  • title on a property comes only from an override. A field’s godoc becomes its description; codescan never derives a property title from prose, so swagger:title (as on label) is the only way to set one.
  • plain keeps its godoc β€” no override means no change. Overrides are strictly opt-in; un-annotated declarations behave exactly as before.

Multi-line descriptions

swagger:description may span several lines. The lines immediately following the annotation fold into one description (joined with newlines) and the body terminates at the first blank line, keyword, annotation, or end of comment. The notes field above shows this: its two prose lines fold together, and the ordinary godoc paragraph after the blank line is discarded.

To carry a body past a blank line β€” a markdown table, a multi-paragraph description β€” end the annotation line with a | literal block marker; see Markdown descriptions.

Keeping a co-located validation keyword

Because the override annotations dispatch through the schema family, a validation keyword on the same field still applies β€” they co-exist rather than one shadowing the other. The capacity field carries both swagger:description and maximum: 1000, and the output keeps both.

Suppressing a godoc comment

A bare swagger:description (no text, empty body) applies the empty value β€” a deliberate way to drop a godoc comment from the spec without deleting it from the source. Because a stray bare marker could also be an accident, codescan raises a scan.empty-override warning through OnDiagnostic. The suppressed field above emits no description at all.

Overrides beside a $ref

title and description are symmetric $ref siblings: on a field whose Go type is a referenced model, they follow the same preservation rule a prose description does. Under the default flags they drop to a bare $ref; with EmitRefSiblings they ride alongside the $ref as direct siblings.

Default β€” dropped to a bare $ref
{
  "$ref": "#/definitions/Gadget"
}

Full source: docs/examples/shaping/overrides/testdata/gadget_bare.json

EmitRefSiblings β€” kept as siblings
{
  "description": "The attached gadget, described for API consumers.",
  "title": "Gadget Ref",
  "$ref": "#/definitions/Gadget"
}

Full source: docs/examples/shaping/overrides/testdata/gadget_siblings.json

Responses and headers

swagger:description also overrides the description of a swagger:response and of its response headers. OpenAPI 2.0 Response and Header objects have no title field, so a swagger:title on a response or header is rejected with a parse.context-invalid diagnostic β€” the description override still applies.

// ErrorResponse is the Go-facing response doc, written for Go readers β€” it
// should not leak into the API spec.
//
// swagger:response errorResponse
// swagger:description The error payload returned to API consumers.
// swagger:title Ignored β€” responses have no title
type ErrorResponse struct {
	// XErrorCode is the Go-facing header doc.
	//
	// swagger:description The machine-readable error code.
	XErrorCode string `json:"X-Error-Code"`

	// ErrorBody carries the structured error.
	//
	// in: body
	Body ErrorBody `json:"body"`
}

// ErrorBody is the error payload returned in the response body.
//
// swagger:model
type ErrorBody struct {
	Message string `json:"message"`
}

Full source: docs/examples/shaping/overrides/overrides.go

{
  "description": "The error payload returned to API consumers.",
  "schema": {
    "$ref": "#/definitions/ErrorBody"
  },
  "headers": {
    "X-Error-Code": {
      "type": "string",
      "description": "The machine-readable error code."
    }
  }
}

Full source: docs/examples/shaping/overrides/testdata/errorresponse.json

Info

Precedence. An override always wins over the godoc-derived value. Absent β†’ the godoc is used unchanged. Empty (bare marker) β†’ the empty value is applied and scan.empty-override is raised. swagger:title is schema-only; on a response/header it is dropped with parse.context-invalid.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Markdown descriptions

A multi-line swagger:description normally folds its body with the Option B rule: contiguous prose lines up to the first blank line, each trimmed. That’s right for a paragraph of prose, but it destroys markdown β€” a blank line ends the description, and leading indentation and table pipes are stripped. So a table or a multi-paragraph body never survives the trip into the spec.

Ending the annotation line with a lone | β€” the YAML literal block-scalar marker β€” opts the body into verbatim capture instead. Everything below is taken exactly as written β€” blank lines, indentation, table pipes and --- all preserved β€” until the next annotation or the end of the comment. It is opt-in per annotation; a plain swagger:description (no |) keeps the Option B behaviour unchanged.

Plain prose vs a verbatim body

These two models carry the same markdown body. The first uses an ordinary annotation; the second adds the | marker:

// Plain uses an ordinary description annotation.
//
// swagger:description
// Option B folds prose up to the first blank line, so only this sentence
// survives β€” the markdown table below never reaches the spec.
//
// | name | purpose |
// |------|---------|
// | foo  | bars    |
//
// swagger:model Plain
type Plain struct {
	Name string `json:"name"`
}

Full source: docs/examples/shaping/markdowndesc/markdowndesc.go

// Markdown opts into a verbatim body with the literal block marker.
//
// swagger:description |
// The body is captured **verbatim** β€” pipes, blank lines and all:
//
// | name | purpose |
// |------|---------|
// | foo  | bars    |
//
// - point one
// - point two
//
// swagger:model Markdown
type Markdown struct {
	// Name of the widget.
	//
	// swagger:description |
	// The name must be:
	//
	//   1. unique
	//   2. lowercase
	Name string `json:"name"`
}

Full source: docs/examples/shaping/markdowndesc/markdowndesc.go

The emitted descriptions diverge sharply:

Plain β€” Option B folds
{
  "description": "Option B folds prose up to the first blank line, so only this sentence\nsurvives β€” the markdown table below never reaches the spec.",
  "type": "object",
  "title": "Plain uses an ordinary description annotation.",
  "properties": {
    "name": {
      "type": "string",
      "x-go-name": "Name"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/markdowndesc"
}

Full source: docs/examples/shaping/markdowndesc/testdata/plain.json

swagger:description | β€” verbatim
{
  "description": "The body is captured **verbatim** β€” pipes, blank lines and all:\n\n| name | purpose |\n|------|---------|\n| foo  | bars    |\n\n- point one\n- point two",
  "type": "object",
  "title": "Markdown opts into a verbatim body with the literal block marker.",
  "properties": {
    "name": {
      "description": "The name must be:\n\n  1. unique\n  2. lowercase",
      "type": "string",
      "x-go-name": "Name"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/markdowndesc"
}

Full source: docs/examples/shaping/markdowndesc/testdata/markdown.json

  • Option B stops at the first blank line. The plain model’s description is just the opening sentence β€” the table that follows the blank line is dropped entirely (the original go-swagger#3211 grievance).
  • The | body is captured whole. Table leading pipes, the significant blank line, and the bullet list after it all ride through verbatim.
  • The marker never leaks. The trailing |, the swagger:description line itself, and the single godoc // convention space per line are all stripped; interior indentation and trailing whitespace (markdown hard breaks) are kept.
  • The title is unaffected. It still comes from the godoc preamble above the annotation β€” only the description body becomes verbatim.

It works on a field description just the same β€” the name property above keeps the indentation of its ordered list ( 1. unique).

Where the block ends

The literal block runs until the next annotation at the start of a line, or the end of the doc comment. In the examples above, the trailing swagger:model Widget line closes the block.

A swagger: token mid-line is ordinary prose and stays in the body β€” only a line that begins with an annotation terminates. Indentation doesn’t shield such a line, though: the comment prefix and leading whitespace are stripped before the check, so a line-leading swagger: inside an indented markdown code block still ends the block. Keep annotation-looking lines out of the verbatim body, or place them before the | annotation.

Note

This reframes go-swagger#3211: markdown is authored explicitly via swagger:description |, never recovered from ambient godoc prose. A plain doc comment stays plain β€” godoc and the spec keep their separate conventions.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Single-line comments as descriptions

By the first-sentence convention, a single-line doc comment that ends in punctuation becomes the object’s title (on a model or the info block) or summary (on an operation); without trailing punctuation it is a description. That is the right default for most codebases, but some use single-line comments purely as prose β€” and then a stray period silently promotes the line to a title.

Options.SingleLineCommentAsDescription opts out of the promotion: a single-line comment is always a description, never a title / summary.

codescan.Run(&codescan.Options{
    Packages:                       []string{"./..."},
    ScanModels:                     true,
    SingleLineCommentAsDescription: true,
})

The witness pairs a model and an operation, each with a single-line comment that ends in a period:

// Gadget is a small device.
//
// swagger:model
type Gadget struct {
	Name string `json:"name"`
}

Full source: docs/examples/shaping/singleline/singleline.go

// swagger:route GET /gadgets gadgets listGadgets
//
// Lists every gadget in the catalog.
//
// responses:
//
//	200: gadgetsResponse

Full source: docs/examples/shaping/singleline/singleline.go

The same source, scanned both ways β€” the comment moves from title / summary to description uniformly:

Default β€” title / summary
{
  "modelDescription": "",
  "modelTitle": "Gadget is a small device.",
  "operationDescription": "",
  "operationSummary": "Lists every gadget in the catalog."
}

Full source: docs/examples/shaping/singleline/testdata/off.json

SingleLineCommentAsDescription: true
{
  "modelDescription": "Gadget is a small device.",
  "modelTitle": "",
  "operationDescription": "Lists every gadget in the catalog.",
  "operationSummary": ""
}

Full source: docs/examples/shaping/singleline/testdata/on.json

Info

Only the single-line case changes. A multi-line doc comment keeps the existing split β€” the first line (or the paragraph before the first blank line) stays the title, and the rest becomes the description. Reach for this option when your house style writes one-line prose descriptions and you don’t want them landing in title / summary; otherwise leave it off (the default) and write a two-line comment, or drop the trailing period, when you want a description.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Keeping annotations out of the godoc

A godoc comment and an API description pursue different goals. The godoc is for the Go developers reading the package; the API text is for the consumers of the generated spec. By default codescan reads its annotations from the doc comment above a declaration, which mixes the two concerns β€” a swagger:model, maxProperties: or swagger:strfmt line sits right in the middle of the prose a Go reader sees.

AfterDeclComments separates them. With the option on, codescan also reads annotations placed inside a struct body (its leading comment) or inlined as a trailing comment on the same line as the declaration. The godoc above stays concise and human-facing while the swagger machinery lives out of it β€” same annotation grammar, no new syntax. It is the placement counterpart to overriding titles & descriptions, which separates the same two concerns at the text level.

Each pane below pairs the annotated Go (left) with the exact fragment the scanner emits (right), from the test-covered docs/examples/shaping/afterdecl package.

Inside a struct body, or trailing on a field

The swagger:model annotation (and any decl-level keyword such as maxProperties:) can live as the leading comment inside the struct body, above the first field. A field-level annotation like swagger:strfmt can ride a trailing comment on the field line. The godoc above Widget says nothing about swagger:

// Widget is a widget. This godoc stays clean β€” no swagger machinery here.
type Widget struct {
	// Widget is exposed to API consumers.
	//
	// swagger:model widgetModel
	// maxProperties: 5

	Name string `json:"name"`

	// Created is documented with a clean godoc; the format annotation is
	// inlined as a trailing comment.
	Created string `json:"created"` // swagger:strfmt date
}

Full source: docs/examples/shaping/afterdecl/afterdecl.go

Inlined on a defined type or alias

For a non-struct type β€” a defined type or a type alias β€” the annotation rides a trailing comment after the declaration:

// Count is a plain count. Clean godoc above; annotation inlined below.
type Count int // swagger:model countType

// Stamp is a string alias. Clean godoc above; annotation inlined trailing.
type Stamp = string // swagger:model stampType

Full source: docs/examples/shaping/afterdecl/afterdecl.go

Turning it on

AfterDeclComments is opt-in and defaults to off β€” so existing code, where a clean comment that happens to look like an annotation is just prose, is never reinterpreted:

codescan.Run(&codescan.Options{
    Packages:          []string{"./..."},
    ScanModels:        true,
    AfterDeclComments: true,
})

With the option off, the inside-body and trailing annotations above are inert β€” the clean godoc carries no annotation, so nothing is discovered. With it on, the same source yields the three definitions, each with its keywords applied (and the clean godoc still supplies the human-facing title):

Default β€” annotations inert
{}

Full source: docs/examples/shaping/afterdecl/testdata/off.json

AfterDeclComments β€” discovered
{
  "countType": {
    "type": "integer",
    "format": "int64",
    "title": "Count is a plain count. Clean godoc above; annotation inlined below.",
    "x-go-name": "Count",
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/afterdecl"
  },
  "stampType": {
    "type": "string",
    "title": "Stamp is a string alias. Clean godoc above; annotation inlined trailing.",
    "x-go-name": "Stamp",
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/afterdecl"
  },
  "widgetModel": {
    "description": "Widget is exposed to API consumers.",
    "type": "object",
    "title": "Widget is a widget. This godoc stays clean β€” no swagger machinery here.",
    "maxProperties": 5,
    "properties": {
      "created": {
        "description": "Created is documented with a clean godoc; the format annotation is\ninlined as a trailing comment.",
        "type": "string",
        "format": "date",
        "x-go-name": "Created"
      },
      "name": {
        "type": "string",
        "x-go-name": "Name"
      }
    },
    "x-go-name": "Widget",
    "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/afterdecl"
  }
}

Full source: docs/examples/shaping/afterdecl/testdata/on.json

Info

Scope (v0.36). The opt-in covers type declarations β€” a struct’s inside-body leading comment, a struct field’s trailing comment, and the trailing comment of a defined type or alias. Routes and operations are already position-agnostic: a swagger:route / swagger:operation block inside a function body is discovered with or without this option. Const-based enums are a planned follow-up.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Cleaning godoc doc-links

A Go doc comment can use godoc’s doc-link syntax β€” [Gadget], [Order.CustName], reference-style [text]: url lines. Those render as live links in pkg.go.dev, but carried verbatim into a spec title / description they read as bracket noise, and the bracketed Go identifier is rarely the name the schema is actually exposed under.

CleanGoDoc tidies that up. With the option on, godoc doc-link brackets are removed and β€” when a link resolves to a scanned schema β€” the span is recomposed to the name that schema is exposed under, so the prose stays true to the generated definitions. It applies only to godoc-derived prose; an author-written swagger:title / swagger:description override is deliberate text and is never touched.

Each pane below pairs the annotated Go (left) with the exact fragment the scanner emits (right), from the test-covered docs/examples/shaping/godoclinks package.

What gets cleaned

This model β€” its doc comment and its fields β€” is dense with doc-link syntax: a self-reference, links to other models, a pointer, a cross-package link, an unknown identifier, ordinary brackets, and a reference-definition line:

// Widget is the primary [Gadget] holder and references [Order.CustName].
//
// More detail mentions a [*Gadget] pointer, a [inventory.Ledger], and an
// unknown [Sprocket].
//
// swagger:model gizmo
type Widget struct {
	// Holder points at the [Gadget] that owns this widget.
	Holder string `json:"holder"`

	// Ledger is the cross-package [inventory.Ledger] reference.
	Ledger *inventory.Ledger `json:"ledger"`

	// Index is element [0] in the [see notes] list; the [id] stays bare.
	Index int `json:"index"`

	// Spec points at [Gadget]; the reference-definition line below is godoc
	// link plumbing that carries no prose.
	//
	// [the spec]: https://example.com/spec
	Spec string `json:"spec"`
}

Full source: docs/examples/shaping/godoclinks/godoclinks.go

Scanned with CleanGoDoc off the godoc is emitted verbatim; on, every doc-link is resolved or humanized and the reference-definition line is dropped:

Default β€” verbatim
{
  "description": "More detail mentions a [*Gadget] pointer, a [inventory.Ledger], and an\nunknown [Sprocket].",
  "type": "object",
  "title": "Widget is the primary [Gadget] holder and references [Order.CustName].",
  "properties": {
    "holder": {
      "description": "Holder points at the [Gadget] that owns this widget.",
      "type": "string",
      "x-go-name": "Holder"
    },
    "index": {
      "description": "Index is element [0] in the [see notes] list; the [id] stays bare.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Index"
    },
    "ledger": {
      "$ref": "#/definitions/Ledger"
    },
    "spec": {
      "description": "Spec points at [Gadget]; the reference-definition line below is godoc\nlink plumbing that carries no prose.\n\n[the spec]: https://example.com/spec",
      "type": "string",
      "x-go-name": "Spec"
    }
  },
  "x-go-name": "Widget",
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/godoclinks"
}

Full source: docs/examples/shaping/godoclinks/testdata/gizmo_off.json

CleanGoDoc β€” cleaned
{
  "description": "More detail mentions a Gadget pointer, a Ledger, and an\nunknown sprocket.",
  "type": "object",
  "title": "Gizmo is the primary Gadget holder and references Order.customer_name.",
  "properties": {
    "holder": {
      "description": "Holder points at the Gadget that owns this widget.",
      "type": "string",
      "x-go-name": "Holder"
    },
    "index": {
      "description": "Index is element [0] in the [see notes] list; the [id] stays bare.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Index"
    },
    "ledger": {
      "$ref": "#/definitions/Ledger"
    },
    "spec": {
      "description": "Spec points at Gadget; the reference-definition line below is godoc\nlink plumbing that carries no prose.",
      "type": "string",
      "x-go-name": "Spec"
    }
  },
  "x-go-name": "Widget",
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/godoclinks"
}

Full source: docs/examples/shaping/godoclinks/testdata/gizmo_on.json

Reading the cleaned pane:

  • Links recompose to the exposed name. [Gadget] β†’ Gadget (no override, so its Go name); the [Order.CustName] member link β†’ Order.customer_name (the model name plus the field’s json name); and the leading self-name Widget β†’ Gizmo, because the model is published as swagger:model gizmo (restored to sentence case). A cross-package [inventory.Ledger] resolves through the file’s imports to Ledger.
  • Unresolved links are humanized. [Sprocket] names no scanned model, so it becomes the plain word sprocket rather than a dangling bracket.
  • Reference-definition lines are dropped. The [the spec]: https://… line on the spec field is link plumbing carrying no prose, so the whole line is removed.
Info

It recomposes to the final exposed name. The substitution runs after codescan resolves definition names, so a link to a model that gets renamed to deconflict a collision points at the renamed definition, not the original Go identifier.

Conservative by design

Only a genuine doc-link is rewritten β€” a dotted chain ([pkg.Type]) or an uppercase-led identifier ([Widget]). Ordinary prose brackets are left exactly as written, as the index field above shows: [0], [see notes] and the bare-lowercase [id] all survive untouched.

CleanGoDoc is opt-in and defaults to off β€” with it off, output is byte-identical to before, so existing specs never shift under you.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Field types & formats

These knobs act at the level of a single property: the format it carries, whether a pointer is advertised as nullable, and the vendor extensions that record its Go provenance.

  • Override a Go-derived format (e.g. the vendor uint64/uint32 formats) with an official, JSON-conformant one using a field-level swagger:strfmt.
  • How io.Reader, multipart.File and the other stream types render β€” type: file on a formData parameter, base64 bytes everywhere else β€” and how to say what the bytes actually are.
  • Mark pointer-typed fields as nullable with x-nullable, via SetXNullableForPointers.
  • Control the x-go-* vendor extensions codescan emits, or suppress them with SkipExtensions.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Field types & formats

Forcing a conformant format

codescan derives a JSON-Schema type and format from each Go type. For the unsized and large integer kinds it emits Go-specific vendor formats β€” uint64 β†’ {type: integer, format: uint64}, uint32 β†’ {integer, uint32}, and so on. These round-trip cleanly back to Go, but they are not part of the Swagger 2.0 format set, and a uint64 value can exceed what a JSON number safely represents.

When you need conformant, precision-safe output, place a field-level swagger:strfmt on the field to override just that property’s format. Overriding to int64 publishes the value as a string-encoded {type: string, format: int64}:

model
// Measurement forces a JSON-conformant format on a field. A uint64 field emits
// the Go-specific `{integer, format: uint64}` by default; overriding it with a
// field-level `swagger:strfmt int64` (below) yields a precision-safe,
// string-encoded `{string, format: int64}`.
//
// swagger:model
type Measurement struct {
	// Raw keeps the default Go-derived vendor format (uint64).
	Raw uint64 `json:"raw"`

	// Bounded is forced to a conformant, string-encoded int64.
	//
	// swagger:strfmt int64
	Bounded uint64 `json:"bounded"`
}

Full source: docs/examples/shaping/formats/formats.go

#/definitions/Measurement
{
  "description": "Measurement forces a JSON-conformant format on a field. A uint64 field emits\nthe Go-specific `{integer, format: uint64}` by default; overriding it with a\nfield-level `swagger:strfmt int64` (below) yields a precision-safe,\nstring-encoded `{string, format: int64}`.",
  "type": "object",
  "properties": {
    "bounded": {
      "description": "Bounded is forced to a conformant, string-encoded int64.",
      "type": "string",
      "format": "int64",
      "x-go-name": "Bounded"
    },
    "raw": {
      "description": "Raw keeps the default Go-derived vendor format (uint64).",
      "type": "integer",
      "format": "uint64",
      "x-go-name": "Raw"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/formats"
}

Full source: docs/examples/shaping/formats/testdata/measurement.json

Raw keeps the default {integer, uint64} vendor format; Bounded carries swagger:strfmt int64, so it renders as a string-encoded int64. The override is per-field β€” the underlying Go type is untouched everywhere else.

Info

swagger:strfmt also names a custom string format on a type declaration (e.g. a UUID type β†’ {string, format: uuid}); see Model definitions β†’ swagger:strfmt. The swagger:type annotation is the related tool when you want to override the whole type, not just its format β€” see Type discovery and the swagger:type reference.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

File uploads and byte streams

A Go type like io.Reader says that bytes will flow. It says nothing about what they are, how they are framed, or how long they run. codescan recognizes these types and answers with the only two things Swagger 2.0 lets it say about opaque bytes β€” picked by where the field sits, not by anything in the declaration.

The two answers

PositionRendering
in: formData parametertype: file
model field, body, response body, header, other parameters{type: string, format: byte}

type: file is the canonical upload shape, and formData is the only location Swagger 2.0 permits it in. Everywhere else the bytes travel inside a JSON document, which cannot carry raw octets β€” so they render as format: byte, the base64-encoded string the specification defines for exactly this.

Uploading a file

Put the stream in a formData parameter and consume multipart/form-data:

parameters
// UploadParams uploads a file and its metadata.
//
// swagger:parameters uploadAttachment
type UploadParams struct {
	// Upload is the file to store.
	//
	// in: formData
	Upload multipart.File `json:"upload"`

	// Caption describes the upload.
	//
	// in: formData
	Caption string `json:"caption"`
}

Full source: docs/examples/shaping/streams/streams.go

parameters
[
  {
    "type": "file",
    "x-go-name": "Upload",
    "x-go-type": "mime/multipart.File",
    "description": "Upload is the file to store.",
    "name": "upload",
    "in": "formData"
  },
  {
    "type": "string",
    "x-go-name": "Caption",
    "description": "Caption describes the upload.",
    "name": "caption",
    "in": "formData"
  }
]

Full source: docs/examples/shaping/streams/testdata/upload_params.json

upload becomes type: file; the sibling caption is an ordinary form field. multipart.File and io.Reader are interchangeable here β€” both are recognized.

Streams in a model or a body

Anywhere that is not a formData parameter, the same types render as base64 bytes:

model
// Attachment carries opaque byte streams as model fields.
//
// A stream says nothing about its own framing, so codescan does not invent one:
// each field renders as `{string, format: byte}` β€” the base64-encoded string
// Swagger 2.0 uses for arbitrary bytes.
//
// swagger:model
type Attachment struct {
	// Content is the attachment payload.
	Content io.Reader `json:"content"`

	// Thumbnail is a closeable stream; the same answer applies.
	Thumbnail io.ReadCloser `json:"thumbnail"`

	// Checksum says what its bytes are, so the annotation wins over the default.
	//
	// swagger:strfmt base64
	Checksum io.Reader `json:"checksum"`
}

Full source: docs/examples/shaping/streams/streams.go

#/definitions/Attachment
{
  "description": "A stream says nothing about its own framing, so codescan does not invent one:\neach field renders as `{string, format: byte}` β€” the base64-encoded string\nSwagger 2.0 uses for arbitrary bytes.",
  "type": "object",
  "title": "Attachment carries opaque byte streams as model fields.",
  "properties": {
    "checksum": {
      "description": "Checksum says what its bytes are, so the annotation wins over the default.",
      "type": "string",
      "format": "base64",
      "x-go-name": "Checksum",
      "x-go-type": "io.Reader"
    },
    "content": {
      "description": "Content is the attachment payload.",
      "type": "string",
      "format": "byte",
      "x-go-name": "Content",
      "x-go-type": "io.Reader"
    },
    "thumbnail": {
      "description": "Thumbnail is a closeable stream; the same answer applies.",
      "type": "string",
      "format": "byte",
      "x-go-name": "Thumbnail",
      "x-go-type": "io.ReadCloser"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/streams"
}

Full source: docs/examples/shaping/streams/testdata/attachment.json

content and thumbnail carry {string, byte}. checksum carries swagger:strfmt base64, and the annotation wins β€” which is the point of the next section.

Say what the bytes are

The default is deliberately uninformative, because a stream is uninformative. When you know more, say so and codescan will step aside:

  • swagger:strfmt β€” name the format (base64, binary, a custom one);
  • swagger:type β€” override the type outright;
  • swagger:file β€” force the file shape where you want it and the position allows it.

What is recognized

PackageTypes
ioReader, ReadCloser, ReadSeeker, ReadSeekCloser, ReadWriter, ReaderAt, ReaderFrom, LimitedReader, ByteReader, ByteScanner
mime/multipartFile
github.com/go-openapi/runtimeNamedReadCloser

Recognition is by identity β€” the exact named type β€” never by shape. An interface of your own that happens to have a Read method is your type and is documented as you declared it.

Because both renderings erase which stream it was β€” every type in the table produces the same schema β€” each one also carries an x-go-type extension naming the Go type it came from, so a consumer can tell an io.Reader field from a multipart.File one. SkipExtensions suppresses it along with the rest of the x-go-* family.

Note

io.Writer is not recognized, nor are the write-only closers. A sink the caller writes into is not something that travels on the wire, so codescan does not assume what you meant by putting one in an API type β€” it documents the type structurally, and you override it if you had something in mind.

What’s next

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Nullable pointers

Swagger 2.0 has no native nullable flag; the go-openapi toolchain uses the x-nullable vendor extension. Options.SetXNullableForPointers decides whether pointer-typed struct fields acquire it automatically. The model below has two pointer fields:

// Profile has required and optional (pointer) fields.
//
// swagger:model
type Profile struct {
	// Name is always present.
	Name string `json:"name"`

	// Nickname is optional.
	Nickname *string `json:"nickname"`

	// Age is optional.
	Age *int32 `json:"age"`
}

Full source: docs/examples/shaping/nullable/nullable.go

Scanned with the option off (default) and on, the pointer fields differ:

Default
{
  "type": "object",
  "title": "Profile has required and optional (pointer) fields.",
  "properties": {
    "age": {
      "description": "Age is optional.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Age"
    },
    "name": {
      "description": "Name is always present.",
      "type": "string",
      "x-go-name": "Name"
    },
    "nickname": {
      "description": "Nickname is optional.",
      "type": "string",
      "x-go-name": "Nickname"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nullable"
}

Full source: docs/examples/shaping/nullable/testdata/off.json

SetXNullableForPointers: true
{
  "type": "object",
  "title": "Profile has required and optional (pointer) fields.",
  "properties": {
    "age": {
      "description": "Age is optional.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Age",
      "x-nullable": true
    },
    "name": {
      "description": "Name is always present.",
      "type": "string",
      "x-go-name": "Name"
    },
    "nickname": {
      "description": "Nickname is optional.",
      "type": "string",
      "x-go-name": "Nickname",
      "x-nullable": true
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/nullable"
}

Full source: docs/examples/shaping/nullable/testdata/on.json

codescan.Run(&codescan.Options{
    Packages:                []string{"./..."},
    ScanModels:              true,
    SetXNullableForPointers: true,
})
Info

omitempty changes the meaning. A pointer field tagged json:"…,omitempty" is treated as optional (may be absent) rather than nullable (may be null), so it does not receive x-nullable even with the option on. Drop omitempty when you mean the value can be present-but-null.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Vendor extensions

By default codescan records where each spec object came from in Go via x-go-name (and x-go-package on definitions) β€” useful for round-tripping and code generation. Options.SkipExtensions removes them for a leaner spec.

// Widget is a small model.
//
// codescan records each field's Go origin as vendor extensions unless
// SkipExtensions is set.
//
// swagger:model
type Widget struct {
	// Label is the display label.
	Label string `json:"label"`

	// Size is the widget size in pixels.
	Size int32 `json:"size"`
}

Full source: docs/examples/shaping/extensions/extensions.go

Default
{
  "description": "codescan records each field's Go origin as vendor extensions unless\nSkipExtensions is set.",
  "type": "object",
  "title": "Widget is a small model.",
  "properties": {
    "label": {
      "description": "Label is the display label.",
      "type": "string",
      "x-go-name": "Label"
    },
    "size": {
      "description": "Size is the widget size in pixels.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Size"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/extensions"
}

Full source: docs/examples/shaping/extensions/testdata/off.json

SkipExtensions: true
{
  "description": "codescan records each field's Go origin as vendor extensions unless\nSkipExtensions is set.",
  "type": "object",
  "title": "Widget is a small model.",
  "properties": {
    "label": {
      "description": "Label is the display label.",
      "type": "string"
    },
    "size": {
      "description": "Size is the widget size in pixels.",
      "type": "integer",
      "format": "int32"
    }
  }
}

Full source: docs/examples/shaping/extensions/testdata/on.json

codescan.Run(&codescan.Options{
    Packages:       []string{"./..."},
    ScanModels:     true,
    SkipExtensions: true,
})

SkipExtensions removes the scanner-derived x-go-* extensions. Extensions you author yourself (via the Extensions: keyword) are not affected, and neither is x-deprecated (it carries semantic intent β€” see Other type decorators).

Stamping x-go-type

x-go-name and x-go-package record where a definition came from, but not the originating type’s own name. Options.EmitXGoType adds an x-go-type extension carrying the fully-qualified Go type (<package path>.<type name>) β€” useful for round-tripping a generated spec back to its source types:

codescan.Run(&codescan.Options{
    Packages:    []string{"./..."},
    ScanModels:  true,
    EmitXGoType: true,
})
Default β€” no x-go-type
{
  "description": "codescan records each field's Go origin as vendor extensions unless\nSkipExtensions is set.",
  "type": "object",
  "title": "Widget is a small model.",
  "properties": {
    "label": {
      "description": "Label is the display label.",
      "type": "string",
      "x-go-name": "Label"
    },
    "size": {
      "description": "Size is the widget size in pixels.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Size"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/extensions"
}

Full source: docs/examples/shaping/extensions/testdata/off.json

EmitXGoType: true
{
  "description": "codescan records each field's Go origin as vendor extensions unless\nSkipExtensions is set.",
  "type": "object",
  "title": "Widget is a small model.",
  "properties": {
    "label": {
      "description": "Label is the display label.",
      "type": "string",
      "x-go-name": "Label"
    },
    "size": {
      "description": "Size is the widget size in pixels.",
      "type": "integer",
      "format": "int32",
      "x-go-name": "Size"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/extensions",
  "x-go-type": "github.com/go-openapi/codescan/docs/examples/shaping/extensions.Widget"
}

Full source: docs/examples/shaping/extensions/testdata/xgotype.json

The stamp lands on the definition, beside x-go-package. It is opt-in and default-off, so existing specs are unchanged; it is presence-guarded, so it never overwrites the deliberate x-go-type the special-type recognizers already set (error, the unmodellable generic-type fallback). Like the other x-go-* extensions it rides the SkipExtensions umbrella β€” set SkipExtensions and no x-go-type is emitted either.

Enum descriptions

A swagger:enum type backed by Go const declarations folds the constβ†’value mapping into the field’s description and duplicates it in the x-go-enum-desc extension. When the prose already says everything you want, the folded mapping is noise. Options.SkipEnumDescriptions keeps the authored prose as the description; the mapping then rides x-go-enum-desc only:

codescan.Run(&codescan.Options{
    Packages:             []string{"./..."},
    ScanModels:           true,
    SkipEnumDescriptions: true,
})

This knob is independent of SkipExtensions: set both to drop the mapping everywhere (no description folding, no x-go-enum-desc).

Authoring x-* on parameters and headers

The x-go-* extensions above are scanner-derived. To attach your own vendor extension β€” say x-example for a tool like Dredd β€” use an Extensions: block in the doc comment. It works on a model (the x-* lands on the definition), a model field, a parameter, and a response header alike. (A bare // x-example: 2 line would be read as the description; the Extensions: block is the supported form.)

// ListWidgetsParams decorates a query parameter with an author-supplied vendor
// extension through an Extensions: block β€” useful for tools (e.g. Dredd) that
// read x-example. A bare `x-example:` line would be swallowed as the
// description, so the Extensions: block is the supported form.
//
// swagger:parameters listWidgets
type ListWidgetsParams struct {
	// Page is the page number.
	//
	// in: query
	//
	// Extensions:
	//   x-example: 2
	Page int32 `json:"page"`
}

// WidgetList responds with a header that also carries a vendor extension β€”
// parameters and response headers both honour Extensions:.
//
// swagger:response widgetList
type WidgetList struct {
	// X-Rate-Limit is the per-window request budget.
	//
	// Extensions:
	//   x-units: requests-per-minute
	XRateLimit int32 `json:"X-Rate-Limit"`
}

// swagger:route GET /widgets widgets listWidgets
//
// responses:
//
//	200: widgetList

Full source: docs/examples/shaping/extensions/extensions.go

{
  "parameter": {
    "type": "integer",
    "format": "int32",
    "x-example": 2,
    "description": "Page is the page number.",
    "name": "page",
    "in": "query"
  },
  "responseHeader": {
    "type": "integer",
    "format": "int32",
    "description": "X-Rate-Limit is the per-window request budget.",
    "x-units": "requests-per-minute"
  }
}

Full source: docs/examples/shaping/extensions/testdata/paramext.json

Author-supplied extensions are not stripped by SkipExtensions β€” the fragment above is produced with SkipExtensions: true, yet x-example and x-units survive, because the flag only removes the scanner-derived x-go-* set.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Response bodies

When a handler’s actual Go return type doesn’t map cleanly to the payload you want documented, these knobs let you pin the response body the spec describes β€” inline on the route, or via a doc-only struct that stands in for a generic envelope.

  • Declare a route’s responses inline with the body: sub-language β€” a primitive, an array, or a model $ref β€” without writing a swagger:response struct.
  • Your handlers return one generic envelope with an interface{} payload, but you want the spec to describe a concrete type per operation. Doc-only structs that embed the envelope and shadow the payload field close the gap.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Response bodies

Inline response bodies

The Routes & operations tutorial declares each response as a swagger:response struct with a Body field. That is the right tool when a response is reused across operations or has headers. But when a response body is just “a string”, “an array of Pet, or “a Pet, the wrapper struct is pure boilerplate.

The Responses: block of a swagger:route accepts the body: sub-language, which names the body shape directly:

  • body:string (or number / integer / boolean) β€” a primitive body;
  • body:Pet β€” a $ref to the Pet definition;
  • body:[]Pet β€” an array of that $ref (repeat [] to nest deeper);
  • any trailing words after the body token become the response description; omit them and codescan derives one β€” the referenced model’s godoc (default above β†’ the Pet doc comment), or the HTTP status reason for a numeric code (400 above β†’ “Bad Request”).
swagger:route
// swagger:route GET /pets pets listPets
//
// Lists pets. Each response is declared inline with the body: sub-language β€” a
// primitive, an array of a model, or a single model $ref β€” so no wrapper
// response type is needed. Trailing words become the response description; omit
// them and codescan derives one (the model's godoc, or the HTTP status reason).
//
//	Responses:
//	  200: body:[]Pet the list of pets
//	  400: body:string
//	  default: body:Pet

Full source: docs/examples/shaping/inlineresponses/inlineresponses.go

paths[/pets]
{
  "get": {
    "description": "Lists pets. Each response is declared inline with the body: sub-language β€” a\nprimitive, an array of a model, or a single model $ref β€” so no wrapper\nresponse type is needed. Trailing words become the response description; omit\nthem and codescan derives one (the model's godoc, or the HTTP status reason).",
    "tags": [
      "pets"
    ],
    "operationId": "listPets",
    "responses": {
      "200": {
        "description": "the list of pets",
        "schema": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Pet"
          }
        }
      },
      "400": {
        "description": "Bad Request",
        "schema": {
          "type": "string"
        }
      },
      "default": {
        "description": "Pet is the model the inline responses reference.",
        "schema": {
          "$ref": "#/definitions/Pet"
        }
      }
    }
  }
}

Full source: docs/examples/shaping/inlineresponses/testdata/pathitem.json

No swagger:response struct is defined β€” the three responses are produced entirely from the body: tokens, and the Pet model is pulled into definitions because the body $refs reach it.

Info

A bare untagged token is read as a response name, never a type: 200: string is a (dangling) $ref to a response called string, not a primitive body. Use the explicit body:string form for a primitive. The full grammar β€” tags, untagged-token rules, and the reserved array/object/file keywords β€” is in sub-languages β†’ Responses.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Documenting generic responses

A common Go pattern is a single response envelope β€” a JSend-style wrapper that every handler returns, with an open any (interface{}) field for the payload:

The generic envelope
// APIResponse is the one generic envelope every handler returns. Because Data is
// an open type (any, i.e. interface{}), the scanner can only render it as an
// open schema β€” it has no way to know which concrete payload a given operation
// puts there.
//
// swagger:model
type APIResponse struct {
	Status  string `json:"status"`
	Data    any    `json:"data"`
	Message string `json:"message,omitempty"`
}

Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go

definitions[APIResponse]
{
  "description": "APIResponse is the one generic envelope every handler returns. Because Data is\nan open type (any, i.e. interface{}), the scanner can only render it as an\nopen schema β€” it has no way to know which concrete payload a given operation\nputs there.",
  "type": "object",
  "properties": {
    "data": {
      "x-go-name": "Data"
    },
    "message": {
      "type": "string",
      "x-go-name": "Message"
    },
    "status": {
      "type": "string",
      "x-go-name": "Status"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}

Full source: docs/examples/shaping/genericenvelopes/testdata/apiresponse.json

codescan reads this faithfully: Data is any, so in the spec it becomes an open schema β€” {"x-go-name":"Data"}, no type, no $ref. That is correct (the field genuinely accepts anything), but it is not what you want in an API contract, where each operation returns a specific payload.

codescan will not grow a per-route override syntax like swaggo’s body:APIResponse{Data: StatusReport} β€” that asks the scanner to invent a type the code never declares. Instead, declare the type: a doc-only struct that mirrors the envelope but pins the payload.

Embed the envelope, shadow the payload

The DRY way is to embed the generic envelope β€” promoting its Status and Message β€” and re-declare only the one opaque field with a concrete type:

Doc-only envelope
// StatusEnvelope documents the /status response: it embeds APIResponse and
// shadows the open Data with the concrete StatusReport. Handlers keep returning
// APIResponse β€” this type just gives the scanner a concrete shape.
//
// swagger:model
type StatusEnvelope struct {
	APIResponse

	// Data carries the concrete status report.
	Data StatusReport `json:"data"`
}

Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go

definitions[StatusEnvelope]
{
  "description": "StatusEnvelope documents the /status response: it embeds APIResponse and\nshadows the open Data with the concrete StatusReport. Handlers keep returning\nAPIResponse β€” this type just gives the scanner a concrete shape.",
  "type": "object",
  "properties": {
    "data": {
      "x-go-name": "Data",
      "$ref": "#/definitions/StatusReport"
    },
    "message": {
      "type": "string",
      "x-go-name": "Message"
    },
    "status": {
      "type": "string",
      "x-go-name": "Status"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}

Full source: docs/examples/shaping/genericenvelopes/testdata/statusenvelope.json

The locally-declared Data is shallower than the embedded one, so it wins β€” both in codescan’s view and in encoding/json at runtime. The result is a clean flat object whose data is a concrete $ref, with status and message carried over from the embed:

Generic β€” data is open
{
  "description": "APIResponse is the one generic envelope every handler returns. Because Data is\nan open type (any, i.e. interface{}), the scanner can only render it as an\nopen schema β€” it has no way to know which concrete payload a given operation\nputs there.",
  "type": "object",
  "properties": {
    "data": {
      "x-go-name": "Data"
    },
    "message": {
      "type": "string",
      "x-go-name": "Message"
    },
    "status": {
      "type": "string",
      "x-go-name": "Status"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}

Full source: docs/examples/shaping/genericenvelopes/testdata/apiresponse.json

Doc-only β€” data is concrete
{
  "description": "StatusEnvelope documents the /status response: it embeds APIResponse and\nshadows the open Data with the concrete StatusReport. Handlers keep returning\nAPIResponse β€” this type just gives the scanner a concrete shape.",
  "type": "object",
  "properties": {
    "data": {
      "x-go-name": "Data",
      "$ref": "#/definitions/StatusReport"
    },
    "message": {
      "type": "string",
      "x-go-name": "Message"
    },
    "status": {
      "type": "string",
      "x-go-name": "Status"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}

Full source: docs/examples/shaping/genericenvelopes/testdata/statusenvelope.json

You restate one field, not the whole envelope. Point the route’s response at the doc-only struct with the body: sub-language, and specialise the same envelope per operation with a different payload each time:

swagger:route
// swagger:route GET /status status getStatus
//
// Returns the service status wrapped in the generic envelope. The response is
// declared as the doc-only StatusEnvelope, so data is the concrete StatusReport
// rather than an open schema.
//
//	Responses:
//	  200: body:StatusEnvelope the status envelope

// swagger:route GET /users/{id} users getUser
//
// Returns a user wrapped in the same envelope, specialised to UserSummary.
//
//	Responses:
//	  200: body:UserEnvelope the user envelope

Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go

paths
{
  "/status": {
    "get": {
      "description": "Returns the service status wrapped in the generic envelope. The response is\ndeclared as the doc-only StatusEnvelope, so data is the concrete StatusReport\nrather than an open schema.",
      "tags": [
        "status"
      ],
      "operationId": "getStatus",
      "responses": {
        "200": {
          "description": "the status envelope",
          "schema": {
            "$ref": "#/definitions/StatusEnvelope"
          }
        }
      }
    }
  },
  "/users/{id}": {
    "get": {
      "tags": [
        "users"
      ],
      "summary": "Returns a user wrapped in the same envelope, specialised to UserSummary.",
      "operationId": "getUser",
      "responses": {
        "200": {
          "description": "the user envelope",
          "schema": {
            "$ref": "#/definitions/UserEnvelope"
          }
        }
      }
    }
  }
}

Full source: docs/examples/shaping/genericenvelopes/testdata/paths.json

Info

The handler never changes. Your code keeps returning the generic APIResponse; the doc-only structs exist only to give the scanner a concrete shape. They are valid Go, though β€” because the shadowing Data wins, StatusEnvelope{} marshals to exactly the same JSON the generic envelope would, so you may return one directly if you prefer a typed handler.

When embedding doesn’t fit

If your envelope has fields you would rather not promote β€” or you want the documented type to live behind a reusable swagger:response β€” restate the fields explicitly instead of embedding:

// swagger:model
type StatusEnvelope struct {
    Status  string       `json:"status"`
    Data    StatusReport `json:"data"`
    Message string       `json:"message,omitempty"`
}

This produces the same concrete data, at the cost of repeating every field. The embed-and-shadow form above is preferred whenever the envelope’s other fields map through unchanged.

Composing a body from several models

A related need is wrapping a response with an extra payload β€” say a domain model plus an auth token β€” rather than specialising one open field. Embed the parts with swagger:allOf and the body renders as the union of their $refs, with no open field at all:

swagger:allOf body
// AuthToken is an extra payload some responses wrap alongside the domain model.
//
// swagger:model
type AuthToken struct {
	Token string `json:"token"`
}

// LoginResult composes UserSummary with AuthToken via swagger:allOf, so the
// response body is the union of both models β€” a way to wrap a response with an
// added payload without an open Data field. The body renders as
// allOf:[{$ref:UserSummary},{$ref:AuthToken}].
//
// swagger:model
type LoginResult struct {
	// swagger:allOf
	UserSummary

	// swagger:allOf
	AuthToken
}

Full source: docs/examples/shaping/genericenvelopes/genericenvelopes.go

definitions[LoginResult]
{
  "description": "LoginResult composes UserSummary with AuthToken via swagger:allOf, so the\nresponse body is the union of both models β€” a way to wrap a response with an\nadded payload without an open Data field. The body renders as\nallOf:[{$ref:UserSummary},{$ref:AuthToken}].",
  "allOf": [
    {
      "$ref": "#/definitions/UserSummary"
    },
    {
      "$ref": "#/definitions/AuthToken"
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/genericenvelopes"
}

Full source: docs/examples/shaping/genericenvelopes/testdata/compose.json

Point a response body at LoginResult, or embed the same swagger:allOf fields directly in an in:body field to compose the allOf inline on the response schema.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Annotation index

The complete swagger:* vocabulary, one row each.

By example jumps to the tutorial that shows the annotation as runnable Go next to the spec it produces; Reference jumps to the exhaustive rule in the Maintainers compendium.

AnnotationAttaches toProducesBy exampleReference
swagger:additionalPropertiestype docobject additionalProperties (open / closed / typed)examplereference
swagger:alias (deprecated)type aliasno effect β€” alias rendering is controlled by Go aliases + optionshow-toreference
swagger:allOfembedded field / structan allOf compositionexamplereference
swagger:default (deprecated)anywhereno effect β€” use the default: keyword, or a default: response codehow-toreference
swagger:descriptiontype / field / response docoverrides the description (verbatim body with |)how-toreference
swagger:enumnamed typean enum array (+ x-go-enum-desc)examplereference
swagger:fileparam / response field{type: file}examplereference
swagger:ignoretype / field docexcludes the declarationexamplereference
swagger:metapackage doctop-level info, host, basePath, schemes, …examplereference
swagger:modeltype declarationa definitions entryexamplereference
swagger:namefield / method docrenames a JSON propertyexamplereference
swagger:omitembed / type docdrops named fields from what an embed promoteshow-toreference
swagger:operationfunc / var doca paths entry (YAML body)examplereference
swagger:parametersstruct declarationparameters on the named operation(s)examplereference
swagger:patternPropertiestype doctyped patternProperties (regex β†’ value)examplereference
swagger:responsestruct declarationa responses entryexamplereference
swagger:routefunc / var doca paths entry + operationexamplereference
swagger:strfmttype declaration{type: string, format: …} at every useexamplereference
swagger:titletype / field docoverrides the titlehow-toreference
swagger:typetype / field docoverrides the inferred Swagger typeexamplereference

Keywords, not annotations

Validations, examples and defaults inside a block are driven by keywords (minimum:, pattern:, enum:, example:, default:, …), not annotations. See the Validations and Examples & defaults tutorials, and the Keyword reference.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Maintainers

This section is the reference compendium: the precise, exhaustive description of the language codescan parses, and of the tools built around it. It is written for people who want the full contract β€” annotation authors looking up an exact rule, and contributors porting, extending, or debugging the parser.

Looking up an option rather than an annotation? That moved: the Options reference lives under Usage, beside the flag and configuration-key spellings of the same knobs.

If you are learning codescan by example, start with the Tutorials instead β€” they show the same concepts as runnable Go with the spec they produce, side by side. The Annotation index cross-references every annotation to both its tutorial and its entry here.

The reference documents

  • The swagger:* annotation vocabulary: what each produces, where it attaches, and the keywords it admits.
  • The keyword: value forms recognised inside annotation blocks β€” grouped by class, with the annotation contexts that accept each one and its value shape.
  • The smaller languages embedded in annotation bodies: the Parameters/Responses grammars, YAML surfaces, and prose classification.
  • The formal ISO-14977 EBNF the parser implements, from comment preprocessing through the typed walker.
  • How the three CLI tools are put together.

    Why there are three of them, where their shared flag surface is declared, and what keeps it whole.

  • What a scan costs, and what six months of work did to it.

    The two independent gains β€” the annotation parser and the package loader β€” measured on the same generated server, emitting the same document.

  • Annotations β€” the swagger:* vocabulary: what each annotation does, where it attaches, its argument shape, and the keywords it admits. The author-facing normative reference.
  • Keywords β€” the per-keyword reference card: every keyword: value form, its value shape, and the contexts where it is legal.
  • Sub-languages β€” the smaller languages embedded inside annotation bodies (Parameters: / Responses: grammars, YAML surfaces, prose classification).
  • Grammar β€” the formal ISO-14977 EBNF the parser implements, from comment preprocessing through the typed walker.
  • The commands β€” how the three CLI tools are put together: why each lives where it does, where their shared flag surface is declared, and what keeps it whole.
  • Performance β€” what a scan costs: what the grammar and the loader each changed since the code left go-swagger, and how the loader options compare warm and cold.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Maintainers

Roadmap

What’s next with this project?

timeline
    title Planned releases
    section Q1 2026
    βœ… v0.32.x (March 2026) : Repo carved out of go-swagger
                    : relint
                    : library setup (not env. sensitive)
                    : go1.25+
    section Q2 2026
    βœ… v0.33.x (April 2026) : Reduced exposed interface
                    : type array for parameters
                    : new package layout (internal, layered)
    βœ… v0.34.x (May 2026) : Grammar-based parser
                    : Replace regexp-based parser by lexer+grammar
                    : Fixed many parsing quirks
    βœ… v0.35.0 (June 2026) : Large bug-bashing
                    : Documentation site
                    : Fixes ~200+ go-swagger issues
                    : All validations
                    : Parser diagnostics
    section Q3 2026
    βœ… v0.35.x (July 2026) : Minor features
                    : more tunable knobs, new annotations
                    : Name conflict handling & circular $ref, missing validations, ... 
                    : go doc filter, private comments, inner markdown
    βœ… v0.36.0 (July 2026) : TUI
                    : interactive spec building with TUI tool
                    : polymorphic subtypes discovery
    βœ… v0.36.x (August 2026) : faster code scanner
                    : Optimized incremental type scanner
                    : toolchain-independent loader
                    : playground UI (wasi build)
                    : dedicated CLI, independent from go-swagger
                    : more go-swagger backlog fixes & tunable knobs
    ⬜ v0.37.0 (September 2026) : decouple from `Spec`
                    : go1.26+
                    : Internal document model
    section Q4 2026
    πŸ” v0.38.x (Oct 2026) : LSP & IDE integration (tentative)
    πŸ” v0.39.x (Oct-Nov 2026) : OAI v3 support (tentative)
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Annotations

Annotations are the swagger:<name> markers the scanner recognises in Go doc comments. Each annotation classifies the surrounding declaration β€” telling the scanner “this is a model definition”, “this is a route handler”, “this is meta-information about the API” β€” and opens the door for keywords inside the same comment block.

There are twenty annotations. They divide cleanly by what they attach to:

  • Spec-level: swagger:meta.
  • Model declarations: swagger:model, swagger:strfmt, swagger:enum, swagger:allOf, swagger:alias, swagger:additionalProperties, swagger:patternProperties.
  • Operation declarations: swagger:route, swagger:operation.
  • Companion declarations: swagger:parameters, swagger:response.
  • Local hints & overrides: swagger:ignore, swagger:omit, swagger:name, swagger:title, swagger:description, swagger:type, swagger:file.
  • Deprecated no-ops, parsed and reported but without effect: swagger:alias, swagger:default.

This section is the author-first reference. Each annotation has its own page covering what it produces, where it goes, its EBNF-like syntax, the keywords legal inside its block, and at least one worked example. Browse them below (sorted alphabetically), or start from the Annotation index for the one-row-each overview.

  • Deprecated no-op β€” alias rendering is controlled by Go aliases + options.
  • Marks a struct as participating in an allOf composition.
  • Deprecated no-op β€” defaults are carried by the default: keyword, or a default response code.
  • Overrides the godoc-derived description on a model, field, response, or header.
  • Marks a named type as an enum and collects its const values.
  • Marks a parameter or response body as a binary file ({type: file}).
  • Excludes the surrounding declaration (or one field) from the generated spec.
  • Declares the package as the top-level OpenAPI spec container.
  • Overrides the emitted property name of a struct field or interface method.
  • Drops named fields from what an embed promotes into the enclosing schema.
  • Declares an HTTP route + operation in one annotation.
  • Overrides the godoc-derived title on a model or field.
  • Replaces a field’s or named type’s inferred Swagger type with an inlined type.

For the per-keyword reference, see keywords.md. For the embedded sub-languages (Parameters: and Responses: body grammars, YAML extensions, etc.), see sub-languages.md. For the formal grammar, see grammar.md.


How annotations attach

An annotation is recognised when it appears at the start of a comment line in a doc comment. Leading whitespace, the // marker, and any /* */ block-comment continuation noise are stripped β€” the lexer applies the same content-prefix-trim that every other godoc-aware tool does.

Annotations attach to whichever Go declaration owns the comment group:

  • Package doc (// Package foo … followed by package foo) β€” carries swagger:meta.
  • Type declaration (type T struct { … }, type T int, type T = Other) β€” carries swagger:model, swagger:strfmt, swagger:enum, swagger:allOf, swagger:alias, swagger:ignore, swagger:type. Inside a grouped declaration (type ( A …; B … )) the comment on each individual spec is honoured independently β€” the annotation attaches to its own TypeSpec, not to the enclosing group β€” so two types in one group can carry distinct docs and annotations.
  • Function or variable declaration (func ServeAPI() { … }, var DoIt = func() { … }) β€” carries swagger:route, swagger:operation. These two are recognised whether the annotation sits in the function’s doc comment or inside the function body. A swagger:model or swagger:parameters declared on a type local to a function body is likewise discovered.
  • Struct field doc β€” carries swagger:name, swagger:type, swagger:ignore, plus any of the keyword reference entries legal in schema / param / header context.

One comment group may carry MORE than one annotation when the combinations are semantically compatible β€” e.g. swagger:model + swagger:type together overrides the auto-detected Go type while still publishing the model. The grammar parses both and the builder honours both.

The first annotation in source order wins as the “primary” classifier β€” for example, a comment carrying swagger:model followed by swagger:ignore produces a model (the ignore is silently overridden because only the source-order-first annotation drives the short-circuit). Subsequent annotations are still parsed and visible via Block.AnnotationKind()-iteration, but the primary classifier determines which builder owns the decl.

Warning

Recognition is purely positional: any comment line that begins with a swagger:<name> token is treated as that annotation β€” even when you meant it as prose. A description line like swagger:type controls the emitted type on a type’s doc comment is parsed as a swagger:type annotation. Keep annotation names mid-sentence in descriptions (The swagger:type directive …) or wrap them in backticks so the line does not start with the token.

Annotation argument shapes

After the swagger:<name> head, an annotation may carry positional arguments. The shapes:

  • No args: swagger:meta, swagger:ignore, swagger:enum, swagger:allOf, swagger:file β€” bare annotation, the surrounding decl supplies the entity name. swagger:default also accepts a bare form, but its argument is optional and unread β€” it is deprecated.
  • One IDENT arg: swagger:model Pet, swagger:response errorResponse, swagger:strfmt uuid, swagger:name fullName, swagger:type integer, swagger:alias TimestampAlias β€” the argument overrides or names the entity.
  • One IDENT arg, optional: swagger:model (bare β€” derives the name from the Go decl) vs swagger:model Pet (overrides).
  • List of IDENT args: swagger:parameters listItems createItem β€” declares the parameters group as legal for multiple operations.
  • Header line: swagger:route GET /pets pets users listPets and swagger:operation GET /pets users listPets β€” a structured header carrying method, path, tags, and operation ID. See the per-annotation pages for the exact rules.

Annotation Γ— keyword compatibility matrix

A quick orientation for which annotations can carry which keyword families. See keywords.md for the per-keyword contracts, and each annotation’s own page for the detail.

AnnotationNumeric/length validationsSchema decoratorsin:Meta keywordsParameters: bodyResponses: bodyYAML body
swagger:metaβ€”β€”β€”βœ…β€”β€”βœ… (security defs, extensions)
swagger:modelβœ… (on fields)βœ…β€”β€”β€”β€”β€”
swagger:strfmtβ€”β€”β€”β€”β€”β€”β€”
swagger:enumβ€”(enum keyword via const)β€”β€”β€”β€”β€”
swagger:allOfβœ… (on member fields)βœ…β€”β€”β€”β€”β€”
swagger:aliasβ€”β€”β€”β€”β€”β€”β€”
swagger:routeβ€”(deprecated only)β€”(schemes/consumes/produces/security)βœ…βœ…(extensions)
swagger:operationβ€”β€”β€”β€”β€”β€”βœ… (full op as YAML)
swagger:parametersβœ… (on fields)βœ… (on fields)βœ…β€”β€”β€”β€”
swagger:responseβœ… (on header fields)βœ… (on body field)βœ… (body/header)β€”β€”β€”β€”
swagger:ignoreβ€”β€”β€”β€”β€”β€”β€”
swagger:nameβ€”β€”β€”β€”β€”β€”β€”
swagger:titleβ€”βœ… (override)β€”β€”β€”β€”β€”
swagger:descriptionβ€”βœ… (override)β€”βœ… (body/header)β€”β€”β€”
swagger:typeβ€”β€”β€”β€”β€”β€”β€”
swagger:additionalPropertiesβ€”βœ… (object schema)β€”β€”β€”β€”β€”
swagger:patternPropertiesβ€”βœ… (object schema)β€”β€”β€”β€”β€”
swagger:fileβ€”β€”β€”β€”β€”β€”β€”
swagger:default (deprecated)β€”β€”β€”β€”β€”β€”β€”

A blank cell means the keyword family is not legal in that context; attempting to use it emits CodeContextInvalid and the keyword is dropped.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Annotations

swagger:additionalProperties

Usage

// swagger:additionalProperties ( true | false | <type> )

What it does

Sets a schema’s additionalProperties β€” the policy for keys beyond the named properties.

On a struct it complements the named properties; on a map type it overrides the element-derived value schema; on a type that resolved to a bare $ref it defines a clean object. See the Maps & free-form objects tutorial.

Where it goes

On a type declaration (alongside swagger:model). A field-level equivalent exists as the additionalProperties: keyword.

Grammar (EBNF)

AdditionalPropertiesAnnotation = ANN_ADDITIONAL_PROPERTIES , ( BOOL_VALUE | ValueType ) ;
ValueType                      = TYPE_REF | IDENT_NAME | "[]" , ValueType ;

The required token is one of:

  • true β€” allow arbitrary extra keys (additionalProperties: true);
  • false β€” forbid extra keys, closing the object (additionalProperties: false);
  • a value type β€” a primitive / Go-builtin / []T, or a known type name (which resolves to a $ref, and is registered for discovery). This reuses the /codescan/maintainers/annotations/swagger-type/ value grammar, except a type name becomes a $ref rather than an inline expansion.

Supported keywords

None of its own. It composes with maxProperties / minProperties / patternProperties.

Example

Annotated Go
// Settings is an open object: it keeps its named property and complements it
// with typed (integer) extra values β€” the swagger:additionalProperties marker
// sets the policy for keys beyond the named ones.
//
// swagger:model
// swagger:additionalProperties integer
type Settings struct {
	Name string `json:"name"`
}

Full source: docs/examples/concepts/maps/maps.go

Generated spec
{
  "description": "Settings is an open object: it keeps its named property and complements it\nwith typed (integer) extra values β€” the swagger:additionalProperties marker\nsets the policy for keys beyond the named ones.",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "x-go-name": "Name"
    }
  },
  "additionalProperties": {
    "type": "integer"
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/addlpropstyped.json

Precedence β€” lowest priority. additionalProperties only rides on an object. If a prior rule fixed a non-object type (a swagger:type scalar, swagger:strfmt, a special type), the marker is dropped with a CodeShapeMismatch diagnostic. It has no OAS-2 SimpleSchema form, so it never applies on a non-body parameter or response header.

Full example. testdata/enhancements/additional-properties/api.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:alias

Warning

Deprecated. swagger:alias no longer affects the emitted spec. It is an empty sink that only raises a validate.deprecated diagnostic.

Usage

// swagger:alias [ IDENT_NAME ]

What it does

Nothing, today. Earlier documentation claimed it published a $ref to the alias target; that was never accurate. Its only real effect was to force a named primitive type to inline its scalar (e.g. {type: string}) instead of producing the $ref a named type otherwise gets β€” and that force-inline behaviour has been removed.

Where it went

On a type alias / named-type declaration.

Grammar (EBNF)

AliasBlock = ANN_ALIAS , [ IDENT_NAME ] , [ Title ] , [ Description ] ;

The optional IDENT_NAME is ignored β€” the annotation has no effect.

Migration

  • To inline a type at a use site, use swagger:type inline on the field (see swagger:type).
  • To publish a type as a first-class definition that fields $ref, use swagger:model.
  • To control alias rendering globally, use the RefAliases / TransparentAliases options. A plain (unannotated) Go alias type T = Other dissolves to its target by default. See Alias rendering.

Supported keywords

None β€” the annotation is inert.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:allOf

Usage

// swagger:allOf

What it does

Marks a struct as participating in an allOf composition.

The struct’s fields plus any embedded swagger:model-tagged base produce an allOf: [$ref base, {inline fields}] schema. The companion convention is to embed the base type as an anonymous field with this annotation on the embedding’s doc comment (or on the embedded type itself).

Where it goes

On a struct field that embeds another type, or on a struct type that has at least one embedded base.

Grammar (EBNF)

AllOfBlock = ANN_ALLOF , [ Title ] , [ Description ] ;

The annotation takes no arguments; an optional title/description may follow on the doc comment.

Supported keywords

Schema-context keywords on the inline-object member (the second allOf element).

Do not put other annotations beside it

swagger:allOf takes no arguments, and no other classifier annotation belongs in an embedded field’s doc comment. swagger:strfmt and swagger:type written there are ignored, and codescan reports them under scan.ineffective-annotation:

type Wrong struct {
	// swagger:allOf
	// swagger:strfmt uuid   ← ignored, and warned about
	Token
}

The reason is that an embed contributes the shape of the type it embeds, and that type’s own declaration fixes the shape β€” never the site that embeds it. So the annotation belongs one level down:

// Token is rendered as a formatted string wherever it appears.
//
// swagger:strfmt uuid
type Token [16]byte

type Right struct {
	// swagger:allOf
	Token
}

This is not specific to allOf: the same annotations are ignored on a plain (uncomposed) embed too, and reported the same way. They are honoured on an ordinary β€” non-embedded β€” field, which is what makes the mistake an easy one.

Example

A struct embedding a swagger:model base with swagger:allOf on the embed produces an allOf of the base $ref and an inline-object member carrying the embedding struct’s own fields:

Annotated Go
// Animal is one abstract base.
//
// swagger:model
type Animal struct {
	// Kind discriminates the animal.
	Kind string `json:"kind"`
}

// Tagged is a second reusable base.
//
// swagger:model
type Tagged struct {
	// Tags label the resource.
	Tags []string `json:"tags"`
}

// Dog composes two base models plus its own fields: each embedded base becomes
// a $ref arm of the allOf, and the struct's own (non-embedded) fields β€” which
// are new and cannot be a $ref β€” form the final inline arm.
//
// swagger:model
type Dog struct {
	// swagger:allOf
	Animal

	// swagger:allOf
	Tagged

	// Breed is the dog's breed.
	Breed string `json:"breed"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Dog composes two base models plus its own fields: each embedded base becomes\na $ref arm of the allOf, and the struct's own (non-embedded) fields β€” which\nare new and cannot be a $ref β€” form the final inline arm.",
  "allOf": [
    {
      "$ref": "#/definitions/Animal"
    },
    {
      "$ref": "#/definitions/Tagged"
    },
    {
      "type": "object",
      "properties": {
        "breed": {
          "description": "Breed is the dog's breed.",
          "type": "string",
          "x-go-name": "Breed"
        }
      }
    }
  ],
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/allof.json

The same composition applies when the embedding struct is a swagger:response body: the embedded base emits an allOf: [{$ref}, …] arm only when it is a swagger:model (a definition exists to point at); an embedded swagger:response has its fields inlined instead.

Full example. testdata/enhancements/allof-edges/types.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:default

Warning

Deprecated. swagger:default never emitted a default into the spec, in any placement or form. It is now an empty sink that only raises a validate.deprecated diagnostic. Use the default: keyword, or a default response code in a route’s Responses: body.

Usage

// swagger:default [ VALUE ]

What it does

Nothing. It is parsed, reported as deprecated, and ignored.

Previously it also suppressed the schema of a named basic type it was placed on: the classifier claimed the target without writing it, so the declared type published a typeless definition and every field referencing it emitted a typeless property, silently. That is fixed β€” an annotated type now emits exactly what it would emit unannotated.

Why it was retired

Every place OpenAPI 2.0 admits a default is already served, so the annotation had no meaning left to implement:

Where a default can appearHow to write it
Schema object β€” a model field, or a type declarationdefault: keyword
Parameter object (non-body)default: keyword
Items objectdefault: keyword
Header objectdefault: keyword
Responses object β€” an operation’s default responsedefault: as the response code in a Responses: body

The keyword’s context set is exactly the list of OAS 2.0 objects that carry a default; the response-code head closes the remainder.

Where it goes

Anywhere it used to β€” the annotation is still recognised so existing source keeps scanning. It has no effect wherever it appears.

Grammar (EBNF)

DefaultClassifierBlock = ANN_DEFAULT , [ VALUE ] , [ Title ] , [ Description ] ;

The value argument is optional and unread. It used to be mandatory, which made the bare form this page once documented a hard parse error.

Supported keywords

None.

Example

Replace it with the keyword:

// Port is the listen port.
//
// swagger:model Port
// default: 8080
type Port int

For an operation’s default response, use the response code:

// swagger:route GET /things things listThings
//
// Responses:
//   200: thingList
//   default: genericError
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:description

Usage

// swagger:description <text>   (single line, or a blank-terminated body)
// swagger:description |        (opens a verbatim literal markdown block)

What it does

Replaces the godoc-derived description on a schema with explicit text.

By default a description comes from a declaration’s doc comment; swagger:description overrides it when the godoc prose isn’t what you want to publish. It is a schema-family override β€” a sibling of swagger:title.

A trailing | opens a verbatim literal markdown block: the body is captured exactly β€” blank lines, indentation, and table pipes preserved β€” until the next line-leading annotation or end of comment. See Markdown descriptions.

Where it goes

On a type (model) doc comment, a struct-field doc comment, a swagger:response struct, or a response header field.

Grammar (EBNF)

DescriptionAnnotation = ANN_DESCRIPTION , RAW_VALUE ;

RAW_VALUE is the rest of the head line; under Option B a blank-terminated body extends it, and a trailing | switches the body to verbatim literal capture. The annotation dispatches through the schema parser (not the classifier parser), so validation keywords co-located on the same comment group still surface.

Supported keywords

None of its own β€” the text (plus any folded body) is the entire argument. A bare swagger:description with no text suppresses the godoc-derived description and emits a CodeEmptyOverride diagnostic.

Example

A plain override on a model and its fields:

Annotated Go
// Widget is the Go-facing widget doc, written for Go readers.
//
// It explains internal Go usage that should not leak into the API spec.
//
// swagger:model
// swagger:title A Public Widget
// swagger:description A widget exposed via the public API.
type Widget struct {
	// ID explains the Go field for Go readers.
	//
	// swagger:description The unique widget identifier.
	ID string `json:"id"`

	// Label is the Go-facing field doc. Fields carry no title by default;
	// the override is the only way a property gets one.
	//
	// swagger:title Display Label
	// swagger:description Human-readable label shown to API consumers.
	Label string `json:"label"`

	// Plain keeps its godoc description because it carries no override.
	Plain string `json:"plain"`

	// Capacity combines a description override with an inline validation
	// keyword on the same field: the override applies AND maximum is kept,
	// because the override annotations dispatch through the schema family.
	//
	// swagger:description The maximum capacity, in liters.
	// maximum: 1000
	Capacity int64 `json:"capacity"`

	// Suppressed has a godoc that a bare swagger:description suppresses: the
	// empty value is applied (description omitted) and scan.empty-override is
	// raised, in case the bare marker was left behind by mistake.
	//
	// swagger:description
	Suppressed string `json:"suppressed"`

	// Notes carries a multi-line description override: the lines following the
	// annotation fold into the description until the blank line, joined with
	// newlines.
	//
	// swagger:description Free-form notes about the widget.
	// They may span several lines, all folded into one description.
	//
	// The blank line above terminates the override body; this paragraph is
	// ordinary godoc and is discarded (the override won).
	Notes string `json:"notes"`

	// Gadget is a $ref field carrying title + description overrides. They are
	// symmetric $ref siblings: kept under EmitRefSiblings, dropped to a bare
	// $ref under the default flags β€” the same rule a prose description follows.
	//
	// swagger:title Gadget Ref
	// swagger:description The attached gadget, described for API consumers.
	Gadget Gadget `json:"gadget"`
}

// Gadget is a plain referenced model.
//
// swagger:model
type Gadget struct {
	Serial string `json:"serial"`
}

Full source: docs/examples/shaping/overrides/overrides.go

Generated spec
{
  "description": "A widget exposed via the public API.",
  "type": "object",
  "title": "A Public Widget",
  "properties": {
    "capacity": {
      "description": "The maximum capacity, in liters.",
      "type": "integer",
      "format": "int64",
      "maximum": 1000,
      "x-go-name": "Capacity"
    },
    "gadget": {
      "$ref": "#/definitions/Gadget"
    },
    "id": {
      "description": "The unique widget identifier.",
      "type": "string",
      "x-go-name": "ID"
    },
    "label": {
      "description": "Human-readable label shown to API consumers.",
      "type": "string",
      "title": "Display Label",
      "x-go-name": "Label"
    },
    "notes": {
      "description": "Free-form notes about the widget.\nThey may span several lines, all folded into one description.",
      "type": "string",
      "x-go-name": "Notes"
    },
    "plain": {
      "description": "Plain keeps its godoc description because it carries no override.",
      "type": "string",
      "x-go-name": "Plain"
    },
    "suppressed": {
      "type": "string",
      "x-go-name": "Suppressed"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/overrides"
}

Full source: docs/examples/shaping/overrides/testdata/widget.json

The | literal block captures a verbatim markdown body (table and list preserved):

Annotated Go
// Markdown opts into a verbatim body with the literal block marker.
//
// swagger:description |
// The body is captured **verbatim** β€” pipes, blank lines and all:
//
// | name | purpose |
// |------|---------|
// | foo  | bars    |
//
// - point one
// - point two
//
// swagger:model Markdown
type Markdown struct {
	// Name of the widget.
	//
	// swagger:description |
	// The name must be:
	//
	//   1. unique
	//   2. lowercase
	Name string `json:"name"`
}

Full source: docs/examples/shaping/markdowndesc/markdowndesc.go

Generated spec
{
  "description": "The body is captured **verbatim** β€” pipes, blank lines and all:\n\n| name | purpose |\n|------|---------|\n| foo  | bars    |\n\n- point one\n- point two",
  "type": "object",
  "title": "Markdown opts into a verbatim body with the literal block marker.",
  "properties": {
    "name": {
      "description": "The name must be:\n\n  1. unique\n  2. lowercase",
      "type": "string",
      "x-go-name": "Name"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/markdowndesc"
}

Full source: docs/examples/shaping/markdowndesc/testdata/markdown.json

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:enum

Usage

// swagger:enum [ IDENT_NAME ]
Note

Not to be confused with the enum: keyword, which produces the same spec keyword from the opposite direction: it takes the members you write literally, typed from the schema it sits on, whereas this annotation collects them from a Go const block, typed from the declared Go type. Side-by-side in the enumerations tutorial.

What it does

Marks a named type over a string, integer, number or boolean as an enum and collects the type’s const declarations.

Values come from the Go type-checker, so any constant expression is collected β€” iota (including the implicit specs, which carry neither a type nor a value), computed members (1 << 3), references to earlier members, negative values, every integer base, values above MaxInt64 in an unsigned enum, rune literals (as code points), true / false, and both string forms. The emitted type / format come from the declared Go type, never from the members, so an int8 enum is {integer, int8} and reordering the const block cannot change the type. A type declared over another named type keeps that type’s format (type Kind strfmt.UUID stays format: uuid).

Two shapes do not work: an alias to a basic type cannot host an enum (the type-checker erases the alias, leaving nothing to collect β€” this raises a parse.invalid-enum-option warning suggesting a named type), and a rune or byte enum emits integers, which is what those types are on the wire. An alias to a named enum type is fine, and is not warned about. See Enumerations.

  • Without swagger:model (the default): the values are applied inline on each model field that references the type β€” the property gets an enum array plus an x-go-enum-desc extension carrying the per-value godoc descriptions in <value> <doc-text> shape. The enum type itself is not a standalone definition.
  • With swagger:model: the enum becomes a first-class definition carrying the enum array (+ x-go-enum-desc), and referencing fields point at it via $ref β€” the general swagger:model β‡’ definition + $ref rule applied to enums.

If swagger:enum names a type for which no matching const values are found, the enum semantics are dropped and the type falls through to ordinary type resolution (typically a plain $ref, no enum array).

Where it goes

On a named type declaration. The type’s const values are discovered via Go’s type-system traversal; they do not need to live in the same file. The values surface only when a model reaches the enum type through a field.

Grammar (EBNF)

EnumBlock = ANN_ENUM , [ IDENT_NAME ] , [ Title ] , [ Description ] ;

The optional IDENT_NAME names the type whose const values to collect. On a type declaration the name is redundant, so the bare swagger:enum form is accepted and infers the name from the declared type: swagger:enum Priority and a bare swagger:enum on type Priority … are equivalent.

Supported keywords

Schema-context keywords. The enum: keyword can ALSO be used inline on the type doc to force a value set; when present, it overrides the const-derived values and the x-go-enum-desc is recomputed (or dropped) accordingly.

Example

A named type marked swagger:enum with const values, referenced by a model field, lands the values on that property (not on a standalone definition) together with the x-go-enum-desc extension:

Annotated Go
// Priority is the urgency level on a task.
//
// swagger:enum Priority
type Priority string

const (
	// PriorityLow is for tasks that can wait.
	PriorityLow Priority = "low"
	// PriorityMedium is the default.
	PriorityMedium Priority = "medium"
	// PriorityHigh is for tasks that must run soon.
	PriorityHigh Priority = "high"
)

// Task is a unit of work carrying an enum-typed field. Referencing Priority
// from a model is what makes the enum reachable, and so emitted.
//
// swagger:model
type Task struct {
	// Priority is the task's urgency.
	Priority Priority `json:"priority"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Task is a unit of work carrying an enum-typed field. Referencing Priority\nfrom a model is what makes the enum reachable, and so emitted.",
  "type": "object",
  "properties": {
    "priority": {
      "description": "Priority is the task's urgency.\nlow PriorityLow is for tasks that can wait.\nmedium PriorityMedium is the default.\nhigh PriorityHigh is for tasks that must run soon.",
      "type": "string",
      "enum": [
        "low",
        "medium",
        "high"
      ],
      "x-go-enum-desc": "low PriorityLow is for tasks that can wait.\nmedium PriorityMedium is the default.\nhigh PriorityHigh is for tasks that must run soon.",
      "x-go-name": "Priority"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/enum.json

By default the constβ†’value mapping is folded into the property’s description and duplicated in x-go-enum-desc. Set the scanner option SkipEnumDescriptions: true to keep the authored prose as the description; the mapping then rides x-go-enum-desc only. See Vendor extensions.

Full example. testdata/enhancements/enum-overrides/types.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:file

Note

Prefer swagger:type file. The two are exact synonyms β€” same output, same location gate. swagger:file is expected to be deprecated as an extraneous annotation; it is not deprecated yet and still works.

Usage

// swagger:file

What it does

Marks a parameter or response body as a binary file ({type: file}).

The scanner emits the file-type marker without further introspection of the Go type.

Where it goes

On a struct field doc inside a swagger:parameters (multipart file upload) or swagger:response (file download) struct.

Grammar (EBNF)

FileBlock = ANN_FILE , [ Title ] , [ Description ] ;

Takes no argument β€” an optional title/description may follow on the doc comment.

Supported keywords

Standard parameter / response keywords; the file marker stacks with in: and other parameter shape keywords. See the keywords reference.

Example

Annotated Go
// swagger:route POST /pets/{id}/photo pets uploadPetPhoto
//
// responses:
//
//	200: petsResponse

// UploadParams is the multipart upload for the uploadPetPhoto operation.
//
// swagger:parameters uploadPetPhoto
type UploadParams struct {
	// Photo is the image to upload.
	//
	// in: formData
	// swagger:file
	Photo io.ReadCloser `json:"photo"`
}

Full source: docs/examples/concepts/routes/routes.go

Generated spec
[
  {
    "type": "file",
    "x-go-name": "Photo",
    "description": "Photo is the image to upload.",
    "name": "photo",
    "in": "formData"
  }
]

Full source: docs/examples/concepts/routes/testdata/file.json

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:ignore

Usage

// swagger:ignore

What it does

Excludes the surrounding declaration from the generated spec.

The scanner sees the decl and the doc, classifies it, then drops it.

When swagger:ignore appears after another classifier on the same comment block (e.g. swagger:model first, then swagger:ignore), the first annotation wins and the ignore is silently overridden. Place swagger:ignore first if you genuinely want the decl excluded.

Where it goes

On a type declaration to exclude the whole type, or on a struct field doc to exclude that one field.

Grammar (EBNF)

IgnoreBlock = ANN_IGNORE , [ Title ] , [ Description ] ;

Takes no argument β€” an optional title/description may follow on the doc comment.

Supported keywords

None β€” the annotation is a stateless classifier marker.

Example

swagger:ignore produces no schema, so there is no live spec pane here: the type below is scanned, classified, then dropped β€” it never reaches definitions. On a type it excludes the whole declaration; on a struct field it excludes just that one property (e.g. a PasswordHash kept out of the wire shape).

// Secret never reaches the spec.
//
// swagger:ignore
type Secret struct {
	// Token is internal.
	Token string `json:"token"`
}

Full source: docs/examples/concepts/models/models.go

Full example. testdata/enhancements/top-level-kinds/types.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:meta

Usage

// swagger:meta

What it does

Declares the package as the OpenAPI spec container.

The scanner reads the package doc comment for the top-level spec fields: title (via stripPackagePrefix of the doc’s first line), description, license, contact, host, basePath, version, schemes, consumes, produces, securityDefinitions, extensions, and the rest of the meta keyword surface.

Where it goes

On the package doc comment. No arguments β€” a bare annotation.

Grammar (EBNF)

MetaBlock = ANN_META , [ Title ] , [ Description ] , MetaBody ;

The body is a MetaBody of single-line MetaKeywords (version, host, basePath, license, contact, schemes) and MetaRawBlocks (consumes, produces, security, securityDefinitions, tos). See grammar Β§meta-family.

Supported keywords

All meta single-line keywords (schemes, version, host, basePath, license, contact) plus the meta-scope body keywords (consumes, produces, security, securityDefinitions, extensions, infoExtensions, tos, externalDocs, tags). A Tags: block declares the spec’s top-level tags (name, description, nested externalDocs, x-* extensions per tag).

Example

Annotated Go
// Package meta Pet Store.
//
// A small API that demonstrates the document-level swagger:meta block: the
// package doc comment carries the spec's top-level metadata.
//
//	Schemes: https
//	Host: api.example.com
//	BasePath: /v1
//	Version: 1.2.0
//	License: Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0.html
//	Contact: API Team <api@example.com> https://example.com/support
//
//	Consumes:
//	  - application/json
//
//	Produces:
//	  - application/json
//
//	ExternalDocs:
//	  description: Full API guide
//	  url: https://example.com/docs
//
//	Tags:
//	- name: pets
//	  description: Everything about your Pets
//	  externalDocs:
//	    description: Find out more
//	    url: https://example.com/docs/pets
//	- name: store
//	  description: Access to Petstore orders
//	  x-display-name: Store
//
//	SecurityDefinitions:
//	  basic_auth:
//	    type: basic
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//
//	Security:
//	  basic_auth:
//
//	InfoExtensions:
//	  x-logo:
//	    url: https://example.com/logo.png
//	    altText: Example
//
// swagger:meta
package meta

Full source: docs/examples/concepts/meta/doc.go

Generated spec
{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A small API that demonstrates the document-level swagger:meta block: the\npackage doc comment carries the spec's top-level metadata.",
    "title": "Pet Store.",
    "contact": {
      "name": "API Team",
      "url": "https://example.com/support",
      "email": "api@example.com"
    },
    "license": {
      "name": "Apache 2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    },
    "version": "1.2.0",
    "x-logo": {
      "altText": "Example",
      "url": "https://example.com/logo.png"
    }
  },
  "host": "api.example.com",
  "basePath": "/v1",
  "paths": {},
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "basic_auth": {
      "type": "basic"
    }
  },
  "security": [
    {
      "basic_auth": []
    }
  ],
  "tags": [
    {
      "description": "Everything about your Pets",
      "name": "pets",
      "externalDocs": {
        "description": "Find out more",
        "url": "https://example.com/docs/pets"
      }
    },
    {
      "description": "Access to Petstore orders",
      "name": "store",
      "x-display-name": "Store"
    }
  ],
  "externalDocs": {
    "description": "Full API guide",
    "url": "https://example.com/docs"
  }
}

Full source: docs/examples/concepts/meta/testdata/meta.json

Full example. testdata/goparsing/spec/api.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:model

Usage

// swagger:model [<name>]   (where <name> overrides the definition name; defaults to the Go type name)

What it does

Declares a Go type as a published model.

The scanner walks the type, emits a schema into the spec’s definitions map, and resolves cross-references between models.

The title/description split follows a heuristic: a single-line comment ending in a period becomes the title. One without a trailing period becomes the description; a multi-line comment uses the first line as title and the rest as description.

The descriptive prose must come before the swagger:model line β€” an annotation-first block still publishes the model but drops its title and description.

Where it goes

On a type declaration (type T struct { … }, type T int, type T = Other, …).

Grammar (EBNF)

ModelAnnotation = ANN_MODEL , [ IDENT_NAME ] ;

The optional IDENT_NAME is the name the model takes in definitions (default: the Go type’s name). It must be a plain identifier (a JSON label), not a Go-qualified name β€” a dotted name such as utils.Error is rejected with a warning and dropped. Cross-package types resolve automatically, so reference a model by its bare name.

The annotation opens a SchemaBlock body β€” its fields and their doc comments carry the schema validations.

Supported keywords

Every schema decorator and validation keyword is accepted on a field doc comment. A keyword that is not compatible with the field’s inferred schema type (e.g. minLength on an integer) is ignored and raises a diagnostic.

Example

The doc comment above the type drives the model’s name, title and description:

// Pet is the petstore's primary entity.            <- title (first line, ends with a period)
//
// A pet can be any little animal you care about.   <- description
// In this example the model name is inferred from the type name, here "Pet".
//
// swagger:model
type Pet struct {
	// ID is the unique identifier.
	ID int64 `json:"id"`

	// Name is the pet's display name.
	Name string `json:"name"`

	// Tags categorise the pet.
	Tags []string `json:"tags,omitempty"`
}

Pass an argument to override the name; the type is then published as #/definitions/PetWithExtras:

// swagger:model PetWithExtras
type DetailedPet struct { … }

A single field group declaring several names emits one property per name. A json: tag on the group cannot rename the individual fields β€” each keeps its own name β€” though tag options still apply:

Annotated Go
// Color is an RGBA colour. A single field group declaring several names emits
// one property per name β€” R, G, B and A each become their own integer property.
// A json tag on the group cannot rename the individual fields (each keeps its
// own name), though tag options such as omitempty still apply.
//
// swagger:model
type Color struct {
	R, G, B, A uint8 `json:",omitempty"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Color is an RGBA colour. A single field group declaring several names emits\none property per name β€” R, G, B and A each become their own integer property.\nA json tag on the group cannot rename the individual fields (each keeps its\nown name), though tag options such as omitempty still apply.",
  "type": "object",
  "properties": {
    "A": {
      "type": "integer",
      "format": "uint8"
    },
    "B": {
      "type": "integer",
      "format": "uint8"
    },
    "G": {
      "type": "integer",
      "format": "uint8"
    },
    "R": {
      "type": "integer",
      "format": "uint8"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/multiname.json

Full example. testdata/enhancements/named-struct-tags-ref/types.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:name

Usage

// swagger:name IDENT_NAME

What it does

Overrides the JSON property name that a struct field or interface method renders as.

By default the scanner derives names from json:"…" struct tags (or the Go identifier for fields / methods with no tag); swagger:name overrides that derivation when the tag-based shape isn’t appropriate β€” typically on interface methods, which cannot carry struct tags.

Where it goes

On a struct field doc OR an interface method doc.

Grammar (EBNF)

NameAnnotation = ANN_NAME , IDENT_NAME ;

The required IDENT_NAME is the JSON property name to use.

Supported keywords

None β€” the override name is the entire surface.

Example

On an interface method, swagger:name overrides the property name the method would otherwise publish under (PascalCase Go method name) with the chosen JSON name:

Annotated Go
// Car is exposed as a schema via its method set. Interface methods cannot carry
// a json tag, so by default each property takes the camelCased method name;
// swagger:name overrides that where the default is not what you want.
//
// swagger:model
type Car interface {
	// Maker is the manufacturer. With no override the property is the
	// camelCased method name, "maker".
	Maker() string

	// StructType is the polymorphic class. Without the override the property
	// would be "structType"; swagger:name publishes it as "jsonClass".
	//
	// swagger:name jsonClass
	StructType() string
}


// Account shows the universal name: keyword renaming model struct fields. The
// same keyword used on parameters and response headers also sets a property key
// here, winning over a json tag, the legacy swagger:name annotation, and the Go
// field name.
//
// swagger:model
type Account struct {
	// Bal has no json tag; the keyword sets the property key directly.
	//
	// name: balance
	Bal float64

	// Currency carries both naming forms; the keyword wins over the
	// legacy annotation and the json tag.
	//
	// name: currencyCode
	// swagger:name legacyCurrency
	Currency string `json:"currency"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Car is exposed as a schema via its method set. Interface methods cannot carry\na json tag, so by default each property takes the camelCased method name;",
  "type": "object",
  "properties": {
    "jsonClass": {
      "description": "StructType is the polymorphic class. Without the override the property\nwould be \"structType\"; swagger:name publishes it as \"jsonClass\".",
      "type": "string",
      "x-go-name": "StructType"
    },
    "maker": {
      "description": "Maker is the manufacturer. With no override the property is the\ncamelCased method name, \"maker\".",
      "type": "string",
      "x-go-name": "Maker"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/name.json

Full example. testdata/enhancements/interface-methods/types.go.

Deprecated / legacy forms

swagger:name is the legacy annotation form. The canonical, universal field-naming mechanism is the name: keyword, which works at every field site β€” model properties, interface methods, parameters, and response headers β€” with the precedence name: > swagger:name > json: tag > Go field name. swagger:name remains honoured (and idiomatic on interface methods, shown above), but reach for name: in new code; it is the only form that works on parameters and headers.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:omit

Usage

// swagger:omit <field>[,<field>…]

What it does

Stops the named fields being promoted out of an embedded type, so they never reach the enclosing schema.

Embedding a shared type is how Go reuses a struct, but the reused type often carries more than one particular endpoint should: server-assigned fields on a create request, or a field the enclosing struct re-declares for itself. swagger:omit is how the author resolves that β€” codescan does not guess which fields were meant, it documents the type as written unless told otherwise.

Names are Go field names, never JSON aliases: the annotation acts before names are computed, so it is indifferent to json tags and to NameFromTags.

It is a pre-filter, not an edit of the finished schema, so it reads the same whether the embed is inlined or composed into an allOf member (see Composing embeds with allOf): the field is simply never written.

Where it goes

Two placements:

  • on the embed β€” targets are plain field names of that embedded type. This is the ergonomic form and needs no qualification;
  • on the type declaration β€” for an embed you cannot annotate (a type you do not own, or one nested deeper). A bare name resolves against the promoted set; a dotted path names the embed chain, Base.ID or Outer.Inner.Deep.
// swagger:omit Base.ID,Created
type Decorated struct {
	Base
	// …
}

Embeds only. Every path segment but the last must name an embedded field: swagger:omit removes promoted content, which is the only thing the enclosing schema owns. To exclude a struct’s own field, use swagger:ignore on the field itself.

Grammar (EBNF)

OmitBlock  = ANN_OMIT , OmitTarget , { "," , OmitTarget } ;
OmitTarget = GoIdent , { "." , GoIdent } ;

The whole remainder of the line is the argument list; spaces after commas are allowed (swagger:omit ID, Created).

Supported keywords

None. swagger:omit is a classifier: it takes arguments and opens no keyword block.

Example

The go-swagger#1992 shape: a request body embeds the shared domain type, and the server-assigned fields are dropped from this body only.

Annotated Go
// CreateUserParams is the request body: the same User, minus the fields the
// server assigns. `swagger:omit` sits on the embed, so the targets are plain
// field names of the embedded type.
//
// swagger:parameters createUser
type CreateUserParams struct {
	// in: body
	Body struct {
		// swagger:omit ID,Created
		User
	}
}

Full source: docs/examples/concepts/omit/omit.go

The request body
{
  "type": "object",
  "properties": {
    "Name": {
      "type": "string"
    }
  }
}

Full source: docs/examples/concepts/omit/testdata/body.json

The shared type is never touched, so its own definition β€” which the response $refs β€” still documents every field:

{
  "description": "The point of the idiom is that you do not have to touch it.",
  "type": "object",
  "title": "User is the shared domain type β€” deliberately free of any swagger annotation.",
  "properties": {
    "Created": {
      "type": "string",
      "format": "date-time"
    },
    "ID": {
      "type": "integer",
      "format": "int64"
    },
    "Name": {
      "type": "string"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/omit"
}

Full source: docs/examples/concepts/omit/testdata/user.json

Diagnostics

All three are Hints β€” informational, never blocking:

codefires when
scan.omit-unresolvedthe target names no field of the embedded type: a typo, or a field renamed upstream
scan.omit-behind-refthe embed is composed as a $ref (an annotated swagger:model); Swagger 2.0 cannot subtract a property from a reference, so the omission is dropped rather than silently forking the definition
scan.shadowed-embed-fielda field re-declared with json:"-" carries the Go name of a promoted one β€” see below

swagger:omit is the only annotation whose output depends on a name the Go compiler never checks; everything else codescan emits is derived from types. scan.omit-unresolved reports it when a field is renamed upstream, instead of letting the annotation rot silently, so wire OnDiagnostic if you rely on the annotation.

json:"-" does not hide a promoted field

Re-declaring a promoted field with json:"-" looks like it should hide it. It does not: encoding/json ignores a - field entirely, so it never enters the name set, never shadows the promoted one, and Go keeps marshalling the embedded field. swagger:omit is the annotation that removes it for real.

Deprecated

No. Added in v0.37.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:operation

Usage

// swagger:operation METHOD PATH [tag …] OPERATION_ID

What it does

Declares an HTTP route + operation with a YAML-document body.

Same header line as /codescan/maintainers/annotations/swagger-route/ (method, path, optional tags, operation ID), but with a different body shape: instead of the structured Parameters: / Responses: keyword surface, swagger:operation’s body is a single YAML document spelling out the OpenAPI operation object directly.

Use swagger:operation when you want to author the operation in YAML (closer to the OpenAPI spec text) or when the operation has shapes the keyword surface doesn’t cover.

Where it goes

On a function or variable declaration whose doc comment carries the annotation. The Go entity itself doesn’t have to be a handler β€” the annotation publishes a path/operation independent of the carrier.

Grammar (EBNF)

InlineOperationBlock = ANN_OPERATION , OperationArgs
                     , [ Title ] , [ Description ] , InlineOperationBody ;

OperationArgs        = HTTP_METHOD , URL_PATH , { IDENT_NAME } , IDENT_NAME ;

InlineOperationBody is an OPAQUE_YAML document. The trailing IDENT_NAME is the operation ID; the run before it is the tag list. The header line shape authors rely on:

swagger:operation <METHOD> <path> [tag1 tag2 …] <operationID>
  • <METHOD> β€” GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS. Case insensitive.
  • <path> β€” starts with /; supports path-parameter braces (/items/{id}). Only RFC 6570 Level-1 expansion (simple {name} substitution) is allowed; an inline regex constraint (/items/{id:[0-9]+}) is stripped to the bare form with a warning.
  • [tag1 tag2 …] β€” optional whitespace-separated tag list (at least two characters each).
  • <operationID> β€” the unique operation identifier.

Supported keywords

None inside the YAML body β€” it is structurally YAML, not the keyword grammar. The header line is the entire annotation surface.

Example

Annotated Go
// swagger:operation GET /pets/{id} pets getPet
//
// ---
// summary: Get a pet by ID.
// parameters:
//   - name: id
//     in: path
//     required: true
//     type: integer
//     format: int64
// responses:
//   '200':
//     description: the requested pet
//     schema:
//       $ref: '#/definitions/Pet'
//   default:
//     $ref: '#/responses/errorResponse'

Full source: docs/examples/concepts/routes/routes.go

Generated spec
{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Get a pet by ID.",
    "operationId": "getPet",
    "parameters": [
      {
        "type": "integer",
        "format": "int64",
        "name": "id",
        "in": "path",
        "required": true
      }
    ],
    "responses": {
      "200": {
        "description": "the requested pet",
        "schema": {
          "$ref": "#/definitions/Pet"
        }
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/operation.json

The --- delimits the YAML body; everything between the fences is parsed as an OpenAPI 2.0 operation object.

Full example. testdata/enhancements/parameters-map-postdecl/api.go.

Deprecated / legacy forms

swagger:operation also accepts a structured Parameters: body (shared with /codescan/maintainers/annotations/swagger-route/). In that body the per-parameter chunk sigil + name: is the historic chunk-start; - name: is accepted as a YAML-friendly alias and is preferred for YAML-correctness. See the chunk grammar in /codescan/maintainers/sub-languages/#parameters.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:parameters

Usage

// swagger:parameters OPERATION_ID [OPERATION_ID …]

What it does

Declares a Go struct as the parameter set for one or more operations.

Each field becomes one parameter on the named operation(s), and the field’s doc comment carries its in:, required:, validations, and description.

  • A parameter’s name comes from the field’s json: tag, falling back to the Go field name when there is no tag (the form: tag is not consulted). A name: keyword in the field doc takes precedence over both and is the canonical, preferred way to set the name β€” the legacy swagger:name annotation is inert here and emits a context-invalid diagnostic pointing at name:. See the universal name keyword.
  • Operation IDs accumulate: the same ID may appear in several swagger:parameters lines to compose a set from multiple structs, and one struct may carry several lines splitting a long ID list.
  • swagger:parameters declarations are collected across all scanned packages and matched to operations by ID, so a shared set can live in its own package.

Where it goes

On a struct declaration. A bare slice variable (var Filters []string) carries no per-field in:/type:/required:, so parameters must be a struct.

Grammar (EBNF)

ParametersAnnotation = ANN_PARAMETERS , IDENT_NAME , { IDENT_NAME } ;

The IDENT_NAME arguments are the operation IDs this set applies to (at least one). The first argument may instead be a * wildcard (spec-level shared #/parameters/{name}) or a /path (inlined into that exact path-item) β€” see Sharing parameters & responses.

The annotation opens a SchemaBlock body β€” field doc comments carry the parameter validations.

Supported keywords

param-context keywords on fields: in, required, the numeric / length / format validations, default, example, enum, allowEmptyValue, collectionFormat.

Example

Annotated Go
// ListPetsParams is the parameter set for the listPets operation. Each field
// becomes one parameter; the operation IDs after swagger:parameters name the
// operations the set applies to.
//
// swagger:parameters listPets
type ListPetsParams struct {
	// Tag filters pets by tag.
	//
	// in: query
	Tag string `json:"tag"`

	// Limit caps the number of results.
	//
	// in: query
	// minimum: 1
	// maximum: 100
	Limit int32 `json:"limit"`
}

Full source: docs/examples/concepts/routes/routes.go

Generated spec
[
  {
    "type": "string",
    "x-go-name": "Tag",
    "description": "Tag filters pets by tag.",
    "name": "tag",
    "in": "query"
  },
  {
    "maximum": 100,
    "minimum": 1,
    "type": "integer",
    "format": "int32",
    "x-go-name": "Limit",
    "description": "Limit caps the number of results.",
    "name": "limit",
    "in": "query"
  }
]

Full source: docs/examples/concepts/routes/testdata/parameters.json

Full example. testdata/enhancements/simple-schema-violation/api.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:patternProperties

Usage

// swagger:patternProperties "<regex>": <type> [ , "<regex>": <type> … ]

What it does

Adds typed patternProperties entries β€” each maps a property-name regex to a value schema.

It is the typed counterpart of the regex-only patternProperties: keyword (which uses an empty, any-value schema).

Note

patternProperties is a JSON-Schema (draft-4) keyword, beyond the Swagger 2.0 subset. codescan emits it ungated β€” your downstream tooling must understand it.

Where it goes

On a type declaration (alongside swagger:model).

Grammar (EBNF)

PatternPropertiesAnnotation = ANN_PATTERN_PROPERTIES , PatternPair , { "," , PatternPair } ;
PatternPair                 = STRING_VALUE , ":" , ValueType ;
ValueType                   = TYPE_REF | IDENT_NAME | "[]" , ValueType ;

A comma-separated list of "<regex>": <spec> pairs. The regex (STRING_VALUE) is double-quoted β€” it may contain spaces, colons, commas; only \" is an escape inside it, other backslashes like \d are preserved. Each <spec> reuses the /codescan/maintainers/annotations/swagger-type/ value grammar (primitive / []T / type-name β†’ $ref).

Supported keywords

None of its own. It composes with maxProperties / minProperties / additionalProperties.

Example

Annotated Go
// TypedPatterns maps property-name regexes to typed value schemas. Each quoted
// regex pairs with a value spec β€” a primitive or a model name (which becomes a
// $ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0
// subset; codescan emits it ungated.
//
// swagger:model
// swagger:patternProperties "^x-": string, "^\d+$": integer, "^item-": Thing
type TypedPatterns struct {
	Known string `json:"known"`
}

Full source: docs/examples/concepts/maps/maps.go

Generated spec
{
  "description": "TypedPatterns maps property-name regexes to typed value schemas. Each quoted\nregex pairs with a value spec β€” a primitive or a model name (which becomes a\n$ref). The pattern-properties keyword is JSON-Schema, beyond the Swagger 2.0\nsubset; codescan emits it ungated.",
  "type": "object",
  "properties": {
    "known": {
      "type": "string",
      "x-go-name": "Known"
    }
  },
  "patternProperties": {
    "^\\d+$": {
      "type": "integer"
    },
    "^item-": {
      "$ref": "#/definitions/Thing"
    },
    "^x-": {
      "type": "string"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/maps"
}

Full source: docs/examples/concepts/maps/testdata/patterntyped.json

Precedence. Same lowest-priority, object-only rule as swagger:additionalProperties. Each regex is RE2-hygiene-checked: one that does not compile raises a CodeInvalidAnnotation warning but is preserved; a structurally malformed pair list is dropped with a diagnostic.

Full example. testdata/enhancements/pattern-properties-typed/api.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:response

Usage

// swagger:response [ IDENT_NAME ]

What it does

Declares a Go struct as a named response object.

It is emitted into the spec’s top-level responses map. Routes / operations reference it by name via the response sub-language (Responses: body in swagger:route, or the YAML $ref form in swagger:operation).

The struct’s fields contribute the response shape:

  • A field named Body (or carrying in: body) becomes the response body schema. The body may be a struct, a $ref’d model, or a primitive β€” Body string emits schema: {type: string}.
  • Other fields default to response headers: a field with neither Body/in: body nor in: header is treated as a header, not a body property. A header’s key comes from the json: tag / Go field name, or a name: keyword (e.g. name: X-Rate-Limit) β€” the canonical, preferred form, see the name keyword.
  • An anonymously embedded struct marked in: body is the body (a $ref to the model), not a promotion of its fields.
  • An interface{} / any-typed field emits an empty schema ({}, or {type: array, items: {}} for a slice) β€” “any type”, valid OpenAPI 2.0.

Where it goes

On a struct declaration.

Grammar (EBNF)

ResponseAnnotation = ANN_RESPONSE , [ IDENT_NAME ] ;

The optional IDENT_NAME is the published response name (default: the Go type’s name). A * wildcard (swagger:response *) explicitly marks the response as a shared one, registered at #/responses/{name} for operations to $ref by name β€” see Sharing parameters & responses.

The annotation opens a SchemaBlock body.

Supported keywords

  • Body field: schema-context keywords.
  • Header field: header-context keywords β€” numeric / length / format validations, pattern, enum, default, example, collectionFormat. required: is silently dropped (the OAS v2 Header object has no required field).

Example

Annotated Go
// PetsResponse is the list returned by listPets.
//
// swagger:response petsResponse
type PetsResponse struct {
	// in: body
	Body []Pet
}

// ErrorResponse is the default error payload.
//
// swagger:response errorResponse
type ErrorResponse struct {
	// in: body
	Body struct {
		// Message is a human-readable error message.
		Message string `json:"message"`
	}
}

Full source: docs/examples/concepts/routes/routes.go

Generated spec
{
  "description": "PetsResponse is the list returned by listPets.",
  "schema": {
    "type": "array",
    "items": {
      "$ref": "#/definitions/Pet"
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/response.json

Routes can then reference it via response:genericError in their Responses: body.

Full example. testdata/enhancements/routes-full-petstore-shape/handlers.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:route

Usage

// swagger:route METHOD PATH [tag …] OPERATION_ID

What it does

Declares an HTTP route + operation in one annotation.

The header line carries the method, path, optional tags, and the operation ID; the comment body carries the operation’s metadata (consumes / produces / schemes / security / parameters / responses / extensions).

This is the terser of the two operation-declaration annotations. Most go-swagger projects use swagger:route for hand-written operations; see /codescan/maintainers/annotations/swagger-operation/ for the YAML-body alternative.

Where it goes

On a function or variable declaration whose doc comment carries the annotation. The Go entity itself doesn’t have to be a handler β€” the annotation publishes a path/operation independent of the carrier.

A godoc-style identifier may precede the annotation on the same comment line (// ListPets swagger:route GET /pets pets users listPets); that leading identifier is recognised as a godoc convention and is not part of the annotation surface.

Grammar (EBNF)

RouteBlock    = ANN_ROUTE , OperationArgs
              , [ Title ] , [ Description ] , RouteBody ;

OperationArgs = HTTP_METHOD , URL_PATH , { IDENT_NAME } , IDENT_NAME ;

The trailing IDENT_NAME is the operation ID; the run before it is the tag list. The header line shape authors rely on:

swagger:route <METHOD> <path> [tag1 tag2 …] <operationID>
  • <METHOD> β€” GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS. Case insensitive.
  • <path> β€” starts with /; supports path-parameter braces (/items/{id}). Only RFC 6570 Level-1 expansion (simple {name} substitution) is allowed; an inline regex constraint (/items/{id:[0-9]+}) is stripped to the bare form with a warning.
  • [tag1 tag2 …] β€” optional whitespace-separated tag list (at least two characters each).
  • <operationID> β€” the unique operation identifier.

Supported keywords

All body keywords legal in route context (consumes, produces, schemes, security, parameters, responses, extensions, externalDocs) plus inline deprecated: and a body tags: list (a string list, unioned and deduplicated with the header-line tags). The Parameters: and Responses: sub-languages are documented in /codescan/maintainers/sub-languages/#parameters and /codescan/maintainers/sub-languages/#responses.

Example

Annotated Go
// swagger:route GET /pets pets listPets
//
// Lists pets in the store, optionally filtered by tag.
//
// responses:
//
//	200: petsResponse
//	default: errorResponse

Full source: docs/examples/concepts/routes/routes.go

Generated spec
{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Lists pets in the store, optionally filtered by tag.",
    "operationId": "listPets",
    "parameters": [
      {
        "type": "string",
        "x-go-name": "Tag",
        "description": "Tag filters pets by tag.",
        "name": "tag",
        "in": "query"
      },
      {
        "maximum": 100,
        "minimum": 1,
        "type": "integer",
        "format": "int32",
        "x-go-name": "Limit",
        "description": "Limit caps the number of results.",
        "name": "limit",
        "in": "query"
      }
    ],
    "responses": {
      "200": {
        "$ref": "#/responses/petsResponse"
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/route.json

Full example. testdata/enhancements/routes-full-petstore-shape/handlers.go.

Deprecated / legacy forms

In the Parameters: body the per-parameter chunk sigil + name: (used in the sample above) is the historic chunk-start; - name: is accepted as a YAML-friendly alias and is preferred for YAML-correctness. See the chunk grammar in /codescan/maintainers/sub-languages/#parameters.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:strfmt

Usage

// swagger:strfmt FORMAT_NAME

What it does

Marks a named type as a custom string format.

Wherever the type appears as a field, the emitted schema is {type: string, format: <name>}. Useful for UUID, Email, URL-style types that have a Go type but should serialise as a JSON string with a known format.

A field typed by the marked type emits with the format; the underlying type does NOT appear as a top-level model definition (strfmt-tagged types are replaced by their format at every reference). A slice carries the format onto its items: {type: array, items: {type: string, format: …}}.

Where it goes

On a type declaration whose underlying form is a string-marshalable type (typically implementing encoding.TextMarshaler / encoding.TextUnmarshaler). swagger:strfmt may also sit on a struct field doc to override just that field’s published format.

Grammar (EBNF)

StrfmtBlock = ANN_STRFMT , IDENT_NAME , [ Title ] , [ Description ] ;

The required IDENT_NAME is the format name (uuid, email, mac, …) β€” the entire surface of the annotation.

Supported keywords

None at the type level beyond swagger:strfmt itself; the format name is the entire surface.

Example

A named type marked swagger:strfmt (here a MarshalText/UnmarshalText hardware address) emits as {type: string, format: …} wherever it is referenced β€” a field typed MAC comes out as {type: string, format: mac}:

Annotated Go
// MAC is a hardware address rendered as a colon-separated hex string.
//
// swagger:strfmt mac
type MAC string

func (m MAC) MarshalText() ([]byte, error)  { return []byte(m), nil }
func (m *MAC) UnmarshalText(b []byte) error { *m = MAC(b); return nil }

// Device exposes a strfmt-typed field: wherever MAC appears it renders inline
// as {type: string, format: mac}.
//
// swagger:model
type Device struct {
	// Addr is the hardware address.
	Addr MAC `json:"addr"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Device exposes a strfmt-typed field: wherever MAC appears it renders inline\nas {type: string, format: mac}.",
  "type": "object",
  "properties": {
    "addr": {
      "description": "Addr is the hardware address.",
      "type": "string",
      "format": "mac",
      "x-go-name": "Addr"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/strfmt.json

Adding swagger:model opts the type into a first-class definition carrying the full {type: string, format: …} schema, with referencing fields pointing at it via $ref β€” the general swagger:model β‡’ definition + $ref rule. Without swagger:model, the format inlines at every reference.

A field-level override targets one field’s format β€” e.g. // swagger:strfmt int64 on a uint64 field emits {type: string, format: int64}, a precision-safe, JSON-conformant string encoding (the conformant alternative to the Go-specific {integer, format: uint64} codescan emits for unsized/large ints by default).

Full example. testdata/enhancements/text-marshal/types.go.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:title

Usage

// swagger:title <text>   (where <text> is the rest of the line)

What it does

Replaces the godoc-derived title on a schema with explicit text.

By default a model’s title comes from the first line of its doc comment; swagger:title overrides that when the prose isn’t the title you want to publish. It is a schema-family override β€” a sibling of swagger:description.

Where it goes

On a type (model) doc comment or a struct-field doc comment. It is schema-only: a response has no title (the annotation is ignored there), and on a non-body parameter or response header it is rejected with a CodeContextInvalid diagnostic.

Grammar (EBNF)

TitleAnnotation = ANN_TITLE , RAW_VALUE ;

RAW_VALUE is the rest of the head line, captured verbatim. The annotation dispatches through the schema parser (not the classifier parser), so validation keywords co-located on the same comment group still surface as schema validations.

Supported keywords

None of its own β€” the text is the entire argument. A blank swagger:title emits a CodeEmptyOverride diagnostic.

Example

Annotated Go
// Widget is the Go-facing widget doc, written for Go readers.
//
// It explains internal Go usage that should not leak into the API spec.
//
// swagger:model
// swagger:title A Public Widget
// swagger:description A widget exposed via the public API.
type Widget struct {
	// ID explains the Go field for Go readers.
	//
	// swagger:description The unique widget identifier.
	ID string `json:"id"`

	// Label is the Go-facing field doc. Fields carry no title by default;
	// the override is the only way a property gets one.
	//
	// swagger:title Display Label
	// swagger:description Human-readable label shown to API consumers.
	Label string `json:"label"`

	// Plain keeps its godoc description because it carries no override.
	Plain string `json:"plain"`

	// Capacity combines a description override with an inline validation
	// keyword on the same field: the override applies AND maximum is kept,
	// because the override annotations dispatch through the schema family.
	//
	// swagger:description The maximum capacity, in liters.
	// maximum: 1000
	Capacity int64 `json:"capacity"`

	// Suppressed has a godoc that a bare swagger:description suppresses: the
	// empty value is applied (description omitted) and scan.empty-override is
	// raised, in case the bare marker was left behind by mistake.
	//
	// swagger:description
	Suppressed string `json:"suppressed"`

	// Notes carries a multi-line description override: the lines following the
	// annotation fold into the description until the blank line, joined with
	// newlines.
	//
	// swagger:description Free-form notes about the widget.
	// They may span several lines, all folded into one description.
	//
	// The blank line above terminates the override body; this paragraph is
	// ordinary godoc and is discarded (the override won).
	Notes string `json:"notes"`

	// Gadget is a $ref field carrying title + description overrides. They are
	// symmetric $ref siblings: kept under EmitRefSiblings, dropped to a bare
	// $ref under the default flags β€” the same rule a prose description follows.
	//
	// swagger:title Gadget Ref
	// swagger:description The attached gadget, described for API consumers.
	Gadget Gadget `json:"gadget"`
}

// Gadget is a plain referenced model.
//
// swagger:model
type Gadget struct {
	Serial string `json:"serial"`
}

Full source: docs/examples/shaping/overrides/overrides.go

Generated spec
{
  "description": "A widget exposed via the public API.",
  "type": "object",
  "title": "A Public Widget",
  "properties": {
    "capacity": {
      "description": "The maximum capacity, in liters.",
      "type": "integer",
      "format": "int64",
      "maximum": 1000,
      "x-go-name": "Capacity"
    },
    "gadget": {
      "$ref": "#/definitions/Gadget"
    },
    "id": {
      "description": "The unique widget identifier.",
      "type": "string",
      "x-go-name": "ID"
    },
    "label": {
      "description": "Human-readable label shown to API consumers.",
      "type": "string",
      "title": "Display Label",
      "x-go-name": "Label"
    },
    "notes": {
      "description": "Free-form notes about the widget.\nThey may span several lines, all folded into one description.",
      "type": "string",
      "x-go-name": "Notes"
    },
    "plain": {
      "description": "Plain keeps its godoc description because it carries no override.",
      "type": "string",
      "x-go-name": "Plain"
    },
    "suppressed": {
      "type": "string",
      "x-go-name": "Suppressed"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/shaping/overrides"
}

Full source: docs/examples/shaping/overrides/testdata/widget.json

See Overriding titles & descriptions for the full walkthrough.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

swagger:type

Usage

// swagger:type <type>   (where <type> is a scalar, []T, inline, or a known type name)

What it does

Replaces a field’s (or named type’s) inferred Swagger type with an inlined type.

swagger:type is an inline directive β€” it never emits a $ref; the chosen type is rendered directly in place (the default $ref-for-named-types is the no-annotation behaviour).

Where it goes

On a type declaration, a struct field doc, OR a swagger:parameters field doc.

Note

On a parameter field the override collapses the field to a simple parameter β€” useful when a struct- or defined-typed field would otherwise come out typeless (invalid Swagger 2.0). The argument is restricted to a scalar or a []-wrapped scalar there: the inline and type-name forms are rejected with a diagnostic, since a non-body parameter has no schema to inline a type into. A compatible swagger:strfmt on the same field still rides as a supplementary format.

Grammar (EBNF)

TypeBlock = ANN_TYPE , TYPE_REF , [ Title ] , [ Description ] ;

The required TYPE_REF is one of:

  • a scalar type β€” string, integer, number, boolean, object (or a Go-builtin spelling such as int64, uint32);
  • []T β€” an array whose items are the inlined T (recursive: [][]int64, []Custom);
  • inline β€” expand the field’s own Go type in place, instead of the $ref a named type would otherwise produce;
  • a known type name β€” inline that type’s schema (again, no $ref).

An unknown name falls back to inlining the field’s Go type, with a validate.unsupported-type diagnostic.

Supported keywords

None β€” the override type is the entire surface.

Example

Type-level override β€” a named type whose underlying shape is irrelevant to the wire form is inlined to the chosen scalar; fields typed by it emit as {type: string} regardless of the underlying shape:

Annotated Go
// ULID is a 128-bit identifier stored as bytes but rendered as a string.
//
// swagger:type string
type ULID [16]byte

// Token carries a field whose inferred type is overridden, inline.
//
// swagger:model
type Token struct {
	// ID renders as a string despite its [16]byte Go type.
	ID ULID `json:"id"`
}


// RawID is a custom 16-byte identifier β€” an array under the hood, so left to
// itself a field of this type would render as an array of integers.
type RawID [16]byte

// Coupon overrides the type of a single field directly on the field doc β€” no
// wrapper-type annotation. Code publishes as a bare string while RawID is left
// untouched everywhere else it appears.
//
// swagger:model
type Coupon struct {
	// Code is an opaque identifier published as a string.
	//
	// swagger:type string
	Code RawID `json:"code"`

	// Amount is the discount in cents.
	Amount int64 `json:"amount"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "type": "object",
  "title": "Token carries a field whose inferred type is overridden, inline.",
  "properties": {
    "id": {
      "description": "ID renders as a string despite its [16]byte Go type.",
      "type": "string",
      "x-go-name": "ID"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/type.json

Field-level override β€” the same directive on a single struct field replaces just that field’s inferred type in place (e.g. an opaque payload published as a string blob):

Annotated Go
// RawID is a custom 16-byte identifier β€” an array under the hood, so left to
// itself a field of this type would render as an array of integers.
type RawID [16]byte

// Coupon overrides the type of a single field directly on the field doc β€” no
// wrapper-type annotation. Code publishes as a bare string while RawID is left
// untouched everywhere else it appears.
//
// swagger:model
type Coupon struct {
	// Code is an opaque identifier published as a string.
	//
	// swagger:type string
	Code RawID `json:"code"`

	// Amount is the discount in cents.
	Amount int64 `json:"amount"`
}

Full source: docs/examples/concepts/models/models.go

Generated spec
{
  "description": "Coupon overrides the type of a single field directly on the field doc β€” no\nwrapper-type annotation. Code publishes as a bare string while RawID is left\nuntouched everywhere else it appears.",
  "type": "object",
  "properties": {
    "amount": {
      "description": "Amount is the discount in cents.",
      "type": "integer",
      "format": "int64",
      "x-go-name": "Amount"
    },
    "code": {
      "description": "Code is an opaque identifier published as a string.",
      "type": "string",
      "x-go-name": "Code"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/models"
}

Full source: docs/examples/concepts/models/testdata/typefield.json

Interaction with swagger:strfmt. swagger:type wins on the type axis; a swagger:strfmt format on the same field is kept only when compatible with the resolved type (a string accepts any format, numeric types accept the numeric width formats), otherwise it is dropped with a shape-mismatch diagnostic. swagger:strfmt alone is unchanged. See swagger:strfmt.

Interaction with swagger:model. On a type declaration that also carries swagger:model, the override shapes the type’s first-class definition (e.g. swagger:type string + swagger:model β†’ a {type: string} definition) and referencing fields $ref it. The field-level inline form above is the behaviour without swagger:model.

Full example. testdata/enhancements/named-struct-tags-ref/types.go.

Deprecated / legacy forms

  • The array argument is deprecated β€” use inline, or []T for an explicit element type. It still works, with a validate.deprecated warning.

  • file used to be rejected as an argument. It is now accepted, and is the preferred spelling: file is an OAS v2 type name like any other, so the annotation that names types names it too. It is a synonym for swagger:file, which is expected to be deprecated as an extraneous annotation.

    file is legal in exactly two places β€” a formData parameter and a response body. Both spellings pass through the same location gate, so neither can put file anywhere OAS 2.0 forbids it; elsewhere the override is refused with a diagnostic and the Go type stands.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Keyword reference

Keywords are the keyword: value lines that decorate an annotation block. They come in two flavours: inline (one line, keyword: value, the value classified by a value shape) and body (a header line plus indented continuation lines β€” a flat token list, a YAML map, or a per-line sub-language). Three things matter about any keyword: the class it belongs to, the annotation contexts that accept it, and its value shape.

This section groups the surface by class β€” pick the page that matches what you’re decorating. For the formal productions see grammar.md; for the value-shape and context-token reference tables see the Appendix.

Keyword classes

ClassCoversKeywords
Parameters & responsesrequest parameters and response headers (the reduced SimpleSchema surface)in, name, collectionFormat, examples, + the shared validations
Schema validations & decoratorsmodel schemas and struct fieldsmaximum/minimum/multipleOf, maxLength/minLength, maxItems/minItems, maxProperties/minProperties, pattern, patternProperties, additionalProperties, unique, default, example, enum, required, readOnly, discriminator, deprecated
Routes & operationsswagger:route / swagger:operation metadataschemes, consumes, produces, responses, parameters, tags
Securityauthentication requirements & scheme definitionssecurity, securityDefinitions
Spec metadataswagger:meta top-of-document fieldsversion, host, basePath, license, contact, tos, infoExtensions, externalDocs, extensions, tags
  • Keywords that decorate swagger:parameters fields and swagger:response headers β€” the reduced OAS 2.0 SimpleSchema surface, plus the parameter location and response-level examples.
  • Keywords that constrain and decorate a model schema or struct field β€” bounds, lengths, patterns, enums, defaults, and structural markers.
  • Keywords carried in a swagger:route or swagger:operation block β€” the operation’s transport metadata and its parameter and response bodies.
  • Keywords that wire authentication β€” the requirements that gate a spec, route, or operation, and the scheme catalogue declared once in meta.
  • Top-of-document keywords authored under swagger:meta β€” version, host, base path, license, contact, terms of service β€” plus the cross-cutting vendor-extension and external-docs keywords.
  • Reference tables β€” the value shapes the lexer classifies, and the meaning of each annotation-context token.

Context matrix

Which annotation family accepts a given keyword β€” the transpose of the annotation Γ— keyword matrix. A βœ… means the keyword is legal on that annotation (on the annotation’s own block or on one of its fields); a blank means it is rejected there with a CodeContextInvalid diagnostic. The detailed entry for each keyword lives on its class page (linked above).

Keywordmetamodelparametersresponserouteoperation
maximum minimum multipleOfβœ…βœ…βœ…
maxLength minLengthβœ…βœ…βœ…
maxItems minItems uniqueβœ…βœ…βœ…
patternβœ…βœ…βœ…
collectionFormatβœ…βœ…
maxProperties minPropertiesβœ…
patternProperties additionalPropertiesβœ…
default example enumβœ…βœ…βœ…
requiredβœ…βœ…
readOnly discriminatorβœ…
deprecatedβœ…βœ…βœ…
inβœ…
nameβœ…βœ…βœ…
examplesβœ…
schemes consumes producesβœ…βœ…βœ…
securityβœ…βœ…βœ…
securityDefinitionsβœ…
responses parametersβœ…βœ…
tagsβœ…βœ…βœ…
version host basePath license contact tosβœ…
infoExtensionsβœ…
externalDocsβœ…βœ…βœ…βœ…
extensionsβœ…βœ…βœ…βœ…βœ…βœ…

The parameters / response columns also cover the items sub-context (array elements): the array-element validations ride there too. model covers swagger:allOf member fields. See the Appendix for the precise meaning of each context token (param, header, schema, items, …).

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Subsections of Keyword reference

Parameters & responses

These keywords decorate swagger:parameters fields and swagger:response headers. Both sites ride the reduced OAS 2.0 SimpleSchema surface: the validations constrain a primitive (or array-of-primitive) value, but the full-Schema-only keywords (maxProperties / minProperties / patternProperties / additionalProperties / readOnly / discriminator / externalDocs) do not apply here. The location keyword in and the universal name keyword are at home on this page; the validations are shared with Schema validations & decorators.

Summary

KeywordAliasesShapeContexts
inβ€”string (closed-vocab)param
nameβ€”stringparam, header, schema, items
collectionFormatcollection format, collection-formatstring (closed-vocab)param, header, items
examplesβ€”YAML map (mime β†’ payload)response
maximummaxnumberparam, header
minimumminnumberparam, header
multipleOfmultiple of, multiple-ofnumberparam, header
maxLengthmax length, maxLen, …integerparam, header
minLengthmin length, minLen, …integerparam, header
maxItemsmax items, maximumItems, …integerparam, header
minItemsmin items, minimumItems, …integerparam, header
patternβ€”stringparam, header
uniqueβ€”booleanparam, header
defaultβ€”raw-valueparam, header
exampleβ€”raw-valueparam, header
enumβ€”raw-valueparam, header
requiredβ€”booleanparam

The validation rows (maximum … required) are visiting here: they behave exactly as on schemas β€” see Schema validations & decorators. Two SimpleSchema restrictions apply on this page: required is dropped on response headers (it sets parameter.required only on a body/non-body param), and the object / structural keywords (maxProperties, minProperties, patternProperties, additionalProperties, readOnly, discriminator, externalDocs) are not legal here β€” placing one on a SimpleSchema site drops it with CodeUnsupportedInSimpleSchema.

Worked example(s)

A parameter set, every field carrying the SimpleSchema validation surface:

Annotated Go
// SearchParams is the simple-schema parameter set for searchProducts. Query
// parameters accept the reduced OAS 2.0 validation surface.
//
// swagger:parameters searchProducts
type SearchParams struct {
	// Q is the search text.
	//
	// in: query
	// min length: 3
	// max length: 50
	Q string `json:"q"`

	// Limit caps the number of results.
	//
	// in: query
	// minimum: 1
	// maximum: 100
	Limit int32 `json:"limit"`

	// Sort lists the sort fields.
	//
	// in: query
	// collection format: csv
	// unique: true
	Sort []string `json:"sort"`
}

Full source: docs/examples/concepts/validations/validations.go

Generated spec
[
  {
    "maxLength": 50,
    "minLength": 3,
    "type": "string",
    "x-go-name": "Q",
    "description": "Q is the search text.",
    "name": "q",
    "in": "query"
  },
  {
    "maximum": 100,
    "minimum": 1,
    "type": "integer",
    "format": "int32",
    "x-go-name": "Limit",
    "description": "Limit caps the number of results.",
    "name": "limit",
    "in": "query"
  },
  {
    "uniqueItems": true,
    "type": "array",
    "items": {
      "type": "string"
    },
    "collectionFormat": "csv",
    "x-go-name": "Sort",
    "description": "Sort lists the sort fields.",
    "name": "sort",
    "in": "query"
  }
]

Full source: docs/examples/concepts/validations/testdata/param.json

A response with a validated header (note in is absent on header fields):

Annotated Go
// RateLimited is a response carrying a validated header (a simple schema).
//
// swagger:response rateLimited
type RateLimited struct {
	// XRateRemaining is the remaining request budget.
	//
	// minimum: 0
	XRateRemaining int32 `json:"X-Rate-Remaining"`
}

Full source: docs/examples/concepts/validations/validations.go

Generated spec
{
  "description": "RateLimited is a response carrying a validated header (a simple schema).",
  "headers": {
    "X-Rate-Remaining": {
      "minimum": 0,
      "type": "integer",
      "format": "int32",
      "description": "XRateRemaining is the remaining request budget."
    }
  }
}

Full source: docs/examples/concepts/validations/testdata/header.json

Parameter location

in

Where the parameter value comes from. Closed-vocab:

  • query β€” query string parameter.
  • path β€” path-parameter substitution.
  • header β€” request header.
  • body β€” request body (JSON, etc.).
  • formData β€” form-data body field (note: form is accepted as an alias inside swagger:route Parameters: chunks; the lexer normalises it to formData at the canonical surface).

A non-matching value emits a context-invalid diagnostic; the parameter loses its in and may end up incorrectly classified downstream. The keyword is parameter-only β€” it has no meaning on a response header (the header name is the location).

Field naming

name

Sets the published name of any field it decorates, overriding the json: tag / Go field name. It is the one canonical field-naming keyword and works at every field site: a swagger:model property, an interface method, a swagger:parameters field (the parameter name), and a swagger:response header field (the Headers map key). Being structural, it is stripped from the description rather than leaking into it.

Precedence, most-explicit-wins and identical in every context:

name: keyword  >  swagger:name annotation  >  json: tag  >  Go field name

swagger:name is the older annotation form β€” still honoured, and idiomatic on interface methods β€” but name: is the universal keyword. Using swagger:name in a parameter or response-header context (where name: is canonical) is inert and now emits a context-invalid diagnostic pointing you at the keyword.

Wire serialisation

collectionFormat

How an array value is serialised on the wire. Closed-vocab:

  • csv β€” comma-separated (default).
  • ssv β€” space-separated.
  • tsv β€” tab-separated.
  • pipes β€” pipe-separated.
  • multi β€” repeated ?key=val&key=val2 (query params only).

Aliases: collection format, collection-format. Maps to parameter.collectionFormat / items.collectionFormat. This is a SimpleSchema-only concept β€” schema-level contexts ignore it (schemas serialise via application/json). When the source value doesn’t match the closed vocab, the raw value is preserved verbatim on the parameter (so pipe as a typo for pipes round-trips).

Response examples

examples

Response-level examples on a swagger:response struct β€” a YAML map whose first-level keys are mime types and whose values are the example payloads, populating the OAS2 Response.examples field. This is the plural, response-scoped keyword; contrast the singular, schema/param/header-scoped example decorator. The swagger:operation YAML body carries examples natively (it is unmarshalled straight into the spec types); this keyword is the struct-swagger:response counterpart.

// swagger:response widgetResponse
//
// examples:
//
//	application/json:
//	  name: alice
//	  count: 3
//	application/xml: "<widget><name>alice</name></widget>"
type WidgetResponse struct {
	// in: body
	Body Widget `json:"body"`
}

See also Spec metadata for the document-level keywords that frame these operations.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Schema validations & decorators

These keywords decorate a swagger:model schema or any struct field doc comment. The validations constrain a value; the decorators carry defaults, examples, and structural markers. Several also apply to parameters and response headers β€” there they ride the reduced SimpleSchema surface (Parameters & responses).

Summary

KeywordAliasesShapeContexts
maximummaxnumberparam, header, schema, items
minimumminnumberparam, header, schema, items
multipleOfmultiple of, multiple-ofnumberparam, header, schema, items
maxLengthmax length, maxLen, …integerparam, header, schema, items
minLengthmin length, minLen, …integerparam, header, schema, items
maxItemsmax items, maximumItems, …integerparam, header, schema, items
minItemsmin items, minimumItems, …integerparam, header, schema, items
maxPropertiesmax properties, …integerschema
minPropertiesmin properties, …integerschema
patternβ€”stringparam, header, schema, items
patternPropertiespattern properties, pattern-propertiesstring (regex)schema
additionalPropertiesadditional properties, additional-propertiestrue/false/typeschema
uniqueβ€”booleanparam, header, schema, items
defaultβ€”raw-valueparam, header, schema, items
exampleβ€”raw-valueparam, header, schema, items
enumβ€”raw-valueparam, header, schema, items
requiredβ€”booleanparam, schema
readOnlyread only, read-onlybooleanschema
discriminatorβ€”booleanschema
deprecatedβ€”booleanoperation, route, schema

The shared rows above (param/header/schema/items) are detailed here; on parameters and headers they behave the same, with the OAS 2.0 SimpleSchema restrictions noted on the Parameters & responses page. collectionFormat and in/name live there too.

Worked examples

Every validation on a model’s fields, side by side with the schema it produces:

Annotated Go
// Product is a model whose fields carry the full JSON-schema validation surface.
//
// swagger:model
type Product struct {
	// SKU is the stock code.
	//
	// required: true
	// pattern: ^[A-Z]{3}-[0-9]{4}$
	SKU string `json:"sku"`

	// Price is the price in cents.
	//
	// minimum: 1
	// maximum: 1000000
	// multipleOf: 1
	Price int64 `json:"price"`

	// Name is the display name.
	//
	// min length: 1
	// max length: 120
	Name string `json:"name"`

	// Grade is a quality band.
	//
	// enum: A,B,C
	Grade string `json:"grade"`

	// Tags label the product.
	//
	// min items: 1
	// max items: 10
	// unique: true
	Tags []string `json:"tags"`
}

Full source: docs/examples/concepts/validations/validations.go

Generated spec
{
  "type": "object",
  "title": "Product is a model whose fields carry the full JSON-schema validation surface.",
  "required": [
    "sku"
  ],
  "properties": {
    "grade": {
      "description": "Grade is a quality band.",
      "type": "string",
      "enum": [
        "A",
        "B",
        "C"
      ],
      "x-go-name": "Grade"
    },
    "name": {
      "description": "Name is the display name.",
      "type": "string",
      "maxLength": 120,
      "minLength": 1,
      "x-go-name": "Name"
    },
    "price": {
      "description": "Price is the price in cents.",
      "type": "integer",
      "format": "int64",
      "maximum": 1000000,
      "minimum": 1,
      "multipleOf": 1,
      "x-go-name": "Price"
    },
    "sku": {
      "description": "SKU is the stock code.",
      "type": "string",
      "pattern": "^[A-Z]{3}-[0-9]{4}$",
      "x-go-name": "SKU"
    },
    "tags": {
      "description": "Tags label the product.",
      "type": "array",
      "maxItems": 10,
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "type": "string"
      },
      "x-go-name": "Tags"
    }
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/validations"
}

Full source: docs/examples/concepts/validations/testdata/field.json

The object-validation keywords constrain the map of properties rather than named fields:

Annotated Go
// Attributes is a free-form object constrained by the object-validation
// keywords: it must carry between 1 and 10 properties, and any property whose
// name matches the regex is permitted. Object validations constrain the map of
// (additional) properties rather than named struct fields.
//
// minProperties: 1
// maxProperties: 10
// patternProperties: ^x-
//
// swagger:model Attributes
type Attributes map[string]any

Full source: docs/examples/concepts/validations/validations.go

Generated spec
{
  "description": "Attributes is a free-form object constrained by the object-validation\nkeywords: it must carry between 1 and 10 properties, and any property whose\nname matches the regex is permitted. Object validations constrain the map of\n(additional) properties rather than named struct fields.",
  "type": "object",
  "maxProperties": 10,
  "minProperties": 1,
  "additionalProperties": {},
  "patternProperties": {
    "^x-": {}
  },
  "x-go-package": "github.com/go-openapi/codescan/docs/examples/concepts/validations"
}

Full source: docs/examples/concepts/validations/testdata/object.json

Numeric validations

Apply to numeric schema types (integer, number). On a typed schema with a non-numeric type they emit CodeShapeMismatch and drop; on a typeless schema they apply best-effort.

maximum / minimum

Upper / lower bound on a numeric value (aliases max / min). The value may carry a leading comparison operator that sets the exclusive/inclusive bound:

  • maximum: 10 β€” inclusive (≀ 10);
  • maximum: <10 β€” exclusive (< 10);
  • maximum: <=10 / maximum: =10 β€” inclusive.

Map to schema.maximum/exclusiveMaximum and schema.minimum/exclusiveMinimum.

multipleOf

Divisibility constraint; the value must be a positive number. Aliases multiple of, multiple-of. Maps to schema.multipleOf.

Length, array & object validations

maxLength / minLength apply only to string-typed schemas; maxItems / minItems only to array-typed; maxProperties / minProperties / patternProperties only to object-typed. The wrong pairing emits CodeShapeMismatch and drops. The object keywords are additionally full-Schema-only β€” no SimpleSchema (non-body param, header, items) form exists in OAS 2.0, so on such a site they drop with CodeUnsupportedInSimpleSchema.

maxLength / minLength

String length bounds. Many ergonomic aliases (max length, max-length, maxLen, maximumLength, …; min likewise). Map to schema.maxLength / schema.minLength.

maxItems / minItems

Array length bounds (aliases max items, maximumItems, …). Map to schema.maxItems / schema.minItems.

maxProperties / minProperties

Property-count bounds on an object schema (aliases max properties, …). Map to schema.maxProperties / schema.minProperties. Schema-only.

patternProperties

Constrains the names of properties on an object schema by regex. The argument is one regex string; each line adds an entry to schema.patternProperties mapping the regex to an empty value schema ({} β€” any value allowed). Repeated lines accumulate. Aliases pattern properties, pattern-properties. The regex is RE2-hygiene-checked: one that doesn’t compile raises CodeInvalidAnnotation but is preserved.

For typed value schemas (a regex β†’ primitive or model $ref), use the decl-level swagger:patternProperties marker. patternProperties is JSON-Schema, beyond the Swagger 2.0 subset β€” see Maps & free-form objects.

additionalProperties

Policy for keys beyond the named properties on an object schema: true (allow any), false (close the object), or a value type (primitive / []T, or a model name β†’ $ref). On a map field it overrides the Go element schema; on a $ref’d field the value rides an allOf sibling so the reference is kept. Aliases additional properties, additional-properties. Lowest-priority and object-only β€” dropped with CodeShapeMismatch on a non-object. The decl-level swagger:additionalProperties marker does the same on a type.

Format

pattern

A regex constraint on a string value, preserved verbatim on schema.pattern β€” including backslash escapes (\d, \., \n reach the spec as literal two-character sequences). The grammar runs a best-effort RE2 compile; a failure surfaces CodeInvalidAnnotation but the value still lands (downstream tools may use a wider regex dialect).

unique

Marks an array-typed schema as set-valued (no duplicates). Boolean. Maps to schema.uniqueItems.

Schema decorators

default

Default value for a schema or simple-schema field. Raw-value shape β€” the post-colon text is captured verbatim and coerced against the resolved schema type at write time (ParseDefault / CoerceValue). Single-line for primitives (default: 1), multi-line bodies for complex literals:

// default:
//   { "rps": 100, "burst": 200 }

example

An example value for the schema, surfaced in tooling. Same raw-value shape as default. Maps to schema.example (or parameter.example for SimpleSchema). This is the singular, schema-scoped keyword; for the plural response-scoped examples (a map keyed by mime type) see Parameters & responses.

enum

A closed set of allowed values. Accepted forms: comma list (enum: red, green), bracketed comma list (enum: [red, green]), JSON array (enum: ["red","green"]), or a multi-line - list. Each element is coerced against the resolved type; maps to schema.enum.

For string enums driven by Go consts the swagger:enum annotation is more idiomatic — it picks up the constant names + godoc and produces x-go-enum-desc. The enum: keyword is the manual override. (Set SkipEnumDescriptions: true to keep the const→value mapping on x-go-enum-desc only, out of the description.)

Do not confuse the two: the annotation collects members from a Go const block and types them from the declared Go type; the keyword takes the members you write and types them from the schema it sits on. Side-by-side comparison in the enumerations tutorial.

required

Marks a field as required. Boolean.

  • On a swagger:model field: adds the field name to the schema’s required array.
  • On a swagger:parameters field: sets parameter.required.
  • On a swagger:response header: not applicable β€” silently dropped.

readOnly

Marks a schema property read-only. Aliases read only, read-only. Maps to schema.readOnly. Schema-only β€” inside a SimpleSchema context it drops with CodeUnsupportedInSimpleSchema.

discriminator

Marks the property as the discriminator for an allOf polymorphic schema. Boolean; writes the property name onto the schema’s discriminator. Schema-only. The property should also be required. Subtypes that allOf-embed the base inherit it; each subtype’s discriminator value is its definition name. See Polymorphic models.

deprecated

Marks the carrying entity deprecated. Boolean. On operations/routes it writes the native OAS 2.0 deprecated; OAS 2.0 has no Schema-object deprecated, so on a model or field it emits x-deprecated: true. A godoc Deprecated: paragraph is an exact synonym recognised in any context β€” and is idiomatic on Go doc comments. Because it carries intent, x-deprecated survives even under SkipExtensions.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Routes & operations

These keywords live inside a swagger:route or swagger:operation block. They carry the operation’s transport metadata β€” the URL schemes, the media types it consumes and produces β€” and the two sub-language bodies that declare its parameters and responses. Several of them double as document-wide defaults under swagger:meta (Spec metadata), where an operation-level value overrides the default.

Summary

KeywordAliasesShapeContexts
schemesβ€”flex-listmeta, route, operation
consumesβ€”flex-listmeta, route, operation
producesβ€”flex-listmeta, route, operation
responsesβ€”sub-language (<code>: <tokens>)route, operation
parametersβ€”sub-language (+ name: chunks)route, operation
tagsβ€”string list / tag objectsmeta, route, operation
deprecatedβ€”booleanoperation, route, schema
securityβ€”YAML sequence (raw-block)meta, route, operation
externalDocsexternal docs, external-docs{description, url}meta, route, operation, schema
extensionsβ€”x-* YAML maproute, operation (cross-cutting)

The visiting rows are documented where they primarily apply: deprecated is detailed under Schema validations & decorators; security (and its securityDefinitions catalogue) under Security; externalDocs and extensions under Spec metadata.

Worked examples

A swagger:route block β€” the path-line annotation plus its transport metadata and a Responses: body β€” side by side with the path item it produces:

Annotated Go
// swagger:route GET /pets pets listPets
//
// Lists pets in the store, optionally filtered by tag.
//
// responses:
//
//	200: petsResponse
//	default: errorResponse

Full source: docs/examples/concepts/routes/routes.go

Generated spec
{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Lists pets in the store, optionally filtered by tag.",
    "operationId": "listPets",
    "parameters": [
      {
        "type": "string",
        "x-go-name": "Tag",
        "description": "Tag filters pets by tag.",
        "name": "tag",
        "in": "query"
      },
      {
        "maximum": 100,
        "minimum": 1,
        "type": "integer",
        "format": "int32",
        "x-go-name": "Limit",
        "description": "Limit caps the number of results.",
        "name": "limit",
        "in": "query"
      }
    ],
    "responses": {
      "200": {
        "$ref": "#/responses/petsResponse"
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/route.json

The swagger:operation long form carries the same metadata in a YAML body, including an inline parameters sequence:

Annotated Go
// swagger:operation GET /pets/{id} pets getPet
//
// ---
// summary: Get a pet by ID.
// parameters:
//   - name: id
//     in: path
//     required: true
//     type: integer
//     format: int64
// responses:
//   '200':
//     description: the requested pet
//     schema:
//       $ref: '#/definitions/Pet'
//   default:
//     $ref: '#/responses/errorResponse'

Full source: docs/examples/concepts/routes/routes.go

Generated spec
{
  "get": {
    "tags": [
      "pets"
    ],
    "summary": "Get a pet by ID.",
    "operationId": "getPet",
    "parameters": [
      {
        "type": "integer",
        "format": "int64",
        "name": "id",
        "in": "path",
        "required": true
      }
    ],
    "responses": {
      "200": {
        "description": "the requested pet",
        "schema": {
          "$ref": "#/definitions/Pet"
        }
      },
      "default": {
        "$ref": "#/responses/errorResponse"
      }
    }
  }
}

Full source: docs/examples/concepts/routes/testdata/operation.json

Transport metadata

schemes

Accepted URL schemes for the operation. Flexible list β€” comma inline, multi-line bare, YAML - markers, or any combination all produce the same output (Schemes: http, https ≑ a - http / - https block). See sub-languages Β§flex-lists for the unified rule.

Maps to schemes on the enclosing operation. It is also a document default under swagger:meta (spec.schemes), where an operation-level value overrides the meta-level one β€” see Spec metadata.

consumes / produces

Media-type lists β€” the request body MIME types the operation consumes and the response MIME types it produces. Same flex-list rule as schemes: comma inline, multi-line bare, YAML - markers, or any combination.

Consumes:
  - application/json
  - application/xml

Produces: application/json

Map to consumes / produces on the surrounding scope. Like schemes, both are also swagger:meta document defaults overridden per operation.

Body sub-languages

responses

Per-route / per-operation response declarations. The body is one response per line in the form <code>: <tokens>, where <code> is an HTTP status (or default) and <tokens> names the body schema and/or description:

Responses:
  200: body:User the requested user
  404: description: not found
  default: response:genericError

The full per-line grammar lives at sub-languages Β§responses.

parameters

Per-route / per-operation parameter declarations. The body is a sequence of + name: chunks β€” the + is the chunk-start sigil (- is accepted as an alias) β€” each chunk a small key/value block describing one parameter:

Parameters:
  + name: id
    in: path
    type: integer
    required: true
  + name: limit
    in: query
    type: integer
    default: 20
    minimum: 1
    maximum: 100

The full per-chunk grammar lives at sub-languages Β§parameters.

tags

Tag declarations whose shape depends on context:

  • In swagger:route / swagger:operation the body is a plain string list. It is unioned and deduplicated with the tags written on the annotation’s header line, and the result lands on the operation’s tags:

    Tags:
      - pets
      - store
  • In swagger:meta the body is instead a YAML sequence of tag objects emitted into the spec’s top-level tags β€” each with a name, an optional description, a nested externalDocs, and any x-* vendor extensions:

    Tags:
    - name: pets
      description: Everything about your Pets
      externalDocs:
        description: Find out more
        url: https://example.com/docs/pets
    - name: store
      x-display-name: Store

    The meta tag-objects form is also referenced from Spec metadata.

Visiting keywords

These keywords also appear in a route/operation block but are detailed on their home page:

  • deprecated β€” marks the operation deprecated (native OAS 2.0 deprecated). See Schema validations & decorators.
  • security β€” the per-route / per-operation requirement list (an empty Security: [] on an operation is an explicit public opt-out). See Security.
  • externalDocs β€” the operation’s external-documentation pointer. See Spec metadata.
  • extensions β€” vendor x-* entries on the operation. See Spec metadata.
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Security

Two keywords carry authentication into the spec. security lists the requirements that gate the document, a route, or a single operation; securityDefinitions is the scheme catalogue β€” declared once in swagger:meta and referenced by name from every requirement. A requirement is only meaningful when the scheme it names is defined, so the two are almost always authored together (see Spec metadata for the rest of the swagger:meta surface and Routes & operations for where per-route requirements live).

Summary

KeywordAliasesShapeContexts
securityβ€”YAML sequence (raw-block)meta, route, operation
securityDefinitionssecurity definitions, security-definitionsYAML map (raw-block)meta

Worked example

The scheme catalogue and the document-wide default requirement, declared once in the package swagger:meta block β€” the schemes golden captures both securityDefinitions and the top-level security:

Annotated Go
// Package security Reports API.
//
// The swagger:meta block declares the security schemes once and sets the
// document-wide default requirement.
//
//	Version: 1.0.0
//
//	SecurityDefinitions:
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//	  oauth2:
//	    type: oauth2
//	    flow: accessCode
//	    authorizationUrl: https://example.com/auth
//	    tokenUrl: https://example.com/token
//	    scopes:
//	      read: read reports
//	      write: write reports
//
//	Security:
//	  - api_key: []
//
// swagger:meta
package security

Full source: docs/examples/concepts/security/doc.go

Generated spec
{
  "security": [
    {
      "api_key": []
    }
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "oauth2": {
      "type": "oauth2",
      "flow": "accessCode",
      "authorizationUrl": "https://example.com/auth",
      "tokenUrl": "https://example.com/token",
      "scopes": {
        "read": "read reports",
        "write": "write reports"
      }
    }
  }
}

Full source: docs/examples/concepts/security/testdata/schemes.json

A route then overrides that default with its own Security: requirement β€” here oauth2 with the read and write scopes:

Annotated Go
// listReports inherits the document-wide default requirement (api_key) β€” no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement β€”
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once β€” two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely β€” an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

Generated spec
[
  {
    "oauth2": [
      "read",
      "write"
    ]
  }
]

Full source: docs/examples/concepts/security/testdata/route.json

Keyword details

security

A YAML sequence of requirement objects parsed from the Security: body. The semantics are OAS 2.0:

  • multiple keys within one sequence item are ANDed β€” all of those schemes are required together ({api_key, oauth2} in one item);
  • separate items are ORed β€” satisfying any one item grants access;
  • a scheme’s value is its scope list, a flow ([read, write]) or block list. For non-scoped schemes (apiKey, basic) the list is empty (api_key: []), meaning the scheme is required with no scopes;
  • an empty top-level Security: [] on an operation emits an explicit empty requirement β€” an intentional public opt-out that overrides the document-wide default rather than inheriting it.

A bare top-level mapping (api_key: / oauth2: read, write, comma-split scopes) is still read as one OR requirement per key for back-compatibility. Maps to security on the enclosing object. Legal in swagger:meta (the document default), swagger:route, and swagger:operation. The full per-line body grammar lives at sub-languages Β§security requirements.

securityDefinitions

A YAML map, parsed directly into the spec.securityDefinitions shape β€” each entry is a named scheme (apiKey, oauth2, basic) with its OAS 2.0 fields (type, in, name, flow, authorizationUrl, tokenUrl, scopes, …); see OAS v2 Β§securityDefinitionsObject. Aliases security definitions, security-definitions. Meta-only β€” the scheme catalogue is declared once at the top of the document and referenced by name from every security requirement. Its detail anchor is #securitydefinitions.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Spec metadata

These keywords author the spec’s top-level fields. Most live in the package doc comment carrying the swagger:meta block; a couple are cross-cutting and merely have their home here β€” extensions lands on whatever scope it decorates, and externalDocs rides meta, operations, schemas, and struct fields alike. The remaining document-level concerns (schemes/consumes/produces, security/ securityDefinitions, the meta tags form) are owned by sibling pages and only visit this one.

Summary

KeywordAliasesShapeHome
versionβ€”stringhere
hostβ€”stringhere
basePathbase path, base-pathstringhere
licenseβ€”Name [URL]here
contactcontact info, contact-infoName <email> [URL]here
tosterms of service, terms-of-service, termsOfServiceprosehere
infoExtensionsinfo extensions, info-extensionsx-* YAML maphere
extensionsβ€”x-* YAML maphere (cross-cutting)
externalDocsexternal docs, external-docs{description, url}here (cross-cutting)
schemesβ€”flex-list/codescan/maintainers/keywords/routes-and-operations/
consumes / producesβ€”flex-list/codescan/maintainers/keywords/routes-and-operations/
securityβ€”YAML/codescan/maintainers/keywords/security/
securityDefinitionssecurity definitions, security-definitionsYAML map/codescan/maintainers/keywords/security/
tagsβ€”YAML sequence/codescan/maintainers/keywords/routes-and-operations/#tags

The visiting rows are documented where they primarily apply: schemes, consumes, and produces are document-wide defaults overridden per operation (Routes & operations); security and securityDefinitions are detailed under Security; the swagger:meta tag-objects form of tags is described under Routes & operations.

Worked example

A complete swagger:meta block, side by side with the document-level spec it produces:

Annotated Go
// Package meta Pet Store.
//
// A small API that demonstrates the document-level swagger:meta block: the
// package doc comment carries the spec's top-level metadata.
//
//	Schemes: https
//	Host: api.example.com
//	BasePath: /v1
//	Version: 1.2.0
//	License: Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0.html
//	Contact: API Team <api@example.com> https://example.com/support
//
//	Consumes:
//	  - application/json
//
//	Produces:
//	  - application/json
//
//	ExternalDocs:
//	  description: Full API guide
//	  url: https://example.com/docs
//
//	Tags:
//	- name: pets
//	  description: Everything about your Pets
//	  externalDocs:
//	    description: Find out more
//	    url: https://example.com/docs/pets
//	- name: store
//	  description: Access to Petstore orders
//	  x-display-name: Store
//
//	SecurityDefinitions:
//	  basic_auth:
//	    type: basic
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//
//	Security:
//	  basic_auth:
//
//	InfoExtensions:
//	  x-logo:
//	    url: https://example.com/logo.png
//	    altText: Example
//
// swagger:meta
package meta

Full source: docs/examples/concepts/meta/doc.go

Generated spec
{
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "schemes": [
    "https"
  ],
  "swagger": "2.0",
  "info": {
    "description": "A small API that demonstrates the document-level swagger:meta block: the\npackage doc comment carries the spec's top-level metadata.",
    "title": "Pet Store.",
    "contact": {
      "name": "API Team",
      "url": "https://example.com/support",
      "email": "api@example.com"
    },
    "license": {
      "name": "Apache 2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    },
    "version": "1.2.0",
    "x-logo": {
      "altText": "Example",
      "url": "https://example.com/logo.png"
    }
  },
  "host": "api.example.com",
  "basePath": "/v1",
  "paths": {},
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "basic_auth": {
      "type": "basic"
    }
  },
  "security": [
    {
      "basic_auth": []
    }
  ],
  "tags": [
    {
      "description": "Everything about your Pets",
      "name": "pets",
      "externalDocs": {
        "description": "Find out more",
        "url": "https://example.com/docs/pets"
      }
    },
    {
      "description": "Access to Petstore orders",
      "name": "store",
      "x-display-name": "Store"
    }
  ],
  "externalDocs": {
    "description": "Full API guide",
    "url": "https://example.com/docs"
  }
}

Full source: docs/examples/concepts/meta/testdata/meta.json

Meta single-line keywords

Single-line keywords under swagger:meta. The value is taken as-is from the post-colon string.

version

API version string. Maps to info.version.

host

Default host for the API. Defaults to localhost when empty. Maps to spec.host.

basePath

URL base path applied to every route. Maps to spec.basePath. Aliases: base path, base-path.

license

License declaration, in two accepted forms:

License: Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0.html

The trailing token starting with a URL scheme becomes license.url; the prefix becomes license.name. A bare name with no URL is accepted too. Maps to info.license.

contact

Contact declaration. The author writes a Name <email> URL triple, in any order; the grammar recognises:

  • Name <email@example.com> β€” Go’s net/mail.ParseAddress form;
  • Name <email@example.com> https://example.com β€” same, plus a trailing URL;
  • just a URL, with no name.

Aliases: contact info, contact-info. Maps to info.contact.

Document-level keywords

tos

Terms-of-service prose paragraph. The multi-line body is joined with \n after dropping whitespace-only lines. Aliases: terms of service, terms-of-service, termsOfService. Maps to info.termsOfService. Meta-only.

infoExtensions

Vendor-extension declarations as a YAML map, landed on info.extensions. Keys must start with x- or X-; a non-x-* key emits CodeInvalidAnnotation and drops. Meta-only. Aliases: info extensions, info-extensions.

InfoExtensions:
  x-logo:
    url: https://example.com/logo.png
    altText: Example

For the same map on the surrounding scope rather than info, use extensions.

extensions

Vendor-extension declarations as a YAML map, landed on the surrounding scope rather than on info: spec.extensions, operation.extensions, schema.extensions, parameter.extensions, header.extensions, and so on β€” including on parameters and response headers. Keys must start with x- or X-; a non-x-* key emits CodeInvalidAnnotation and drops.

Extensions:
  x-internal-id: 42
  x-feature-flags:
    - alpha
    - beta
  x-nested:
    enabled: true
    rate: 0.5

This keyword is cross-cutting β€” it is documented here as its home, but applies wherever a YAML body is parsed. For the meta-only info.extensions variant see infoExtensions.

externalDocs

External-documentation pointer as a YAML map with description and url keys. Aliases: external docs, external-docs.

Emitted on:

  • swagger:meta β†’ the top-level externalDocs object (and, nested under a Tags: entry, that tag’s externalDocs);
  • swagger:route / swagger:operation β†’ the operation’s externalDocs;
  • swagger:model (and any full Schema, e.g. a body parameter’s schema) β†’ the schema’s externalDocs;
  • a struct field β†’ the property’s externalDocs. On a $ref’d field (whose property is a bare $ref) it is lifted onto the wrapping allOf compound, alongside the field’s description and x-* siblings.

An empty block (no description/url) is skipped rather than emitting a bare externalDocs: {}. It is a full-Schema-only keyword: on a SimpleSchema site (a non-body parameter, response header, or items chain) it drops with a CodeUnsupportedInSimpleSchema diagnostic.

ExternalDocs:
  description: Reference documentation
  url: https://example.com/docs

Like extensions, this keyword is cross-cutting; it is documented here as its home.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Appendix: shapes & contexts

The two reference tables behind the keyword class pages: the value shapes the lexer assigns to a keyword’s value, and the context tokens used in each page’s scoped summary table.

Value shapes

The grammar’s lexer classifies every value into one of these shapes. The shape determines which Walker callback fires for the property and which field of Property.Typed carries the parsed value.

ShapeTyped payloadExample value forms
numberfloat64 (with optional </<=/>/>=/= prefix)5, 1.5, <10, >=0, =42
integerint645, 100
booleanbooltrue, false, 1, 0
stringraw string^[a-z]+$, date-time, multipart/form-data
comma-listraw string; split on , by Property.AsList()http, https, a,b,c
enum-optiontyped string (closed-vocab match)csv, pipes for collectionFormat:
raw-blockaccumulated body lines on Property.Bodymulti-line YAML, indented token lists
raw-valuethe verbatim post-colon text on Property.Value42, "orange", [1, 2, 3]

When typing fails (e.g. maximum: notanumber) the lexer emits a CodeInvalidNumber / CodeInvalidInteger / CodeInvalidBoolean diagnostic and the property reaches the Walker with a zero-value payload. Consumers gate on Property.IsTyped() to skip malformed-typed values; the corresponding builder field stays unwritten.

Annotation contexts

The closed set of contexts a keyword can legally appear in. Each class page’s scoped summary table combines these in its Contexts column.

ContextMeaning
paramParameter doc on a swagger:parameters struct field, or a + name: chunk inside swagger:route Parameters:
headerHeader field on a swagger:response struct
schemaTop-level model or struct field on a swagger:model
itemsItems-level (array element) validation on either parameter or schema
routeRoute-level metadata under swagger:route
operationInline operation metadata under swagger:operation
metaPackage-level metadata under swagger:meta
responseResponse-level decorations

Using a keyword outside its legal contexts emits a CodeContextInvalid diagnostic and the keyword is dropped from the affected block. The Context matrix maps these tokens onto the annotation families.

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Sub-languages

The annotation body grammar is not a single language β€” it’s a top-level keyword grammar that embeds several smaller languages inside specific body keywords. Each embedded language has its own shape rules.

This document catalogs the embedded languages and how they fit together. For the per-keyword surface, see keywords.md; for the formal grammar that hosts them, see grammar.md.


Table of contents


Prose classification

Comment lines that don’t match any keyword head OR YAML fence OR annotation marker are classified as prose β€” free-form text. The lexer splits prose into two token kinds:

  • TITLE β€” the first paragraph of prose, expected to fit on a short summary line.
  • DESC β€” every prose paragraph after the title (or following a blank line within the first paragraph).

Three heuristics decide the title-vs-desc boundary, evaluated in order. The first to fire wins:

  1. Blank-line split. Any blank line inside the prose run ends the title paragraph and starts the description.
  2. Closing punctuation. If the first prose line ends with Unicode punctuation (., ?, !, …, :, …), the title is just that one line; everything after becomes description.
  3. Markdown ATX heading. If the first prose line matches markdown’s # Heading shape, the # markers are stripped and the remaining text becomes the title.

When no heuristic fires, the entire prose run is title (the schema builder later collapses to a description-only schema when appropriate).

Package <name> prefix strip

The swagger:meta annotation’s title comes from the package doc comment, which by Go convention starts with Package <name>. The spec builder strips that prefix before publishing:

// Package petstore Petstore API.
//
// Description of the petstore service.
//
// swagger:meta
package petstore

Produces info.title = "Petstore API." (the Package petstore prefix stripped) and info.description = "Description of the petstore service."

Only the capital-P Package form is recognised β€” author prose like “package this carefully” is not chopped.

Comment-marker noise stripping

Block-comment routes (/* swagger:route … */) typically carry indented continuation lines:

/* swagger:route POST /pets pets createPet

	Create a pet based on the parameters.

	Consumes:
		- application/json
*/
func CreatePet() {}

The lexer strips the leading whitespace (\t, *, /, |) per line via trimContentPrefix before classification.

Tool-directive markers are dropped

Two families of non-swagger directive lines are filtered out of the prose surface, so they never leak into a title or description:

  • Go directives β€” //go:generate, //nolint:foo, //lint:ignore (a lowercase word + : + an immediate argument, no leading space).
  • Kubernetes-style +marker comments β€” any line whose content begins with + immediately followed by a letter: +kubebuilder:…, +genclient, +k8s:…, +optional, as emitted by kubebuilder / controller-gen. Requiring a letter after the + keeps ordinary prose (+1 for …) intact. This is also why a stray +kubebuilder:default:=false no longer crashes the scan (go-swagger#3007) β€” the marker is dropped, not parsed as a keyword.

The +marker filter runs at the prose-classification stage (after annotation bodies are folded), so the swagger:route Parameters: + name: chunk separator β€” a + followed by a space β€” is unaffected.

Markdown semantics that survive

  • Bullet lists in descriptions are preserved. A line starting with - foo lands in the description as "- foo" (not "foo"). A markdown-style * foo or + foo bullet is recognised the same way and normalised to - foo (the same rewrite gofmt applies), so the two forms agree.
  • --- lines open a YAML fence β€” see YAML extensions below.

Flex-list

Body keywords that publish a flat list of tokens (schemes:, consumes:, produces:) accept multiple surface forms uniformly. The unified reader is Property.AsList().

Accepted forms

# Inline, comma-separated
Schemes: http, https

# Multi-line, indented bare lines
Schemes:
  http
  https

# Multi-line, YAML-style dash markers
Schemes:
  - http
  - https

# Inline value plus indented continuation
Schemes: http
  - https

# All combinations of the above
Consumes: application/json, application/xml
  - application/protobuf

All five forms produce the same ["http", "https"] (or ["application/json", "application/xml", "application/protobuf"]) output. The - marker may also be written as a markdown * or + bullet β€” all are normalised to the same list.

Algorithm

For each input line β€” Property.Value first (if non-empty), then each line of Property.Body:

  1. Trim surrounding whitespace.
  2. Drop a leading - YAML marker if present.
  3. Re-trim whitespace.
  4. Comma-split.
  5. Trim each token; drop empties.

Aggregate into a single slice in source order.

What flex-list does NOT touch

  • Enum values (enum: ...) β€” their elements may themselves be complex (JSON arrays, quoted strings with commas). enum: keeps its raw-value path; the value coercion layer handles array / comma-list / multi-line shapes per the schema type.
  • Parameters chunks β€” the + name: chunk grammar is not a simple token list; see Β§parameters.
  • YAML structural bodies β€” securityDefinitions:, extensions:, infoExtensions: parse the body as YAML directly; their structure isn’t a flat list. See Β§yaml-extensions.

Parameters

The Parameters: body in swagger:route and swagger:operation carries a sequence of parameter declarations separated by + name: chunks (the + is the chunk-start sigil; - is accepted as an alias for forward compatibility with proper YAML).

Chunk shape

Parameters:
  + name: id
    in: path
    type: integer
    description: the item identifier
    required: true
  + name: limit
    in: query
    type: integer
    minimum: 1
    maximum: 100
    default: 20
  + name: body
    in: body
    type: User
    required: true

Per-chunk fields

The fields are classified into head fields (consumed by the orchestrator to populate the *spec.Parameter shell) and validation fields (lowered to grammar properties and dispatched through the standard validation pipeline).

Head fields:

FieldLands onNotes
name:parameter.nameRequired. Identifies the parameter.
in:parameter.inOne of path / query / header / body / formData. form accepted as an alias for formData.
type:parameter.type (for SimpleSchema) or determines the body $refFor non-body: one of string / integer / number / boolean / array. For body: a Go ident referring to a swagger:model-declared type, optionally with [] array prefixes ([][]Pet). bool accepted as an alias for boolean.
format:parameter.format or parameter.schema.formatFree-form string. Applied after validation dispatch so it doesn’t interfere with default/example coercion.
description:parameter.descriptionFree-form prose.
required:parameter.requiredBoolean.
allowempty: / allowemptyvalue:parameter.allowEmptyValueBoolean.

Validation fields: any other recognised keyword β€” min, max, minLength, maxLength, minItems, maxItems, pattern, unique, collectionFormat, default, example, enum. These are looked up via grammar.Lookup (which accepts canonical names + aliases) and dispatched through the standard handlers seam.

Empty chunks and unknown keys

  • A bare + (or -) sigil with no follow-up content emits a CodeInvalidAnnotation diagnostic and is dropped. The legacy parser silently emitted an empty Parameter{} object β€” current behaviour rejects it.
  • Unknown keys (typos like defualt:) emit CodeInvalidAnnotation and drop. The legacy parser silently discarded them.

Body parameters

When in: body, the orchestrator looks up type: as either:

  • A primitive (string, integer, number, boolean, array, object) β€” emits a typed schema with the primitive on parameter.schema.type.
  • A Go ident β€” emits a $ref to #/definitions/<Ident>. With [] prefixes, wraps the ref in nested array schemas.

Validation properties on a body chunk apply to the schema, gated by the schema’s resolved type via checkShape. A min: 0 on a body chunk with type: Pet (object) emits CodeShapeMismatch and drops; a min: 0 with type: integer lands on the schema’s minimum.

Validation on SimpleSchema (non-body) parameters

For in: other than body, validation properties apply directly to the parameter (not to a sub-schema). Type-gating still applies: minLength on type: integer emits a diagnostic and drops.


Responses

The Responses: body in swagger:route carries one response declaration per line. Each line has the shape:

<code>: <token>*

where <code> is default (case-insensitive) or a decimal HTTP status code, and <token> is either a tag:value form or an untagged token.

Recognised tags

TagValue shapeLands on
body:A scalar primitive (string / number / integer / boolean) OR a Go ident, each with optional [] prefixes (body:[]string, body:[]Pet)A primitive emits a typed schema; a Go ident emits a $ref to #/definitions/<name> β€” array-wrapped per [] count. The reserved keywords array / object / file / null are rejected with a diagnostic (use []T or a model name)
response:Go ident referring to a swagger:response-declared typeA $ref to #/responses/<name>
description:Free-form prose (rest of line)response.description

Untagged token rules

  • The first untagged token defaults to a response ref. The orchestrator resolves it against the operation’s responses map first, then falls back to definitions β€” if found in definitions (not responses), it’s silently promoted to a body ref. An untagged token is always read as a NAME, never a type: a bare 200: string is a (dangling) response ref, not a primitive body β€” use the unambiguous body:string form for a primitive body.
  • Subsequent untagged tokens accumulate into the description.

This block is a line-based sub-language, not YAML: each entry is a single <code>: <token>* line. A description must sit on that same line β€” either via the description: tag (403: description: Unauthorized) or as trailing untagged tokens (200: listResponse all the users). A nested description: written on an indented continuation line under a bare 403: is not parsed and yields an empty description; for that style, spell the operation out with a swagger:operation YAML body instead.

Examples

Responses:
  200: User the user as returned                  # untagged β†’ response="User", desc="the user as returned"
  200: body:string the version                    # primitive body (use the body: tag) + description
  200: body:[]integer the id list                 # array-of-primitive body
  200: body:User the user                         # body ref + description
  200: response:userResponse the user             # named response ref
  201: body:Pet the created pet
  404: description: not found
  default: response:genericError
  default: body:[]ErrorList the error list        # array-wrapped body ref

Diagnostics

  • Unknown tag (200: weird:value) β€” emits CodeInvalidAnnotation and drops the line.
  • Duplicate body/response tags on one line (200: body:Pet response:errors) β€” emits CodeInvalidAnnotation; the line drops.
  • Space-separated body Foo (instead of body:Foo) β€” detected as a likely typo and dropped with diagnostic. The legacy parser silently treated it as response="body" (a dangling ref to a non-existent response).
  • Unresolvable response ref β€” when a response name appears in neither responses nor definitions, the line drops with diagnostic. The legacy parser emitted a dangling $ref. When the unresolved name is a primitive type spelling (200: string), the diagnostic points the author at the body: form (200: body:string).
  • Reserved body: type (200: body:object, body:file, body:array, body:null) β€” these look like a type but are not valid response body types; the line drops with a diagnostic suggesting a scalar primitive, []T, or a model name.

Empty value lines

A line like 204: with nothing after the colon produces a Response with the code and an empty description. This is intentional β€” some authors want a 204 No Content with no body and no description.


YAML extensions

Several body keywords parse their body as YAML directly:

  • extensions: and infoExtensions: β€” a YAML map of x-* entries.
  • securityDefinitions: β€” a YAML map matching OAS v2’s securityDefinitions shape.
  • externalDocs: β€” a YAML map with description and url keys.

Extension typing

Extension values are NOT coerced to strings β€” they preserve their YAML-typed form: bool, float64, string, []any, or map[string]any for nested structures.

Extensions:
  x-feature-flags:
    - alpha
    - beta
  x-rate-limit:
    requests: 100
    window: 60
  x-internal: true
  x-version: 0.5

Produces (extract):

"x-feature-flags": ["alpha", "beta"],
"x-rate-limit": {"requests": 100, "window": 60},
"x-internal": true,
"x-version": 0.5

x-* name gating

Keys that don’t start with x- or X- emit a CodeInvalidAnnotation diagnostic and drop. The build still succeeds. Authors who relied on the legacy “hard error on non-x-*” behaviour see a diagnostic + a clean spec missing the typo’d key.

Extensions:
  x-good: 1
  not-good: 2   # β†’ diagnostic, dropped

YAML body delimitation

The YAML extension bodies use indentation to delimit. A line that returns to the indentation level of the keyword head β€” or introduces a sibling keyword β€” terminates the body. The grammar also recognises --- fence pairs around the body (matching the swagger:operation YAML shape) and absorbs them silently.


Security requirements

The security: body (in swagger:meta, swagger:route, and swagger:operation) carries OAuth-style security requirements where each line is one requirement.

Shape

Each line: schemeName: scope1, scope2, …

  • schemeName matches a scheme declared in securityDefinitions.
  • Scope list is comma-separated; trimmed; empties dropped.
  • An empty scope list (schemeName:) means “this scheme is required, no scopes.” Common for apiKey and basic.

Example

Security:
  api_key:
  oauth2: read, write
  oauth2: admin

Produces:

"security": [
  {"api_key": []},
  {"oauth2": ["read", "write"]},
  {"oauth2": ["admin"]}
]

Each requirement is a single-key map; the array is an OR relationship (the request satisfies security if it matches ANY entry).


Contact / License

Inline single-line meta keywords with structured value parsing.

Contact

The contact: value carries up to three components: name, email, URL. Recognised forms:

Contact: Name <email@example.com> https://example.com
Contact: Name <email@example.com>
Contact: https://example.com
Contact: <email@example.com>

The grammar splits the value on the first URL prefix it finds (https://, http://, ftps://, ftp://, wss://, ws://), then parses the prefix portion as Name <email> via Go’s net/mail.ParseAddress.

  • A malformed Name <email> head (e.g., unbalanced angle brackets) surfaces as an error from Block.Contact(); the meta builder propagates it as a build failure.
  • An empty contact line produces an empty Contact value (no error, no diagnostic β€” equivalent to omitting the keyword).

Aliases: contact info, contact-info.

License

The license: value is split similarly:

License: Apache 2.0 https://www.apache.org/licenses/LICENSE-2.0
License: MIT
License: https://opensource.org/licenses/Custom

Same URL-prefix detection. Everything before the URL is the license name; the URL (when present) is the license URL. Either part may be empty.

License does NOT use mail.ParseAddress β€” the name is taken as raw text up to the URL boundary.


Sub-language interactions

Two interaction points worth flagging:

  • Block-comment continuation lines and the parameters/responses sub-languages. A /* swagger:route … */ block with Parameters: inside requires the chunk-start sigils (+ / - ) to be at the start of the trimmed line. Block-comment continuation noise (\t, *) is stripped first; if your editor inserts a * continuation marker, the lexer handles it transparently.
  • Flex-list and description: on a parameter chunk. description: is a head field, not a list β€” it does NOT comma-split. Authors who write description: foo, bar get a single description "foo, bar", not two descriptions. (This was a real ambiguity in older versions of go-swagger; the current grammar resolves it cleanly.)
Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Grammar

The formal grammar of the codescan annotation surface. This document specifies the language a Go comment must conform to so that the scanner classifies it, dispatches it to the right builder, and populates the OpenAPI spec deterministically.

Audience. Implementers β€” anyone porting, extending, or debugging the parser. Annotation authors typically need annotations.md and keywords.md instead.

The grammar is layered:

  1. Preprocess β€” comment-marker stripping (see Β§preprocess).
  2. Lex β€” terminal token emission, including multi-line body accumulation (see Β§lexer).
  3. Parse β€” block construction, family dispatch, keyword classification (see Β§parser).
  4. Walk β€” typed dispatch through grammar.Walker callbacks to the builders (see Β§walker).

The productions below operate on the lexer’s terminal alphabet, not raw text. Per-terminal lexical detail (how the lexer recognises a number, a string, an annotation, …) is described in Β§lexer; the EBNF that follows consumes pre-classified terminals.

The grammar is rigorous ISO-14977 EBNF. Required vs. optional arguments, value typing, and family membership are grammar-visible β€” every legality constraint expressible by token sequencing is expressed that way.


Table of contents


Preprocess

Input is a *ast.CommentGroup from go/parser. Each *ast.Comment in the group is one source-level comment node (either // … or /* … */). The preprocessor produces a flat sequence of Line structs, each one source line with:

  • Line.Text β€” content after comment-marker stripping and leading content-prefix trim.
  • Line.Raw β€” content after comment-marker stripping only (preserves leading whitespace).
  • Line.Pos β€” token.Position of the first content byte.

Stripping rules:

  • For // comments: drop the // marker. Line.Text runs trimContentPrefix (strips leading \t*/|); Line.Raw keeps the post-marker spacing.
  • For /* */ block comments: split body on newlines.
    • First line: drop the /* marker.
    • Continuation lines: run stripBlockContinuation (strips leading whitespace + optional * continuation marker + one following space), then trimContentPrefix.
    • Last line: drop the trailing */.

trimContentPrefix strips \t*/ and a single trailing | from the line head. It does NOT strip - (so YAML list markers and markdown dash items survive intact).

For synthetic per-line comments produced by upstream tooling (notably parsers.ParseRoutePathAnnotation), a // prefix is prepended before stripping so the // branch fires and the leading whitespace gets shed correctly.


Lexer

The lexer turns a []Line into a []Token ending in TokenEOF. Pipeline:

  1. Line classifier β€” emit one preliminary token per line (annotation / keyword / fence / blank / text).
  2. Body accumulator β€” fold multi-line bodies (OPAQUE_YAML, RAW_BLOCK_, RAW_VALUE_) into single body tokens.
  3. Prose classifier β€” re-type surviving text tokens as TokenTitle / TokenDesc.

Terminal vocabulary

Annotation name terminals (TokenAnnotation)

Each recognises an annotation name only β€” positional arguments are emitted as separate terminals.

TerminalAnnotation
ANN_MODELswagger:model
ANN_RESPONSEswagger:response
ANN_PARAMETERSswagger:parameters
ANN_ROUTEswagger:route
ANN_OPERATIONswagger:operation
ANN_METAswagger:meta
ANN_STRFMTswagger:strfmt
ANN_ALIASswagger:alias
ANN_NAMEswagger:name
ANN_ALLOFswagger:allOf
ANN_ENUMswagger:enum
ANN_IGNOREswagger:ignore
ANN_DEFAULTswagger:default
ANN_TYPEswagger:type
ANN_ADDITIONAL_PROPERTIESswagger:additionalProperties
ANN_PATTERN_PROPERTIESswagger:patternProperties
ANN_FILEswagger:file
ANN_TITLEswagger:title
ANN_DESCRIPTIONswagger:description

Argument terminals

TerminalRecognises
IDENT_NAMEIdentifier-shaped token. Used for every named arg and reference.
JSON_VALUERFC-8259 JSON literal (string / number / boolean / null / array / object).
RAW_VALUEVerbatim non-LF text β€” fallback when JSON_VALUE recognition fails.
TYPE_REFClosed vocab: string / integer / number / boolean / array / object / file / null.
HTTP_METHODGET / POST / PUT / PATCH / HEAD / DELETE / OPTIONS / TRACE (case-insensitive).
URL_PATHRFC-3986 URL path token (used as the second positional arg of OperationArgs).

Keyword head terminals (TokenKeyword)

Each recognises the keyword name only. See keywords.md for the complete keyword surface.

Inline value terminals

The lexer types values per their lexical shape; semantic coercion against the Go target happens in the analyzer.

TerminalRecognises
NUMBER_VALUESigned decimal literal (integer or fractional).
INT_VALUEUnsigned decimal integer.
BOOL_VALUEtrue / false (case-insensitive).
STRING_VALUEVerbatim non-LF text.
COMMA_LIST_VALUEComma-separated list of strings, trim-stripped.
ENUM_OPTION_VALUEOne of a closed token set declared per keyword (query/path/… for in:, csv/ssv/… for collectionFormat).

When the lexer fails to type a value against its keyword’s expected shape, the property reaches the analyzer with Property.Typed.Type == ShapeNone and a CodeInvalidNumber / CodeInvalidInteger / CodeInvalidBoolean diagnostic is emitted.

Multi-line body terminals

Single tokens spanning multiple source lines. The lexer absorbs the head and the body lines.

TerminalParent keywordBody shape
RAW_BLOCK_CONSUMESconsumesFlat token list (see sub-languages Β§flex-list)
RAW_BLOCK_PRODUCESproducesFlat token list
RAW_BLOCK_SCHEMESschemesFlat token list
RAW_BLOCK_SECURITYsecuritySecurity requirements (see sub-languages Β§security-requirements)
RAW_BLOCK_SECURITY_DEFINITIONSsecurityDefinitionsYAML map
RAW_BLOCK_RESPONSESresponsesResponse sub-language (see sub-languages Β§responses)
RAW_BLOCK_PARAMETERSparametersParameter chunk sub-language (see sub-languages Β§parameters)
RAW_BLOCK_EXTENSIONSextensionsYAML map of x-* entries
RAW_BLOCK_INFO_EXTENSIONSinfoExtensionsYAML map of x-* entries
RAW_BLOCK_TOStosFree-form prose paragraph
RAW_BLOCK_EXTERNAL_DOCSexternalDocsYAML map
RAW_VALUE_DEFAULTdefaultRaw value text
RAW_VALUE_EXAMPLEexampleRaw value text
RAW_VALUE_ENUMenumComma list, JSON array, or YAML dash list

Body accumulation

A raw-block / raw-value keyword opens a body. The body terminates at the next sibling structural token in the same family β€” either another TokenAnnotation, another body-keyword head whose context makes it a sibling, or TokenEOF.

Blank lines do NOT terminate the body. They are absorbed as visual separators inside list-shaped bodies.

For raw-block heads, the inline post-colon value (when non-empty) is prepended to the body as its first line. This means Consumes: application/json (inline single value) and Consumes:\n - application/json (multi-line body) both yield the same body content; consumers don’t need to special-case the inline form.

YAML fence handling

A line whose trimmed content is exactly --- opens (or closes) a YAML fence. While the cursor sits between matching fences:

  • Annotation and keyword recognition is suspended; every line emits as tokenRawLine carrying the verbatim source text.
  • The body accumulator captures the fenced region as a single OPAQUE_YAML token attached to the surrounding annotation (typically swagger:operation or a fenced extensions body).
  • A missing closing fence emits a CodeUnterminatedFence diagnostic; the OPAQUE_YAML token is marked truncated and the builder degrades gracefully.

Prose classification

Surviving tokenText tokens (not consumed by a body, not an annotation or keyword head) re-type as either TokenTitle or TokenDesc per three heuristics evaluated in order:

  1. Blank-line split β€” a blank line inside the prose run ends the title and starts the description.
  2. Closing punctuation β€” if the first prose line ends with Unicode punctuation, the title is just that one line.
  3. Markdown ATX heading β€” if the first prose line matches markdown’s # Heading shape, the # markers are stripped and the line becomes the title.

When no heuristic fires, the entire prose run is title.

See sub-languages.md Β§prose-classification for the author-facing description.


Parser

The parser consumes the lexer’s terminal stream and produces typed Block values, one per *ast.CommentGroup. A single comment group may produce MORE than one Block when multiple annotations appear (each annotation closes the preceding Block and opens a fresh one).

Top-level dispatch

CommentBlock     = AnnotatedBlock | UnboundBlock ;

AnnotatedBlock   = SchemaBlock
                 | OperationFamilyBlock
                 | MetaBlock
                 | ClassifierBlock ;

UnboundBlock     = [ Description ] , UnboundBlockBody ;

The dispatcher reads the first ANN_* terminal; its identity selects the family. If no annotation appears, the input is an UnboundBlock β€” typically a Go struct field with description-only documentation.

Block.AnnotationKind() returns the family discriminator. Block.AnnotationArg() returns the leading IDENT argument (if any) without requiring the caller to type-assert on the typed Block kind.

Schema family

Bodies of swagger:model, swagger:parameters, swagger:response, swagger:name.

SchemaBlock          = SchemaAnnotation
                     , [ Title ]
                     , [ Description ]
                     , SchemaAnnotationBody ;

SchemaAnnotation      = ModelAnnotation
                      | ResponseAnnotation
                      | ParametersAnnotation
                      | NameAnnotation
                      | TitleAnnotation
                      | DescriptionAnnotation ;

ModelAnnotation       = ANN_MODEL ,       [ IDENT_NAME ] ;
ResponseAnnotation    = ANN_RESPONSE ,    [ IDENT_NAME ] ;
ParametersAnnotation  = ANN_PARAMETERS ,  IDENT_NAME , { IDENT_NAME } ;
NameAnnotation        = ANN_NAME ,        IDENT_NAME ;
TitleAnnotation       = ANN_TITLE ,       RAW_VALUE ;
DescriptionAnnotation = ANN_DESCRIPTION , RAW_VALUE ;

SchemaAnnotationBody = { SchemaBodyItem } ;
UnboundBlockBody     = { SchemaBodyItem } ;

SchemaBodyItem       = Validation
                     | SchemaDecorator
                     | ExtensionsBlock
                     | ExternalDocsBlock
                     | BLANK ;

Validation           = NumericValidation
                     | StringValidation
                     | ArrayValidation
                     | EnumValidation
                     | RequiredLine
                     | ReadOnlyLine ;

NumericValidation    = NumericKw , NUMBER_VALUE ;
NumericKw            = KW_MAXIMUM | KW_MINIMUM | KW_MULTIPLE_OF ;

StringValidation     = KW_PATTERN , STRING_VALUE
                     | StringLengthKw , INT_VALUE ;
StringLengthKw       = KW_MAX_LENGTH | KW_MIN_LENGTH ;

ArrayValidation      = ArrayCountKw , INT_VALUE
                     | KW_UNIQUE , BOOL_VALUE
                     | KW_COLLECTION_FORMAT , ENUM_OPTION_VALUE ;
ArrayCountKw         = KW_MAX_ITEMS | KW_MIN_ITEMS ;

EnumValidation       = RAW_VALUE_ENUM ;
RequiredLine         = KW_REQUIRED , BOOL_VALUE ;
ReadOnlyLine         = KW_READ_ONLY , BOOL_VALUE ;

SchemaDecorator      = RAW_VALUE_DEFAULT
                     | RAW_VALUE_EXAMPLE
                     | DiscriminatorLine
                     | DeprecatedLine ;

DiscriminatorLine    = KW_DISCRIMINATOR , BOOL_VALUE ;
DeprecatedLine       = KW_DEPRECATED , BOOL_VALUE ;

swagger:title / swagger:description are schema-family overrides β€” they replace the godoc-derived title / description on a model, field, response, or header. They dispatch through the schema parser (not the classifier parser), so validation keywords co-located on the same comment group still surface. The RAW_VALUE is the rest of the head line; swagger:description additionally folds a blank-terminated body (Option B) or, with a trailing |, a verbatim literal markdown block. A blank override emits CodeEmptyOverride; swagger:title is rejected with CodeContextInvalid on a non-body parameter or response header.

Operation family

swagger:route and swagger:operation are distinct block productions because their bodies differ structurally β€” swagger:route accepts the structured keyword surface; swagger:operation accepts an OPAQUE_YAML body.

OperationFamilyBlock = RouteBlock | InlineOperationBlock ;

RouteBlock           = ANN_ROUTE , OperationArgs
                     , [ Title ]
                     , [ Description ]
                     , RouteBody ;

InlineOperationBlock = ANN_OPERATION , OperationArgs
                     , [ Title ]
                     , [ Description ]
                     , InlineOperationBody ;

OperationArgs        = HTTP_METHOD , URL_PATH , { IDENT_NAME } , IDENT_NAME ;
                      (* Trailing IDENT_NAME is the OperationID;
                         the run between URL_PATH and the OpID is
                         the tag list. *)

RouteBody            = { CommonOperationBodyItem | BLANK } ;

InlineOperationBody  = { CommonOperationBodyItem
                       | OPAQUE_YAML
                       | BLANK } ;

CommonOperationBodyItem = OperationKeyword
                        | OperationDecorator
                        | OperationRawBlock
                        | ExtensionsBlock
                        | ExternalDocsBlock ;

OperationKeyword     = KW_SCHEMES , COMMA_LIST_VALUE ;

OperationDecorator   = DeprecatedLine ;

OperationRawBlock    = RAW_BLOCK_CONSUMES
                     | RAW_BLOCK_PRODUCES
                     | RAW_BLOCK_SECURITY
                     | RAW_BLOCK_RESPONSES
                     | RAW_BLOCK_PARAMETERS ;

…where both share the header arguments:

The <GoIdent> swagger:route ... godoc-prefix exception (which allows a leading Go identifier on the route annotation line) is absorbed by the lexer; the EBNF sees a plain ANN_ROUTE.

Meta family

swagger:meta defines top-of-spec metadata.

MetaBlock            = ANN_META
                     , [ Title ]
                     , [ Description ]
                     , MetaBody ;

MetaBody             = { MetaBodyItem | BLANK } ;

MetaBodyItem         = MetaKeyword
                     | MetaRawBlock
                     | ExtensionsBlock
                     | InfoExtensionsBlock
                     | ExternalDocsBlock ;

MetaKeyword          = KW_VERSION , STRING_VALUE
                     | KW_HOST , STRING_VALUE
                     | KW_BASE_PATH , STRING_VALUE
                     | KW_LICENSE , STRING_VALUE
                     | KW_CONTACT , STRING_VALUE
                     | KW_SCHEMES , COMMA_LIST_VALUE ;

MetaRawBlock         = RAW_BLOCK_CONSUMES
                     | RAW_BLOCK_PRODUCES
                     | RAW_BLOCK_SCHEMES
                     | RAW_BLOCK_SECURITY
                     | RAW_BLOCK_SECURITY_DEFINITIONS
                     | RAW_BLOCK_TOS ;

Classifier family

Single-purpose annotations that classify the surrounding declaration without carrying their own body.

ClassifierBlock      = StrfmtBlock
                     | AliasBlock
                     | AllOfBlock
                     | EnumBlock
                     | IgnoreBlock
                     | DefaultClassifierBlock
                     | TypeBlock
                     | FileBlock ;

StrfmtBlock          = ANN_STRFMT , IDENT_NAME , [ Title ] , [ Description ] ;
AliasBlock           = ANN_ALIAS ,  [ IDENT_NAME ] , [ Title ] , [ Description ] ;
AllOfBlock           = ANN_ALLOF , [ Title ] , [ Description ] ;
EnumBlock            = ANN_ENUM , [ IDENT_NAME ] , [ Title ] , [ Description ] ;
IgnoreBlock          = ANN_IGNORE , [ Title ] , [ Description ] ;
DefaultClassifierBlock = ANN_DEFAULT , [ Title ] , [ Description ] ;
TypeBlock            = ANN_TYPE , TYPE_REF , [ Title ] , [ Description ] ;
FileBlock            = ANN_FILE , [ Title ] , [ Description ] ;

Classifiers are stateless markers β€” they carry no validation body of their own. The surrounding declaration’s other annotations (or the absence thereof) determine where the classification lands.


Cross-cutting productions

These appear in multiple families and share a single production.

ExtensionsBlock      = RAW_BLOCK_EXTENSIONS ;
InfoExtensionsBlock  = RAW_BLOCK_INFO_EXTENSIONS ;
ExternalDocsBlock    = RAW_BLOCK_EXTERNAL_DOCS ;

Title                = TokenTitle ;
Description          = TokenDesc , { TokenDesc | BLANK , TokenDesc } ;
BLANK                = TokenBlank ;

Vendor extensions (ExtensionsBlock, InfoExtensionsBlock) accept YAML map bodies; non-x-* keys emit CodeInvalidAnnotation and drop. The lexer additionally surfaces them via Block.Extensions() with an Extension.Source discriminator (KwExtensions vs KwInfoExtensions) so consumers can route to the correct spec field (spec.extensions vs info.extensions).


Walker

Block.Walk(grammar.Walker{...}) dispatches Properties through typed callbacks. The Walker maps a Property to a callback by Keyword.Shape:

ShapeCallbackPayload
ShapeNumberNumber(p, float64, exclusive bool)
ShapeIntInteger(p, int64)
ShapeBoolBool(p, bool)
ShapeStringString(p, string) β€” value on p.Value
ShapeEnumOptionString(p, string) β€” closed-vocab token on p.Typed.String
ShapeRawBlockRaw(p) β€” caller reads p.Body / p.Raw
ShapeRawValueRaw(p)
ShapeCommaListRaw(p) β€” caller splits via Property.AsList
ShapeNone (failed typing)Raw(p) β€” diagnostic fired separately

Additional callbacks fire outside the per-Property dispatch:

  • Title(s string) β€” once, before any property, if non-empty.
  • Description(s string) β€” once, before any property, if non-empty.
  • Extension(ext grammar.Extension) β€” once per typed extension.
  • Diagnostic(d grammar.Diagnostic) β€” block-level diagnostics fire before Title; per-property diagnostics fire immediately before the property’s main callback.

Walker.FilterDepth gates property callbacks by Property.ItemsDepth. Pass 0 for level-0 properties (default); pass N for items-level N; pass AllDepths (-1) for every depth.

For full Walker contract see the grammar package README.


Diagnostics

The grammar emits typed diagnostics for malformed input, recovered where possible:

CodeSeverityTrigger
CodeInvalidAnnotationWarningUnknown tag, malformed annotation arg, dropped malformed property
CodeInvalidNumberWarningNumber-typed value failed lexical parse
CodeInvalidIntegerWarningInteger-typed value failed lexical parse
CodeInvalidBooleanWarningBoolean-typed value failed lexical parse
CodeShapeMismatchWarningKeyword applied to a schema type that doesn’t accept it (e.g. minLength on a number)
CodeContextInvalidWarningKeyword used outside its legal annotation context
CodeUnsupportedInSimpleSchemaWarningFull-schema-only keyword used in SimpleSchema (non-body param, header)
CodeInvalidYAMLExtensionsWarningYAML parse failed inside an extensions body
CodeUnterminatedFenceWarningYAML fence opened but not closed before EOF

All diagnostics drop the offending property / annotation / extension and continue the build. The accumulator on common.Builder collects them in source order; the consumer’s OnDiagnostic callback (if wired) fires inline.


What this grammar does not describe

The grammar’s job ends at producing typed Property and Block values. The analyzer (builders / spec orchestrator) owns:

  • Type coercion β€” default: 1.5 against an integer schema is a lexical success and an analyzer rejection. validations.CoerceValue and validations.ParseDefault apply the schema-type-aware coercion at write time.
  • Cross-reference resolution β€” $ref targets, alias-chain resolution, post-decl discovery. The grammar emits the names; the analyzer resolves them.
  • Schema-shape gating β€” validations.IsLegalForType decides whether minLength applies to the resolved schema type. The grammar always emits the property; the handler dispatch decides whether to write it.
  • Ordering & merging across multiple comment groups β€” when several swagger:parameters Foo Bar Baz declarations contribute to the same operation, the spec builder merges them.

The grammar is also deliberately single-pass β€” it never revisits a *ast.CommentGroup after Parse(cg) returns. The common.Builder blockCache memoises results across the analyzer’s recursive type descent (see common README Β§blockcache).

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

The commands

Three commands ship from this repository β€” genspec, genspec-tui and genspec-wasi β€” and they run the same scan. This page is about how they are arranged, which matters to anyone adding an option, adding a command, or wondering why the library stays as light as it does.

For what the flags and configuration keys mean, see Setting options and the Options reference.

Why three, and where each one lives

The split is about dependencies, not about features.

CommandModuleBecause
genspec-wasithe main moduleit must add nothing to what the library already needs β€” that is what lets it cross-compile to wasip1/wasm and run with no toolchain and no subprocess
genspecits own moduleit takes koanf, for the configuration sources it will be asked for next
genspec-tuiits own moduleit takes bubbletea and the tree that comes with it

So installing the terminal UI pulls none of bubbletea into your project, and a program importing the library gets neither. A command that needed a dependency the library does not want is a reason to give it a module, not a reason to argue about the dependency.

Note

The submodules are tagged and have no replace directive, so go install github.com/go-openapi/codescan/cmd/genspec-tui@latest really works from a clean environment.

One flag surface, declared once

Every knob the library takes is declared as a flag in cmd/internal/cliopts, once, and each command registers the whole of it. That is what makes -name-from-tags mean the same thing whichever one you reach for, and what makes a knob added to the library reachable from all three at the same moment.

Written per command instead, the mapping would be partial in a different way each time, and an option added to Options would reach whichever command somebody remembered.

Three properties hold it together:

  • Entries are keyed by the field’s setter, never by a name derived from the field. Naming the field in a string would let a rename pass the compiler and leave the flag writing nowhere.
  • A guard in the package tests fails when a value-typed option lands with no flag β€” or no recorded excuse. A caller cannot use an option that has no flag, and would find out by meeting flag provided but not defined after writing something against a surface that was never there. The pull request that adds the option is a better place to find out.
  • A flag is the kebab-case of the field, without exception. No rule can derive that JSONify is one word where HTTPServer is two, so coverage is decided by writing through a setter and seeing what moved β€” never by mangling a name.

The package sits under cmd/ rather than under the root internal/, so the commands that are modules of their own can import it.

Options that are not values β€” a filesystem, a document to merge into, the callbacks β€” are deliberately absent from the table. Those are the command’s business, and the tests carry the list with a reason for each. See What has no flag.

One configuration contract, two readers

cmd/internal/cliconf owns where a .codescan.yaml is found, what may be in it, and how it loses to anything typed β€” and nothing else. The values themselves arrive as a plain flat map, so how they are read stays the command’s business:

  • genspec feeds the file through koanf, for the environment variables and further formats it will be asked for next;
  • genspec-tui β€” which reads one file, once, to decide what a session starts with β€” calls cliconf.Parse, which needs nothing this repository does not already have.

That seam is why the package can be shared with a command that must cross-compile to WebAssembly: cliconf.YAML satisfies koanf’s parser interface structurally, so the package owes koanf no import.

Sections come from cliopts.ConfigSchema() merged with each command’s own, which is why a section one command does not recognize is skipped rather than rejected, and a key inside a section it does know must name one of its flags.

Everything a file sets lands through flag.FlagSet.Set β€” the same path the command line takes β€” so a value is parsed and validated exactly once, and a file cannot express anything an argument could not.

See also

Last edited by: dependabot[bot] Sep 4, 2026
Copyright 2015-2025 go-openapi maintainers. This documentation is under an Apache 2.0 license.

Performance

codescan was extracted from go-swagger and has been reworked twice since, in ways that turned out to matter for what a scan costs. This page is the short version of the evidence: three points in time, then the loader options available today, measured on one corpus.

The full method, both corpora and the raw tables live in internal/benchmarks.

The corpus

Everything below scans kubeapi: an API server stub that go-swagger generates from the kubernetes API manifest β€” not kubernetes itself, and not code that ever runs. It is here because of its shape: 2352 Go files, ~344k lines, and an annotated surface of 222 model definitions and 260 route/operation blocks, which is as much work as an annotation parser is ever asked to do.

Every figure on this page comes from a scan that emitted the same 222 definitions and 260 paths. A configuration that got faster by scanning less would not be an improvement, so the emitted document is checked alongside the cost.

Three points in time

Versions

v0.33.3 is roughly the state the code was extracted from go-swagger in. v0.35.1 is the first debugged release of the grammar-based annotation parser. current is master in its default configuration.

xychart-beta
    title "Time for one scan of kubeapi (warm build cache, seconds)"
    x-axis ["v0.33.3", "v0.35.1", "current"]
    y-axis "seconds" 0 --> 8
    bar [7.087, 1.758, 1.506]
xychart-beta
    title "Memory: total allocated (bars) and peak RSS (line), MB"
    x-axis ["v0.33.3", "v0.35.1", "current"]
    y-axis "MB" 0 --> 4800
    bar [4555, 1217, 1215]
    line [1154, 807, 751]

End to end, out of the box: 4.7Γ— faster and 3.7Γ— less allocated, for the identical document. Either of the loader options in the next section takes it further β€” compiled dependencies reach 0.970 s and 447 MB, which is 7.3Γ— faster and 10Γ— less allocated than v0.33.3.

All three bars are the same configuration, so the series measures six months of work and nothing else. That matters because the largest single step available today is not on this chart at all: it is a flag, and mixing it in here would credit the calendar for it.

The two charts do not tell the same story, and the difference is the interesting part. The step from v0.33.3 to v0.35.1 removes 3.3 GB of allocation but only 346 MB of peak β€” the old regexp-based parser allocated and discarded, so the high-water mark stayed near the floor that the loaded package graph sets. Nothing since has moved that floor by default (βˆ’56 MB). Asking for a different loader moves it, and is worth βˆ’395 to βˆ’501 MB.

What each step changed

v0.33.3 β†’ v0.35.1 β€” the annotation parser. The grammar replaced a regexp engine. That was a correctness, diagnostics and completeness project, not a performance one, and the speed is a byproduct of dropping Go’s regexp from the hot path. It is worth knowing how large the byproduct is on route-heavy code: scanning only this corpus’s models β€” nothing to emit β€” the two versions allocate the same to within 0.5 MB, while scanning the routes goes from 4476 MB to 1216 MB. The whole gain is in reading annotations, and it appears where route and operation bodies do.

v0.35.1 β†’ current β€” the package loader. Resolving and type-checking the package graph is the bulk of a scan, and it is now possible to ask for a loader that reads less of it. In the default configuration this stretch bought speed only β€” a quarter-second on both corpora, allocation-neutral to within 2 MB. The memory is all in the asking. That is the next section.

How each loader option fares

Three ways to get the package graph, all producing the same document. They differ in what they read and what they hold.

Note the axes: the two charts below are the same three configurations, timed on a warm build cache and on an empty one. Only the scale differs β€” and it differs by an order of magnitude.

xychart-beta
    title "Time for one scan, WARM build cache (seconds)"
    x-axis ["source deps", "pure-Go", "compiled deps"]
    y-axis "seconds" 0 --> 2.5
    bar [1.506, 1.331, 0.970]
xychart-beta
    title "Time for one scan, COLD build cache (seconds)"
    x-axis ["source deps", "pure-Go", "compiled deps"]
    y-axis "seconds" 0 --> 15
    bar [2.208, 1.359, 14.511]
xychart-beta
    title "Memory: total allocated (bars) and peak RSS (line), MB"
    x-axis ["source deps", "pure-Go", "compiled deps"]
    y-axis "MB" 0 --> 1300
    bar [1215, 643, 447]
    line [751, 412, 306]
configurationoptionwarmcoldallocatedpeak RSSbuild cache it writes
source dependenciesthe default1.506 s2.208 s1215 MB751 MB7.7 MB
pure-Go loaderToolchainFreeLoader1.331 s1.359 s643 MB412 MB4 KB
compiled dependenciesCompiledDependencies0.970 s14.511 s447 MB306 MB231 MB

No configuration wins both cache states, which is the whole reason there is a choice to make:

  • Compiled dependencies take dependency types from the compiler instead of reading their source β€” 31 packages read from source rather than 296. Fastest and smallest by a wide margin on a warm cache. On a cold one it must compile the closure before it can read it, so it is more than 6Γ— slower than reading source, and it writes 231 MB of build cache. Opt in where the cache is warm by construction; it is off by default because a CI job regenerating a spec from a clean checkout is not.
  • The pure-Go loader never invokes the go command, so there is no metadata to populate and nothing to compile: it writes 4 KB of build cache and its cold time equals its warm time. It is the only choice whose cost is predictable, and it holds about 45% less memory than the standard loader.

Which to reach for, and what each one gives up, is on the Options reference.

Reading these numbers

They are indicative, not a promise. Measured on one machine (Ryzen 7 5800X, 31 GB, go1.26.5, Linux), three warm rounds and a single cold one β€” cold being a single-shot state by definition.

Two cautions carry over to any corpus of your own:

  • The two gains scale with different things. The loader acts on the dependency closure, so its win is roughly a fixed amount per project; the parser acts on the annotated surface, so its win is large on a route-heavy server and small on a client carrying annotations only on its models. On a smaller tree the loader’s share of the total looks much bigger than it does here.
  • Wall clock needs an idle machine. Under load these timings move by 3Γ— while the allocation and peak-RSS figures reproduce to four digits. Distrust a timing difference under 10%; nothing concluded above rests on one.

To measure your own tree, the harness takes it as an extra corpus alongside the two it ships with β€” see its README.