If you work with Kafka long enough, someone puts Avro in front of you. The docs are dense, so here's the working knowledge you actually need: what a schema looks like, the two rules that cause 90% of production errors, and how to debug a binary payload.
Why Avro instead of JSON?
Two reasons: size (binary encoding, field names aren't repeated in every message — a JSON message of 500 bytes often becomes ~100) and contracts (the schema is enforced at produce time, so a malformed message never enters the topic). With a schema registry, consumers always know how to decode what producers wrote — including messages written under older schema versions.
The schema, minimally
{
"type": "record",
"name": "OrderCreated",
"namespace": "com.shop.events",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "amount", "type": "long" },
{ "name": "currency", "type": "string", "default": "KRW" },
{ "name": "couponCode", "type": ["null", "string"], "default": null }
]
}
The parts that trip people up:
-
There is no "optional" keyword. An optional field is a union with null —
["null", "string"]— and the default must benull, with"null"listed first in the union. Order matters;["string", "null"]with a null default is invalid. - Defaults are not runtime fallbacks. A default is used when a reader decodes data written by an older schema that lacked the field. Producers still must set every field.
- Timestamps are
longwith a logical type:{ "type": "long", "logicalType": "timestamp-millis" }.
The two evolution rules that matter
Schema evolution is why Avro exists, and it boils down to:
-
Adding a field? It must have a default. Otherwise old messages can't be read with the new schema (
BACKWARDcompatibility breaks — the registry will reject it). - Removing a field? Only remove fields that had a default. Never rename — a rename is a remove plus an add, and old data loses the value silently. Add an alias instead.
Everything else (changing types, reordering unions) — check against your registry's compatibility mode before assuming.
Debugging: "what's actually in this message?"
Binary Avro is unreadable in kafka-console-consumer, and the classic gotcha is the magic byte: messages produced through Confluent serializers carry a 5-byte header (0x00 + 4-byte schema ID) before the Avro payload. If your decoder chokes immediately, that header is usually why.
For quick inspection without spinning up kafka-avro-console-consumer, I use these browser tools (client-side only, nothing uploaded):
- Avro decoder — paste base64/hex bytes + the schema, get JSON back
- Avro schema generator — paste a sample JSON message, get a starting-point schema with proper null unions
- Avro message generator — generate test payloads from a schema
TL;DR
- Optional field =
["null", "type"], null first, default null. - New fields need defaults; never rename, alias instead.
- Defaults serve schema evolution, not producer laziness.
- Decoder failing on byte 0? Strip the 5-byte Confluent header.
Top comments (0)