It seems that classes that extend RepresentationModel now produce 500 if you access them from an endpoint that returns Flux<MyModel>.

You can reproduce this with the following sample application

@SpringBootApplication
@EnableHypermediaSupport(type = [EnableHypermediaSupport.HypermediaType.HAL], stacks = [WebStack.WEBFLUX])
class HateApplication

@Relation(
    itemRelation = "foo",
    collectionRelation = "foos",
)
data class Foo(val foo: String) : RepresentationModel<Foo>()

@RestController
@RequestMapping("/foos")
class FooController {

    @GetMapping(produces = [MediaTypes.HAL_JSON_VALUE])
    fun fooWithHAl() = findAll()
        .collectList()
        .map { CollectionModel.of(it) }

    @GetMapping(produces = [MediaType.APPLICATION_NDJSON_VALUE])
    fun findAll() = Flux.range(0, 10)
        .map { Foo(it.toString()) }

}

fun main(args: Array<String>) {
    runApplication<HateApplication>(*args)
}

The resulting error is something along

org.springframework.http.converter.HttpMessageNotWritableException: No Encoder for [com.example.hate.Foo] with preset Content-Type 'null'
    at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:181) ~[spring-webflux-5.3.7.jar:5.3.7]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ Handler com.example.hate.FooController#findAll() [DispatcherHandler]
    |_ checkpoint ⇢ org.springframework.web.filter.reactive.ServerWebExchangeContextFilter [DefaultWebFilterChain]
    |_ checkpoint ⇢ HTTP GET "/foos" [ExceptionHandlingWebHandler]
Stack trace:
        at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:181) ~[spring-webflux-5.3.7.jar:5.3.7]
        at org.springframework.web.reactive.result.method.annotation.AbstractMessageWriterResultHandler.writeBody(AbstractMessageWriterResultHandler.java:105) ~[spring-webflux-5.3.7.jar:5.3.7]
        at org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler.handleResult(ResponseBodyResultHandler.java:86) ~[spring-webflux-5.3.7.jar:5.3.7]
        at org.springframework.web.reactive.DispatcherHandler.handleResult(DispatcherHandler.java:179) ~[spring-webflux-5.3.7.jar:5.3.7]
        at org.springframework.web.reactive.DispatcherHandler.lambda$handle$2(DispatcherHandler.java:154) ~[spring-webflux-5.3.7.jar:5.3.7]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:125) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1815) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:199) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.complete(MonoIgnoreThen.java:284) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onNext(MonoIgnoreThen.java:187) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onSubscribe(MonoIgnoreThen.java:134) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxFlatMap.trySubscribeScalarMap(FluxFlatMap.java:192) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoFlatMap.subscribeOrReturn(MonoFlatMap.java:53) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:57) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:236) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:203) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:181) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators.complete(Operators.java:136) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:120) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:157) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:73) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:281) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:860) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2397) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:169) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2193) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2067) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:96) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onSubscribe(MonoPeekTerminal.java:152) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.drain(FluxConcatMap.java:448) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.onSubscribe(FluxConcatMap.java:218) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:164) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:86) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.Mono.subscribe(Mono.java:4150) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:255) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:64) ~[reactor-core-3.4.6.jar:3.4.6]
        at reactor.netty.http.server.HttpServer$HttpServerHandle.onStateChange(HttpServer.java:915) ~[reactor-netty-http-1.0.7.jar:1.0.7]
        at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:654) ~[reactor-netty-core-1.0.7.jar:1.0.7]
        at reactor.netty.transport.ServerTransport$ChildObserver.onStateChange(ServerTransport.java:478) ~[reactor-netty-core-1.0.7.jar:1.0.7]
        at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:526) ~[reactor-netty-http-1.0.7.jar:1.0.7]
        at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:94) ~[reactor-netty-core-1.0.7.jar:1.0.7]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:209) ~[reactor-netty-http-1.0.7.jar:1.0.7]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324) ~[netty-codec-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296) ~[netty-codec-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:795) ~[netty-transport-native-epoll-4.1.65.Final-linux-x86_64.jar:4.1.65.Final]
        at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480) ~[netty-transport-native-epoll-4.1.65.Final-linux-x86_64.jar:4.1.65.Final]
        at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378) ~[netty-transport-native-epoll-4.1.65.Final-linux-x86_64.jar:4.1.65.Final]
        at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989) ~[netty-common-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.65.Final.jar:4.1.65.Final]
        at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.65.Final.jar:4.1.65.Final]
        at java.base/java.lang.Thread.run(Thread.java:831) ~[na:na]

