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

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_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.

Previous: 2. The SSE lifecycle · Next: 4. STOMP