roas-asyncapi
Rust implementation of the AsyncAPI Specification (v2.6 / v3.0 / v3.1): parse and validate AsyncAPI documents.
An AsyncAPI document describes an event-driven API: the channels an application sends to and receives from, the messages that travel over them, and the servers and protocols that carry them. It is the event-driven counterpart to an OpenAPI description.
This crate is a sibling of roas (the typed parser / validator / merger for OpenAPI 2.0–3.2), roas-overlay, and roas-arazzo — whose v1.1 workflows can point at the AsyncAPI documents this crate models. It provides the typed document model plus a Validate framework that collects every diagnostic in one pass.
Versions
| AsyncAPI version | Feature flag | Status | Notes |
|---|---|---|---|
| 3.0 | v3_0 |
✅ implemented | — |
| 3.1 | v3_1 (default) |
✅ implemented | Adds its own schemaFormat values and the ros2 bindings |
| 2.6 | v2_6 |
✅ implemented | The pre-v3 model: channels keyed by path, publish / subscribe |
v2_6, v3_0, and v3_1 are independent — enable whichever you need. With v3_0 and v3_1 both enabled, an impl From<v3_0::Document> for v3_1::Document upconverts a 3.0 document; since 3.1 left the object model untouched, nothing is dropped or approximated and only the asyncapi version string changes. With v2_6 and v3_0 both enabled, v3_0::from_v2_6::convert converts a 2.6 document — v3 reorganized the document rather than extending it, so that conversion is genuinely lossy and returns a ConversionReport saying where a name had to be invented and what had nowhere to go.
Quick start
use EnumSet;
use Document;
use Validate;
let doc: Document = from_str.unwrap;
doc.validate.expect;
assert_eq!;
YAML documents work the same way — parse with serde_yaml_ng (or any other YAML crate) into Document.
Validation
Validate::validate returns every diagnostic it finds rather than failing on the first one. Diagnostics carry a JSONPath-flavor path (e.g. #.channels.lightMeasured.parameters). Beyond required / non-empty fields and the component-key pattern (^[A-Za-z0-9\.\-_]+$), the checks are:
- Cross-reference integrity — an operation's
channelnames a declared channel; itsmessagesare a subset of that channel's messages (a message borrowed from another channel is reported, as is a component message the channel does not list, one that is not declared at all, or a local pointer that names something other than a message); a channel'sserversname declared servers; the same for an operation'sreply. - Runtime expressions —
correlationId.location,parameter.location, andreply.address.locationare parsed against the$message.header#/…/$message.payload#/…grammar. The#is mandatory, per the schema pattern;$message.payload#selects the whole payload. schemaFormat— required to be non-empty. It is not checked against a fixed list: the specification types it asanyOf: [string, <enum>], so a custom dialect is legal and simply keeps its schema as raw JSON. The documented formats are exposed per version asSUPPORTED_SCHEMA_FORMATS/is_supported_schema_formatfor callers that want to ask: every 2.0.0–2.6.0 and 3.0.0 AsyncAPI dialect plus OpenAPI 3.0.0, Avro 1.9.0, RAML 1.0 and JSON Schema draft-07, and inv3_1the threeversion=3.1.0dialects on top.- Channel address ↔ parameters — every
{placeholder}in an address is declared, and every declared parameter is used. Serverhost/pathnameplaceholders are checked againstvariablesthe same way. - Security-scheme variants — each
typegets both halves of its contract: the fields it requires (httpneedsscheme,oauth2needsflows,httpApiKeyneedsname+in) and the fields it forbids, since every branch of the specification'soneOfisadditionalProperties: false.bearerFormatis accepted only alongsidescheme: bearer. Each OAuth grant type is checked the same way — required URLs plusavailableScopes, and the URL its grant type does not use (implicitforbidstokenUrl;passwordandclientCredentialsforbidauthorizationUrl). - Schema keyword constraints — from the draft-07 meta-schema:
typenames a real JSON Schema type and its array form is non-empty and duplicate-free,enumis non-empty and duplicate-free under draft-07 instance equality (so1and1.0collide),allOf/anyOf/oneOfare non-empty,multipleOfis positive, tuple-formitemsis non-empty,requiredentries are unique, adiscriminatornames a property that this schema itself declares and requires (a composition keyword does not delegate that to its subschemas), and bounds are not inverted. - Other coherence — a
defaultis one of theenumvalues, and a message example definesheadersand/orpayload.
One limit is worth stating: enum uniqueness compares numbers after the parser has rounded non-integer literals to f64, so two decimals differing only past f64's precision (17+ significant digits) are treated as one. Preserving the exact text would need serde_json's arbitrary_precision, which changes how every serde_json::Value serializes through other serializers — YAML output becomes $serde_json::private::Number maps — and Cargo feature unification would impose that on the sibling crates as well.
ValidationOptions (EnumSet): IgnoreEmptyInfoTitle, IgnoreEmptyInfoVersion, IgnoreUnusedChannelParameter, and ErrorOnExternalReference. Behind the clap feature, the enum implements clap::ValueEnum so downstream CLIs can surface it directly.
What 2.6 does differently
2.6 is a different document rather than an earlier draft of v3, so v2_6 is its own model rather than a variation on v3_0:
| 2.6 | 3.x | |
|---|---|---|
| Channels | required, keyed by the channel path | keyed by a name, with a separate address |
| Operations | publish / subscribe under a channel, from the consumer's point of view |
a top-level map, send / receive from the application's point of view |
| Messages | one message, or { "oneOf": [...] }, on the operation |
the channel's messages map, referenced by the operation |
| Payload dialect | schemaFormat on the message, payload alongside it |
a Multi Format Schema Object wrapping both |
| Parameters | carry a full Schema |
strings constrained by enum / default / examples |
| Security | OpenAPI-style requirement maps (name → scopes) | inline schemes; OAuth's scopes renamed availableScopes |
tags / externalDocs |
at the document root | under info |
| Servers | one url |
host + pathname |
The publish / subscribe inversion is the migration trap worth naming: 2.6's publish describes messages others publish to the channel, so the application being described receives them — which is why it maps to v3's receive, not send.
Scope
The model and its validators are the whole surface. Out of scope for this release:
$refresolution across documents. Cross-reference checks run on document-local pointers; an external$refis accepted without further checking unlessErrorOnExternalReferenceasks for a self-contained document.- Trait merging.
message.traitsandoperation.traitsare parsed and validated, not applied. - Typed protocol bindings. AsyncAPI 3.0 types ~17 protocols across server / channel / operation / message, each with its own independently versioned
bindingVersion. Bindings are held as raw JSON keyed by protocol, so they round-trip losslessly and typed accessors can be layered on later without a breaking change. - Payload dialects other than the default. Only the AsyncAPI Schema Object dialect — JSON Schema draft-07 plus
discriminator/externalDocs/deprecated— is typed, and every draft-07 keyword is modeled so a schema round-trips unchanged. A payload carrying aschemaproperty is a Multi Format Schema Object (that presence is the discriminator, exactly as the specification'sanySchemadefines it, not the optionalschemaFormat), and itsschemastays raw JSON whatever dialect it names.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.