You generate a Go client from an OpenAPI document whose operations each carry a paragraph of
description. On pkg.go.dev the client shows template sentences, or the paragraph pasted as
written: a bullet list run together into one line, two asterisks around Note, and a block of
@param tags. gofmt -l lists every generated file.
What you get
You will end up with three Go clients generated from one document, a report counting what
reached each one’s doc comments, and go doc and gofmt -l runs on the result. This is for
you if you publish a Go client you regenerate.
Short answer
Run oapi-codegen or OpenAPI Generator over the document: both carry each operation’s summary
and description into the method’s doc comment behind the method name, and oapi-codegen
carries schema examples too. Neither converts CommonMark, so a list or bold text in a
description renders as literal text in go doc and on pkg.go.dev, and only oapi-codegen
writes gofmt-clean files. The sdkgen Go target writes template prose and leaves the
document’s text in its model and reference.
You will need
Go 1.24 or later, an OpenAPI 3 document with summary and description on every operation,
and a Go module you regenerate. Verified 2026-10-21 against Go 1.24.7, oapi-codegen 2.8.0, OpenAPI
Generator 7.26.0, @voxgig/sdkgen 4.35.0, and @voxgig/create-sdkgen 0.30.8. The
Operation Object says a
description MAY use CommonMark, and Go doc comments have a
syntax of their own.1
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| Hand-written doc comments | A small client, and prose that explains more than the document says | Every change to an operation is an edit nobody is reminded to make, so the comments drift from the document without a signal | The document changes more than once a quarter |
| oapi-codegen | A Go-only client from one document, with the summary, description, and examples in the comments and output gofmt leaves alone | CommonMark pasted as written, a comment template you can override only by replacing the whole client.tmpl, and a runtime module dependency | You generate clients in other languages from the same document |
| OpenAPI Generator Go client | One generator for every language your API ships, with the Go templates extractable and editable | Java-style @param lines in every method comment, no examples, files gofmt rewrites unless you hook it into post-processing, and a Java runtime | Go is your only target |
| sdkgen Go target | An entity-shaped SDK across languages, with the document’s field text in its model and reference | Template prose in the doc comments, no operation summary or description anywhere in the output, and files gofmt rewrites | The doc comments must say what the document says |
Voxgig maintains sdkgen. This page compares it with oapi-codegen and OpenAPI Generator.
oapi-codegen carries the most and costs you a runtime module and a template you can replace
only whole. OpenAPI Generator carries the same prose plus a block of @param tags that Go renders as
code, and costs you a Java runtime and a gofmt step of your own. sdkgen carries the field
descriptions into its model and its reference file, but no operation prose reaches its Go, so
its comments say what its templates say.
Generate three clients from one document
The document has four operations whose descriptions are written the way most are: a second
paragraph, backticks, a bullet list, bold text, a link, and a Markdown heading. The schemas
carry description and example on most properties.
"post": {
"operationId": "createMeter",
"summary": "Registers a meter.",
"description": "Registers a meter at a site. The meter starts in the `pending` state until its first reading arrives.\n\n- `serial` must be unique for the account\n- `site` must name an existing site\n\n**Note:** a meter cannot be moved between sites. Register a new one instead.",
generate.sh ran the three generators once, with the network, and its output is committed
under generated/. Each output is a Go module of its own, so the report at the root parses
all three while go test ./... builds only the report.
mkdir -p generated/oapi-codegen
GOTOOLCHAIN=auto go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.8.0 \
-config oapi-codegen.yaml openapi.json
(cd generated/oapi-codegen && go mod init meterco.example/oapi-codegen && go mod tidy)
curl -sSfO https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.26.0/openapi-generator-cli-7.26.0.jar
java -jar openapi-generator-cli-7.26.0.jar generate -g go -i openapi.json -o oag-out \
--additional-properties=packageName=meterco --git-user-id=meterco --git-repo-id=meterco-go
mkdir -p generated/openapi-generator
cp oag-out/*.go oag-out/go.mod oag-out/go.sum generated/openapi-generator/
rm -rf oag-out openapi-generator-cli-7.26.0.jar
npx --yes @voxgig/create-sdkgen@0.30.8 meterco --def openapi.json --target go
(cd meterco-sdk/.sdk && npm run generate)
Count what each generator carried over
report.go parses every generated file with go/parser, never importing it, and reads each
exported symbol’s doc comment. It then looks for the document’s text in those comments: the
four summaries, the first sentence of the four descriptions, the thirteen distinct property
descriptions, and the seven example values.
// Symbol is one exported func, method or type with the doc comment it carries.
type Symbol struct {
Name string
Kind string // func, method or type
Doc string
File string
}
// Markers of CommonMark that a Go doc comment renders as literal text.
var markdown = regexp.MustCompile("`[^`]+`|\\*\\*[^*]+\\*\\*|\\[[^\\]]+\\]\\([^)]+\\)|(?m)^- ")
go run .
document: 4 operations with a summary and a description, 13 field descriptions, 7 example values
generator files exported documented start name summaries descriptions fields examples markdown longest gofmt
oapi-codegen 2.8.0 1 73 69 66 4/4 4/4 13/13 7/7 18 167 0/1
OpenAPI Generator 7.26.0 11 261 119 112 4/4 4/4 13/13 0/7 4 167 10/11
sdkgen 4.35.0 55 215 86 45 0/4 0/4 0/13 0/7 3 93 12/55
The two OpenAPI generators carry every summary, every description, and every field
description, and oapi-codegen alone carries the examples, as Example: mtr_01HZX4 lines under
the field. Both keep the longest description as one 167-character line. sdkgen’s doc comments
carry none of it. The field descriptions reach its model files, REFERENCE.md, and the
configuration map its Go code builds, and the operation prose reaches nothing in the output.
The typed Meter struct it writes has no fields, while its model lists six.
// Meter A physical meter and where it is installed.
type Meter struct {
// DecommissionedAt When the meter was taken out of service, or null while it is still in service.
DecommissionedAt *time.Time `json:"decommissionedAt"`
// Id The id.
//
// Example: mtr_01HZX4
Id string `json:"id"`
The start name column counts comments that open with the identifier, which is the Go
convention and the synopsis pkg.go.dev shows. The same run names the exceptions.
go run .
oapi-codegen 2.8.0: 1 documented funcs or methods do not open with their name, such as NewClient
NewClient: "Creates a new Client, with reasonable defaults"
OpenAPI Generator 7.26.0: 7 documented funcs or methods do not open with their name, such as CreateMeterExecute, GetMeterExecute, Site
CreateMeterExecute: "Execute executes the request"
sdkgen 4.35.0: 35 documented funcs or methods do not open with their name, such as Record, Record, String
Record: "The record a context serialises as; the options and the client stay out."
sdkgen 4.35.0: the document's prose appears outside Go comments, in REFERENCE.md, core/config.go, model/meter.aontu, model/reading.aontu
Both OpenAPI generators put the method name in front of the summary, so Registers a meter.
becomes CreateMeter Registers a meter. rather than a sentence that opens on a verb. The
exceptions are template text: OpenAPI Generator documents CreateMeterExecute as Execute.
ST1020 in staticcheck flags that shape once
you enable it, because it is off by default, and go vet does not check it.
Read the comments the way pkg.go.dev renders them
go doc prints a comment through the same go/doc/comment syntax the site uses, so it shows
what a reader will see without a publish.
cd generated/openapi-generator && go doc . DefaultAPIService.CreateMeter
package meterco // import "github.com/meterco/meterco-go"
func (a *DefaultAPIService) CreateMeter(ctx context.Context) ApiCreateMeterRequest
CreateMeter Registers a meter.
Registers a meter at a site. The meter starts in the `pending` state until
its first reading arrives.
- `serial` must be unique for the account - `site` must name an existing
site
**Note:** a meter cannot be moved between sites. Register a new one instead.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiCreateMeterRequest
Four things happened to the paragraph. The bullet list became one sentence, because a
Go list is a span of indented lines and these were not
indented. The asterisks and backticks stayed, because the syntax has no emphasis and no code
spans. The @param lines were indented in the template, and an indented span is a
code block, so they print as code.2 The
167-character sentence wrapped at 80 columns, the
printer’s default. One CommonMark feature
survived: the # Units heading in recordReading renders as a heading, because Go uses the
same character.3
sdkgen’s output is the other shape of the problem. Its comments are well-formed Go sentences that name the symbol and say nothing the document said.
cd generated/sdkgen && go doc ./entity Meter
package entity // import "github.com/voxgig-sdk/meterco-sdk/go/entity"
type Meter struct {
}
Meter is the typed data model for the meter entity.
Keep the output formatted and fix the words upstream
gofmt -l names the files it would rewrite. Twenty-two of
the sixty-seven generated files are on the list: ten from OpenAPI Generator and twelve from
sdkgen, mostly for import order, spacing, and alignment. In api_default.go it also puts a
blank line and a tab before the @return line of four comments. oapi-codegen’s one file is
already in the form gofmt writes.
gofmt -l generated
generated/openapi-generator/api_default.go
generated/openapi-generator/client.go
generated/openapi-generator/configuration.go
generated/openapi-generator/model_error.go
OpenAPI Generator runs a formatter for you when you pass --enable-post-process-file and
set GO_POST_PROCESS_FILE to gofmt -w, as its
post-processing page describes.
For sdkgen, run gofmt -w over the output after npm run generate. The formatter fixes the
alignment and leaves the asterisks, which are text.
The words are fixed in one of two places. The document is the first: a description written as
plain sentences, with no list and no emphasis, arrives intact in every target. The template is
the second, and each of the three tools lets you change it without a fork. oapi-codegen takes
output-options.user-templates,
a map from a built-in template’s filename to a local path, a URL, or inline text, so the
method comment changes by replacing client.tmpl whole. OpenAPI Generator’s
author template
command extracts api.mustache for the Go generator, and -t points at your copy. sdkgen
vendors its templates into the project, so the sentence that
documents CreateTyped lives in .sdk/src/cmp/go/fragment/EntityCreateOp.fragment.go in your own
repository. Its customization page warns that a hand edit to a vendored file is the one change
an upgrade can revert.
Check it worked
go test -v . | grep -E '^(=== RUN|PASS$|FAIL$)'
=== RUN TestTheDocumentHasTheProseTheComparisonNeeds
=== RUN TestTheTwoOpenAPIGeneratorsCarryEverySummaryAndDescription
=== RUN TestOnlyOapiCodegenCarriesTheExamples
=== RUN TestSdkgenLeavesTheProseInItsModel
=== RUN TestTheTypedMeterStructHasNoFields
=== RUN TestGofmtPassesOapiCodegenAndFailsTheOtherTwo
=== RUN TestMarkdownSurvivesIntoTheCommentsOfBothOpenAPIGenerators
=== RUN TestTheCommentsThatDoNotOpenWithTheirName
=== RUN TestProseInAGoStringIsOutsideTheComments
PASS
The run ends in PASS, and the grep drops the per-test timings. The gofmt test pins the
counts at 0, 10, and 12, and the struct test pins the empty Meter. A generator release
that fills the struct or formats its own output turns a test red.
When it goes wrong
A method’s synopsis names the wrong thing. OpenAPI Generator writes Execute executes the request as the comment on CreateMeterExecute, so the package index on pkg.go.dev lists a method whose
first word is not its name. The template is api.mustache; extract it, change the comment,
and pass the directory with -t.
The list in a description comes out as one line. Go recognizes a list only when its items are indented, and CommonMark reads an indented block as code, so the two formats cannot hold the same list. Write the items as sentences in the document, or move the list into the template where the indentation is yours.
The summary reads as a label. ListMeters List meters is what both generators print when the
summary is List meters, because the name goes in front of a title. A summary written as the
method’s predicate, lists the meters installed for the account, reads as a sentence after
the prefix. Every other renderer of the document then shows a strange heading, so decide which
reader comes first.
When not to do this
Do not edit a generated file to fix its comments. oapi-codegen and OpenAPI Generator rewrite their output on every run, and sdkgen’s scaffold resyncs vendored files on upgrade. The edit lasts until the next regeneration, which happens without a word to the editor.
Do not generate comments from a document whose descriptions were never written for a reader.
The report counts The id. as a carried field description, because it is one.
The fix is the sentence in the document, which both OpenAPI generators then copy faithfully.
Do not pick the sdkgen Go target for the comments. At this version its Go output carries no
operation summary or description, and its typed Meter struct has no fields to hang a
description on. The document’s field text sits in the model, the Go configuration map, and
REFERENCE.md instead. Pick sdkgen
for the entity shape and the shared behavior across languages, and expect to write the Go
comments into its vendored templates yourself.
A CommonMark list, emphasis, or link in a description earns nothing in a Go target that plain text would not, so leave them out when Go is one of your targets.
Related how-tos
Last verified
Verified 2026-10-21 against Go 1.24.7, oapi-codegen 2.8.0, OpenAPI Generator 7.26.0,
@voxgig/sdkgen 4.35.0, and @voxgig/create-sdkgen 0.30.8. Every output block is what the
command preceding it printed, run in the page’s code directory. The generation ran once,
through generate.sh, with the network, Java 21, and Node 22, and its output is committed
under generated/; the checks parse that output and never regenerate it. A second run of the
sdkgen step reproduced its committed tree byte for byte. pkg.go.dev was not
fetched: the rendering claims rest on go doc, which reads the syntax the Go doc comment page
defines for both tools.
Footnotes
-
The Go doc comment page allows that Markdown aficionados can view its syntax as a simplified subset of Markdown, and in the same breath rules out font changes and raw HTML. The OpenAPI specification, for its part, says a description MAY use CommonMark. The two formats share the heading character and the list marker, and part with each other on the list’s indentation, the emphasis, and the link. ↩︎ Back to text
-
The
@paramand@returnlines come from OpenAPI Generator’sapi.mustache, which writes a Java documentation comment into a Go file, one leading space and all. Java’s documentation tool reads an@tag as a tag. Go reads a leading space as indentation, and indentation as a code block, so the tags arrive on pkg.go.dev as code, which is the one rendering neither language intended. ↩︎ Back to text -
The
#heading syntax arrived in Go 1.19. Before it, a heading was any single-line paragraph with no terminating punctuation, identified by inference, andgofmtrewrites those implicit headings into the explicit form. The Markdown heading in this page’s document therefore renders correctly by coincidence of character. ↩︎ Back to text