avro-c clib Data Formats & Serialization

Apache Avro binary serialization with schema evolution. The C reference implementation — compact row-oriented binary format, JSON schema, no compile step. Core of the Kafka Schema Registry ecosystem.

Version1.0.0
Typeclib
CategorySerialization
Requireslibavro-dev
CLib package — Ubuntu/Debian: sudo apt install libavro-dev
ppm install avro-c

What is Apache Avro?

Apache Avro is a data serialization system developed within the Hadoop ecosystem. It uses a compact binary encoding where field names are not stored in the binary data — the schema carries all structural information, making payloads significantly smaller than JSON or XML.

The avro-c library is the reference C implementation. It supports both binary and JSON encoding from the same schema, dynamic typing (no code generation required), and the full Avro type system including records, enums, arrays, maps, unions, and fixed-width fields.

The defining feature of Avro is schema evolution: producers and consumers can use different versions of a schema as long as evolution rules are respected. This makes Avro the dominant serialization format in the Kafka ecosystem, where producers and consumers are deployed independently.

Schema Definition

Avro schemas are written in JSON. Each schema defines a type, and record schemas list their fields with names, types, and optional defaults.

// SensorReading.avsc
{
  "type":   "record",
  "name":   "SensorReading",
  "fields": [
    {"name": "sensor_id",  "type": "string"},
    {"name": "value",      "type": "double"},
    {"name": "timestamp",  "type": "long"},
    {"name": "unit",       "type": ["null", "string"], "default": null}
  ]
}

The unit field uses a union type ["null", "string"] with a default of null, making it optional. This is the standard Avro pattern for nullable fields.

Schema Evolution Rules

Avro resolves schemas at read time by matching the writer schema (used to serialize) against the reader schema (used to deserialize). The following rules govern compatibility:

ChangeNotesCompatible?
Add field with default value Old readers ignore new field; new readers use the default when reading old data Backward ✅
Remove field with default value New readers ignore removed field; old readers use the field default Forward ✅
Add field without default Old readers cannot supply a value for the missing field — deserialization fails NOT compatible ❌
Rename a field Add the old name to the "aliases" array in the field definition; reader resolves by alias With aliases ✅
Best practice: always provide a "default" for every field you may want to add or remove later. Use the ["null", "type"] union pattern for optional fields.

Avro vs Protobuf vs Parquet

Avro

  • JSON schema, no compile step
  • Schema evolution built-in
  • Row-oriented binary
  • Kafka & Hadoop native
  • Dynamic typing

Protobuf

  • .proto schema file
  • Code generation required
  • Faster encode/decode
  • Wider language support
  • Field numbers, not names

Parquet

  • Columnar storage format
  • Analytics & OLAP workloads
  • Not suitable for streaming
  • Excellent compression
  • Arrow / Spark / DuckDB

Kafka Schema Registry Integration

Confluent Schema Registry stores Avro schemas centrally. Producers serialize a magic byte + 4-byte schema ID prefix before the Avro binary payload. Consumers read the ID from the first five bytes, fetch the schema from the registry, and deserialize the remainder. This is the standard pattern in production Kafka data pipelines.

The avro-c library handles the Avro binary encoding. Combine it with librdkafka for the Kafka transport layer and implement the 5-byte schema ID prefix in your producer/consumer glue code.

Types

Handle Types

TypeDescription
TAvroSchemaCompiled Avro schema handle. Created by ParseSchema or LoadSchema; freed with FreeSchema.
TAvroValueAvro data value instance. Created by NewValue; freed with FreeValue.
TAvroReaderBinary decoder handle for streaming deserialization.
TAvroWriterBinary encoder handle for streaming serialization.
TAvroFileAvro Object Container File handle. Opened with OpenFileWrite / OpenFileRead; closed with CloseFile.

TAvroType Enum

