Help us shape the future of Go! Take the 2026 Go Developer Survey

gogen-asyncapi

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT

README

gogen-asyncapi

Go Reference CI coverage version license imported by

Feed it an AsyncAPI 3 document and it spits out Go types, strict JSON codecs and a client for your WebSocket API. Run it from go generate with a small config file and the path to your spec, and the generated file lands next to your code.

Decoding is strict. A frame either matches its schema or you get an error that names every message it tried and why each one said no.

Contents

Install

As a tool in your module, so go tool gogen-asyncapi runs the version your go.mod pins:

go get -tool github.com/psyb0t/gogen-asyncapi/cmd/gogen-asyncapi@latest

Or as a binary:

go install github.com/psyb0t/gogen-asyncapi/cmd/gogen-asyncapi@latest

Quick start

Drop a config.yaml and a gen.go where the code should go:

# config.yaml
package: chat
output: chat.gen.go
// gen.go
package chat

//go:generate go tool gogen-asyncapi --config=config.yaml ../../api/asyncapi.yaml

Run go generate ./... and chat.gen.go appears. The spec is the last argument, and paths are relative to the directory go generate runs in. -package and -o override the config, or stand in for it:

gogen-asyncapi -package=chat -o=chat.gen.go asyncapi.yaml

Config

package: chat             # Go package name, required
output: chat.gen.go       # defaults to asyncapi.gen.go
generate:
  models: true            # types, codecs, validation (on by default)
  client: true            # decoding, Handler, Dispatch, Client (on by default, needs models)
output-options:
  message-matchers:       # see "Telling messages apart"
    Notice:
      /messages/0/type: notice

Examples

Every snippet below comes from examples/chat: the spec is chat.yaml, the output is chat.gen.go, and chat_test.go runs all of it.

Structs and strict decoding
JoinRequest:
  type: object
  additionalProperties: false
  required: [type, id, room]
  properties:
    type: { type: string, const: join }
    id: { type: string, format: uuid }
    room: { type: string, minLength: 1, maxLength: 64, pattern: '^[a-z0-9-]+$' }
    nickname: { type: string }
type JoinRequest struct {
	Type     string  `json:"type"`
	ID       string  `json:"id"`
	Room     string  `json:"room"`
	Nickname *string `json:"nickname,omitempty"`
}

Required fields are values, optional ones are pointers. The const fills itself in when you marshal:

data, _ := json.Marshal(chat.JoinRequest{ID: "1", Room: "lobby"})
// {"type":"join","id":"1","room":"lobby"}

Decoding checks everything the schema says, and the errors wrap ctxerrors/commerr sentinels:

var req chat.JoinRequest

err := json.Unmarshal([]byte(`{"type":"join","id":"1"}`), &req)
errors.Is(err, commerr.ErrRequiredFieldNotSet) // true, room is missing

err = json.Unmarshal([]byte(`{"type":"join","id":"1","room":"Lobby!"}`), &req)
errors.Is(err, commerr.ErrValidationFailed) // true, pattern

err = json.Unmarshal([]byte(`{"type":"join","id":"1","room":"x","extra":1}`), &req)
errors.Is(err, commerr.ErrValidationFailed) // true, additionalProperties: false

Every type also has Validate(), so you can check a value you built yourself before sending it.

Enums
role:
  type: string
  enum: [owner, member, guest]
type MemberRole string

const (
	MemberRoleOwner  MemberRole = "owner"
	MemberRoleMember MemberRole = "member"
	MemberRoleGuest  MemberRole = "guest"
)

A value outside the list fails decoding with commerr.ErrValidationFailed.

Unions
ServerEvent:
  oneOf:
    - $ref: '#/components/schemas/MessagePosted'   # type: { const: message.posted }
    - $ref: '#/components/schemas/MemberJoined'    # type: { const: member.joined }
    - $ref: '#/components/schemas/Ping'            # type: { const: ping }
  discriminator: type

