OpenAPI 3.0 documents from the Go source of your service. docgen reads your
packages with go/packages, finds your services and the types they take and
return, and writes the YAML. Nothing is generated into your code and nothing is
executed: the documents come from the types, struct tags and doc comments you
already write.
// Get returns one pet.
//
// @desc: Looks a pet up by its id.
// @tags: public
// @response:404,NotFoundErr,There is no pet with that id
func (PetService) Get(ctx context.Context, req *protocol.GetPetReq) (*domain.Pet, error) {becomes POST /api/pet/get, with a request body, a 200 response whose schema
is domain.Pet, a 404 response for NotFoundErr, and tags.
- One module, several documents. An internal document with everything and a public one with the operations and fields you chose, from the same source.
- Check mode for CI.
docgen -checkfails when a checked-in document is not what the source generates, and says where it differs. - Configuration is data. One
docgen.yaml, plus optional overlay and error catalog files. There is no plug-in API to compile against. - Deterministic. The same source gives the same bytes, so the documents can be reviewed in a pull request and diffed.
docgen needs Go 1.25 or later on the machine that runs it. The module it reads can target any Go version that machine can build.
go install github.com/Danceiny/docgen/cmd/docgen@latestOr pin it to your module as a tool (Go 1.24 and later), so that everybody and the CI run the same version:
go get -tool github.com/Danceiny/docgen/cmd/docgen@v0.1.0
go tool docgen -versionexamples/petstore is a small module that uses most of what
docgen does. Its two documents are checked in.
git clone https://github.com/Danceiny/docgen
cd docgen/examples/petstore
docgen # writes docs/api/internal.yaml and docs/api/public.yaml
docgen -check # exits 1 if either file is not what the source generatesTo document your own module, write a docgen.yaml in its root:
version: 1
docs:
- name: api
audience: internal
output: docs/api/openapi.yaml
info: {title: My API, version: 1.0.0}
models: ["*/domain", "*/protocol"] # packages whose types become schemas
services: ["*/service"] # packages whose services become operationsand run docgen there. Patterns are matched against the package path relative to
the module path; * matches any run of characters, slashes included, and every
other character matches itself. So */service matches pet/service and
shop/pet/service, and does not match a package service at the module root: for
a flat layout write services: ["service"] (or both). A pattern that matches no
package is reported, since the document it belongs to would be empty without it.
A service is a struct with a method Name() string that returns a string
constant: a literal, a named constant (of any package of the module) or a
constant expression. Its exported methods are the operations, except Name
itself, the methods marked @apidoc: - and the ones listed in api.skip_methods.
type PetService struct{}
func (PetService) Name() string { return "pet" }
func (PetService) Get(ctx context.Context, req *protocol.GetPetReq) (*domain.Pet, error)- The first parameter may be a
context.Context; it is not documented. - The next parameter is the request: a struct (or pointer to struct) is the
JSON request body, a list or a map too, and a parameter of a basic type is a
body of that type unless a
@paramannotation says it is a query, header, path or cookie parameter. Only the first parameter after the context is read: docgen warns about a method that has more, since the others have no place in the document. - A result that is not an
erroris the200response: a struct is a reference to its component, a list is anarrayof its element, a map anobject, and an instantiated generic type such asPage[Pet]the component ofPage. Anerroris not documented by itself; use@response(below). - The route is
<prefix>/<service>/<method name with its first letter lower-cased>, soGetof the servicepetis/api/pet/get. A service name may contain slashes (store/order). The prefix is/apiunlessapi.prefixsays otherwise.@pathreplaces the method name (see below), and a@paththat starts with the service name loses that, as a plain prefix:@path: /orderListof the serviceorderis/api/order/list, and so is@path: /order/list; the method name itself is never stripped (OrderListis/api/order/orderList). It is a plain prefix, so@path: /orders/listof the serviceorderis/api/order/s/list. - The default HTTP method is
POST;@methodchanges it, and several methods (@method: GET, POST) give an operation each, whose ids have the method in lower case added (pet/get_get,pet/get_post). OpenAPI does not allow a body onGETandDELETE, but docgen documents the request of such an operation as a body all the same, unless the request type is listed inrequest.query, which makes it query parameters (see the configuration). - The tags of an operation are the service name, the ones of
@tagsand the ones of@permission, without repeats, sorted. The first letter of every word of the service name is made upper case and the rest is left as it is (myService/SubisMyService/Sub); the tags of@tagsand@permissionare made lower case except for the first letter of each word (OAuthisOauth,x-yisX-Y). - The summary is the first line of the doc comment; the description is
@desc.
The package route is the executable specification of the route rule.
If you have a router of your own, test it against route.Resolve.
Written in the doc comment of the method, one per line. An annotation docgen does
not know is left alone, since other tools read comments too; one that is a letter
away from a known one (@respone, @Tags) is reported.
| Annotation | Meaning |
|---|---|
@desc: text |
The description of the operation. It runs to the next annotation, so it may span paragraphs and use Markdown. |
@tags: a, b |
Extra tags. |
@permission: pet:write |
Added as a tag too; handy to show what a caller needs. |
@method: GET |
HTTP methods, comma separated: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE. |
@path: /custom |
Replaces the method name in the route. A value that already starts with the prefix is the whole route. |
@doc: Guide: https://example.com/guide |
External documentation, as description: url. |
@response:404,NotFoundErr,text |
An extra response: the status, the name of an error of the error catalog, a description. The status is checked against the catalog. |
@param:query limit int required "how many" |
A parameter that is not the body: <in> <name> <type> <required|optional> ["description"], where in is query, header, path or cookie. name is the name of the Go parameter (or param1, param2, … by position), and is the name in the document. A path parameter must be in the @path, as /get/{id}. |
@headerType: Admin |
Chooses the header type of the operation among those of headers.types. |
@apidoc: - |
The method is not an operation: neither routed, by the convention of route.Skipped, nor documented. |
The named types of the models packages become components
(#/components/schemas/<import path with dots>.<Name>).
- Fields are read as
encoding/jsonreads them. Names come fromjsontags;json:"-"and unexported fields are left out; a declaration of several names (Lat, Lng float64) is a field for each, and a blank field none. An embedded struct is flattened into the struct that embeds it, unless it has a json name (Base `json:"base"`), which makes it a field of that name. - A field is required when its
validateorbindingtag has the rulerequired(validate:"required,email"has it), or it hasrequired:"true".json:"x,nullable"makes it nullable. example:"..."anddefault:"..."tags (orjson:"x,default=...") give the example and the default of a field, read as what its type is: a number for an integer, the text itself for a string, JSON for a list or an object,10mfor atime.Duration. A value that its type does not allow is left out, with a warning that names the field. An overlay that replaces the type of a field afterwards can make the value one that the type no longer allows: that fails the run, and the error says which schema has it.- The comment of a type is the description of its component (for an enum it
comes first, then the list of the values). The comment of a field, above it
or after it, is its description. A field whose type is a component refers to it
from the only member of an
allOfwhen it has something to say of its own, a description,nullable, anexampleor adefault, since OpenAPI 3.0 has no place for those next to a$ref; a field with nothing to say is the$refitself. The comment of a list or a map is that of the list or the map, not of what it holds. - Constants of a named type make it an enum, whichever way they are declared
(
iota, shifts, expressions): the values the compiler computes are listed inenum, with the names and the comments of the constants in the description. A type is an enum when at least one of its constants is declared with the type (A Status = iota), and a name made withtype Status = stringis a name forstring, which only the constants declared with that name make an enum. Constants that are other names for a value are one value, and the unexported ones, such as the sentinel that counts the others, are not listed. - A map is an
objectwhoseadditionalPropertiesis the schema of its values (truefor a map of any value),anyandinterface{}are the empty schema, a slice of bytes is a base64stringand a byte a number. - A type that contains itself, directly or through other types, is a
$refto itself. - A generic type is documented as declared, and an instance of it,
Page[Product], is a reference to it, whatever its type arguments are. A field of a type-parameter type has no schema unless its comment lists the types it may be,@generic: Product, Order, which makes it aoneOfof all of those components, for every instance. An interface with methods holds any JSON value, unless its comment says@autowire: true, which makes it aoneOfof the types of its package that implement it. time.Timeis adate-timestring andtime.Durationanint64number of nanoseconds, as JSON writes them.type_mapsays otherwise, and what it says of a type is what the type is where it is used, whatever order its declaration comes in; a type of the module that it names, in a package thatmodelsmatches, is a component all the same.
A document has an audience, internal or public, which decides what the
visibility rules show.
On a type, in its doc comment:
//apidoc:public shown in the public document, and so in the internal one
//apidoc:internal shown in the internal document only
//apidoc:hidden never shown
//apidoc:public:StatusActive,StatusNew an enum: only these values are shown
//apidoc:public:-StatusLegacy* an enum: every value but those that start with StatusLegacyThere is no space after the slashes (// apidoc:hidden is an ordinary comment),
and the values of an enum are the names of its constants, not what they are on
the wire. Docgen warns about a directive it cannot read.
On a field, as a struct tag:
Notes string `json:"notes" apidoc:"internal"` // the internal document only
Beta string `json:"beta" apidoc:"public"` // the public document only
Cost int `json:"cost" apidoc:"hidden"` // never (`apidoc:"-"` too)A document also leaves out every type whose name starts with a prefix of its
hide_type_prefixes, unless the type's own //apidoc: says otherwise. Anything
with no annotation is shown everywhere.
A hidden type has no schema, and neither has anything that would refer to it: a
field of the type, of a list or map of it, or that embeds it, is left out, and so
is an operation that takes or returns it (with a warning that says which). That
includes a type that is declared as a hidden type, type State InternalState or
type States []InternalState, whatever its own directive says: there is nothing
left to describe it by. An enum with an //apidoc:hidden directive is the
exception: it is shown, with its values hidden; one that is hidden by its name is
left out like any type.
An older form, apidoc:"Staff", shows the field only in the documents that list
Staff in legacy_field_tokens. Any other value of the tag hides the field from
every document, and docgen warns about it, since a typo and a description put in
the wrong tag are the two ways to lose a field that way. The tag
api.header:"X-Request-Id" names the HTTP header a field of a header type travels
in.
docgen.yaml is strict: an unknown key (with the keys that are there, and the one
it is probably a typo of), a bad value or a type key written with slashes is an
error that names the key; the keys that do not decode are reported together, and
so are the bad values. A key that names a type of the module that the module does
not declare, and a pattern that matches no package, are warnings.
The petstore configuration is a complete
example; this is the reference.
version: 1 # the only version
api:
prefix: /api # starts every route; default /api
skip_methods: [GetChildren] # never operations, besides Name
vendor_extensions: false # add x-apifox-*, x-enum-varnames, x-enum-comments,
# x-display-name, x-primary-property, x-go-interface
keep_empty_tags: false # keep the empty tag of an operation without @tags
compat: # only for documents generated by an older tool,
legacy_operation_types: false # see "Documents generated before"
legacy_output: false
type_map: # fix the schema of a type instead of reading it
example.com.shop.types.ID: {type: string, description: id}
example.com.shop.types.IDs: {type: array, items: {type: string}}
github.com.shopspring.decimal.Decimal: {type: number}
time.Duration: {type: string, format: duration, example: 1h30m}
headers: # the header type each operation takes, as its
default: Auth # "Headers" parameter
types: {Auth: example.com.shop.protocol.AuthHeader}
request:
multipart: # request types that are multipart/form-data
example.com.shop.protocol.UploadReq:
- {name: file, kind: file, required: true}
- {name: caption, kind: scalar, scalar_to: string}
query: # request types read from the URL query: no body. The
# parameters are listed by hand, for every operation that
# takes the type, and are not checked against its fields
example.com.shop.protocol.ListReq:
- {name: status, type: string, description: Limit the list}
runtime_only: # bindings no JSON value can satisfy
- {path: /api/shop/setHook, type: example.com.shop.Hook}
response:
envelope: {code: code, message: message, data: data} # wrap the data; without
# one, the data is the body
default_statuses: {"401": Unauthorized} # on every operation
binary: # response types that are files
example.com.shop.protocol.DownloadResp:
description: The file
content_types: [image/png]
errors: {"404": Not found}
errors: # the error catalog @response refers to
file: errors.json
component_prefix: example.com.shop.errors.
generic_titles: [] # only for documents that keep compat.legacy_output: titles
# of schemas whose oneOf is kept whole, not narrowed by
# the type arguments in the title that holds them
docs:
- name: internal # selects the document with -doc
audience: internal # internal | public
output: docs/api/internal.yaml
info: {title: Shop API, version: 1.0.0, description: ...}
servers: [{url: "http://localhost:8080", description: Local}]
models: ["*/domain", "*/protocol"]
services: ["*/service"]
legacy_field_tokens: [] # values of apidoc:"..." tags shown in this document
hide_type_prefixes: [] # types whose name starts with one are left out
overlay: # hand-written schemas, see Overlays
- {file: overlay.yaml, stage: after_models}
- name: public
audience: public
output: docs/api/public.yaml
info: {title: Shop API, version: 1.0.0}
models: ["*/domain", "*/protocol"]
services: ["*/service"]
force_keep: [] # components kept though no operation uses them
public: # only for audience: public
tag: public # the operations with this tag are the public ones
strip_tags_containing: [] # remove the tags that contain one of these strings
errors_last: false # put error alternatives of oneOf/anyOf/allOf lastmodels must contain the package of every type an operation takes or returns, or
the run fails with the reference that has no schema. docgen loads ./... and
fails, saying why, when any package of the module does not build.
errors.json lists the errors a service can answer with:
[
{"name": "NotFoundErr", "code": 40401, "message": "not found", "httpCode": 404}
]Each becomes a component (named from errors.component_prefix), and
@response:404,NotFoundErr,text refers to it by name. If the status of the
annotation is not the httpCode of the error, docgen warns and uses the error's.
Generate the file from your own error definitions with a few lines of Go, and
check it in with the documents.
Some schemas cannot be read from Go: a type declared outside the packages you document, a webhook payload, a union written by hand. An overlay file adds them, or replaces what was generated:
version: 1
schemas:
example.com.shop.protocol.ImportReq:
type: object
required: [source]
properties:
source: {type: string, description: Where the pets come from}
pets:
type: array
items: {$ref: '#/components/schemas/example.com.shop.domain.Pet'}An after_models overlay is applied before the operations are generated, which
is what lets an operation refer to a schema that only the overlay has; an
after_apis overlay is applied after. A schema of an overlay has $ref, title,
description, type, format, required, properties, items,
additionalProperties and oneOf; any other key (enum, example, nullable,
allOf and the constraints) is rejected with the keys that are understood. To
give a type such values, describe it in Go, or in type_map.
docgen [-C dir] [-config file] [-doc name]... [-check] [-v] [-cpuprofile file] [-memprofile file]
docgen -version
| Flag | |
|---|---|
-C dir |
Run as if started in dir, the root of the module to document. |
-config file |
The configuration file, relative to dir; docgen.yaml by default. |
-doc name |
Generate only this document; repeat for several. |
-check |
Generate into a temporary directory, compare with the files on disk and exit 1 if one differs. Writes nothing. |
-v |
Also log what helps to follow a run, such as each type hidden by its annotation. |
-cpuprofile file, -memprofile file |
Write a CPU or a memory profile, for finding what makes a run slow. |
-version |
Print the version. |
Warnings go to standard error; the ones about code say where it is. They do not
fail the run: they say that something in the source or the configuration does
nothing, or does something else than it looks like, so read them as a to-do list.
-check compares the text of the documents and takes a checkout that has turned
line feeds into carriage returns and line feeds (core.autocrlf) for current.
In CI:
go tool docgen -checkdocgen was extracted from the tool that generated the documents of one large service, and those documents have been reviewed and checked in for years. A few things the first tool got wrong are fixed in docgen, and a project that has such documents does not want them rewritten by an upgrade, so each fix that changes them has a switch that keeps the old reading. A new project should not set either.
compat:
legacy_operation_types: true
legacy_output: true-
legacy_operation_types: the types of the parameters and results of an operation are read as a list being its element type ([]PetisPet), and a map, an interface and an instantiated generic type being of an unknown type, so that a result of one has no content. -
legacy_output: everything else docgen fixed that changes a document. What was wrong in the schemas:- of a declaration of several names (
Lat, Lng float64) only the first is a field, and a blank field is a property called_; - an embedded struct with a json name is flattened;
validate:"required,email"does not make a field required, onlyvalidate:"required"does;anyis anobjectandinterface{}a string, an integer or an object, andjson.RawMessageis a string;- the values of an enum are the constants declared with the type and a literal,
so
iotaand expressions give an empty value and a constant that repeats the type implicitly is missing, and atype_mapentry for an enum type is ignored; - the values of a map are lost (
additionalProperties: true), a slice of bytes is an array of strings and a byte a string; - the comment of a type is not its description, the comment of a field is
copied onto the elements of its list, and a field that has a comment and a type
declared after its struct is replaced by a copy of the type, while one
declared before it is a
$refthat has lost the comment; - example and default tags are read by the Go type of the field, and a value the type does not allow stops the run;
- a type declared as another type (
type Money2 Money) and an interface with methods are the placeholder{type: object, pattern: default}; - a type of the module outside
modelshas the comment of the first field that uses it as its description and is nullable if that field is,type_mapdoes not describe a struct, a field of a struct that an embedded struct has too is the embedded one when it is written after it, an embedded list or map is left out, and an enum hidden by its name or a map of maps of a hidden type is not hidden; - the directive of a type declared in a group,
type ( ... ), does not filter the values of an enum; - an instance of a generic type is a copy of it with its type arguments in the
title, which narrows the
oneOfof its fields to them, and a struct that has only a type-parameter field is titled with its first candidate, which narrows the field to that one for every instance (generic_titleskeeps aoneOfwhole); a type thattype_mapnames is a reference to its component when its declaration comes first and a copy of the schema when it does not; @genericwithout a semicolon leaves the candidates in the description, and a candidate after a comma and a space is lost (@generic: Product, Orderis the candidateProduct);- an
@autowireunion keeps the markerpattern: defaultnext to its alternatives and has the line@autowire: truein its description, and it has no alternatives when a field that uses it is read between its declaration and the point where they are added; a list of a type parameter has the placeholder as its items; - a type declared as a hidden type (
type S InternalState,type S []InternalState) is not hidden with it but is a placeholder, and a struct that has a field of the name of one it embeds is required to have it when the embedded one is; - the order of the fields a schema lists (
x-apifox-orders) has a name twice when a field shadows an embedded one, and names fields that a hidden type took away;
And what was wrong in the documents: a public document writes the types that fields use in place of references, which makes it grow with the number of paths to a type, not with the number of types, and an operation whose comment starts with an annotation has that line as its summary. The check of the document follows every reference as well, so for a module of thousands of types that refer to each other it takes minutes, not seconds.
- of a declaration of several names (
docgen is young. These are known, and will change in a later release; do not rely on them.
- Types of other modules are written out where they are used, as inline
schemas, not as components; a type that contains itself there is an
objectthe second time. Do not use one as the request or response type of an operation: wrap it in a type of your module. A custom marshaler is not read, so a type that writes itself as something else than its Go shape (net.IP,big.Int, a UUID that is an array of bytes) needs atype_mapentry. - The marker
pattern: defaultshows on a schema that docgen could not fill in, such as an@autowireinterface with nothing that implements it; docgen warns about each one. json:",string"is ignored, and a pointer is not nullable unlessjson:"x,nullable"says so.- Two embedded structs that have a field with the same JSON name are an ambiguity that encoding/json answers by letting the shallower field win and, at the same depth, by writing neither. docgen describes one of them, and which one can depend on the order the declarations come in.
- A field of a type that is declared as another type of the module (
type A B,type A = B) refers to A or to B, depending on which of the two declarations comes first in the source. Both describe the same schema. - The constants of an enum must be declared in the package of the type.
- Generics are documented as declared, as said above: only one type parameter
is supported, a type-parameter field without
@generichas no schema, and a list of a type parameter (Items []T) is a list of anything, whatever the type argument of an instance is. - Query parameters are listed by hand in
request.query, not read from the struct, and apply to every operation that takes the type. - The request and response types of an operation must be declared in a package
that matches
models. - The documents are OpenAPI 3.0; kin-openapi does not write 3.1.
- Every run loads and type-checks the whole module (
./...) once, for all of its documents, and fails, saying why, when a package of it does not build.
Before 1.0 the command line, the configuration and the output may change in a minor release. The changes are listed in CHANGELOG.md, and the output of a version does not change within it. Pin the version, and check the documents in.
The only Go package you can import is route, which has no
dependencies. The rest is internal.
See CONTRIBUTING.md. In short: go test ./... must pass and
go run ./cmd/docgen -C examples/petstore -check must succeed.
MIT. The third-party code docgen builds on is listed in THIRD_PARTY_NOTICES.md.