diff --git a/src/py_avro_schema/_alias.py b/src/py_avro_schema/_alias.py index 19288f9..6eb2d83 100644 --- a/src/py_avro_schema/_alias.py +++ b/src/py_avro_schema/_alias.py @@ -29,6 +29,14 @@ class Aliases: aliases: list[str] +class Opaque: + """ + This is a marker for complex Avro fields (e.g., maps) that are serialized to a simple string. + """ + + pass + + def get_fully_qualified_name(py_type: type) -> str: """Returns the fully qualified name for a Python type""" module = getattr(py_type, "__module__", None) @@ -107,6 +115,11 @@ def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, args = get_args(py_type) actual_type, annotation = args[0], args[1] + # When a field is annotated with the Opaque class, we return bytes as type. + # The object serializer is responsible for dumping the entire attribute as a JSON string + if isinstance(annotation, type) and issubclass(annotation, Opaque): + return [], str + # Annotated type but not an alias. We do nothing. if type(annotation) not in (Alias, Aliases): return [], py_type diff --git a/tests/test_plain_class.py b/tests/test_plain_class.py index 7aef325..c8d21de 100644 --- a/tests/test_plain_class.py +++ b/tests/test_plain_class.py @@ -15,7 +15,7 @@ import pytest import py_avro_schema -from py_avro_schema._alias import Alias, register_type_aliases +from py_avro_schema._alias import Alias, Opaque, register_type_aliases from py_avro_schema._testing import assert_schema @@ -130,3 +130,16 @@ def __init__( ), ): assert_schema(PyType, {}) + + +def test_opaque_field(): + class Details: + name: str + surname: str + age: int + + class PyType: + details: Annotated[Details, Opaque] + + expected = {"fields": [{"name": "details", "type": "string"}], "name": "PyType", "type": "record"} + assert_schema(PyType, expected)