skip to content

What is the difference between declaring @RequestBody User user and @RequestBody Mono<User> user in a WebFlux handler?

level: middleimportance: should knowfreq 55%

answer

  1. Plain T = decode-then-invoke
  2. Mono<T> = invoke-then-decode-lazily
  3. Flux<T> = element-by-element streaming
  4. @Valid works; error = WebExchangeBindException
  5. No BindingResult with reactive body

basics

~20 s

@RequestBody User fully decodes the body before the method runs. @RequestBody Mono<User> hands you a publisher of the body — decoding is deferred until you subscribe, so you compose the request body into your reactive pipeline.

solid answer

~40 s

With `@RequestBody User user`, the framework reactively reads and decodes the entire request body first, then invokes your method with the ready object — the decoding is still non-blocking, but the argument is materialized before the handler body executes. With `@RequestBody Mono<User> user` (or `Flux<User>` for a stream), the method is invoked immediately and you receive a **publisher**; the body is only decoded when the reactive chain subscribes. This lets you flatMap the body into downstream reactive calls without any intermediate blocking wait, and it's the only way to stream a large or multi-element JSON payload element-by-element (`Flux<User>`). Validation still works: `@Valid @RequestBody Mono<User>` validates each decoded element and raises `WebExchangeBindException` on failure. Prefer the wrapped form when the body feeds directly into a reactive operation.

code

java · 19 lines
java
@RestController
@RequestMapping("/users")
class UserController {
    private final UserRepository repo;
    UserController(UserRepository repo) { this.repo = repo; }

    // deferred: body composed into the pipeline
    @PostMapping
    Mono<User> create(@Valid @RequestBody Mono<User> body) {
        return body.flatMap(repo::save);
    }

    // streaming: process each element as it arrives
    @PostMapping(value = "/bulk",
                 consumes = MediaType.APPLICATION_NDJSON_VALUE)
    Mono<Void> bulk(@RequestBody Flux<User> users) {
        return repo.saveAll(users).then();
    }
}

go deeper

for a junior

Know that Mono<User> defers decoding until subscribe while plain User is ready when the method runs.

for a middle

Explain composition benefits and the Flux streaming case; know @Valid raises WebExchangeBindException.

for a senior

Discuss memory/backpressure implications of Flux bodies and error-handling without BindingResult.

for a principal

Reason about when deferred decoding matters for end-to-end backpressure and large-payload memory profiles.

## `@RequestBody` in WebFlux `@RequestBody` binds the HTTP request body to a method argument, decoded by a reactive `HttpMessageReader` (e.g. Jackson's reactive decoder). WebFlux lets you declare that argument in **two shapes**, and the difference is *when* decoding happens. ### 1. Materialized form — `@RequestBody User user` The framework reads the whole body, decodes it into a `User`, and **only then calls your method** with the finished object. This is still non-blocking under the hood (the event-loop thread isn't parked; the handler is invoked via a callback once the body is ready), but from your code's perspective the object already exists when the method runs. ```java @PostMapping("/users") Mono<User> create(@RequestBody User user) { return repo.save(user); // user is already decoded } ``` Use this when the payload is a single small object and you don't need to interleave decoding with other async work. ### 2. Deferred form — `@RequestBody Mono<User> user` The method is invoked **immediately**, before the body is read, and you receive a `Mono<User>` (or `Flux<User>`). Decoding happens lazily when the returned pipeline is subscribed. You compose it: ```java @PostMapping("/users") Mono<User> create(@RequestBody Mono<User> body) { return body.flatMap(repo::save); // decode -> save, all reactive } ``` Why prefer it? Because the body becomes a first-class part of your reactive graph — no hidden 'await the whole body' step, cleaner composition, and it plays well with backpressure. ### 3. Streaming form — `@RequestBody Flux<User>` If the client streams many JSON objects (e.g. NDJSON, or a large JSON array), `Flux<User>` lets you process **element by element** as they arrive, instead of buffering the entire payload in memory: ```java @PostMapping(value = "/users", consumes = MediaType.APPLICATION_NDJSON_VALUE) Mono<Void> bulk(@RequestBody Flux<User> users) { return repo.saveAll(users).then(); } ``` This is impossible with the materialized `List<User>` form, which must buffer everything. ## Validation `@Valid` (or `@Validated`) works with all three shapes. With a reactive wrapper, Spring validates **each decoded element** as it flows through; a violation surfaces as a `WebExchangeBindException` (a `ResponseStatusException` subclass → 400 Bad Request). Note: with the `Mono`/`Flux` form you cannot inject a `BindingResult` argument to inspect errors inline — the validation happens inside the reactive pipeline, so handle it via `@ExceptionHandler` / `onErrorResume`. ## Edge cases & gotchas - **Empty body**: for a materialized `@RequestBody User` the body is required by default → missing body yields 400. A `Mono<User>` that completes empty simply emits nothing downstream, which may silently do nothing — guard with `switchIfEmpty`. - **You still can't block**: neither form lets you call `.block()` on the body; that defeats the purpose and can throw on the event loop. - **Content negotiation** (`consumes`) works the same as MVC; the reactive decoder is chosen from `MediaType`. ## When to use which - Single small object, straightforward save → either form; materialized reads slightly simpler. - Body feeds a reactive chain / you want clean composition → `Mono<T>`. - Large or truly streamed multi-element payload → `Flux<T>`.

  • Can you inject a BindingResult next to @RequestBody Mono<User> to read validation errors inline?
    No. BindingResult works with the materialized form where validation runs before the method. With a reactive wrapper, validation happens inside the pipeline, so failures surface as WebExchangeBindException (400) that you handle via @ExceptionHandler or onErrorResume, not a BindingResult argument.
  • Why would you ever choose Flux<User> as a request body?
    To process a large or streamed multi-element payload element-by-element without buffering the whole thing in memory — e.g. NDJSON bulk uploads. The materialized List<User> form must read and hold the entire body first.

saying these in an interview costs you the question

  • Claiming @RequestBody User blocks the request thread while decoding
  • Thinking @Valid does not work with Mono/Flux request bodies
  • Trying to use BindingResult with a reactive request body
  • Calling .block() on the injected Mono<User>

context