Spring reactive WebClient GET json response with Content-Type "text/plain;charset=UTF-8"
Asked Answered
D

3

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.

Detain answered 16/4, 2020 at 18:57 Comment(7)
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();Wheezy
This issue is not about my request headers, I already explained it in the post.Detain
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?Wheezy
Also can you post your DTO class here?Wheezy
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 localhost/response.json, If I change the file extension to response.txt it throws the same exception.Detain
does this help you: #48598733 read the question's last update section and the accepted answer. also, can you share the response and the DTO class?Wheezy
That answer solves a different issue. I already found a solution. Thank youDetain
D
13

In case someone needs it, here is the solution:

This answer (https://mcmap.net/q/1149492/-reactive-webclient-get-request-with-text-html-response) 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!

Detain answered 21/4, 2020 at 12:10 Comment(0)
A
4

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();
Abuzz answered 27/1, 2021 at 4:50 Comment(1)
Sometimes get error java.util.ConcurrentModificationExceptionCranny
M
0

Set content type for webclient.

webClient.get()
            .uri(endpoint)
           .contentType(MediaType.APPLICATION_JSON_UTF8)
Mien answered 16/4, 2020 at 19:33 Comment(2)
It doesn't solve the problem. I added to the post more information.Detain
this is deprecatedCorrespond

© 2022 - 2024 — McMap. All rights reserved.