AsyncAPI Generator for Java / Spring-Boot

Maven Central GitHub

ZenWave AsyncAPI Generator

The ZenWave AsyncAPI Generator solves a long-standing issue in event-driven Java applications:

👉 keeping message models and channel contracts fully aligned with their AsyncAPI specification.

It provides build-time read-only code generation from AsyncAPI files sourced from their canonical locations: local files, classpath resources, or authenticated remote URLs.

This approach eliminates API drift by enforcing the AsyncAPI file as the single source of truth for message schemas, channel definitions, and producer or consumer interfaces.

All generated classes are strongly typed and annotated for validation, and any breaking change in the specification results in a compile-time error.

This model aligns with the proven pattern used by the OpenAPI Maven Generator plugin, which solved API drift for OpenAPI in the Java ecosystem, generating non-editable interfaces and DTOs, and ensures contract consistency across consumers and producers.

You just need to focus on implementing your business logic, and configuring either Spring Cloud Streams or Spring Kafka for message transport.

A complete working example is available in the zenwave-playground repository: asyncapi-shopping-cart.

Features

Generated Code

  • Complete Producer and Consumer implementations for Java and Spring Boot with thin wrappers around:
    • Spring Cloud Stream for multi-broker support
    • Spring Kafka for native Kafka integrations
  • JSON DTOs generated via jsonschema2pojo library
  • Avro DTOs generated via Apache Avro library

Supported Capabilities

  • AsyncAPI v2 and v3
  • Local files, classpath files, and authenticated remote URLs
  • DTO generation for JSON Schema and Avro
  • Automatic Avro schema ordering for Avro versions prior to 1.12.0
  • Strongly typed header objects from AsyncAPI message definitions

Advanced Patterns

  • Transactional Outbox pattern using Spring Modulith
  • Role reversal to generate only the side you need (provider or client)
  • Operation-level filtering for selective interface generation
  • Automatic header mapping at runtime through the x-runtime-expression extension

Quick Configuration

Use these essential configuration options to get the generator running quickly:

  • inputSpec: Path or URL for the AsyncAPI specification
  • role: provider (default) or client
  • templates: SpringCloudStream (default) or SpringKafka
  • modelPackage: Java package for generated DTOs (required for JSON Schema)
  • producerApiPackage: Java package for producer interfaces (provider role)
  • consumerApiPackage: Java package for consumer interfaces (client role)
  • avroCompilerProperties.imports: Comma-separated Avro files or folders
  • operationIds: Filter to generate only selected operations
  • authentication: Credentials for authenticated remote AsyncAPI files

Advanced customization is available through:

  • avroCompilerProperties.xxx: Settings passed to the underlying Avro compiler
  • jsonschema2pojo.xxx: Settings passed to jsonschema2pojo

Why ZenWave doesn’t source Spring Cloud Stream / Spring Kafka config from AsyncAPI

ZenWaveSDK provides all the building blocks to prevent API drift, keeping your source code aligned with your AsyncAPI specification. But it does not attempt to auto-configure Spring Cloud Stream or Spring Kafka from the AsyncAPI file.

While configuring Spring Cloud Stream or Spring Kafka from AsyncAPI would be convenient, it is not currently feasible.

AsyncAPI, even with specific bindings like Kafka bindings, does not yet provide enough information to fully configure Spring Kafka or Spring Cloud Stream:

  • no standard fields for acks, transaction.id, idempotence, and similar settings
  • Avro SerDes configuration is too ambiguous to infer automatically
  • Kafka bindings define clientId and consumerGroupId per operation, although these are typically application-wide
  • you can configure a schema registry endpoint but not compatibility modes (BACKWARD, FORWARD, etc.) for subjects or topics

This is why ZenWave does not attempt full auto-configuration from AsyncAPI. The specification is not expressive enough for these frameworks.

Still, this is a one-time operation that belongs in your Spring configuration files, where it can be controlled explicitly, versioned with your application, and tuned independently from your AsyncAPI contract.

Command Line Usage

jbang zw -p AsyncAPIGeneratorPlugin \
authentication[0].key=API_KEY \
authentication[0].value=API_KEY_VALUE \
apiFile=https://raw.githubusercontent.com/ZenWave360/zenwave-playground/refs/heads/main/examples/asyncapi-shopping-cart/apis/asyncapi.yml \
consumerApiPackage=io.example.api.consumer \
producerApiPackage=io.example.api.producer \
modelPackage=io.example.api.model \
avroCompilerProperties.imports="\
https://raw.githubusercontent.com/ZenWave360/zenwave-playground/refs/heads/main/examples/asyncapi-shopping-cart/apis/avro/Item.avsc,\
https://raw.githubusercontent.com/ZenWave360/zenwave-playground/refs/heads/main/examples/asyncapi-shopping-cart/apis/avro/ShoppingCart.avsc" \
targetFolder=target/generated-sources

Maven Usage

Properties Configuration

