Skip to content

Document Info

@info

typespec
extern dec info(target: Namespace, info: valueof AsyncAPIInfo);

Fills the AsyncAPI info block on the service namespace. The argument's shape:

FieldTypeRequired
versionstringyes
descriptionstringno
termsOfServicestringno
contact{ name?, url?, email? }no
license{ name, url? }no
typespec
@service(#{ title: "Order Service API" })
@info(#{
  version: "1.0.0",
  description: "Order events.",
  contact: #{ name: "API Support", email: "support@example.com" },
  license: #{ name: "MIT", url: "https://opensource.org/licenses/MIT" }
})
namespace Orders;
yaml
info:
  title: Order Service API
  version: 1.0.0
  description: Order events.
  contact:
    name: API Support
    email: support@example.com
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

Without @info, info.version falls back to 0.0.0. If @info sets no description, a @doc (or /** ... */ doc comment) on the namespace fills it instead.

@externalDocs

typespec
extern dec externalDocs(target: unknown, url: valueof string, description?: valueof string);

Attaches an external documentation link. The target is declared unknown because external docs attach in several places. The emitter reads it at six of them:

Applied toEmitted on
The service namespaceinfo.externalDocs
A namespace carrying @serverevery server it declares
A @message modelthat message's externalDocs
Any model, scalar, or property that becomes a schemathat schema's externalDocs
A @channel interfacethat channel's externalDocs
A @send/@receive operationthat operation's externalDocs

AsyncAPI's Schema Object defines externalDocs alongside discriminator and deprecated as one of the three fields it adds on top of JSON Schema draft-07. A @message model emits the link on both the message and its payload schema, which is what @doc already does.

The servers come from the service namespace, and info reads that same namespace, so one link on that namespace appears in both places. AsyncAPI defines externalDocs on both objects.

The url must be an absolute URL. AsyncAPI marks the field with the uri format, so a relative one such as /docs makes a parser reject the whole document. A url that is not absolute raises an invalid-url error, and the whole application is dropped.

typespec
@externalDocs("https://example.com/docs", "Service Documentation")
namespace Orders;
yaml
info:
  externalDocs:
    url: https://example.com/docs
    description: Service Documentation
typespec
@message
@externalDocs("https://example.com/order-created", "How to consume this message.")
model OrderCreated {
  id: string;
}
yaml
components:
  messages:
    OrderCreated:
      name: OrderCreated
      externalDocs:
        url: https://example.com/order-created
        description: How to consume this message.
      payload:
        $ref: "#/components/schemas/OrderCreated"

@asyncTag

typespec
extern dec asyncTag(target: unknown, name: valueof string, metadata?: valueof AsyncAPITag);

model AsyncAPITag {
  description?: string;
  externalDocs?: ExternalDocs;
}

model ExternalDocs {
  url: string;
  description?: string;
}

Adds one tag, with its metadata, to the emitted object. Repeatable: each application adds one tag, and the emitted array follows source order.

It is named asyncTag and not tag on purpose. The built-in @tag lives in the global TypeSpec namespace, which is always in scope. A second tag in the AsyncAPI namespace would make a plain @tag(...) ambiguous for anyone who writes using AsyncAPI;, and every existing @tag would have to be rewritten as @TypeSpec.tag(...).

Two things separate it from the built-in @tag:

Built-in @tag@asyncTag
ArgumentA name, and nothing elseA name plus description and externalDocs
TargetNamespace | Interface | OperationAnything, Model included

AsyncAPI puts a full Tag Object on each item, where OpenAPI puts a bare string. And a message is a model, so the built-in @tag cannot tag a message at all — the compiler rejects the application.

typespec
@message
@asyncTag("orders", #{
  description: "Everything about orders.",
  externalDocs: #{ url: "https://example.com/orders", description: "The order guide." }
})
@asyncTag("public")
model OrderCreated {
  id: string;
}
yaml
components:
  messages:
    OrderCreated:
      name: OrderCreated
      tags:
        - name: orders
          description: Everything about orders.
          externalDocs:
            url: https://example.com/orders
            description: The order guide.
        - name: public
      payload:
        $ref: "#/components/schemas/OrderCreated"

The emitter reads it at five places:

Applied toEmitted on
The service namespaceinfo.tags
A namespace carrying @serverevery server it declares
A @message modelthat message's tags
A @channel interfacethat channel's tags
A @send/@receive operationthat operation's tags