Comment From: odrotbohm

Can you provide a working example project that contains a test that fails? Ideally in Java, so that mere mortals are able to debug it in their prehistoric IDEs? 😬 Also, what does the actual request look like that causes the error?

Information about the Accept header seems to be relevant here as you customize the content negotiation quite a bit. In fact, the controller method invoked (findAll()) uses MediaType.APPLICATION_NDJSON_VALUE and it's not clear what this is supposed to be or how that relates to any hypermedia affordances.

Comment From: kschlesselmann

@odrotbohm Hehe … and I always thought you're the one with super powers ;-)

Here you are: https://github.com/kschlesselmann/stream-demo

Bit of explanation: In our system we provide a proper hal+json API for our front end customers but internally we almost always stick stream support of webflux. So NDJSON isn't related to hypermedia affordances at all but rather streams the response as one JSON object per line to the client. Up until 2.5.0 this worked fine. Now it seems that if your class extend RepresentationModel it breaks. If you just remove it in the sample the test is green.

Comment From: odrotbohm

@odrotbohm Hehe … and I always thought you're the one with super powers ;-)

They're already stretched out with having to get a Gradle project properly working in Eclipse and source code using spaces instead of tabs 😬. Just kidding.

tl;dr

You can get this to work by defining a bean like this:

@Bean
HalConfiguration halConfiguration() {
  return new HalConfiguration().withMediaType(MediaType.APPLICATION_NDJSON);
}

but we should investigate how we can make this work better.

Details

