How do you send Java objects (POJOs) as JSON with KafkaTemplate, and what should you know about Spring's JsonSerializer?
answer
- JsonSerializer -> Jackson -> JSON bytes
- __TypeId__ header carries the class name
- addTypeInfo=false for non-Java consumers
- consumer: trusted packages / type mapping
- SerializationException thrown from send() synchronously
basics
~10 sConfigure the value-serializer as Spring's JsonSerializer. It uses Jackson to turn your POJO into JSON bytes. Then send(topic, key, pojo) works directly. The consumer side uses JsonDeserializer to reconstruct the object.
solid answer
~40 sSet the producer's value-serializer to org.springframework.kafka.support.serializer.JsonSerializer, which uses a Jackson ObjectMapper to write your POJO as JSON bytes. In Spring Boot you set spring.kafka.producer.value-serializer to that class; then KafkaTemplate<String, MyEvent> lets you send(topic, key, myEvent) directly. By default JsonSerializer adds type information in a __TypeId__ header so a matching JsonDeserializer can rebuild the exact class. Key points: (1) you can disable type headers with addTypeInfo=false when the consumer is non-Java or you don't want class coupling; (2) provide/customize the ObjectMapper for date formats, unknown properties, and modules; (3) on the consumer side use trusted packages (spring.kafka.consumer.properties.spring.json.trusted.packages) to avoid deserializing arbitrary types. Serialization runs on the calling thread, so a bad object throws SerializationException synchronously from send().
code
java · 25 lines@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, OrderCreated> producerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.ACKS_CONFIG, "all");
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.build();
// pass serializer instances so the custom mapper is used
return new DefaultKafkaProducerFactory<>(
props, new StringSerializer(), new JsonSerializer<>(mapper));
}
@Bean
public KafkaTemplate<String, OrderCreated> kafkaTemplate(
ProducerFactory<String, OrderCreated> pf) {
return new KafkaTemplate<>(pf);
}
}go deeper
Know you set value-serializer to JsonSerializer to send POJOs as JSON.
Explain Jackson backing, the TypeId header, and the consumer's JsonDeserializer.
Discuss custom ObjectMapper injection, addTypeInfo trade-offs, trusted packages, and type mappings across services.
Weigh JSON vs Avro/Protobuf+registry for schema evolution and cross-language contracts; design wire-identity decoupling.
**The problem.** Kafka moves `byte[]`. To send a domain object like `OrderCreated` you need a *serializer* that turns it into bytes, and the consumer needs a matching *deserializer*. For JSON, Spring for Apache Kafka provides `org.springframework.kafka.support.serializer.JsonSerializer<T>` (and `JsonDeserializer<T>`), backed by **Jackson**. **Wiring the producer.** ```yaml spring: kafka: producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer ``` Now declare `KafkaTemplate<String, OrderCreated>` and call `send("orders", order.getId(), order)`. JsonSerializer calls its ObjectMapper's `writeValueAsBytes(order)`. **Type headers (`__TypeId__`).** By default JsonSerializer sets `addTypeInfo=true`, writing the fully-qualified class name into a Kafka **header** named `__TypeId__`. The paired `JsonDeserializer` reads that header to know which class to instantiate — convenient for Java-to-Java flows because one deserializer can handle multiple event types. Turn it off with `JsonSerializer.ADD_TYPE_INFO_HEADERS=false` (or `serializer.setAddTypeInfo(false)`) when the consumer is a non-JVM app, or when you don't want to leak/couple concrete class names. Then the consumer must be told the target type another way (default type, or type-mapping headers). **Customizing the ObjectMapper.** The default mapper may not handle Java 8 dates or unknown fields the way you want. Construct the serializer with your own mapper: ```java ObjectMapper mapper = JsonMapper.builder() .addModule(new JavaTimeModule()) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .build(); ProducerFactory<String, OrderCreated> pf = new DefaultKafkaProducerFactory<>( configProps, new StringSerializer(), new JsonSerializer<>(mapper)); ``` Passing serializer instances to `DefaultKafkaProducerFactory` (instead of just class names in the config map) is the clean way to inject a configured mapper. **Consumer-side safety — trusted packages.** `JsonDeserializer` using `__TypeId__` will try to instantiate whatever class name the header says. Deserializing arbitrary types from untrusted input is a security risk. Restrict it with `spring.kafka.consumer.properties.spring.json.trusted.packages=com.myco.events` (or `*` only when you trust producers). This is a real interview gotcha. **Type mismatches.** If the producer writes class `a.b.OrderCreated` in the header but the consumer app has it under a different package, deserialization fails unless you configure **type mappings** (`spring.json.type.mapping`) that alias logical names to local classes — decoupling wire identity from Java package structure. This is the robust pattern for evolving schemas across services. **Where errors surface.** Serialization happens on the *calling* thread inside `send()`, before buffering. A non-serializable object or Jackson failure throws `SerializationException` **synchronously** from `send()`, not via the future — so wrap send in try/catch if inputs can be bad. **Alternatives.** JsonSerializer is easy and human-readable but verbose and schema-less. For strong schema/evolution guarantees teams often use Avro or Protobuf with a Schema Registry (`KafkaAvroSerializer`). Those are out of scope here but worth naming. **When to use.** JSON via JsonSerializer is a great default for internal Java-to-Java event flows and quick starts; reach for Avro/Protobuf + registry when you need enforced schema evolution or cross-language contracts at scale.
- What is the __TypeId__ header and why might you disable it?It's a Kafka header JsonSerializer adds carrying the payload's fully-qualified Java class name so JsonDeserializer can rebuild the exact type. Disable it (addTypeInfo=false) for non-JVM consumers, to avoid coupling to concrete class names, or when the consumer resolves the type by config instead.
- Why are 'trusted packages' important on the consumer side?JsonDeserializer will instantiate whatever class the __TypeId__ header names. If producers are untrusted, that's an unsafe-deserialization risk. Restricting spring.json.trusted.packages limits which classes can be reconstructed, preventing arbitrary type instantiation.
saying these in an interview costs you the question
- Thinking JsonSerializer enforces a schema or does versioning (it doesn't; that's Avro/registry territory)
- Assuming type headers work automatically across services with mismatched package names
- Setting trusted.packages=* blindly, ignoring the deserialization security risk
- Believing a Jackson failure surfaces via the future rather than being thrown from send()