1
0
Files
spring-messaging-demo/sse-websocket/docs/03-the-payload.md
2026-09-04 00:52:18 +05:30

84 lines
3.6 KiB
Markdown

# 3. The payload, and a media type that does less than you think
Previous: [2. The SSE lifecycle](02-sse-lifecycle.md) · Next: [4. STOMP](04-stomp.md)
---
The advice you will find is: always pass a media type to `data(..)`, or your object is written
with `toString()`. Both halves of that turn out to be wrong on Boot 4.1, and the mechanism
underneath explains a real trap that the advice misses.
## What actually happens
`SseEmitter.send(Object data, MediaType mediaType)` walks the application's configured
`HttpMessageConverter` list and uses **the first converter whose `canWrite(type, mediaType)`
returns true**. The media type argument is a *filter*, not a preference. A `null` media type
filters nothing, so the first converter that can write the type at all wins.
`/diag` prints the list in order:
```
ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
ResourceRegionHttpMessageConverter
AllEncompassingFormHttpMessageConverter
JacksonJsonHttpMessageConverter
Jaxb2RootElementHttpMessageConverter
```
Two consequences, both measured by
[`SseDataConversionTest`](../src/test/java/com/ankurm/ssews/SseDataConversionTest.java)
([`sse-payload-conversion.txt`](output/sse-payload-conversion.txt)):
**For a record or POJO, the media type changes nothing.** No converter ahead of Jackson claims it,
so Jackson writes it either way:
```
=== SseEmitter.event().data(record, APPLICATION_JSON) ===
{"seq":7,"host":"node-a","cpu":0.42,"heapMb":512,"at":"2026-09-03T10:00:00Z"}
=== SseEmitter.event().data(record) [no media type] ===
{"seq":7,"host":"node-a","cpu":0.42,"heapMb":512,"at":"2026-09-03T10:00:00Z"}
=== POJO: identical? true ===
```
**For a `String`, the media type *still* changes nothing** — and this is the part that
surprised me enough to rewrite the test. `StringHttpMessageConverter` supports `MediaType.ALL` and
sits ahead of Jackson, so it claims the write even when you explicitly ask for
`application/json`:
```
=== String payload, no media type -> {"already":"json"}
=== String payload, APPLICATION_JSON -> {"already":"json"}
```
There is no way to make `send()` JSON-quote a `String`. Good news if you are streaming
pre-rendered JSON — it goes out verbatim, not double-encoded. A trap if you assumed a
`String` field would be escaped for you: it will not be, and a payload containing a newline
becomes two `data:` lines rather than one.
## Jackson 3, and the `Instant` that proves it
`"at":"2026-09-03T10:00:00Z"` — an ISO-8601 string, not an epoch number. Boot 4.1.1 manages
Jackson **3.1.5** under `tools.jackson`, and `JacksonJsonHttpMessageConverter` is the Jackson 3
converter. The Jackson 2 sibling (`MappingJackson2HttpMessageConverter`) is still on the
classpath in the messaging stack.
**Rule for Boot 4: a `2` in a Spring class name means the *previous* Jackson.** The unnumbered
name is current, which is the opposite of the convention you would guess, and it is the same fork
that catches people in [`../kafka-basics`](../kafka-basics/README.md) and
[`../rabbitmq`](../rabbitmq/README.md). For STOMP it is
`JacksonJsonMessageConverter` you want, not `MappingJackson2MessageConverter`.
## Practical guidance
- Pass `MediaType.APPLICATION_JSON` anyway. It costs nothing, it documents intent, and it makes
the code correct against a converter list you did not configure yourself.
- Do not stream a `String` and assume it is escaped. Send the object.
- Keep events small. Every subscriber pays for one serialisation per event; see
[6. Choosing](06-choosing.md).
---
Previous: [2. The SSE lifecycle](02-sse-lifecycle.md) · Next: [4. STOMP](04-stomp.md)