Skip to content

8.3.0: stream application/x-ndjson endpoints without making Spring MVC services reactive - #472

Merged
jemacineiras merged 3 commits into
sngular:mainfrom
joseegman-idoneea:feature/ndjson-streaming
Sep 29, 2026
Merged

jemacineiras merged 3 commits into
sngular:mainfrom
joseegman-idoneea:feature/ndjson-streaming

Conversation

@joseegman-idoneea

@joseegman-idoneea joseegman-idoneea commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Some endpoints need to stream application/x-ndjson while the rest of the service stays a plain Spring MVC application. Until now such a contract generated ResponseEntity<ItemDTO> with produces = "application/json" (one item, wrong content type), ResponseEntity<Mono<ItemDTO>> for WebFlux, and bodyToMono in the WebClient client; an OpenAPI 3.2 itemSchema crashed the generator with a NullPointerException.

An operation whose successful response, or whose request body, is application/x-ndjson or application/jsonl (JSON Lines, the same format) is now detected from the contract. Its schema is the type of each line, or an array of it, and itemSchema is read the same way. Every other operation generates as before.

Spring MVC servers — no Reactor, no WebFlux

The operation is split into the endpoint Spring maps and a plain method to implement:

@Override
public ResponseEntity<Stream<ItemDTO>> listItems(final String filter) {
  return ResponseEntity.ok(itemRepository.streamByFilter(filter));
}

The generated listItemsNdjson endpoint sends it through a generated NdjsonSupport class, which:

  • sends each item as the stream produces it, using StreamingResponseBody on Spring MVC's async executor;
  • uses the application's ObjectMapper, or a default one when there is none;
  • closes the stream, also when sending fails;
  • keeps the status and headers of the returned ResponseEntity.

A streamed request body arrives as a Stream<ItemDTO>, read line by line as it is consumed. When the response also declares application/json, which must then be the array of the items, a …Json endpoint sends the same items as an array. Spring picks one or the other from Accept; */* or no Accept gets JSON.

⚠️ Spring Framework 7.0.6+ (Boot 4) turns the flush() of the body stream into a no-op (spring.http.response.flush.enabled). Even a hand-written StreamingResponseBody buffered the whole response. NdjsonSupport therefore flushes the HttpServletResponse after each line, which works on Servlet 4 to 6.

Reactive flows

  • WebFlux servers take and return Flux<ItemDTO>, with the streamed media type in produces/consumes.
  • WebClient clients:
    • return Flux<ItemDTO> (bodyToFlux), and …WithHttpInfo returns Mono<ResponseEntity<Flux<ItemDTO>>> (toEntityFlux);
    • Accept is the streamed media type;
    • ApiWebClient now registers its Jackson codecs for NDJSON and JSON Lines (both as streaming types), which it rejected before, and sends Publisher bodies.
  • Spring's own codecs stream application/x-ndjson only. For JSON Lines, a WebFlux server or a reactive @HttpExchange backed by the application's WebClient.Builder needs a CodecCustomizer; the README shows it for Boot 4 and 2/3.
  • Reactive @HttpExchange returns Mono<ResponseEntity<Flux<ItemDTO>>> and takes a Flux<ItemDTO> body.

Blocking clients

RestClient/RestTemplate, and @HttpExchange without reactive, read a response whole (BufferingClientHttpRequestFactory). Generation fails for a streaming operation and asks for reactive=true. A real Stream<T> for them is left for a follow-up.

Contract checks (fail generation)

  • application/stream+json: deprecated, no longer streamed by Spring Framework 7, and not read by ApiWebClient's codecs. The error asks for application/x-ndjson or application/jsonl.
  • An application/json alternative of the streamed response that is not the array of its items, e.g. a page wrapper.
  • A streamed request body that also declares another media type.

Also fixed

A response declaring more than one media type rendered a @Content(...)mediaType = ... that does not compile, in both server templates. It now renders content = {@Content(...), @Content(...)}. Single-media-type output is unchanged; no existing asset had the broken form.

Test plan

  • New fixture testNdjsonStreaming. It covers:

    • response kinds: a $ref item, an array of items, JSON + NDJSON, and an inline itemSchema;
    • a streamed request body;
    • a JSON 400 next to a streamed 200;
    • a plain JSON control operation;
    • a JSON Lines response and body.

    Generated for the Spring MVC server on Boot 2 (javax), 3 (jakarta + Jackson 2) and 4 (Jackson 3), for WebFlux, for WebClient, and for reactive @HttpExchange.

  • Error tests: application/stream+json; RestClient and blocking @HttpExchange refused; a JSON alternative that does not match the items; a mixed streamed body.

  • Changed existing asset: testWebClientApiGeneration/springboot4/ApiWebClient.java (NDJSON codecs and Publisher body), reviewed.

  • Engine: 256 tests green, checkstyle green; Maven plugin ITs green; Gradle test integrationTest green (JDK 21)

  • Runtime check outside the repo: the generated code compiled into Spring Boot 2.7.18, 3.5.8 and 4.0.8 apps, exercised over real HTTP; 23 tests green. They check:

    • lines arrive about 0 / 400 / 800 ms apart, as produced, not buffered;
    • the app's ObjectMapper is used (non_null honoured);
    • the stream gets closed;
    • Accept negotiation between the JSON and NDJSON endpoints;
    • a streamed import;
    • JSON Lines responses and bodies, through the MVC server, the generated WebClient client, and reactive @HttpExchange with the README's CodecCustomizer (and failing without it);
    • 501 for an unimplemented operation;
    • the generated WebClient client (2.7) and reactive @HttpExchange (4.0) against the generated MVC server, both streaming and sending Flux bodies.

    The WebFlux server was compiled, not run.

  • README: new "Streaming endpoints (application/x-ndjson)" section

  • Version bumped to 8.3.0 in all 5 places

🤖 Generated with Claude Code

joseegarcia and others added 2 commits September 29, 2026 13:54
…ces reactive

An operation whose successful response or request body is application/x-ndjson
(or application/stream+json) is detected from the contract; its schema, the items
of an array schema, or an OpenAPI 3.2 itemSchema, is the type of each line.

- Spring MVC servers split the operation into the endpoint Spring maps, which is
  not overridden, and a plain method to implement returning
  ResponseEntity<Stream<T>> (or taking a Stream<T> body). The generated
  NdjsonSupport sends each item as it is produced through StreamingResponseBody,
  with the application's ObjectMapper, closes the stream, and flushes the
  servlet response, since Spring Framework 7.0.6 ignores flushing the body
  stream. A JSON alternative of the same response, which must be the array of
  the items, gets its own endpoint picked by the Accept header.
- WebFlux servers, WebClient clients and reactive @HttpExchange interfaces take
  and return Flux<T>; ApiWebClient reads and writes NDJSON and sends Publisher
  bodies.
- Blocking clients read a response whole, so generation fails for a streaming
  operation and asks for reactive=true.

Also: itemSchema no longer crashes the generator, and a response declaring
several media types renders a valid @apiresponse content array.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 32 complexity

Metric Results
Complexity 32

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jemacineiras jemacineiras self-assigned this Sep 29, 2026
@jemacineiras jemacineiras added enhancement New feature or request release Adding this tag to a PR will cause a release on merge labels Sep 29, 2026
@jemacineiras
jemacineiras merged commit 07490fe into sngular:main Sep 29, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request release Adding this tag to a PR will cause a release on merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants