dataclasses-avroschema

repository·master·Indexed 42 days ago

https://github.com/marcosschroh/dataclasses-avroschema

A Python library for generating Avro schemas from dataclasses, Pydantic models, and Faust Records. Version 0.70.9 provides tools for serializing and deserializing Python instances using Avro binary or Avro-JSON, with built-in support for schema evolution, logical types, and integrations with Kafka (aiokafka, kafka-python), Redis (walrus), and RabbitMQ (pika).

Tokens
41.4K
Snippets
141
Records
163
Agent score
46%

What's inside dataclasses-avroschema

  1. Avoid shadowing record names with field names in Avro

    master

    In Avro, if a field name is identical to a record name (e.g., a field named Address inside a record where a type named Address also exists), the generated Python model will suffer from 'shadowing'.

    In the shadowed model, the field's type annotation will be overwritten by the field name itself in the __annotations__ dictionary, breaking type safety and tools like fake().

    Solution: Ensure field names in your Avro schema use lowercase or different naming conventions (e.g., rename the field Address to address) to avoid collisions with the type names.

    # BAD: Field name 'Address' shadows type 'Address'
    @dataclasses.dataclass
    class User(AvroModel):
        Address: typing.Optional[Address] = None
    
    # GOOD: Field name 'address' does not shadow type 'Address'
    @dataclasses.dataclass
    class User(AvroModel):
        address: typing.Optional[Address] = None
  2. Use Unions in Avro Models

    master

    Unions allow a field to hold one of several different types.

    Python Version Compatibility

    • Python <= 3.9: Use typing.Union[Type1, Type2].
    • Python >= 3.10: Use the union type expression operator Type1 | Type2.

    Union with Records

    You can create unions of multiple AvroModel classes. If the models have a Meta.namespace defined, the resulting Avro schema will use the qualified name (e.g., types.Bus).

    Unions with typing.Optional

    typing.Optional[T] is automatically translated into a Union of T and null. In the resulting Avro schema, null is placed at the end of the type array.

    # Python 3.10+ syntax for Unions
    import dataclasses
    from dataclasses_avroschema import AvroModel
    
    @dataclasses.dataclass
    class Bus(AvroModel):
        engine_name: str
        class Meta:
            namespace = "types"
    
    @dataclasses.dataclass
    class Car(AvroModel):
        engine_name: str
        class Meta:
            namespace = "types"
    
    @dataclasses.dataclass
    class UnionSchema(AvroModel):
        lake_trip: Bus | Car
        river_trip: Bus | Car | None = None
  3. Understanding Avro Records and Fields

    master

    In Avro, a record is a complex type used to group multiple fields. When using dataclasses-avroschema, your Python class maps to an Avro record.

    Record Attributes

    • name (required): The name of the record.
    • namespace: A string that qualifies the name.
    • doc: Documentation for the schema.
    • aliases: An array of alternate names.
    • fields (required): An array of field objects.

    Field Attributes

    • name (required): The name of the field.
    • type (required): A JSON object defining the schema or a string naming a record.
    • doc: Documentation for the field.
    • default: A default value used when reading instances that lack this field.
    • order: Specifies sort impact ("ascending" (default), "descending", or "ignore").
    • aliases: Alternate names for the field.
  4. Handle field order issues in generated models

    master

    When working with third-party Avro schemas, optional fields might be declared before required fields. This can lead to invalid Python dataclasses.

    ModelGenerator handles this by automatically generating a field_order property within the model's Meta class. This ensures that the Python dataclass maintains the correct field sequence required by the Avro schema, even if the declaration order in the class body differs.

    # Example of generated Meta class for field ordering
    @dataclasses.dataclass
    class User(AvroModel):
        name: str
        age: int
        has_pets: bool = False
        money: float = 100.3
    
        class Meta:
            field_order = ['has_pets', 'name', 'age', 'money']
  5. Exclude fields from export (dict/json) but not from Avro serialization

    master

    You can use Pydantic's Field(exclude=True) to prevent specific fields from appearing when calling .model_dump() or .model_dump_json().

    Warning: This exclusion only affects the dictionary/JSON export. The fields are not excluded from the instance itself or from Avro binary serialization via .serialize(). The Avro schema will still include these fields.

    from pydantic import Field
    from dataclasses_avroschema.pydantic import AvroBaseModel
    
    class User(AvroBaseModel):
        name: str
        age: int
        pets: list[str] = Field(default_factory=lambda: ["dog"], exclude=True)
    
    user = User(name="bond", age=50, pets=["cat"])
    
    # Excluded from dict
    print(user.model_dump()) # {'name': 'bond', 'age': 50}
    
    # NOT excluded from Avro serialization
    event = user.serialize()
    assert user == User.deserialize(event)
  6. Integrations and ecosystem support

    master

    Dataclasses Avro Schema provides integration examples and support for several popular technologies:

    • Kafka Drivers: aiokafka and kafka-python.
    • Redis Drivers: walrus and redisgears-py.
    • Stream Processing: Can generate Avro schemas from faust.Record.
    • Validation: Pydantic integration.
  7. Encoding unions with avro-json

    master

    When using serialization_type="avro-json" with union types, the resulting JSON is not standard JSON. Each union field is wrapped in an object containing the type name (e.g., {"first_union": {"long": 10}}). This annotation is required so the deserializer knows which branch of the union to use.

    Warning: Do not confuse standard JSON with avro-json. Standard JSON without these type annotations will fail to deserialize when encountering unions.

    import typing
    import dataclasses
    import datetime
    import uuid
    from dataclasses_avroschema import AvroModel
    
    @dataclasses.dataclass
    class UnionSchema(AvroModel):
        first_union: typing.Union[str, int]
        logical_union: typing.Union[datetime.datetime, datetime.date, uuid.uuid4]
    
    my_union = UnionSchema(first_union=10, logical_union=datetime.datetime.now())
    
    # This produces annotated JSON required for deserialization
    event = my_union.serialize(serialization_type="avro-json")
    # Output: b'{"first_union": {"long": 10}, "logical_union": {"long": ...}}'
    
    # Correct deserialization
    my_union.deserialize(event, serialization_type="avro-json")
  8. How to customize serialization and deserialization

    master

    The serialization process is built on top of fastavro. To implement a custom serialization logic, you can override the serialize and deserialize methods in a base class that inherits from AvroModel.

    Key methods to use within your override:

    • self.avro_schema_to_python(): To get the schema as a Python dictionary.
    • self.asdict(): To get the instance data as a dictionary.
    • from_dict(...): To convert a dictionary back into a class instance during deserialization.
    import dataclasses
    import typing
    from dataclasses_avroschema import AvroModel, SerializationType
    
    @dataclasses.dataclass
    class MyAvroModel(AvroModel):
        def serialize(self, serialization_type: SerializationType = "avro") -> bytes:
            schema = self.avro_schema_to_python()
            data = self.asdict()
            # Replace with your custom logic
            return custom_serialization(schema, data, serialization_type=serialization_type)
    
        @classmethod
        def deserialize(
            cls, data: bytes, serialization_type: SerializationType = "avro", create_instance: bool = True
        ) -> typing.Union[typing.Dict, "AvroModel"]:
            schema = cls.avro_schema_to_python()
            payload = custom_deserialize(data, schema, serialization_type=serialization_type)
    
            if create_instance:
                # Use from_dict to reconstruct the instance
                return from_dict(data_class=cls, data=payload)
            return payload
  9. Use naive datetimes for local timestamp defaults

    master
    When working with local-timestamp logical types, the timezone information is lost during the Avro conversion process. To ensure that a generated model matches the original model after a roundtrip (model -> schema -> model), you should always use naive datetime objects to set default values for local-timestamp fields.
  10. Mapping Avro complex types to Python types

    master

    The following table describes how Avro complex types are mapped to Python types, depending on your Python version.

    Python <= 3.10

    Avro TypePython Type
    enumsenum.Enum, typing.Literal[str]
    arraystyping.List, typing.Tuple, typing.Sequence, typing.MutableSequence
    mapstyping.Dict, typing.Mapping, typing.MutableMapping
    fixedtypes.confixed
    unionstyping.Union
    unions with nulltyping.Optional
    recordsPython Class

    Python >= 3.11

    Avro TypePython Type
    enumsstr, enum.Enum, typing.Literal[str]
    arraystyping.List, typing.Tuple, typing.Sequence, typing.MutableSequence
    mapstyping.Dict, typing.Mapping, typing.MutableMapping
    fixedtypes.confixed
    unionstyping.Union
    unions with nulltyping.Optional
    recordsPython Class