Avro References: Resolve the Types Before Checking the Contract
How Avro named types, registry references and version selection interact—and which boundaries need separate tests.
Revised around the current implementation. The original URL and publication date are retained; examples and conclusions reflect this updated edition.
Sharing a named Avro type looks simple: define it once, then use its full name from another schema. The subtle part is not the name. It is how the reader or writer obtains the definition associated with that name, and which version of the definition it uses.
An Avro parser understands named types. It is not, by itself, a schema-registry client. Splitting a schema across registry artifacts introduces a dependency-resolution step that must complete before parsing and compatibility checking can mean what we expect.
A name does not fetch a definition
Consider an Order with an Address field:
{
"type": "record",
"name": "Order",
"namespace": "example",
"fields": [
{"name": "id", "type": "string"},
{"name": "shipping", "type": "example.Address"}
]
}
Parsed in isolation, this document has no definition for example.Address. The string is a named-type reference, not a URL or instruction to fetch another file. An application can preload definitions into a shared parser context, or a registry integration can resolve referenced artifacts first.
A self-contained version defines the type before using it:
{
"type": "record",
"name": "Order",
"namespace": "example",
"fields": [
{"name": "id", "type": "string"},
{
"name": "shipping",
"type": {
"type": "record",
"name": "Address",
"fields": [{"name": "city", "type": "string"}]
}
},
{"name": "billing", "type": "example.Address"}
]
}
The second field creates a definition in the schema’s namespace. The third field can refer to it. Avro also permits recursive records through named references; recursion is not the same thing as a registry being able to resolve an arbitrary cycle of separately stored artifacts.
Registry references add another contract
When definitions live in different artifacts, the registry and client integration need agreement on three things:
| Concern | Question |
|---|---|
| Name | Which fully qualified Avro name will the parser resolve? |
| Location | Which registry artifact supplies the definition? |
| Version | Which immutable version is intended? |
Those are different identifiers. An Avro name is not necessarily the Registry artifact ID, a Kafka topic name or a URL. The reference metadata must connect them using the API and client behavior actually supported by the selected Registry version.
Do not assume that uploading two schemas makes the relationship automatic. Verify the publication request and the deserialization path independently. A producer’s locally preloaded types can make serialization succeed while another process, which only has a registry identifier, cannot reconstruct the same schema.
Evolve the resolved schema graph
A compatibility check compares reader and writer schemas. With references, it needs the relevant resolved definitions too. Checking only an unchanged top-level document can miss that its dependency selection changed.
For example, an old Address contains city, while a new one adds a required postalCode without a default. A new reader expecting that field cannot read an old record that has no value for it. Leaving Order’s reference string unchanged does not erase that incompatibility.
Pin dependencies when reproducibility matters. If a consumer silently resolves a floating reference to the newest definition, the same top-level artifact can behave differently over time. Conversely, a pinned reference does not upgrade merely because a newer shared type was published: adopting it is another explicit version change.
This is also why sharing types should be deliberate. A useful common type reduces duplication, but it introduces coordination across its consumers. Self-contained schemas may be easier to operate when teams have genuinely independent release lifecycles.
Test the resolver and the consumer
A useful regression suite includes:
- Parsing the parent without its required definition, and observing the expected failure.
- Resolving the pinned dependency and successfully parsing the parent.
- Rejecting a missing or differently named dependency with an actionable diagnostic.
- Testing old-writer/new-reader and new-writer/old-reader combinations against resolved definitions.
- Restarting a consumer without a warm local schema cache and reading a representative record.
These checks distinguish parsing, publication and runtime resolution. They also avoid claiming that smaller schema documents produce smaller record payloads: Avro’s binary record encoding does not embed a copy of every named schema definition in each field value.
Connect this to the runnable examples
The data-contract lab exercises a real Registry Avro compatibility rule with self-contained schemas. It is the simplest starting point for understanding reader/writer direction. It does not claim to test cross-artifact reference resolution.
The Kroxylicious validation lab examines the write-path consequences of resolving schemas through Registry. Its executed payload format is JSON Schema; the separate Avro/Protobuf contribution provides implementation context, not evidence that this lab tested every format.
For reference-specific APIs, use the Apicurio Registry documentation and the client version you deploy. The architectural rule remains simple: resolve the intended definitions first, then test the contract that the producer and consumer actually use.