Parse Go files carrying swag-style annotations and emit OpenAPI 3.2 (or 3.1, for backward compatibility) without depending on swaggo/swag.
You write (unchanged swag-style annotations):
package accounts
// @title Accounts API
// @version 1.0.0
type Account struct {
ID int64 `json:"id"`
Email string `json:"email" example:"ada@corp.io"`
Plan string `json:"plan" binding:"oneof=free pro"`
Trial *string `json:"trial_ends"`
}
// GetAccount godoc
// @Summary Fetch one account
// @Produce json
// @Param id path int true "account id" minimum(1)
// @Success 200 {object} Account
// @Router /accounts/{id} [get]
func GetAccount(w http.ResponseWriter, r *http.Request) {}You get (OpenAPI 3.2):
openapi: "3.2.0"
info:
title: "Accounts API"
version: "1.0.0"
paths:
"/accounts/{id}":
get:
operationId: GetAccount
summary: "Fetch one account"
parameters:
- name: id
in: path
required: true
description: "account id"
schema:
type: integer
format: int32
minimum: 1
responses:
"200":
description: OK
content:
application/json:
schema:
"$ref": "#/components/schemas/accounts.Account"
components:
schemas:
accounts.Account:
type: object
properties:
id: {type: integer, format: int64}
email:
type: string
examples: ["ada@corp.io"]
plan:
type: string
enum: [free, pro]
trial_ends:
type: [string, "null"]Tired of swaggo/swag forcing you into Swagger 2.0? Generate OpenAPI 3.2 documents directly from your existing annotations - zero intermediate conversion steps.
oasgen is a from-scratch replacement for swag's front end: existing
annotations keep working, but the parser, schema resolver, and emitter are
all here. Because we own the parser, 3.2 constructs are parsed natively
(@Router /x [query], @tag.parent, webhooks, streaming itemSchema, …)
instead of being smuggled through x- hacks in a post-processor. And there
is no conversion pipeline to babysit: annotations go in, a valid 3.2 (or
3.1) document comes out.
go install github.com/indykite/openapi-parser/cmd/oasgen@latest
oasgen -d . -o openapi.yaml-d selects the directories to scan for annotations (recursively), -o the
output file - format inferred from the extension. Add -oas 3.1.0 if you
need 3.1 output.
Naming: the repository and Go module are
openapi-parser(github.com/indykite/openapi-parser);oasgenis the name of the command it builds (OAS generator), living atcmd/oasgensogo install github.com/indykite/openapi-parser/cmd/oasgen@latestinstalls a binary with the right name.
annotated .go files
│ gen.Parse(dirs)
▼
*gen.API version-agnostic model
│ api.Emit(EmitOptions{Version})
▼
OpenAPI 3.2.0 (or 3.1.0)
The pipeline is one file per stage: extract.go (go/ast → comment groups +
struct defs), lexer.go (lines → directives), parse.go (directives → model),
schema.go (Go types → schemas), emit.go (model → OpenAPI). The model in the
middle is the key design choice: annotation syntax and output spec version
evolve independently, so targeting 3.3 later means touching only the emitter.
go build -o oasgen ./cmd/oasgen
# or: go install github.com/indykite/openapi-parser/cmd/oasgen@latest
# Emit 3.2 from the current directory (and everything under it):
./oasgen -d . -o openapi.json
# YAML output (inferred from the extension, or force with -format yaml):
./oasgen -d ./internal/rest -o openapi.yaml
# Multiple dirs, 3.1 output:
./oasgen -d ./cmd/api,./internal/handlers -o openapi.json -oas 3.1.0How -d is scoped:
- Annotations are harvested recursively from each
-ddir (like swag). Generate one document per service: if several annotated services live under one-droot, they merge into a single document and their general-info blocks overwrite each other. - Types resolve against the whole Go module enclosing the first
-ddir, so cross-package references (model.Account) work without extra flags: no--parseDependency/--parseInternalequivalents needed. Outside a module, only the given dirs are indexed. Types from other modules aren't resolved and degrade to a permissiveobjectschema.
Library use:
import "github.com/indykite/openapi-parser/gen"
api, err := gen.Parse([]string{"./handlers"})
data, err := api.EmitJSON(gen.EmitOptions{Version: "3.2.0"}) // or api.EmitYAML(...)Beyond swag's annotation set, the resolver understands:
- Generics: swaggo bracket syntax
listResponse[AccountResponse]in annotations and Go generic types in struct fields; instantiations are registered asBase-Argcomponents. - Embedded structs: anonymous fields (including pointer embeds) promote their properties and required lists into the parent schema, like Go does.
- Named non-struct types:
type Labels []*Accountresolves inline to its underlying type. - Cross-package references: the whole module is indexed, so
model.Accountresolves through the referencing file's imports. - Multi-word security scheme names:
@securityDefinitions.apikey Bearer Tokenand@Security Bearer Tokenkeep the full name. @schemes: expands host-derived servers into one entry per scheme.- Constraints:
@Paramattributes (minimum(1) maximum(100) default(20) pattern(...)) and struct tags (minimum,maximum,minLength,maxLength,multipleOf,default) emit real JSON Schema keywords, andbinding:/validate:rules map too:oneof→enum,gte/lte/min/max/len→minimum/maxLength/minItems/… by type,gt/lt→exclusiveMinimum/exclusiveMaximum. - Typed scalars: examples, defaults, and enums are coerced to the field's
type (
example:"5"on an int emits5, not"5"). - Nullable pointers:
*Tadds"null"to the type array; pointer struct refs wrap asanyOf: [$ref, null]($ref allows no sibling keywords). - Request body content type: derived from
@Accept(one entry per MIME type) instead of hardcodingapplication/json. - Response descriptions: default to the HTTP status text when the annotation carries none (matches swag).
- Schema
x-extensions: swag'sextensions:"x-nullable,x-unit=kg,!x-hidden"struct tag (bare = true,!= false,x-prefix added if missing).
- Info & license:
@summary(info summary, 3.1+),@license.identifier(SPDX, 3.1+). - Document security:
@security <scheme>in the general block emits top-levelsecurity(operation@Securitystill overrides). - More security schemes:
@securityDefinitions.bearerauth(+@bearerFormat),@securityDefinitions.openidconnect(+@openIdConnectUrl),@securityDefinitions.mutualtls(3.1+). - Servers:
@server.url,@server.name(3.2),@server.description, and@server.variable name default "desc" enums(a,b)for URL templates. - Operation docs:
@externalDocs.url/@externalDocs.descriptioninside an operation block. - Parameter attributes:
deprecated(true),style(form),explode(false),allowReserved(true)alongside the schema constraints. - Response summary (3.2):
@ResponseSummary <code> <text>after the matching@Success/@Failureline. - operationId by default: when no
@IDis given, the documented handler function's name becomes theoperationId(@IDalways wins). - Scheme descriptions: a
@descriptionfollowing a@securityDefinitions.*block attaches to that scheme, not the info block (swag's positional rule).
- Streaming: a
@Produceof a sequential media type (event-stream/sse,jsonl,ndjson,json-seq) emits the response schema as 3.2itemSchema(each event's shape); 3.1 output falls back toschema. - Webhooks:
@Webhook account.created [post]in place of@Routerdocuments an outgoing event under the top-levelwebhooksmap (3.1+); the method defaults topost. $self:@self https://.../openapi.jsonsets the document's own URI (3.2 only).- OAuth2 device flow:
@securityDefinitions.oauth2.devicewith@deviceAuthorizationUrl/@tokenUrl; plus@refreshUrl,@oauth2MetadataUrl, and@deprecatedon any scheme. 3.2-only pieces are dropped cleanly from 3.1 output. - Plus the earlier ones:
@Router /x [query]first-class, non-standard verbs viaadditionalOperations, hierarchical tags (@tag.parent,@tag.kind).
oasparity is a spec-parity regression gate: it parses each service's
annotations with this module and compares the operation and schema inventories
against the checked-in baseline (docs/swagger.yaml), without needing swag
or a YAML dependency. "Service" is just the word for a check unit so any
directory with annotated Go code and its own baseline document counts as one,
whatever it is in your repo: a monorepo has many, a single-binary repo has
exactly one (its root). The baseline may be swag's Swagger 2.0 output (the
migration case) or a previously generated OpenAPI 3.x document (the regression
case). Missing/extra operations or missing schemas fail with exit 1; extra
oasgen components are allowed (unreferenced components are valid so swag
doesn't register embedded base types, we do).
Against a 2.0 baseline, constructs swag cannot express (webhooks, the query
method, additionalOperations verbs) are excluded from the comparison; against
a 3.x baseline they are compared too. 3.x baselines should be 3.2 output:
3.1 downgrades those methods to post, which would diff against the
annotations.
# Services are auto-discovered by the presence of docs/swagger.yaml:
make parity REPO=../my-platform
# Or directly, with an explicit service list and verbose extras:
go run ./cmd/oasparity -repo ../my-platform -services services/a,services/b -v
# A different swag output location:
go run ./cmd/oasparity -repo ../my-platform -docs api/swagger.yaml
# A single-binary repo: the repo root is the one "service":
go run ./cmd/oasparity -repo ../my-apiRun it against any repo you're migrating before releasing a parser change;
when it's green, that repo's swag init invocations can be pointed at
oasgen instead.