Avro Payloads
Contract Testing compares Avro message payloads in consumer contracts with Avro schemas in AsyncAPI provider contracts. Each payload goes through a value check and a structure check. Between them, the checks catch changes that break the consumer, including changes that keep the payload valid but break Avro decoding.For how comparison works for all payload formats, see Features - Testing AsyncAPI.
Requirements
Consumer Contract
Write the consumer contract with the Pact Avro plugin. The plugin embeds the consumer's Avro schema in the contract, and Contract Testing uses it for the structure check.
Use Pact Specification Version 4.
The interaction's content type must be
avro/binary,avro/bytesorapplication/avro. The plugin sets it for you.Reference the AsyncAPI operation in the same way as for JSON messages:
{
"type": "Asynchronous/Messages",
"comments": {
"references": {
"AsyncAPI": { "operationId": "receiveUserSignedUp" }
}
},
"contents": {
"contentType": "avro/binary;record=com.example.UserSignedUp"
}
}Provider Contract
Define the message payload as a Multi Format Schema Object, with
schemaFormatset toapplication/vnd.apache.avro,application/vnd.apache.avro+jsonorapplication/vnd.apache.avro+yaml. Aversionparameter is allowed, for exampleapplication/vnd.apache.avro;version=1.9.0.Embed the Avro schema in the AsyncAPI definition. Contract Testing doesn't follow references to external files or schema registries.
The schema can be a record, or a union of records. The consumer's record must match one of them by name.
components:
messages:
UserSignedUp:
contentType: avro/binary
payload:
schemaFormat: application/vnd.apache.avro;version=1.9.0
schema:
type: record
name: UserSignedUp
namespace: com.example
fields:
- name: userId
type: string
- name: email
type: ["null", "string"]
default: nullHow Avro Payloads Are Compared
Value Check
Contract Testing decodes the consumer's example message and validates it against the provider's schema, in the same way as a JSON payload.
Structure Check
A change to an Avro schema can keep the example valid and still break the consumer. For example, if the provider changes a field from int to long, the consumer's decoder can no longer read the message. The structure check catches these changes.
The structure check uses Avro schema resolution. The writer encodes a message with its schema. The reader decodes it with its own schema and resolves the differences:
Fields are matched by name or alias.
Fields the reader doesn't have are skipped.
Fields the writer doesn't send take the reader's default value.
Types must match, or follow Avro's promotion rules. For example, a reader that expects
longcan read anint.
Which side is the writer depends on the interaction:
InteractionWriter | Writer | Reader |
|---|---|---|
Fire-and-forget message | Provider | Consumer |
Request/reply: the request | Consumer | Provider |
Request/reply: the reply | Provider | Consumer |
The structure check assumes that the reader decodes with the writer's schema, for example by looking it up in a schema registry. This is how Avro schema resolution works.
All fields in the consumer's schema are compared. If the consumer's schema is a projection that has only the fields the consumer uses, only those fields are compared. The consumer's example is used only to decide whether a default value or an enum symbol matters to the consumer.
Structure Check Rules
Errors are reported as message.payload.incompatible, and warnings as message.payload.risky. Where a rule applies, the result message ends with the rule name in parentheses.
Change | Result | Rule |
|---|---|---|
The consumer's record isn't one the provider's message carries | Error | N/A |
The reader expects a field the writer doesn't send, and the reader's schema has no default | Error | READER_FIELD_MISSING_DEFAULT_VALUE |
The consumer expects a field the provider no longer sends. The consumer gets the default value, but its example expects a different value | Error | N/A |
The reader expects a field the writer doesn't send, and the reader's schema has a default | Warning | N/A |
The writer's type can't be read as the reader's type, for example | Error |
|
The writer's union has a branch the reader's union can't read | Error |
|
A record, enum or fixed type has a different name on each side | Error |
|
A fixed type has a different size on each side | Error |
|
The writer's enum has symbols the reader's enum doesn't have, and the reader's enum has no default | Error | MISSING_ENUM_SYMBOLS |
The writer's enum has symbols the reader's enum doesn't have, and the reader's enum has a default | Warning | N/A |
The consumer's example uses an enum symbol the provider no longer has | Error | N/A |
The consumer sends a field the provider doesn't read, so the provider ignores it | Warning | N/A |
Limitations
Embedded schemas only: the Avro schema must be in the AsyncAPI definition. Contract Testing doesn't query schema registries. If the schema is missing or can't be resolved, the result is
message.payload.unvalidatable.Consumer schema required: if the consumer contract doesn't contain the Avro schema, the result is
message.payload.unvalidatable. Write the contract with the Pact Avro plugin.Decoding assumption: the structure check assumes consumers decode with the writer's schema. If your consumers decode with their own schema only, the results may not match what happens at runtime.
Large numbers: long values beyond 2^53 (9,007,199,254,740,992) are rounded to the nearest representable number before the value check. A rounded value could pass or fail a check that the exact value wouldn't.
Warnings don't fail verification: a message.payload.unvalidatable result means the payload wasn't compared. The message still counts as a match.