TIL about that application/x-ndjson media type as some kind of wrapping format for the actual payload. Using that unfortunately creates challenges in content negotiation. WebFlux tries to look up an ObjectMapper for the element type (DemoModel) but for the media type application/x-ndjson. In Spring HATEOAS 1.3 we changed the configuration setup to only register the rendering customizations for the explicit media types that are enabled. I.e. HAL modifications are registered for application/hal+json etc. explicitly. That was done to prevent the default rendering to kick in for media types not activated, because otherwise you could have requested HAL-FORMS and without that being activated for the server, the default JSON rendering would kick in (as it matches application/*+json) and produce some representation that doesn't actually adhere to the format requested.

This new way of applying the customizations is now getting in our way here as the lookup is done for DemoModel and application/x-ndjson, we don't find an explicit match and end up with no renderable media type. I am kind of wondering if that lookup actually makes sense because ndjson is defined to be a wrapper type effectively representing an array of some sorts with the elements being valid JSON objects. I.e. I think trying to find an encoder for ndjson for the element types is wrong and it rather should look for application/json or application/*+json, at least as a fallback. That said, there's likely a reason I miss, why the lookup works the way it works currently. I'm going to consult with @rstoyanchev on how to approach this.

Comment From: rstoyanchev

That said, there's likely a reason I miss, why the lookup works the way it works currently.

We do support ndjson out of the box as a way of expressing a "streaming" mode. Normally if a controller returns Flux<T>, for anything application/*+json we'll aggregate and return an array. For any media type designated as streaming however we render and flush after each element.

How should this work with RepresentationModel rendering I'm not quite sure at the moment. As you said ndjson simply indicates line-delimited JSON and that's all. Not sure if some sort of parameter hint could be used.

Comment From: odrotbohm

To be precise, I am wondering why the lookup for the ObjectMapper when rendering the individual elements uses NDJSON as the reference media type, when NDJSON states that the elements can be arbitrary JSON?

The type to be rendered is a Flux<SomethingThatExtendsRepresentationModel>. In that case, I'd expect Flux and NDSON requested pair up to trigger the streaming mode. The individually rendered elements however should be paired up with application/json instead of NDJSON, right?

Comment From: rstoyanchev

To be precise, I am wondering why the lookup for the ObjectMapper when rendering the individual elements uses NDJSON as the reference media type

Isn't that what's coming from the Accept header?

Comment From: odrotbohm

Yes, but that has already triggered the switch to the streaming mode, in particular line separator delimited rendering of elements.

The NDJSON spec clearly states that each line, and thus each element, has to be valid JSON, but doesn't require the elements to be NDJSON either (as I don't even think this would make sense). Thus, when trying to find an ObjectMapper to render these elements, we must not request NDJSON, but application/json or application/*+json as otherwise, the resolution will fail to find ObjectMapper instances set up to support generic JSON for the particular type to be rendered.

Comment From: odrotbohm

Moving this to Spring Framework as the issue is caused by the way WebFlux handles NDJSON and not specific to Spring HATEOAS. To give proof for this: the issue is reproducible if you register a custom ObjectMapper for application/json and ArbitraryType on the Encoder and subsequently return a Flux<ArbitraryType> from a controller method and a client requesting NDJSON.

Comment From: rstoyanchev

@odrotbohm to take this example:

register a custom ObjectMapper for application/json and ArbitraryType on the Encoder and subsequently return a Flux from a controller method and a client requesting NDJSON

I think that means "application/ndjson" and other streaming media types would no longer be listed explicitly in supportedMediaTypes and would be matched indirectly so that even if someone overrides supportedMediaTypes they should still continue to match.

The purpose of supportedMediaTypes is to indicate what media types are supported. By making streaming media types supported implicitly we take away the ability to express that you don't want to support a streaming media type or any streaming, and instead render Flux as a JSON array. Not sure if that is common but I imagine that it can be an issue.

I think it would make more sense to have NDSON listed explicitly, even if it is a little less convenient.

Comment From: odrotbohm

Can you elaborate why you think this has something to do with the advertised media types? The problem described is an implementation detail from implementing the NDJSON media type. In particular the individual elements of the stream. So the top-level content negotiation has already worked correctly.

Comment From: rstoyanchev

I'm simply taking your example with a Jackson2JsonEncoder configured with only "application/json" in supportedMediaTypes. If I understand correctly, you're suggesting that should match to NDJSON still even though NDJSON is not explicitly listed in supportedMediaTypes, or am I missing something? How would one then indicate that they don't want to support NDJSON?

Comment From: odrotbohm

No, I didn't say or mean that. I've never argued anything should change about content negotiation or top-level encoder configuration. I've always argued that the processing of NDJSON, i.e. the implementation of the media type – in particular the selection of the ObjectMapper – has a bug: it uses the NDJSON media type to look up the ObjectMapper to render individual elements (the call from AbstractJackson2Encoder.encode(…) to selectObjectMapper(…)). In its current state, by-type customization of the mapper for application/json are not found.

Comment From: rstoyanchev

Sorry, I thought you meant an AbstractJackson2Encoder configured with custom media types, but you actually meant an AbstractJackson2Encoder configured with an ObjectMapper registration via registerObjectMappersForType. Hopefully I got this part right now.

The thing is when you register a custom ObjectMapper in this way, you also take over the media types it is associated with. Isn't it also a question of how would you then ensure the custom ObjectMapper is not used for NDJSON?

Comment From: odrotbohm

Can you elaborate what you mean with "… how would you then ensure the custom ObjectMapper is not used for NDJSON?"?

When NDJSON is requested, the JSON encoder is selected as NDJSON is declared a streaming media type by default. The encoder then goes ahead and already implements the essence of NDJSON, meaning the line separated nature of producing a response. Thus the OM is not even selected to implement NDJSON but to render individual elements within an NDJSON stream. The NDJSON spec states the following requirements those elements (emphasis mine):

The most common values will be objects or arrays, but any JSON value is permitted. See json.org for more information about JSON values.

Thus, selecting an OM that supports NDJSON is overly specific as any object mapper to produce JSON in general is a valid one. In case type specific customizations are registered, it's up to the users to define the OM to be used for those types, by registering application/json as type-specifically supported media type. Spring HATEOAS actually implicitly does that by registering application/json for the first hypermedia type listed in @EnableHypermediaSupport(types = …). That one would be used if the encoder looked up the OM for application/json in the first place.

Comment From: rstoyanchev

When NDJSON is requested, the JSON encoder is selected as NDJSON is declared a streaming media type by default.

The supportedMediaTypes property at the Encoder level is independent and only applies to Object types without a custom ObjectMapper registration. When there is a custom ObjectMapper registration, that essentially takes over the selection process for the given Object type, including an independent set of media type mappings.

In other words the Encoder selection is specific to the elementType, based on getEncodableMimeTypes(ResolvableType elementType), and therefore for an Object type with a custom ObjectMapper registration the supportedMediaTypes property is irrelevant.

Comment From: odrotbohm

I think canEncode(…) would have to be adapted to handle NDJSON specifically, too. In case of NDJSON, we don't want to find an OM that supports NDJSON but application/json, as the core trait of NDJSON is already handled in the encoder and only the rendering of the elements is done by the OM.

Comment From: rstoyanchev

I think we're going full circle now but my question was how would one express that NDJSON shouldn't be handled for types associated with the custom ObjectMapper? For a "regular" Object, I can set supportedMediaTypes and omit NDJSON. For an Object with a custom ObjectMapper I gather you are suggesting it should also be based on supportedMediaTypes but that's inconsistent the fact a custom ObjectMapper otherwise has in independent set of media type mappings. That means for example that if I cannot turn off NDJSON off for a custom ObjectMapper only, it has to be for all other Objects too.

Comment From: odrotbohm

how would one express that NDJSON shouldn't be handled for types associated with the custom ObjectMapper?

I'm not quite sure I understand why one would want to or should be able to do that. This hadn't been possible before the changes we made to the API to allow the registration of customized OMs, right? You would always have ended up with the default OM instance registered, whenever NDJSON was requested?

For an Object with a custom ObjectMapper I gather you are suggesting it should also be based on supportedMediaTypes but

No. supportedMediaTypes defines the media types supported in general. I.e. if an encoder supports NDJSON for Fluxes, that encoder should be selected. If the encoder is supposed to check support for the type of the elements rendered, it has to use the media type defines as requirement for NDJSON elements: application/json. While I think it's reasonable to use the primary media type by default, it's too strict for the handling of NDJSON as that has a defined opinion on the media type of the elements.

Comment From: rstoyanchev

I'm not sure either but I think what was possible before is not relevant because the custom ObjectMapper registration feature did not exist.

I see it as a question of consistency. The supportedMediaTypes declares supported media types for all object types. The custom ObjectMapper registration declares supported media types for a subset of object types. They're meant to be independent from each other.

if an encoder supports NDJSON for Flux

We don't support media types for a Flux. The fact it is a Flux plays no role in the selection. You can stream with a Mono if you like or with a synchronous value. It's the media type and the Object type of individual elements always that determines whether an encoder is selected.

Comment From: snicoll

@odrotbohm I can see you're assigned to this issue. Can you provide an updates so that we can triage this issue?

Comment From: odrotbohm

Oh, I'm sorry. This is probably a left-over from the ticket originally filed against Spring HATEOAS. Reviewing the discussion, I still think it's a bug in the NDJSON media type implementation, not content negotiation.

Comment From: bclozel

Duplicates #27327