83 lines
3.7 KiB
Markdown
83 lines
3.7 KiB
Markdown
[← Producing](02-producing.md) · [Module README](../README.md) · [Keys and partitions →](04-keys-and-partitions.md)
|
|
|
|
# 3. Serialisation: two Jackson families, and the one your IDE will suggest is the wrong one
|
|
|
|
Kafka moves `byte[]`. Everything else is a `Serializer` and a `Deserializer`, and under Spring
|
|
Boot 4 there is a fork in the road that no tutorial mentions yet.
|
|
|
|
Spring Kafka 4.1 ships **two complete JSON families**:
|
|
|
|
| Class | Jackson | Mapper type |
|
|
|---|---|---|
|
|
| `JsonSerializer` / `JsonDeserializer` / `JsonSerde` | 2.x | `com.fasterxml.jackson.databind.ObjectMapper` |
|
|
| `JacksonJsonSerializer` / `JacksonJsonDeserializer` / `JacksonJsonSerde` | 3.x | `tools.jackson.databind.json.JsonMapper` |
|
|
|
|
Spring Boot 4 is a Jackson 3 application. `spring-boot-starter-jackson` brings
|
|
`tools.jackson.core:jackson-databind` 3.1.5. But the class named `JsonSerializer` — the one
|
|
every existing example configures, and the one autocomplete offers first — is the Jackson 2 one.
|
|
|
|
It does not fail at startup. It fails on the first payload containing a `java.time` value:
|
|
|
|
```
|
|
org.apache.kafka.common.errors.SerializationException: Can't serialize data
|
|
[OrderEvent[orderId=o-1, ..., placedAt=2026-08-29T10:15:30Z]] for topic [orders]
|
|
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
|
|
Java 8 date/time type `java.time.Instant` not supported by default:
|
|
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
|
|
```
|
|
|
|
The message tells you to add a Jackson 2 module, and doing so works — and leaves you running two
|
|
Jackson stacks, one for your HTTP layer and one for your messaging layer, with independent
|
|
configuration. The better fix is one word:
|
|
|
|
```yaml
|
|
spring:
|
|
kafka:
|
|
producer:
|
|
value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer
|
|
consumer:
|
|
value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer
|
|
```
|
|
|
|
Jackson 3 handles `java.time` with no module and no configuration, and `BigDecimal` keeps its
|
|
scale through a round trip — both asserted in
|
|
[`SerialisationTest`](../src/test/java/com/ankurm/kafkabasics/SerialisationTest.java):
|
|
|
|
```
|
|
{"orderId":"o-1","customerId":"c-1","amount":10.00,"placedAt":"2026-08-29T10:15:30Z"}
|
|
```
|
|
|
|
`amount` is `10.00`, not `10.0` and not `"10.00"`. Use `BigDecimal` for money and this survives;
|
|
use `double` and it does not.
|
|
|
|
## The type header
|
|
|
|
The three-argument `serialize(topic, headers, data)` overload writes a `__TypeId__` header
|
|
naming the class:
|
|
|
|
```java
|
|
headers.lastHeader("__TypeId__") -> "com.ankurm.kafkabasics.OrderEvent"
|
|
```
|
|
|
|
The consumer reads it and builds that class. Which is convenient and is also a remote-code-
|
|
selection primitive, so the deserializer refuses any class outside its trusted packages:
|
|
|
|
```
|
|
... is not in the trusted packages: [java.util, java.lang]
|
|
```
|
|
|
|
Three ways out, in descending order of preference:
|
|
|
|
1. `spring.json.trusted.packages: com.ankurm.kafkabasics` — an allow-list of your own packages.
|
|
2. `spring.json.value.default.type` — ignore the header, always build this class. Best when
|
|
producer and consumer are owned by different teams, because it makes the consumer's contract
|
|
its own decision rather than the producer's.
|
|
3. `spring.json.trusted.packages: "*"` — do not.
|
|
|
|
Option 2 has a second benefit worth stating plainly: the `__TypeId__` header couples the two
|
|
services by **fully qualified class name**. Renaming or moving a class in the producer breaks
|
|
every consumer that trusts the header, at runtime, with no compile-time warning. `type.mapping`
|
|
(a logical name to class map on both sides) is the version of this that survives a refactor.
|
|
|
|
[Keys and partitions →](04-keys-and-partitions.md)
|