Kafka Serialization — JSON vs Avro in Spring
Fast recall
Quick revision
Core answer
Understanding of serialization format trade-offs and schema evolution
Not handling type information in JsonDeserializer — receives a LinkedHashMap instead of the target class
Explain schema evolution rules in Avro (backward vs forward vs full compatibility) and how Schema Registry enforces them
Interview-ready guide
Interview answer
JSON is simple and human-readable, but it has no schema contract. The producer can change fields without the consumer knowing. Incompatibilities show up at runtime.
Avro encodes data with an explicit schema registered in a Schema Registry. Messages are compact (schema ID + values, no field names) and schema-enforced (the registry validates compatibility before allowing changes).
Avro supports schema evolution — adding an optional field with a default doesn't break existing consumers. Forward and backward compatibility checks happen at registration time, not at runtime.
The trade-off: Avro requires Schema Registry infrastructure. JSON requires nothing.
In Spring Kafka, configure KafkaAvroSerializer and point to the Schema Registry URL. For JSON, JsonSerializer with JsonDeserializer works with no infrastructure.
My recommendation: JSON for prototyping and internal tools. Avro for production event-driven architectures where multiple teams consume the same topics and schema governance matters.
Kafka Serialization — JSON vs Avro in Spring
Direct Answer
JsonSerializer/JsonDeserializer work out of the box with Spring Kafka and Jackson. Avro with schema registry enforces schema evolution contracts, provides compact binary encoding, and is preferred for production multi-team environments.
Code Example
JSON is human-readable and schema-loose — great for dev, painful at scale. Avro with a schema registry is binary, compact, and enforces forward/backward compatibility — the standard for production data pipelines.
# ----- JSON (Spring's JsonSerializer) -----
spring:
kafka:
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
spring.json.trusted.packages: "com.example.events" # security: avoid arbitrary class deserialization
# ----- Avro + Confluent Schema Registry -----
spring:
kafka:
producer:
value-serializer: io.confluent.kafka.serializers.KafkaAvroSerializer
consumer:
value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
properties:
schema.registry.url: http://schema-registry:8081
specific.avro.reader: true # deserialize into generated POJOs (not GenericRecord)
// src/main/avro/OrderEvent.avsc — checked into git, code-gen via avro-maven-plugin
{
"namespace": "com.example.events",
"type": "record",
"name": "OrderEvent",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "userId", "type": "long" },
{ "name": "total", "type": { "type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2 } },
{ "name": "status", "type": { "type": "enum", "name": "Status", "symbols": ["NEW","PAID","CANCELLED"] } },
{ "name": "createdAt", "type": { "type": "long", "logicalType": "timestamp-millis" } },
{ "name": "promoCode", "type": ["null", "string"], "default": null } // optional → backward compatible
]
}
// Generated class is com.example.events.OrderEvent
@KafkaListener(topics = "orders")
public void onOrder(OrderEvent ev) { // strongly typed, schema-validated
log.info("order {} for user {} total {}", ev.getOrderId(), ev.getUserId(), ev.getTotal());
}
| Aspect | JSON | Avro + Schema Registry |
|---|---|---|
| Wire size | text + field names | binary, schema by ID (3-5× smaller) |
| Schema | implicit, in code | explicit, versioned, registry-enforced |
| Compatibility checks | none | BACKWARD / FORWARD / FULL enforced at publish |
| Tooling | curl + jq inspect easily | needs kafka-avro-console-consumer |
| Best fit | dev, prototypes, low-volume topics | production pipelines, schema evolution |
- JSON wins on developer ergonomics; payloads are debuggable with
kafkacat. Always setspring.json.trusted.packagesto a tight allowlist — JsonDeserializer can otherwise instantiate arbitrary classes from the headers. - Avro wins everywhere else at scale: 3-5× smaller payloads, binary encoding (CPU-cheap), and the schema registry rejects incompatible schema changes at publish time — production downtime from "we forgot to coordinate the schema" disappears.
- Use TopicNameStrategy (default) when one schema per topic is fine; TopicRecordNameStrategy when one topic carries multiple event types.
- Protobuf is the third option — similar trade-offs to Avro, with stronger code-gen ergonomics in Java/Go but weaker dynamic schema introspection.
Common Mistakes & Pitfalls
Not handling type information in JsonDeserializer — receives a LinkedHashMap instead of the target class
Check your understanding
Answer these without looking back at the article.
- What is schema evolution and why does it matter for Kafka?
- How do you handle a breaking schema change in Avro?
- What is the Schema Registry wire format (magic byte + schema ID)?
Related interview prep categories
2Want a guided curriculum order?
These modules are also sequenced in our flagship backend interview track — open it when you prefer an ordered path instead of browsing by topic.