Table of Contents

Avro payloads

Bielu.AspNetCore.AsyncApi.Avro describes message payloads as Apache Avro schemas. AsyncAPI lets a payload use a schema format other than JSON Schema through schemaFormat. Kafka teams that keep their schemas in a schema registry (Confluent, Apicurio, Karapace) usually expect Avro there.

dotnet add package Bielu.AspNetCore.AsyncApi.Avro
builder.Services.AddAsyncApi(options => options
    .AddServer("kafka", "broker:9092", "kafka")
    .AddAvroPayloads(avro => avro.WithNamespace("com.example.orders")));

Each message payload becomes a multi-format schema:

"payload": {
  "schemaFormat": "application/vnd.apache.avro;version=1.9.0",
  "schema": {
    "type": "record",
    "name": "OrderPlaced",
    "namespace": "com.example.orders",
    "fields": [
      { "name": "orderId", "type": { "type": "string", "logicalType": "uuid" } },
      { "name": "placedAt", "type": { "type": "long", "logicalType": "timestamp-millis" } },
      { "name": "note", "type": ["null", "string"] }
    ]
  }
}

In an AsyncAPI 2.6 document the schemaFormat sits on the message, as that version defines it.

How JSON Schema maps to Avro

The Avro schema is converted from the JSON Schema the generator already produces, so it follows the same .NET types, naming policy and schema transformers. There is no second definition to keep in sync.

JSON Schema Avro
object with properties record, named in PascalCase after its schema or its place in the payload (OrderPlacedLinesItem)
nullable ("type": ["null", …]) union with null first
property not in required union with null, defaulting to null
array array
object with only additionalProperties, or a free-form object map (of string when the values are free-form)
string enum whose values are valid Avro names enum
oneOf / anyOf, or several types union, without duplicate branches
integer with int32 / otherwise int / long
number with float / otherwise float / double
boolean boolean
string with uuid / date-time / date / time uuid / timestamp-millis / date / time-millis logical types
string with byte (byte[]) bytes
any other string string
$ref to a type already defined in the payload its full name, so recursive types stay finite

Avro defines each named type once per schema and refers to it by name after that. A reference that cannot be resolved becomes string.

Options

Option Effect
WithNamespace("com.example") Namespace of every record and enum.
ForMessages("orderPlaced", …) / MessageFilter Only these messages, by their key under components/messages.
AvroVersion The version declared in schemaFormat. Defaults to 1.9.0, the version the AsyncAPI specification lists.

What stays the same

  • The JSON Schemas stay under components/schemas, so anything else that references them keeps working.
  • Headers stay JSON Schema. Avro describes only the payload.
  • A payload already in another schema format is left alone.

AddAvroPayloads() adds a document transformer. Document transformers run in the order they are added, so add it after anything else that adds messages, such as AddWolverine().

Conformance

The test suite validates the output against the official AsyncAPI 2.6.0 and 3.1.0 JSON Schemas. Those schemas include the Avro schema definition, so the Avro output is checked too.