The servers come from the service namespace, and info reads that same namespace, so one tag on that namespace appears in both places. AsyncAPI defines tags on both objects. Each server gets its own copy, so editing one server's tag cannot reach another's.

The name must not be empty. name is required on an AsyncAPI Tag Object, and a blank one names nothing a consumer can match, so @asyncTag("") is reported as empty-tag-name and the tag is dropped.

Merging

One name means one Tag Object per object. Two applications that name one tag on one target merge field by field:

  • Built-in @tag plus @asyncTag, same name. They merge, and the metadata wins. The built-in decorator carries a name and nothing that could disagree with it.
  • Two @asyncTag, same name, different fields. They merge. One description and one externalDocs together make one Tag Object.
  • Two @asyncTag, same name, one field with two different values. This is conflicting-tag-metadata, an error. The first application in source order keeps the field.

One name on two different targets may carry different metadata, and that is not an error. AsyncAPI gives every object its own tags array, and those arrays are independent.

@extension

typespec
extern dec extension(target: unknown, key: valueof string, value: valueof unknown);

Adds one x- specification extension to the object the target emits. The value is any JSON value, and it is emitted as written.

Repeatable: each application adds one key. The emitted keys follow source order, and they come after every specification field of the object.

The emitter reads it at four places:

Applied toEmitted on
The service namespaceinfo
A @channel interfacethat channel object
A @send/@receive operationthat operation object
A @message modelthat message object

A target that emits more than one object gets the extension on each of them. A namespace that is both the service and a channel is one such target.

The key must match the AsyncAPI Specification Extensions pattern, ^x-[\w\d\.\-\_]+$. That is x-, then one or more letters, digits, underscores, dots, or hyphens. AsyncAPI reads no other key as a specification extension. Any other key raises invalid-extension-key and that application is dropped.

One key on one target takes one value. A second application of the same key on the same target raises duplicate-extension-key. The first application in source order is kept.

The value must be one the emitter can write as JSON. A value it cannot write raises unserializable-extension and that application is dropped.

typespec
@service(#{ title: "Order Service API" })
@info(#{ version: "1.0.0" })
@extension("x-owner", "orders-team")
@extension("x-sla", #{ tier: "gold", hours: 24 })
namespace Orders;
yaml
info:
  title: Order Service API
  version: 1.0.0
  x-owner: orders-team
  x-sla:
    tier: gold
    hours: 24
typespec
@message
@extension("x-schema-registry-id", 4711)
model OrderCreated {
  id: string;
}

@channel("orders.created")
@extension("x-retention-days", 7)
interface OrderChannel {
  @send
  @extension("x-audit", true)
  publishOrderCreated(payload: OrderCreated): void;
}
yaml
channels:
  orders.created:
    address: orders.created
    messages:
      OrderCreated:
        $ref: "#/components/messages/OrderCreated"
    x-retention-days: 7
operations:
  publishOrderCreated:
    action: send
    channel:
      $ref: "#/channels/orders.created"
    messages:
      - $ref: "#/channels/orders.created/messages/OrderCreated"
    x-audit: true
components:
  messages:
    OrderCreated:
      name: OrderCreated
      payload:
        $ref: "#/components/schemas/OrderCreated"
      x-schema-registry-id: 4711

@extension and @jsonSchemaExtension

The two write to different layers, and the split does not change.

@extension@jsonSchemaExtension
Writes intoAn AsyncAPI object: info, channel, operation, messageA JSON Schema in components.schemas
Key shape^x-[\w\d\.\-\_]+$Any key
Typical useTooling metadata beside the specification fieldsA JSON Schema keyword this emitter has no decorator for

A @message model produces both a message object and a payload schema. @extension on that model writes the message object. To add a keyword to the payload schema, use @jsonSchemaExtension.

Servers and security schemes are not supported

@extension cannot write an extension on a server or on a security scheme.

Both are declared with a named argument on a namespace, as in @server("production", #{ ... }). One namespace may declare several of them. The target of @extension is the namespace, so the application cannot name which server or which scheme it means.

An extension on the service namespace therefore lands on info alone. It does not reach the servers that namespace declares. This differs from @externalDocs and @asyncTag, which copy onto every server.

A target that emits none of the four supported objects raises extension-target-not-emitted, and every extension on it is dropped.