ValueOrdinalDescription
atString0UTF-8 string
atBytes1Raw byte sequence
atInt32232-bit signed integer
atInt64364-bit signed integer (long)
atFloat432-bit IEEE 754 float
atDouble564-bit IEEE 754 double
atBoolean6Boolean true/false
atNull7Null value
atRecord8Named record with fields
atEnum9Named enumeration
atArray10Ordered sequence of values
atMap11String-keyed map
atUnion12Union of two or more types
atFixed13Fixed-width byte array

TAvroCodec Enum

ValueDescription
acNullNo compression. Fastest write/read; largest files.
acDeflatezlib deflate compression. Good ratio, universally supported.
acSnappySnappy compression. Faster than deflate, slightly lower ratio. Common in Kafka pipelines.
acBzip2Bzip2 compression. Best ratio; slowest.
acZstdZstandard compression. Best balance of speed and ratio.

API Reference

Schema

FunctionSignatureDescription
ParseSchemaParseSchema(const JSON: string): TAvroSchemaParse and compile a schema from a JSON string. Raises on malformed JSON or invalid schema.
LoadSchemaLoadSchema(const Path: string): TAvroSchemaLoad and compile a schema from a .avsc file on disk.
LookupSchemaLookupSchema(Schema: TAvroSchema; const Name: string): TAvroSchemaGet the schema for a named type (record, enum, fixed) by name within a compiled schema.
FreeSchemaFreeSchema(Schema: TAvroSchema)Release all memory held by a compiled schema handle.
SchemaTypeSchemaType(Schema: TAvroSchema): TAvroTypeReturn the top-level Avro type of a schema handle.
SchemaToJSONSchemaToJSON(Schema: TAvroSchema): stringSerialize a compiled schema back to its canonical JSON string representation.

Value Building

FunctionSignatureDescription
NewValueNewValue(Schema: TAvroSchema): TAvroValueAllocate a new value instance conforming to the given schema. All fields are set to their defaults.
FreeValueFreeValue(V: TAvroValue)Release all memory held by a value handle.
ResetValueResetValue(V: TAvroValue)Reset all fields of a value to their schema defaults, allowing the handle to be reused.

Field Setters

ProcedureSignatureDescription
SetStringSetString(V: TAvroValue; const Field, Value: string)Set a string field by name.
SetIntSetInt(V: TAvroValue; const Field: string; Value: Integer)Set a 32-bit integer field by name.
SetLongSetLong(V: TAvroValue; const Field: string; Value: Int64)Set a 64-bit long field by name.
SetFloatSetFloat(V: TAvroValue; const Field: string; Value: Single)Set a 32-bit float field by name.
SetDoubleSetDouble(V: TAvroValue; const Field: string; Value: Double)Set a 64-bit double field by name.
SetBooleanSetBoolean(V: TAvroValue; const Field: string; Value: Boolean)Set a boolean field by name.
SetNullSetNull(V: TAvroValue; const Field: string)Set a field to null (for union fields with null branch).
SetBytesSetBytes(V: TAvroValue; const Field: string; const Data: string)Set a bytes field by name (data is treated as a raw byte sequence).

Field Getters

FunctionSignatureDescription
GetStringGetString(V: TAvroValue; const Field: string): stringRead a string field by name.
GetIntGetInt(V: TAvroValue; const Field: string): IntegerRead a 32-bit integer field by name.
GetLongGetLong(V: TAvroValue; const Field: string): Int64Read a 64-bit long field by name.
GetFloatGetFloat(V: TAvroValue; const Field: string): SingleRead a 32-bit float field by name.
GetDoubleGetDouble(V: TAvroValue; const Field: string): DoubleRead a 64-bit double field by name.
GetBooleanGetBoolean(V: TAvroValue; const Field: string): BooleanRead a boolean field by name.
GetBytesGetBytes(V: TAvroValue; const Field: string): stringRead a bytes field by name.

Serialization

