8

I’m having an issue with Spring 5 reactive WebClient, when I request an endpoint that returns a correctly formated json response with content type "text/plain;charset=UTF-8". The exception is

org.springframework.web.reactive.function.UnsupportedMediaTypeException:
Content type 'text/plain;charset=UTF-8' not supported for bodyType=MyDTOClass

Here is how I made the request:

webClient.get().uri(endpoint).retrieve().bodyToFlux(MyDTOClass.class)

EDIT: Headers are "correctly" setted (Accept, Content-Type), I have tried differents content-types (json, json + UTF8, text plain, text plain + UTF8) conbinations, without success. I think the issue is .bodyToFlux(MyDTOClass.class) doesn't know how to translate "text" into MyDTOClass objects. If I change the request to:

webClient.get().uri(endpoint).retrieve().bodyToFlux(String.class)

I can read the String.

EDIT 2: The next quote is extracted from the Spring documentation (https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-codecs-jackson)

By default both Jackson2Encoder and Jackson2Decoder do not support elements of type String. Instead the default assumption is that a string or a sequence of strings represent serialized JSON content, to be rendered by the CharSequenceEncoder. If what you need is to render a JSON array from Flux<String>, use Flux#collectToList() and encode a Mono<List<String>>.

I think the solution is define a new Decoder/Reader in order to transform the String into MyDTOClass, but i don't know how to do it.

Charlie
  • 311
  • 1
  • 3
  • 9
  • you can try `this.webClient = WebClient.builder() .baseUrl(clientUrl) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_UTF8_VALUE) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE) .build();` – Shoshi Apr 17 '20 at 19:51
  • 1
    This issue is not about my request headers, I already explained it in the post. – Charlie Apr 20 '20 at 09:12
  • Did you tried to map the response in string and print it out for just debugging purpose? Just to be sure what is coming as response. And one more thing what is the endpoint returning? Like is the endpoint returning a response which is really mappable into your DTO? – Shoshi Apr 20 '20 at 09:16
  • Also can you post your DTO class here? – Shoshi Apr 20 '20 at 09:17
  • Yes, I can read the String if I use `.bodyToFlux(String.class)` and my DTO match perfectly with the response.I manualy extracted the response to a file in my resources directory with the name `response.json` and I can read it without problem pointing the webclient to http://localhost/response.json, If I change the file extension to `response.txt` it throws the same exception. – Charlie Apr 20 '20 at 09:35
  • does this help you: https://stackoverflow.com/questions/48598233/deserialize-a-json-array-to-objects-using-jackson-and-webclient read the question's last update section and the accepted answer. also, can you share the response and the DTO class? – Shoshi Apr 20 '20 at 11:11
  • That answer solves a different issue. I already found a solution. Thank you – Charlie Apr 21 '20 at 12:16

3 Answers3

13

In case someone needs it, here is the solution:

This answer (https://stackoverflow.com/a/57046640/13333357) is the key. We have to add a custom decoder in order to specify what and how deserialize the response.

But we have to keep in mind this: The class level anotation @JsonIgnoreProperties is setted by default to the json mapper and does not have effect to other mappers. So if your DTO doesn't match all the response "json" properties, the deserialization will fail.

Here is how to configure the ObjectMapper and the WebClient to deserialize json objects from text responses:

...
WebClient.builder()
        .baseUrl(url)
        .exchangeStrategies(ExchangeStrategies.builder().codecs(configurer ->{
                ObjectMapper mapper = new ObjectMapper();
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                configurer.customCodecs().decoder(new Jackson2JsonDecoder(mapper, MimeTypeUtils.parseMimeType(MediaType.TEXT_PLAIN_VALUE)));
                }).build())
        .build();
...

Cheers!

Charlie
  • 311
  • 1
  • 3
  • 9
3

Following up on this answer from Charlie above, you can now add an extra "codec" without replacing them.

You can also easily build Spring's default configured ObjectMapper via Jackson2ObjectMapperBuilder.json().build()

Here's an example that reuses the ObjectMapper from the built-in Jackson2JsonDecoder

var webClient = webClientBuilder
    .baseUrl(properties.getBaseUrl())
    .codecs(configurer -> {
        // This API returns JSON with content type text/plain, so need to register a custom
        // decoder to deserialize this response via Jackson
        
        // Get existing decoder's ObjectMapper if available, or create new one
        ObjectMapper objectMapper = configurer.getReaders().stream()
            .filter(reader -> reader instanceof Jackson2JsonDecoder)
            .map(reader -> (Jackson2JsonDecoder) reader)
            .map(reader -> reader.getObjectMapper())
            .findFirst()
            .orElseGet(() -> Jackson2ObjectMapperBuilder.json().build());
        
        Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(objectMapper, MediaType.TEXT_PLAIN);
        configurer.customCodecs().registerWithDefaultConfig(decoder);
    })
    .build();
Michael R
  • 1,753
  • 20
  • 18
0

Set content type for webclient.

webClient.get()
            .uri(endpoint)
           .contentType(MediaType.APPLICATION_JSON_UTF8)
Alien
  • 15,141
  • 6
  • 37
  • 57