# 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)