Summary
CanonicalSchema() produces two different output strings from the same schema input, non-deterministically (~14% vs ~86% split), when a field name and a record type name share the same local name within the same inherited namespace.
Reproduction
package main
import (
"fmt"
"github.com/linkedin/goavro/v2"
)
const schema = `{
"type": "record",
"name": "Parent",
"namespace": "com.example",
"fields": [{
"name": "items",
"type": ["null", {
"type": "array",
"items": {
"type": "record",
"name": "items",
"fields": [{"name": "id", "type": "int"}]
}
}],
"default": null
}]
}`
func main() {
seen := map[string]int{}
for i := 0; i < 1000; i++ {
codec, _ := goavro.NewCodec(schema)
seen[codec.CanonicalSchema()]++
}
fmt.Printf("distinct canonical forms: %d\n", len(seen))
for k, v := range seen {
fmt.Printf(" count=%-4d %s\n", v, k)
}
}
Output (example run):
distinct canonical forms: 2
count=862 {"name":"com.example.Parent","type":"record","fields":[{"name":"items","type":["null",{"type":"array","items":{"name":"com.example.items","type":"record","fields":[{"name":"id","type":"int"}]}}]}]}
count=138 {"name":"com.example.Parent","type":"record","fields":[{"name":"com.example.items","type":["null",{"type":"array","items":{"name":"com.example.items","type":"record","fields":[{"name":"id","type":"int"}]}}]}]}
The difference is the "name" of the items field on the outer Parent record:
- Correct (~86%):
"name":"items" — the field's declared local name
- Wrong (~14%):
"name":"com.example.items" — incorrectly namespace-qualified
Expected behaviour
CanonicalSchema() should always produce the same output for the same input schema. Per the Avro Parsing Canonical Form spec, field names are never namespace-qualified — the second form is incorrect.
Root cause
goavro builds an internal map of named types during schema parsing. Go map iteration is non-deterministic. When computing the canonical form for a field, the code appears to consult this map to resolve whether the field name is a type reference, and non-deterministic iteration occasionally causes the field's local name to be replaced with the fully-qualified type name of the same-named record.
The field "name": "items" collides with the record type com.example.items. When goavro's named-type lookup happens to find that match during canonical form computation, it emits "com.example.items" as the field name instead of "items".
Impact
Any system that uses CanonicalSchema() output as a fingerprint (e.g. to compare schemas between a registry and a stored file) will see spurious mismatches ~14% of the time when this collision exists. We encountered this when validating exported Avro OCF files against a Pub/Sub schema registry — ~22 out of 135 daily files were flagged as mismatched despite containing identical schemas.
Workaround
Rename the record type (or field) so they no longer share a local name. This makes CanonicalSchema() deterministic again.
Environment
- goavro version: v2.15.0 (latest)
- Go version: 1.25.4
- OS: darwin/arm64 (also reproducible on linux/amd64 in CI)
Summary
CanonicalSchema()produces two different output strings from the same schema input, non-deterministically (~14% vs ~86% split), when a field name and a record type name share the same local name within the same inherited namespace.Reproduction
Output (example run):
The difference is the
"name"of theitemsfield on the outerParentrecord:"name":"items"— the field's declared local name"name":"com.example.items"— incorrectly namespace-qualifiedExpected behaviour
CanonicalSchema()should always produce the same output for the same input schema. Per the Avro Parsing Canonical Form spec, field names are never namespace-qualified — the second form is incorrect.Root cause
goavro builds an internal map of named types during schema parsing. Go map iteration is non-deterministic. When computing the canonical form for a field, the code appears to consult this map to resolve whether the field name is a type reference, and non-deterministic iteration occasionally causes the field's local name to be replaced with the fully-qualified type name of the same-named record.
The field
"name": "items"collides with the record typecom.example.items. When goavro's named-type lookup happens to find that match during canonical form computation, it emits"com.example.items"as the field name instead of"items".Impact
Any system that uses
CanonicalSchema()output as a fingerprint (e.g. to compare schemas between a registry and a stored file) will see spurious mismatches ~14% of the time when this collision exists. We encountered this when validating exported Avro OCF files against a Pub/Sub schema registry — ~22 out of 135 daily files were flagged as mismatched despite containing identical schemas.Workaround
Rename the record type (or field) so they no longer share a local name. This makes
CanonicalSchema()deterministic again.Environment