The union gets FromX, AsX, Discriminator and Value. Value decodes whichever variant the type field picks:

var event chat.ServerEvent
if err := json.Unmarshal(frame, &event); err != nil {
	return err // unknown type, or the picked variant is invalid
}

value, err := event.Value()
if err != nil {
	return err
}

switch e := value.(type) {
case chat.MessagePosted:
	fmt.Println(e.Author.ID, "said", e.Text)
case chat.MemberJoined:
	fmt.Println(e.Member.ID, "joined")
case chat.Ping:
	// keepalive
}

Building one goes the other way:

var event chat.ServerEvent
err := event.FromPing(chat.Ping{}) // marshals to {"type":"ping"}

Without a discriminator, a property every variant pins with const does the same job. With neither, exactly one variant has to decode.

JSON inside a string

Some APIs send a JSON document as a string field. contentMediaType plus contentSchema says what is inside:

meta:
  type: string
  contentMediaType: application/json
  contentSchema:
    $ref: '#/components/schemas/AttachmentMeta'
meta, err := attachment.MetaValue() // decodes and validates the string as AttachmentMeta

err = attachment.SetMetaValue(chat.AttachmentMeta{Size: 42}) // encodes it back into the string
Sending

Every message of a send operation becomes a client method. It validates the payload, encodes it and writes it to the connection:

conn, err := gorillaws.Dial(ctx, chat.ServerProductionURL, gorillaws.WithBearerToken(token))
if err != nil {
	return err
}

client := chat.NewClient(conn)
defer client.Close()

err = client.JoinRoom(ctx, chat.JoinRequest{ID: id, Room: "lobby"})

gorillaws.Dial also takes WithHeader, WithQueryParam, WithSubprotocols, WithBinaryFrames, WithReadLimit and WithDialer. Any type with WriteMessage, ReadMessage and Close works as a transport.

Receiving

Messages from receive operations and from replies land in a Handler. Embed UnimplementedHandler and write only the methods you care about:

type handler struct {
	chat.UnimplementedHandler
}

func (handler) HandleEvent(ctx context.Context, event chat.ServerEvent) error {
	value, err := event.Value()
	if err != nil {
		return err
	}

	log.Printf("event: %+v", value)

	return nil
}

func (handler) HandleAck(ctx context.Context, ack chat.Ack) error {
	if !ack.Ok {
		log.Printf("request %s failed", ack.ID)
	}

	return nil
}

err := client.Run(ctx, handler{}) // reads until ctx ends or the connection drops

Frames that match nothing go to HandleUnknownMessage with the reason. The default logs a warning and keeps reading. Without the client, chat.DecodeReceivedMessage(frame) and chat.Dispatch(ctx, h, msg) do the same two steps by hand.

Telling messages apart

DecodeReceivedMessage works through the messages the client receives:

  1. Messages whose matchers all hold. Matchers are the const values on the payload's top-level properties, plus your message-matchers.
  2. The remaining messages, decoded strictly. Exactly one has to decode.
  3. Messages with an opaque payload, like an Avro schema kept as raw JSON.

Nothing matching gives asyncapirt.ErrUnknownMessage with every candidate's reason joined in. Two matches give asyncapirt.ErrAmbiguousMessage.

When two messages have the same shape and only a value tells them apart, point a matcher at that value with a JSON pointer:

output-options:
  message-matchers:
    Notice:
      /messages/0/type: notice
    Alert:
      /messages/0/type: alert
Servers and channels

From examples/kitchensink:

servers:
  production:
    host: '{region}.example.com:{port}'
    pathname: /v{version}/ws
    protocol: wss
    variables:
      region: { enum: [eu, us], default: eu }
      port: { enum: ['443', '8443'], default: '443' }
      version: { default: '2' }
channels:
  commands:
    address: tenants/{tenantId}/commands