<properties>
<asyncapiPrefix>classpath:io/example/asyncapi/shoppingcart/apis</asyncapiPrefix>
<asyncapi.inputSpec>${asyncapiPrefix}/asyncapi.yml</asyncapi.inputSpec>
<asyncapi.avro.imports>
${asyncapiPrefix}/avro/
</asyncapi.avro.imports>
<zenwave.asyncapiGenerator.templates>SpringKafka</zenwave.asyncapiGenerator.templates>
<asyncApiProducerApiPackage>${basePackage}.events</asyncApiProducerApiPackage>
<asyncApiConsumerApiPackage>${basePackage}.commands</asyncApiConsumerApiPackage>
</properties>

Plugin Configuration

<plugin>
<groupId>io.zenwave360.sdk</groupId>
<artifactId>zenwave-sdk-maven-plugin</artifactId>
<version>${zenwave.version}</version>
<configuration>
<inputSpec>${asyncapi.inputSpec}</inputSpec>
<skip>false</skip>
<addCompileSourceRoot>true</addCompileSourceRoot>
<addTestCompileSourceRoot>true</addTestCompileSourceRoot>
<authentication>
<authentication>
<key>API_KEY</key>
<value>XXXXXX</value>
</authentication>
</authentication>
</configuration>
<executions>
<execution>
<id>generate-asyncapi</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<generatorName>AsyncAPIGenerator</generatorName>
<configOptions>
<role>provider</role>
<templates>${zenwave.asyncapiGenerator.templates}</templates>
<modelPackage>${asyncApiModelPackage}</modelPackage> <!-- required only for json-schema, otherwise it uses avro package -->
<producerApiPackage>${asyncApiProducerApiPackage}</producerApiPackage>
<consumerApiPackage>${asyncApiConsumerApiPackage}</consumerApiPackage>
<avroCompilerProperties.imports>${asyncapi.avro.imports}</avroCompilerProperties.imports><!-- comma separated list of avro files or folders -->
</configOptions>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>io.zenwave360.sdk.plugins</groupId>
<artifactId>asyncapi-generator</artifactId>
<version>${zenwave.version}</version>
</dependency>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro-compiler</artifactId>
<version>${avro.version}</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</plugin>

Gradle Usage

val asyncapiPrefix = "classpath:io/example/asyncapi/shoppingcart/apis"
val asyncapiInputSpec = "$asyncapiPrefix/asyncapi.yml"
val asyncapiAvroImports = "$asyncapiPrefix/avro/"
val asyncapiGeneratorTemplates = "SpringKafka"
val asyncApiProducerApiPackage = "io.zenwave360.examples.events"
val asyncApiConsumerApiPackage = "io.zenwave360.examples.commands"
plugins {
java
id("dev.jbang") version "0.3.0"
}
tasks.register<dev.jbang.gradle.tasks.JBangTask>("generateAsyncApiProvider") {
group = "asyncapi"
description = "Generates Producer and Consumer code from AsyncAPI specification"
script.set("io.zenwave360.sdk:zenwave-sdk-cli:RELEASE")
jbangArgs.set(listOf(
"--deps=" +
"org.slf4j:slf4j-simple:1.7.36," +
"io.zenwave360.sdk.plugins:asyncapi-generator:RELEASE," +
"org.apache.avro:avro-compiler:1.11.1"
))
args.set(listOf(
"-p", "AsyncAPIGenerator",
"role=provider",
"templates=$asyncapiGeneratorTemplates",
"apiFile=$asyncapiInputSpec",
"targetFolder=${layout.buildDirectory.dir("generated-sources/zenwave").get().asFile.absolutePath}",
"transactionalOutbox=modulith",
"modelPackage=$asyncApiModelPackage",
"producerApiPackage=$asyncApiProducerApiPackage",
"consumerApiPackage=$asyncApiConsumerApiPackage",
"avroCompilerProperties.imports=$asyncapiAvroImports"
))
}
sourceSets {
main {
java {
srcDir(layout.buildDirectory.dir("generated-sources/zenwave/src/main/java").get().asFile)
}
}
test {
java {
srcDir(layout.buildDirectory.dir("generated-sources/zenwave/src/test/java").get().asFile)
}
}
}

Configuring Spring Cloud Stream Bindings

ZenWaveSDK does not derive Spring Cloud Stream bindings from the AsyncAPI specification. Binding configuration must be provided explicitly in your application.yml.

Producing Messages