FunctionSignatureDescription
SerializeSerialize(V: TAvroValue): stringEncode a value to raw Avro binary (no file framing). Returns a byte string.
DeserializeDeserialize(Schema: TAvroSchema; const Data: string): TAvroValueDecode raw Avro binary using the given schema. Returns a new value handle.
ToJSONToJSON(V: TAvroValue): stringSerialize a value to its Avro JSON encoding.
FromJSONFromJSON(Schema: TAvroSchema; const JSON: string): TAvroValueDeserialize a value from Avro JSON encoding.

Object Container Files

Avro Object Container Files (.avro) embed the schema and support block-level compression. They are the standard file format for Avro data at rest.

FunctionSignatureDescription
OpenFileWriteOpenFileWrite(const Path: string; Schema: TAvroSchema; Codec: TAvroCodec): TAvroFileOpen an .avro file for writing. Embeds the schema and configures block compression.
OpenFileReadOpenFileRead(const Path: string): TAvroFileOpen an existing .avro file for reading. The schema is read from the file header.
FileAppendFileAppend(F: TAvroFile; V: TAvroValue)Append a value record to an open write file.
FileReadFileRead(F: TAvroFile; Schema: TAvroSchema): TAvroValueRead the next record from an open read file. Returns 0 (nil handle) at end of file.
FileSchemaFileSchema(F: TAvroFile): TAvroSchemaReturn the schema embedded in an open file. Use after OpenFileRead to obtain the writer schema.
CloseFileCloseFile(F: TAvroFile)Flush buffers and close the file handle. Must be called on both read and write files.

Code Example — Write and Read an .avro File

uses avro_c;

var
  Schema : TAvroSchema;
  V      : TAvroValue;
  F      : TAvroFile;
  Read   : TAvroValue;

begin
  { Parse schema inline }
  Schema := ParseSchema('{"type":"record","name":"Sensor",' +
    '"fields":[{"name":"id","type":"string"},' +
    '{"name":"temp","type":"double"}]}');

  { Open file for writing with Snappy compression }
  F := OpenFileWrite('/data/sensors.avro', Schema, acSnappy);

  { Write first record }
  V := NewValue(Schema);
  SetString(V, 'id', 'sensor-001');
  SetDouble(V, 'temp', 23.7);
  FileAppend(F, V);

  { Reuse value for second record }
  SetString(V, 'id', 'sensor-002');
  SetDouble(V, 'temp', 24.1);
  FileAppend(F, V);

  FreeValue(V);
  CloseFile(F);

  { Read back }
  F := OpenFileRead('/data/sensors.avro');
  Schema := FileSchema(F);

  repeat
    Read := FileRead(F, Schema);
    if Read <> 0 then begin
      WriteLn(GetString(Read, 'id'), ': ', GetDouble(Read, 'temp'):5:1, ' C');
      FreeValue(Read);
    end;
  until Read = 0;

  CloseFile(F);
end.
Tip: Call ResetValue instead of FreeValue + NewValue when writing many records in a loop to avoid repeated allocations. The handle is reused and all fields revert to their schema defaults.

Package Info

Installppm install avro-c
Version1.0.0
Typeclib
Requireslibavro-dev
CategorySerialization
NoteCore of Kafka Schema Registry ecosystem

Schema API

  • ParseSchema
  • LoadSchema
  • LookupSchema
  • FreeSchema
  • SchemaType
  • SchemaToJSON

Value API

  • NewValue / FreeValue
  • ResetValue
  • SetString / GetString
  • SetInt / GetInt
  • SetLong / GetLong
  • SetFloat / GetFloat
  • SetDouble / GetDouble
  • SetBoolean / GetBoolean
  • SetNull / SetBytes / GetBytes

Serialization API

  • Serialize / Deserialize
  • ToJSON / FromJSON
  • OpenFileWrite
  • OpenFileRead
  • FileAppend / FileRead
  • FileSchema / CloseFile
← Back to Package Index