kitchensink.ServerProductionURL                         // "wss://eu.example.com:443/v2/ws"
kitchensink.ServerProductionURLWith("us", "8443", "1")  // "wss://us.example.com:8443/v1/ws"
kitchensink.CommandsAddress("acme")                     // "tenants/acme/commands"
kitchensink.CommandsAddressTemplate                     // "tenants/{tenantId}/commands"

What gets generated

Spec Go
object schema struct with strict UnmarshalJSON, Validate, and MarshalJSON when it has consts
enum named type plus constants
oneOf / anyOf union type with FromX, AsX, Value
allOf one merged struct
contentSchema string <Field>Value() and Set<Field>Value()
message Message<Name> constant, headers type, correlation ID location
send operation Client method
receive operation, reply Handler method, DecodeReceivedMessage, Dispatch, Client.Run
server Server<Name>URL, plus URLTemplate and URLWith for variables
channel <Channel>AddressTemplate, plus <Channel>Address(...) for parameters

Every object and keyword, one row each, is in docs/spec-coverage.md.

Example packages

  • examples/chat: the whole client, tested end to end over an in-memory transport.
  • examples/shapes: schema edge cases, plus an OpenAPI 3.0 payload with nullable and a discriminator mapping.
  • examples/kitchensink: every AsyncAPI 3.0 object in one document, valid against the official JSON Schema.
  • examples/official: the 19 examples from the AsyncAPI spec repository, generated as they are. CI builds them and checks they match the generator.

Development

Everything runs in the Dockerfile.dev image. You need Docker and Make.

make help           # list targets
make generate       # regenerate the examples
make lint           # shellcheck, shfmt, go fix, golangci-lint
make test-coverage  # go test -race with a 90% coverage gate
make sec            # gitleaks, govulncheck, semgrep

License

MIT. The specs in examples/official come from the AsyncAPI specification repository under Apache 2.0.

Directories

