AsyncAPI 3.x → Go code generator. The AsyncAPI counterpart to oapi-codegen (same idea, different spec). Builds on go-jsonschema (imported as a library) for the JSON Schema → Go pass.
Generates, from one *.source.asyncapi.yaml, one Go package containing:
- Payload types for every message (via
go-jsonschema). - Typed Publisher with
Send<MessageName>(ctx, ...params, msg, opts ...SendOption) errorperaction: sendoperation (v0.1+). - Typed Subscriber with
<MessageName>Handlerinterface +Subscribe<MessageName>(ctx, ...params, handler) errorperaction: receiveoperation (v0.2+). PublishProperties+SendOption(WithPriority,WithExpirationMillis, …) honoring message/operation AMQP bindings (v0.3+).- Typed channel parameters — enums become
type X string+ const block (v0.4); pattern-validated parameters becometype X string+NewX/MustXconstructors that regex-check input (v0.4.1).
Minimal:
aapi-codegen SPEC.asyncapi.yamlDefaults: -o ./types.gen.go, package derived from the output directory's basename (with the v1/v2 version-folder rule prepending the parent — lib/schemas/job-message/v1 → jobmessagev1).
With overrides:
aapi-codegen -package PKG -o OUT.go SPEC.asyncapi.yaml
aapi-codegen -config aapi-codegen.config.yaml SPEC.asyncapi.yaml # rareCross-tree $ref → import mappings live inside the AsyncAPI spec, colocated with the contract:
x-aapi-codegen:
schema-packages:
- id: https://schemas.example.com/common/v1/header.schema.json
package: example.com/lib/schemas/common/v1
alias: commonv1$id matches a schema's $id keyword. When a payload $refs into a schema with that $id, the generated Go imports commonv1 instead of inlining the type.
Payloads can be declared inline in the spec — no separate schema file needed:
components:
schemas:
Tag:
type: object
required: [key, value]
properties: { key: {type: string}, value: {type: string} }
channels:
inlineDispatch:
messages:
InlineMessage:
payload:
type: object
required: [id, tags]
properties:
id: { type: string }
tags: { type: array, items: { $ref: '#/components/schemas/Tag' } }Payload Go type name defaults to the message key (InlineMessage); an inline title overrides. components/schemas types are generated once and shared.
Shared message wrappers work the same way via components.messages: define the envelope once, reference it via #/components/messages/X from any number of channels. Multiple references dedupe to one generated Go payload type.
By default, aapi-codegen emits UnmarshalJSON methods that enforce the JSON Schema constraints in the spec: required fields, additionalProperties: false, format keywords (uuid, email, uri, hostname, regex), enum, numeric and string bounds, etc. Invalid JSON is rejected at json.Unmarshal time with an error naming the violation.
Opt out per-spec when you need to hand-write your own UnmarshalJSON (e.g. to accept a legacy PascalCase wire format alongside the canonical camelCase — two UnmarshalJSON methods on the same type would be a compile error):
# inside the spec
x-aapi-codegen:
omit-validation: trueOr via the optional config file:
# aapi-codegen.config.yaml
omit-validation: trueEither source opts out; neither overrides the other's opt-out.
Requires Go 1.26+. The go.mod directive sets the floor; CI tracks the version pinned in .github/versions.env.
go test ./...The build is a single Go binary. The patched go-jsonschema fork is wired in via local replace in go.mod pointing at a sibling ../go-jsonschema checkout — clone both repos as siblings under one root.
PRs run go vet, go test -race, golangci-lint, and a goreleaser snapshot build via .github/workflows/development.yaml. Tag pushes (vX.Y.Z and vX.Y.Z-rc.N) trigger the release workflow.
For distribution to downstream consumers, see examples/use-aapi-codegen.sh — a download-and-cache script modelled on plheide/go-jsonschema's use-go-jsonschema.sh. Pinning the binary version this way avoids the go install/replace-directive incompatibility. Supports Linux, macOS, and Windows (via Git Bash / WSL).
Worked specs and assertions live under internal/test/:
| Fixture | Exercises |
|---|---|
widgetservice/ |
direct exchange, templated and literal addresses, cross-tree x-aapi-codegen.schema-packages import |
notificationservice/ |
topic exchange, single-parameter address |
inlineservice/ |
inline payloads + components.schemas |
sharedmsgservice/ |
components.messages referenced from multiple channels |
messagecollisionservice/ |
regression for inline + component-message keys colliding |
validationservice/ |
omit-validation opt-out |
consumerservice/ |
v0.2 — action: receive, <Msg>Handler interface, Subscribe<Msg>, ErrDrop semantics, queue-mode channels |
bindingsservice/ |
v0.3 — SendOption + message/operation AMQP bindings (priority, expiration, contentEncoding, messageType) |
enumparams/ |
v0.4 — typed channel parameters from schema.type: string + enum, dedup across publisher + subscriber |
validatedparams/ |
v0.4.1 — pattern-validated parameters with NewX/MustX constructors; omit-validation falls back to plain string |
sharedqueueservice/ |
v0.6 — routingKey-mode consumer whose x-aapi-codegen.queue.name differs from the channel address; Subscribe passes queue name + binding keys separately |
multimessagequeueservice/ |
v0.7 — several receive ops sharing one queue (distinct fixed routing keys, distinct message types); one combined Subscribe<Queue> binds all keys and dispatches by routing key |
aapi-codegen covers both publisher and subscriber AMQP code generation from AsyncAPI 3.x specs. Other protocol bindings (Kafka, WebSocket) are the main open scope.
AsyncAPI 3.x (spec)
-
asyncapi: 3.xversion detection -
info.title(used in generated publisher/subscriber doc comment) -
channels-
address— literal (e.g.widget.cancellation) -
address— templated, multi-parameter (e.g.{tenant}.{widgetType}) -
address— templated, single-parameter (e.g.{workflowName}) -
parameters—stringby default; v0.4 types fromschema.type: string + enum; v0.4.1 types fromschema.type: string + patternwithNewX/MustXconstructors -
messages— multi-message channels -
bindings.amqp— typed per channel
-
-
operations-
action: send— emits typedPublisher.Send<MessageName>(ctx, ...params, msg, opts ...SendOption) errormethods -
action: receive— emits<MessageName>Handlerinterface +Subscriber.Subscribe<MessageName>(ctx, ...params, handler) error(v0.2) -
operations.X.channel.$ref— internal ref resolution -
operations.X.messages[].$ref— internal ref resolution (exactly one message per operation)
-
-
components.schemas— shared types declared once, referenced from multiple payloads via#/components/schemas/X -
components.messages— message-wrapper reuse via#/components/messages/X. Shared wrappers produce one Go payload type, not one per reference. Operation-level refs may target either#/channels/.../messages/Y(channel-scoped) or#/components/messages/Y(component-scoped). -
messages.X.payloadvia$refto external JSON Schema file -
messages.X.payloadinline (Go type name defaults to message key; inlinetitleoverrides) - Spec extension
x-aapi-codegen.schema-packages— cross-tree$ref→ Go-import mapping - Spec extension
x-aapi-codegen.omit-publishers/omit-subscribers(v0.2) — opt out of either generated section even when the spec has matching operations -
oneOfpayload dispatch (typedSendmethod per discriminated variant) — IR has the placeholder; templates pending
AMQP binding (spec)
-
exchange.name— emitted as a string literal in the publisher body -
exchange.type: direct,topic,fanout,headers -
bindingVersion— preserved in IR for future use -
is: routingKey— address is the routing key on Exchange (publisher mode) -
is: queue(v0.2) — address is the queue name; subscriber emitsSubscribe<Msg>againstbindings.amqp.queue.{name, durable, autoDelete, exclusive} - Message-level bindings (v0.3):
contentEncoding,messageType - Operation-level bindings (v0.3):
priority,expiration - AMQP delivery headers / correlation-id / reply-id surfaced to receive handlers — subscriber currently passes only
(ctx, routingKey, body)to the dispatch wrapper
- Publisher (example):
Transport.Publish(ctx, exchange, routingKey string, body []byte, props PublishProperties) error— adapt your*amqp091.Channelby wrapping it.Send<MessageName>(ctx, ...params, msg, opts ...SendOption) error— spec bindings become defaults; opts override per call.- Helpers:
WithContentType,WithContentEncoding,WithMessageType,WithPriority,WithExpirationMillis.
- Subscriber (example):
SubscribeTransport.Subscribe(ctx, queueName string, bindingKeys []string, handler func(ctx, routingKey, body) error) error(v0.6) — blocks until ctx cancellation or fatal transport error.queueNamecomes frombindings.{amqp,x-aapi-codegen}.queue.name(falling back to the address);bindingKeysalways derive from the channel address, so a shared queue bound to a fixed routing key (queue ≠ key) is expressible.<MessageName>Handler.Handle<MessageName>(ctx, msg) error— implement on your consumer.- Ack semantics:
nil→ ack;errors.Is(err, ErrDrop)→ nack-no-requeue (poison); any other err → nack-with-requeue. The dispatch wrapper joinsjson.Unmarshalfailures withErrDropso malformed payloads can never loop forever.
- Kafka
- WebSocket
- HTTP / SSE
- AMQP 1.0 (Solace, Azure Service Bus, …)
The IR's Binding interface is binding-agnostic; adding a new binding is "add a typed struct + a template branch", no IR refactor needed.
aapi-codegen delegates the JSON Schema → Go pass to plheide/go-jsonschema (a patched fork of omissis/go-jsonschema). aapi-codegen passes --strict-additional-properties=respect-schema, --validate-formats=all, --struct-name-from-title, --tags json, and --capitalization per the configured initialism list. See the fork's README for the supported JSON Schema surface (object/array/primitives, $defs, cross-file $ref, oneOf/allOf/anyOf, enum, format validation, etc.).
-
requiredfields enforced atjson.Unmarshaltime (default; opt-out viaomit-validation) -
additionalProperties: falseenforced (rejects extra keys) -
formatvalidation:uuid,email,uri,uri-reference,hostname,regex -
enum, numeric bounds, string length/pattern, array constraints (inherited from go-jsonschema) - Per-spec / per-config opt-out via
x-aapi-codegen.omit-validation: true(oromit-validation: trueinaapi-codegen.config.yaml) — for consumers that hand-write their ownUnmarshalJSON
- Minimal invocation:
aapi-codegen SPEC.asyncapi.yaml -
-o OUT.go(defaults to./types.gen.go) -
-package PKG(defaults: basename of output dir, withvNparent-prepending rule) -
-config FILE.yaml(optional; for repos that want one canonical knob) - Additive
capitalizationoverDefaultInitialisms = [ID, URL] -
omit-validation: true(config or spec extension) suppresses generatedUnmarshalJSON
-
go build ./cmd/aapi-codegenfrom a sibling-checkout layout - GoReleaser config + GitHub Actions workflow for release archives (
.goreleaser.yaml,.github/workflows/release.yaml) - Download-and-cache consumer script (
examples/use-aapi-codegen.sh) -
go installfrom a fully-resolved module — blocked on dropping thereplacedirective (requires either upstream-merging the go-jsonschema patches, or remapping the fork's module path).