Repository: fastavro/fastavro

Issue:
Bug in named type aliases
This code should work:

```python
from fastavro import writer, reader, json_writer
from fastavro.schema import parse_schema
from io import BytesIO

# Sample data
input_json = [
    {
        "key1": "value1",
        "key2": "value2",
        "key3": "value3"
    }
]

# Old schema that matches the input_json
old_schema = parse_schema({
    "type": "record",
    "namespace": "com.node40",
    "name": "generated",
    "fields": [
        {
            "name": "key1",
            "type": "string"
        },
        {
            "name": "key2",
            "type": "string"
        },
        {
            "name": "key3",
            "type": "string"
        }
    ]
})

# New schema with old schema names as aliases
new_schema = parse_schema({
    "type": "record",
    "namespace": "com.node40",
    "name": "test",
    "aliases": ["generated"],
    "fields": [
        {
            "name": "k1",
            "type": "string",
            "aliases": ["key1"]
        },
        {
            "name": "k2",
            "type": "string",
            "aliases": ["key2"]
        },
        {
            "name": "k3",
            "type": "string",
            "aliases": ["key3"]
        }
    ]
})
records = [
    {
        "key1": "value1",
        "key2": "value2",
        "key3": "value3"
    }
]

# Write to buffer as serialized avro using old_schema
buffer = BytesIO()
writer(buffer, old_schema, input_json, validator=True)
buffer.seek(0)

for record in reader(buffer, new_schema):
    print(record)
```

However there is a `SchemaResolutionError` because it doesn't properly handle the alias in the new schema. The spec says that the new schema with a name of `com.node40.test` and an alias of `generated` should be treated as a name of `com.node40.generated` which should match the old schema. See https://stackoverflow.com/a/73974296/6131452

Discussion:

Solution patch:
diff --git a/fastavro/_read.pyx b/fastavro/_read.pyx
index 7d973b8..fd2b1e9 100644
--- a/fastavro/_read.pyx
+++ b/fastavro/_read.pyx
@@ -120,9 +120,8 @@ cpdef match_schemas(w_schema, r_schema, named_schemas):
 
             w_unqual_name = w_schema["name"].split(".")[-1]
             r_unqual_name = r_schema["name"].split(".")[-1]
-            if w_unqual_name == r_unqual_name or w_schema["name"] in r_schema.get(
-                "aliases", []
-            ):
+            r_aliases = r_schema.get("aliases", [])
+            if w_unqual_name == r_unqual_name or w_schema["name"] in r_aliases or w_unqual_name in r_aliases:
                 return r_schema
         elif w_type not in AVRO_TYPES and r_type in NAMED_TYPES:
             if match_types(w_type, r_schema["name"], named_schemas):
diff --git a/fastavro/_read_py.py b/fastavro/_read_py.py
index 418ac22..519e321 100644
--- a/fastavro/_read_py.py
+++ b/fastavro/_read_py.py
@@ -114,7 +114,11 @@ def match_schemas(w_schema, r_schema, named_schemas):
             w_unqual_name = w_schema["name"].split(".")[-1]
             r_unqual_name = r_schema["name"].split(".")[-1]
             r_aliases = r_schema.get("aliases", [])
-            if w_unqual_name == r_unqual_name or w_schema["name"] in r_aliases:
+            if (
+                w_unqual_name == r_unqual_name
+                or w_schema["name"] in r_aliases
+                or w_unqual_name in r_aliases
+            ):
                 return r_schema
         elif w_type not in AVRO_TYPES and r_type in NAMED_TYPES:
             if match_types(w_type, r_schema["name"], named_schemas):


Test patch:
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
index 54d9f42..1f02928 100644
--- a/tests/test_aliases.py
+++ b/tests/test_aliases.py
@@ -144,3 +144,154 @@ def test_enum_name_alias():
     records = ["FOO"]
 
     assert roundtrip(schema, records, new_schema) == ["FOO"]
+
+
+def test_alias_in_same_namespace():
+    """https://github.com/fastavro/fastavro/issues/648"""
+
+    # Old schema that matches the input_json
+    old_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "com.node40",
+            "name": "generated",
+            "fields": [
+                {"name": "key1", "type": "string"},
+                {"name": "key2", "type": "string"},
+                {"name": "key3", "type": "string"},
+            ],
+        }
+    )
+
+    # New schema with old schema names as aliases
+    new_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "com.node40",
+            "name": "test",
+            "aliases": ["generated"],
+            "fields": [
+                {"name": "k1", "type": "string", "aliases": ["key1"]},
+                {"name": "k2", "type": "string", "aliases": ["key2"]},
+                {"name": "k3", "type": "string", "aliases": ["key3"]},
+            ],
+        }
+    )
+
+    # Sample data
+    input_records = [{"key1": "value1", "key2": "value2", "key3": "value3"}]
+
+    output_records = roundtrip(old_schema, input_records, new_schema)
+    expected_records = [{"k1": "value1", "k2": "value2", "k3": "value3"}]
+    assert output_records == expected_records
+
+
+def test_alias_in_different_namespace():
+    """Test alias to record in different namespace of the write schema"""
+
+    old_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "the.old.namespace",
+            "name": "old_name",
+            "fields": [{"name": "key1", "type": "string"}],
+        }
+    )
+
+    new_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "the.new.namespace",
+            "name": "new_name",
+            "aliases": ["the.old.namespace.old_name"],
+            "fields": [{"name": "k1", "type": "string", "aliases": ["key1"]}],
+        }
+    )
+
+    input_records = [{"key1": "foo"}]
+
+    output_records = roundtrip(old_schema, input_records, new_schema)
+    expected_records = [
+        {
+            "k1": "foo",
+        }
+    ]
+    assert output_records == expected_records
+
+
+def test_alias_in_union():
+    """
+    Test aliases in union to records in different namespace
+    """
+
+    old_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "the.old.namespace",
+            "name": "OldMainRecord",
+            "fields": [
+                {
+                    "name": "main_union_old",
+                    "type": [
+                        {
+                            "type": "record",
+                            "name": "MessageA",
+                            "fields": [{"name": "key1", "type": "string"}],
+                        },
+                        {
+                            "type": "record",
+                            "name": "MessageB",
+                            "fields": [{"name": "key2", "type": "string"}],
+                        },
+                    ],
+                }
+            ],
+        }
+    )
+
+    new_schema = fastavro.parse_schema(
+        {
+            "type": "record",
+            "namespace": "the.new.namespace",
+            "name": "NewMainRecord",
+            "aliases": ["the.old.namespace.OldMainRecord"],
+            "fields": [
+                {
+                    "name": "main_union_new",
+                    "aliases": ["main_union_old"],
+                    "type": [
+                        {
+                            "type": "record",
+                            "name": "MsgA",
+                            "aliases": ["the.old.namespace.MessageA"],
+                            "fields": [
+                                {"name": "k1", "type": "string", "aliases": ["key1"]}
+                            ],
+                        },
+                        {
+                            "type": "record",
+                            "name": "MsgB",
+                            "aliases": ["the.old.namespace.MessageB"],
+                            "fields": [
+                                {"name": "k2", "type": "string", "aliases": ["key2"]}
+                            ],
+                        },
+                    ],
+                }
+            ],
+        }
+    )
+
+    input_records = [
+        {"main_union_old": ("the.old.namespace.MessageB", {"key2": "the value"})}
+    ]
+
+    output_records = roundtrip(old_schema, input_records, new_schema)
+    expected_records = [
+        {
+            "main_union_new": {
+                "k2": "the value",
+            }
+        }
+    ]
+    assert output_records == expected_records