Repository: hamba/avro
Pull request #541 (2025-06-05 16:46:09)

fix(schema): reject duplicate field names, duplicate enum symbols, an…
## Goal of this PR

This PR adds stricter schema validation to bring hamba/avro in closer alignment with the Avro specification and behavior seen in the official Apache implementations. This ensures proper validation when parsing schemas.

## Changes
1. Reject duplicate field names in record schemas ([Avro Python ref](https://github.com/apache/avro/blob/49fca82579ce865c4332108ad4557ec742afe8db/lang/py/avro/schema.py#L972))
   - Added validation to parseRecord to reject duplicate field names using a map/set.
   - Added unit test: "Duplicate Field Names" in TestRecordSchema.
   -  Updated unit test: TestConfig_ReusesDecoders_WithWriterFingerprint to remove duplicate field name in test record.

2. Reject duplicate symbols in enum schemas   ([Avro Python ref](https://github.com/apache/avro/blob/49fca82579ce865c4332108ad4557ec742afe8db/lang/py/avro/schema.py#L695)
   - Added validation in NewEnumSchema using a map/set to ensure uniqueness.
   - Added unit test: "Duplicate Symbols" in TestEnumSchema.

3. Disallow negative fixed sizes ([Avro Python ref](https://github.com/apache/avro/blob/49fca82579ce865c4332108ad4557ec742afe8db/lang/py/avro/schema.py#L590))
   - Added validation in NewFixedSchema to reject size < 0.
   - Added unit test: "Invalid Size Value" in TestFixedSchema.


## How did I test it?
`make test`: All existing tests pass, and additional test cases have been added and pass to verify the new validation checks.

## References
These changes address issues similar to those reported in the official Avro implementations:
- [Java - AVRO-1355](https://issues.apache.org/jira/browse/AVRO-1355): Schema parser should reject schemas with duplicate field names
- [Rust - AVRO-3827](https://issues.apache.org/jira/browse/AVRO-3827): Disallow duplicate field names


Patch:
diff --git a/schema.go b/schema.go
index eb23930..c04a04f 100644
--- a/schema.go
+++ b/schema.go
@@ -935,10 +935,17 @@ func NewEnumSchema(name, namespace string, symbols []string, opts ...SchemaOptio
 	if len(symbols) == 0 {
 		return nil, errors.New("avro: enum must have a non-empty array of symbols")
 	}
+
+	symbolNames := make(map[string]struct{})
 	for _, sym := range symbols {
 		if err = validateName(sym); err != nil {
 			return nil, fmt.Errorf("avro: invalid symbol %q", sym)
 		}
+
+		if _, exists := symbolNames[sym]; exists {
+			return nil, fmt.Errorf("avro: duplicate symbol %q", sym)
+		}
+		symbolNames[sym] = struct{}{}
 	}
 
 	var def string
@@ -1361,6 +1368,10 @@ func NewFixedSchema(
 		return nil, err
 	}
 
+	if size < 0 {
+		return nil, errors.New("avro: fixed size cannot be negative")
+	}
+
 	reservedProps := fixedReserved
 	if logical != nil {
 		if logical.Type() == Decimal {
diff --git a/schema_parse.go b/schema_parse.go
index b5227c6..cf6fc45 100644
--- a/schema_parse.go
+++ b/schema_parse.go
@@ -280,11 +280,18 @@ func parseRecord(typ Type, namespace string, m map[string]any, seen seenCache, c
 		cache.Add(alias, ref)
 	}
 
+	fieldNames := make(map[string]struct{})
 	for i, f := range r.Fields {
 		field, err := parseField(rec.namespace, f, seen, cache)
 		if err != nil {
 			return nil, err
 		}
+
+		if _, exists := fieldNames[field.name]; exists {
+			return nil, fmt.Errorf("avro: duplicate field name %q", field.name)
+		}
+		fieldNames[field.name] = struct{}{}
+
 		fields[i] = field
 	}
 


Test patch:
diff --git a/config_internal_test.go b/config_internal_test.go
index 4d14625..a743a22 100644
--- a/config_internal_test.go
+++ b/config_internal_test.go
@@ -54,7 +54,7 @@ func TestConfig_ReusesDecoders_WithWriterFingerprint(t *testing.T) {
 		"name": "test",
 		"fields" : [
 			{"name": "a", "type": "long"},
-			{"name": "a", "type": "string", "default": "foo"}
+			{"name": "b", "type": "string", "default": "foo"}
 		]
 	}`
 	typ := reflect2.TypeOfPtr(&testObj{})
diff --git a/schema_test.go b/schema_test.go
index a46c314..61718ba 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -258,6 +258,11 @@ func TestRecordSchema(t *testing.T) {
 			schema:  `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "fields":[{"name": "field", "type": "blah"}]}`,
 			wantErr: require.Error,
 		},
+		{
+			name:    "Duplicate Field Names",
+			schema:  `{"type":"record", "name":"test", "namespace": "org.hamba.avro", "doc": "docs", "fields":[{"name": "field", "type": "int"}, {"name": "field", "type": "string"}]}`,
+			wantErr: require.Error,
+		},
 	}
 
 	for _, test := range tests {
@@ -682,6 +687,11 @@ func TestEnumSchema(t *testing.T) {
 			schema:  `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST+"]}`,
 			wantErr: require.Error,
 		},
+		{
+			name:    "Duplicate Symbols",
+			schema:  `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":["TEST", "TEST"]}`,
+			wantErr: require.Error,
+		},
 		{
 			name:    "Invalid Symbol Type",
 			schema:  `{"type":"enum", "name":"test", "namespace": "org.hamba.avro", "symbols":[1]}`,
@@ -968,6 +978,11 @@ func TestFixedSchema(t *testing.T) {
 			schema:  `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": "test"}`,
 			wantErr: require.Error,
 		},
+		{
+			name:    "Invalid Size Value",
+			schema:  `{"type":"fixed", "name":"test", "namespace": "org.hamba.avro", "size": -1}`,
+			wantErr: require.Error,
+		},
 	}
 
 	for _, test := range tests {