Using Spring Boot 2.7.5

Having an ObjectMapper bean as @Primary

    @Bean
    @Primary
    fun defaultObjectMapper(): ObjectMapper = JsonMapper.builder()
        .addModules(KotlinModule.Builder().build())
        .build()

will make it the only objectMapper that WebClient.Builder will use, even if you pass in codecs to override the default ObjectMapper like so

        val objectMapper = JsonMapper.builder()
            .addModules(KotlinModule.Builder().build(), JavaTimeModule())
            .build()
        builder
            .clone()
            .baseUrl("http://localhost:${serverProperties.webServer.port}")
            .codecs { codecs ->
                codecs.defaultCodecs().let { codec ->
                    codec.jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
                    codec.jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON))
                }
            }
            .build()

This will fail when trying to deserialize a date object even though the new ObjectMapper adds a modules for it. This is because even though I pass in this new ObjectMapper, the WebClient.Builder still seems to be using the @Primary mapper. Add the JavaTimeModule to the @Primary bean makes the call work.

The WebClient.Builder should use the passed in codec and override the @Primary objectMapper. A working example of this is here

Comment From: wilkinsona

This will fail when trying to deserialize a date object

The failure is occurring during serialisation (encoding) not deserialised (decoding):

2022-11-17 14:28:00.454 ERROR 26609 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [596564c7-1]  500 Server Error for HTTP GET "/check"

org.springframework.core.codec.CodecException: Type definition error: [simple type, class java.time.OffsetDateTime]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.OffsetDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.example.webclientbuildercheck.controllers.RestApi$DataAsOffset["date"])
        at org.springframework.http.codec.json.AbstractJackson2Encoder.encodeValue(AbstractJackson2Encoder.java:230) ~[spring-web-5.3.23.jar:5.3.23]
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
        *__checkpoint ⇢ Handler com.example.webclientbuildercheck.controllers.RestApi#getFailingDate() [DispatcherHandler]
        *__checkpoint ⇢ HTTP GET "/check" [ExceptionHandlingWebHandler]
Original Stack Trace:
                at org.springframework.http.codec.json.AbstractJackson2Encoder.encodeValue(AbstractJackson2Encoder.java:230) ~[spring-web-5.3.23.jar:5.3.23]
                at org.springframework.http.codec.json.AbstractJackson2Encoder.lambda$encode$0(AbstractJackson2Encoder.java:150) ~[spring-web-5.3.23.jar:5.3.23]

The WebClient has successfully received the response and deserialised it into a DataAsOffset. In other words, your WebClient configuration is working correctly. The failure occurs when the controller tries to send the DataAsOffSet in its response. This fails as the primary ObjectMapper, which is the one that is used by the server-side WebFlux infrastructure, doesn't have the JavaTimeModule registered.

Comment From: msosa

Ahh yes thank you. I am seeing my problem as stated in a larger project, before it returns to the RestController. I will try to see if I can get a working example where it fails in the correct spot.

Comment From: msosa

@wilkinsona I have updated the project to show the problem correctly. Here is the error happening in the decoding

at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 9] (through reference chain: com.example.webclientbuildercheck.controllers.RestApi$DataAsOffset["date"])
    at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:238) ~[spring-web-5.3.23.jar:5.3.23]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 

When cleaning up the code I inadvertently found a workaround/likely the proper way to do what I need. Using the .codec method on WebClient.Builder works as intended.

However using the .codec function in an ExchangeStrategies.Builder and then passing it into WebClient.Builder.exchangeStrategies does not seem to work as intended. It seems to revert back to using the primary ObjectMapper.

I will use codec in webclient.builder for my own project(as the documentation does say to prefer to use that anyway). Should the exchangeStrategy that is set in exchangeStrategies in the WebClient.Builder be the strategy that is being used though?

Comment From: wilkinsona

That's working as expected from a WebClient perspective as codec customization is applied to the configured exchange strategies. When you clone the builder, you're cloning Boot's codec customization which is then applied to your exchange strategies, overwriting your codec configuration. Direct customization of the codecs is recommended in the javadoc of exchangeStrategies(ExchangeStrategies):

Configure the ExchangeStrategies to use. For most cases, prefer using codecs(Consumer).

Comment From: msosa

Ohhh I understand, that makes sense as to why creating my own WebClient.builder() worked. It didn't have any of that codec customization from Boot so it would use the object mapper I passed into Exchange Strategies.

Thank you for your time! I should have listened to the documentation more closely.