Avro4k

repository·main·Indexed 42 days ago

https://github.com/avro-kotlin/avro4k

A reflection-less Avro serialization library for Kotlin built on top of kotlinx.serialization. It provides support for logical types, schema evolution, and schema generation optimized for the JVM. Includes a Gradle plugin for generating Kotlin data classes from .avsc files and integration with Confluent's Schema Registry via the avro4k-confluent-kafka-serializer.

Tokens
19.5K
Snippets
42
Records
50
Agent score
54%

What's inside Avro4k

  1. Understand the Kotlin Avro Benchmark configuration

    main

    The benchmark compares serialization and deserialization performance across three libraries:

    • Avro4k
    • Jackson Avro
    • Avro (using ReflectData)

    Benchmark Methodology

    • Input/Output: Reading from a prepared byte array and writing to a null output stream.
    • Schema: All libraries use the exact same schema generated by avro4k.
    • Use Cases Covered:
      • Nullable fields
      • Unions
      • Arrays
      • Records
      • Enums
      • Primitives (string, int, float, double, boolean)
      • Logical types (date, timestamp-millis, char, uuid)

    Known Limitations

    • UUIDs: UUIDs are not benchmarked because Jackson uses a different representation (fixed) compared to Avro4k and Apache Avro (string).
    • Jackson Bug: JacksonAvroBenchmark.read is known to fail due to a bug in the Jackson library when combining Kotlin and Avro formats.
  2. How AvroObjectContainer (Data Files) works

    main

    The AvroObjectContainer class is used to encode and decode Avro 'Object Containers' (also known as Avro Data Files). This format prefixes the binary data with the full schema, making it self-describing.

    This is ideal for long-term storage of many objects in a single file.

    Usage Note: AvroObjectContainer.decodeFromStream<T>(...) returns a Sequence. Because this is a 'hot' source, you must consume the sequence before closing the underlying stream. Be cautious with sequence.toList() on very large files to avoid OutOfMemoryError.

    package myapp
    
    import com.github.avrokotlin.avro4k.*
    import kotlinx.serialization.*
    
    @Serializable
    data class Project(val name: String, val language: String)
    
    fun main() {
        // Serializing objects
        val valuesToEncode = sequenceOf(
            Project("kotlinx.serialization", "Kotlin"),
            Project("java.lang", "Java"),
            Project("avro4k", "Kotlin"),
        )
        Files.newOutputStream(Path("your-file.bin")).use { fileStream ->
            AvroObjectContainer.openWriter(fileStream).use { writer ->
                valuesToEncode.forEach { writer.write(it) }
            }
        }
    
        // Deserializing objects
        Files.newInputStream(Path("your-file.bin")).use { fileStream ->
            AvroObjectContainer.decodeFromStream<Project>(fileStream).forEach {
                println(it) // Project(name=kotlinx.serialization, language=Kotlin) ...
            }
        }
    }
  3. Set field default values to null, empty collections, or empty maps

    main

    In v2, many common default behaviors are now implicit. You often do not need to use @AvroDefault for standard types.

    Nullable Fields

    By default, nullable fields are treated as having a null default. If you want to change this, set implicitNulls = false in the Avro configuration and use @AvroDefault("null").

    Empty Collections and Maps

    By default, List and Map fields are treated as having empty defaults ([] or {}). To change this, set implicitEmptyCollections = false in the Avro configuration and use @AvroDefault("[]") or @AvroDefault(")".

    Example (v2 default behavior):

    data class TheDataClass(
        val field: String?,
        val list: List<String>,
        val map: Map<String, String>
    )
  4. How serialization and deserialization modes work

    main

    The library provides three modes of operation depending on your needs for type safety and flexibility:

    1. Generic: Used when you want to serialize any type but deserialize into generic Avro classes (like GenericRecord, GenericEnumSymbol, GenericFixed, GenericArray) or primitive types. This is useful when you don't want to instantiate concrete Kotlin/Java classes.
    2. Reflect: Used to serialize any type but deserialize into specific concrete Kotlin/Java classes using reflection. If no concrete class is found for the schema name or aliases, it falls back to generic deserialization.
    3. Specific: Used when you have a specific type known at compile time. You must provide the type's serializer explicitly or use reified methods.
  5. Distinguish between Nullable and Optional fields in Avro

    main

    To ensure compatibility during schema evolution (e.g., when using a Schema Registry), it is important to distinguish between nullable and optional fields:

    • Nullable field: A field that includes null in its type union. It is not optional unless it also has a default value.
    • Optional field: A field that has a default value (e.g., an int with a default of -1).

    Best Practice: To mark a field as truly optional and facilitate contract evolution, set the default value to null.

  6. How AvroSingleObject encoding works

    main

    The AvroSingleObject class implements the Avro Single Object encoding. This format prefixes the binary data with a 64-bit schema fingerprint, allowing the reader to identify the writer schema.

    This is highly efficient for message brokers like Kafka or RabbitMQ because it is compact and schema-aware. However, to decode the data, you must provide a mechanism (like a schema registry) to resolve the fingerprint back to the actual schema.

    package myapp
    
    import com.github.avrokotlin.avro4k.*
    import kotlinx.serialization.*
    import org.apache.avro.SchemaNormalization
    
    @Serializable
    data class Project(val name: String, val language: String)
    
    fun main() {
        val schema = Avro.schema<Project>()
        val schemasByFingerprint = mapOf(SchemaNormalization.parsingFingerprint64(schema) to schema)
        val singleObjectInstance = AvroSingleObject(schemasByFingerprint::get)
    
        // Serializing objects
        val data = Project("kotlinx.serialization", "Kotlin")
        val bytes = singleObjectInstance.encodeToByteArray(data)
    
        // Deserializing objects
        val obj = singleObjectInstance.decodeFromByteArray<Project>(bytes)
        println(obj) // Project(name=kotlinx.serialization, language=Kotlin)
    }
  7. How the Avro4k plugin generates Kotlin code

    main

    The plugin follows specific rules to ensure type safety and compatibility with Avro4k:

    • Naming: Class names are derived from Avro schema names; package names are derived from the Avro namespace.
    • Unions: Generates a sealed interface where each branch is a nested value class to enforce type safety.
    • Nullability: Nullable record fields become nullable Kotlin properties. Nullable fields without Avro defaults are generated as optional with a null default. Maps/Arrays without defaults are generated with empty collections as defaults.
    • Schema Preservation: Each generated class contains the original normalized JSON schema. This allows the Avro instance to bypass schema inference and ensures compatibility with Schema Registries.
    • Root Types: To prevent losing schema precision for primitives, logical types, or collections, the plugin generates value classes wrapping these types. This ensures the original schema is always used during serialization without requiring the user to manually pass a schema object.
  8. Migrate Pure Avro Serialization from v1 to v2

    main

    In Avro4k v2, the API has been simplified to use Kotlin serialization patterns. You no longer need to pass the serializer explicitly to the top-level Avro methods, and you can use reified type parameters for decoding.

    v1 Pattern:

    val bytes = Avro.default.encodeToByteArray(TheDataClass.serializer(), TheDataClass(...))
    Avro.default.decodeFromByteArray(TheDataClass.serializer(), bytes)

    v2 Pattern:

    val bytes = Avro.encodeToByteArray(TheDataClass(...))
    Avro.decodeFromByteArray<TheDataClass>(bytes)
    // Now
    val bytes = Avro.encodeToByteArray(TheDataClass(...))
    Avro.decodeFromByteArray<TheDataClass>(bytes)
  9. Install avro4k via Maven

    main

    To use avro4k with Maven, you must first configure the kotlin-maven-plugin to include the kotlinx-serialization compiler plugin. Then, add the avro4k-core dependency to your project.

    <!-- 1. Configure Kotlin Serialization Plugin -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <compilerPlugins>
                        <plugin>kotlinx-serialization</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-serialization</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
    
    <!-- 2. Add avro4k dependency -->
    <dependency>
        <groupId>com.github.avro-kotlin.avro4k</groupId>
        <artifactId>avro4k-core</artifactId>
        <version>${avro4k.version}</version>
    </dependency>
  10. Migrate Generic Data Serialization

    main

    To convert Kotlin data classes to generic data structures (like GenericRecord) and back, use the new encodeToGenericData and decodeFromGenericData methods.

    v1 Pattern:

    val genericRecord: GenericRecord = Avro.default.toRecord(TheDataClass.serializer(), TheDataClass(...))
    Avro.default.fromRecord(TheDataClass.serializer(), genericRecord)

    v2 Pattern:

    val genericData: Any? = Avro.encodeToGenericData(TheDataClass(...))
    Avro.decodeFromGenericData<TheDataClass>(genericData)
    // Now
    val genericData: Any? = Avro.encodeToGenericData(TheDataClass(...))
    Avro.decodeFromGenericData<TheDataClass>(genericData)
  11. Install avro4k via Gradle Groovy DSL

    main

    For Gradle projects using the Groovy DSL, apply the Kotlin Multiplatform and Serialization plugins, then add the avro4k-core dependency.

    plugins {
        id 'org.jetbrains.kotlin.multiplatform' version kotlinVersion
        id 'org.jetbrains.kotlin.plugin.serialization' version kotlinVersion
    }
    
    dependencies {
        implementation "com.github.avro-kotlin.avro4k:avro4k-core:$avro4kVersion"
    }