spring:
cloud:
stream:
bindings:
shopping-cart-channel-out: # AsyncAPI channel name (kebab-case) with the `-out` suffix (no `-0`)
destination: shopping-cart
content-type: application/*+avro
producer:
use-native-encoding: true

Consuming Messages

spring:
cloud:
stream:
kafka.binder.enable-observation: true
bindings:
shopping-cart-channel-in-0: # AsyncAPI channel name (kebab-case) with the `-in-0` suffix
destination: shopping-cart
group: shopping-cart-group

Note on binding name suffixes: Producer and consumer bindings use different naming conventions. Producers use the binding name exactly as declared (for example, -out), while consumers use the indexed form (-in-0) required by Spring Cloud Stream. The AsyncAPI generator does not append -0 to producer bindings (unlike the legacy SpringCloudStream3Plugin). This behavior is kept for backward compatibility.

Configuring Spring Kafka Bindings

ZenWaveSDK also does not generate Spring Kafka configuration from the AsyncAPI file. Topic and consumer settings must be defined manually in application.yml.

app:
kafka:
topics:
shopping-cart-channel: # AsyncAPI channel name (kebab-case)
topic: shopping-cart
groupId: shopping-cart-group # consumers only
containerFactory: shopping-cart-container-factory # consumers only

Configuration Options

OptionDescriptionTypeDefaultValues
apiFileAPI Specification FileURI
roleProject role: provider/clientAsyncapiRoleTypeproviderprovider, client
templatesTemplates to use for code generation.StringSpringCloudStreamSpringCloudStream, SpringKafka, FQ Class Name
modelPackageJava Models package nameString
producerApiPackageJava API package name for outbound (producer) services. It can override apiPackage for producers.String{{apiPackage}}
consumerApiPackageJava API package name for inbound (consumer) services. It can override apiPackage for consumer.String{{apiPackage}}
apiPackageJava API package, if producerApiPackage and consumerApiPackage are not set.String
operationIdsOperation ids to include in code generation. Generates code for ALL if left emptyList[]
excludeOperationIdsOperation ids to exclude in code generation. Skips code generation if is not included or is excluded.List[]
transactionalOutboxTransactional outbox type for message producers.TransactionalOutboxTypenonenone, modulith
jsonschema2pojoJsonSchema2Pojo settings for downstream library (docs)Map
avroCompilerPropertiesAvro Compiler PropertiesAvroCompilerPropertiesSee AvroCompilerProperties
avroCompilerProperties.sourceDirectoryAvro schema file or folder containing avro schemasFile
avroCompilerProperties.importsAvro schema files or folders containing avro schemas. It supports local files/folders, classpath: files/folders or https:// file resources.List
avroCompilerProperties.includesA set of Ant-like inclusion patterns used to select files from the source tree that are to be processed. By default, the pattern **/*.avsc is used to include all avro schema files.List[**/*.avsc]
avroCompilerProperties.excludesA set of Ant-like exclusion patterns used to prevent certain files from being processed. By default, this set is empty such that no files are excluded.List
avroCompilerProperties.customLogicalTypeFactoriesCustom Logical Type FactoriesList
avroCompilerProperties.customConversionsCustom ConversionsList
componentPrefixPrefix used in to reference this component in @Component and application.ymlString
componentSuffixSuffix used in to reference this component in @Component and application.ymlString
generatedAnnotationClassAnnotation class to mark generated code (e.g. org.springframework.aot.generate.Generated). When retained at runtime, this prevents code coverage tools like Jacoco from including generated classes in coverage reports.String
authenticationAuthentication configuration values for fetching remote resources.List[]
targetFolderTarget folder to generate code to. If left empty, it will print to stdout.File
modelNamePrefixSets the prefix for model classes and enumsString
modelNameSuffixSets the suffix for model classes and enumsString
runtimeHeadersPropertyAsyncAPI extension property name for runtime auto-configuration of headers.Stringx-runtime-expression
messageNamesMessage names to include in code generation (combined with operationIds). Generates code for ALL if left emptyList[]
sourceFolderSource folder inside folder to generate code to.Stringsrc/main/java
bindingTypesBinding names to include in code generation. Generates code for ALL bindings if left emptyList
avroFilesList of avro schema files to generate code for. It is alternative to sourceDirectory and imports.List
skipProducerImplementationGenerate only the producer interface and skip the implementation.booleanfalse
exposeMessageWhether to expose underlying spring Message to consumers or not.booleanfalse
useEnterpriseEnvelopeInclude support for enterprise envelop wrapping/unwrapping.booleanfalse
envelopeJavaTypeExtensionNameAsyncAPI Message extension name for the envelop java type for wrapping/unwrapping.Stringx-envelope-java-type
methodAndMessageSeparatorTo avoid method erasure conflicts, when exposeMessage or reactive style this character will be used as separator to append message payload type to method names in consumer interfaces.String$
consumerPrefixConsumer object class name prefixString
consumerSuffixConsumer object class name suffixStringConsumer
consumerServicePrefixBusiness/Service interface prefixStringI
consumerServiceSuffixBusiness/Service interface suffixStringConsumerService
formatterCode formatter implementationFormatterspalantirpalantir, spring, google
skipFormattingSkip java sources output formattingbooleanfalse
haltOnFailFormattingHalt on formatting errorsbooleantrue

Getting Help

jbang zw -p io.zenwave360.sdk.plugins.AsyncAPIGeneratorPlugin --help