Path Synopsis
Package asyncapirt is the support code that gogen-asyncapi generated packages call: validation helpers, strict object decoding, incoming message matching, and the transport interface the generated client sends and receives through.
Package asyncapirt is the support code that gogen-asyncapi generated packages call: validation helpers, strict object decoding, incoming message matching, and the transport interface the generated client sends and receives through.
gorillaws
Package gorillaws is an asyncapirt.Transport over gorilla/websocket.
Package gorillaws is an asyncapirt.Transport over gorilla/websocket.
cmd
gogen-asyncapi command
Command gogen-asyncapi generates Go types, strict JSON codecs and a client from an AsyncAPI 3 document:
Command gogen-asyncapi generates Go types, strict JSON codecs and a client from an AsyncAPI 3 document:
examples
chat
Package chat is generated from chat.yaml, a spec that uses every feature gogen-asyncapi supports.
Package chat is generated from chat.yaml, a spec that uses every feature gogen-asyncapi supports.
kitchensink
Package kitchensink is generated from kitchensink.yaml, which uses every AsyncAPI 3.0 object and every schema keyword the generator reads.
Package kitchensink is generated from kitchensink.yaml, which uses every AsyncAPI 3.0 object and every schema keyword the generator reads.
official/adeo_kafka_request_reply
Package adeo_kafka_request_reply is generated from the official AsyncAPI example adeo-kafka-request-reply-asyncapi.yml.
Package adeo_kafka_request_reply is generated from the official AsyncAPI example adeo-kafka-request-reply-asyncapi.yml.
official/anyof
Package anyof is generated from the official AsyncAPI example anyof-asyncapi.yml.
Package anyof is generated from the official AsyncAPI example anyof-asyncapi.yml.
official/application_headers
Package application_headers is generated from the official AsyncAPI example application-headers-asyncapi.yml.
Package application_headers is generated from the official AsyncAPI example application-headers-asyncapi.yml.
official/correlation_id
Package correlation_id is generated from the official AsyncAPI example correlation-id-asyncapi.yml.
Package correlation_id is generated from the official AsyncAPI example correlation-id-asyncapi.yml.
official/gitter_streaming
Package gitter_streaming is generated from the official AsyncAPI example gitter-streaming-asyncapi.yml.
Package gitter_streaming is generated from the official AsyncAPI example gitter-streaming-asyncapi.yml.
official/kraken_websocket_request_reply_message_filter_in_reply
Package kraken_websocket_request_reply_message_filter_in_reply is generated from the official AsyncAPI example kraken-websocket-request-reply-message-filter-in-reply-asyncapi.yml.
Package kraken_websocket_request_reply_message_filter_in_reply is generated from the official AsyncAPI example kraken-websocket-request-reply-message-filter-in-reply-asyncapi.yml.
official/kraken_websocket_request_reply_multiple_channels
Package kraken_websocket_request_reply_multiple_channels is generated from the official AsyncAPI example kraken-websocket-request-reply-multiple-channels-asyncapi.yml.
Package kraken_websocket_request_reply_multiple_channels is generated from the official AsyncAPI example kraken-websocket-request-reply-multiple-channels-asyncapi.yml.
official/mercure
Package mercure is generated from the official AsyncAPI example mercure-asyncapi.yml.
Package mercure is generated from the official AsyncAPI example mercure-asyncapi.yml.
official/not
Package not is generated from the official AsyncAPI example not-asyncapi.yml.
Package not is generated from the official AsyncAPI example not-asyncapi.yml.
official/oneof
Package oneof is generated from the official AsyncAPI example oneof-asyncapi.yml.
Package oneof is generated from the official AsyncAPI example oneof-asyncapi.yml.
official/operation_security
Package operation_security is generated from the official AsyncAPI example operation-security-asyncapi.yml.
Package operation_security is generated from the official AsyncAPI example operation-security-asyncapi.yml.
official/rpc_client
Package rpc_client is generated from the official AsyncAPI example rpc-client-asyncapi.yml.
Package rpc_client is generated from the official AsyncAPI example rpc-client-asyncapi.yml.
official/rpc_server
Package rpc_server is generated from the official AsyncAPI example rpc-server-asyncapi.yml.
Package rpc_server is generated from the official AsyncAPI example rpc-server-asyncapi.yml.
official/simple
Package simple is generated from the official AsyncAPI example simple-asyncapi.yml.
Package simple is generated from the official AsyncAPI example simple-asyncapi.yml.
official/slack_rtm
Package slack_rtm is generated from the official AsyncAPI example slack-rtm-asyncapi.yml.
Package slack_rtm is generated from the official AsyncAPI example slack-rtm-asyncapi.yml.
official/streetlights_kafka
Package streetlights_kafka is generated from the official AsyncAPI example streetlights-kafka-asyncapi.yml.
Package streetlights_kafka is generated from the official AsyncAPI example streetlights-kafka-asyncapi.yml.
official/streetlights_mqtt
Package streetlights_mqtt is generated from the official AsyncAPI example streetlights-mqtt-asyncapi.yml.
Package streetlights_mqtt is generated from the official AsyncAPI example streetlights-mqtt-asyncapi.yml.
official/streetlights_operation_security
Package streetlights_operation_security is generated from the official AsyncAPI example streetlights-operation-security-asyncapi.yml.
Package streetlights_operation_security is generated from the official AsyncAPI example streetlights-operation-security-asyncapi.yml.
official/websocket_gemini
Package websocket_gemini is generated from the official AsyncAPI example websocket-gemini-asyncapi.yml.
Package websocket_gemini is generated from the official AsyncAPI example websocket-gemini-asyncapi.yml.
shapes
Package shapes is generated from shapes.yaml, which covers the schema forms the chat example does not: unions without a discriminator keyword, primitive variants, typed maps, nullable fields, recursive schemas and numeric enums.
Package shapes is generated from shapes.yaml, which covers the schema forms the chat example does not: unions without a discriminator keyword, primitive variants, typed maps, nullable fields, recursive schemas and numeric enums.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL