3.6 KiB
3. The payload, and a media type that does less than you think
Previous: 2. The SSE lifecycle · Next: 4. STOMP
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
(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 and
../rabbitmq. For STOMP it is
JacksonJsonMessageConverter you want, not MappingJackson2MessageConverter.
Practical guidance
- Pass
MediaType.APPLICATION_JSONanyway. 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
Stringand assume it is escaped. Send the object. - Keep events small. Every subscriber pays for one serialisation per event; see 6. Choosing.
Previous: 2. The SSE lifecycle · Next: 4. STOMP