1

I am trying to implement a GitHub REST API client in Spring. I'm new to Spring but reasonably familiar with JSON and Flux.

The problem I have is that the JSON body returned by the GitHub API can sometimes be an array and sometimes a single object, i.e.

{
   foo="bar"
}

or

[
   {
      foo="bar"
   },
   {
      foo="foobar"
   }
]

I'm using ClientResponse#bodyToMono with a ParameterizedTypeReference like so:

WebClient client;
ParameterizedTypeReference<List<T>> typeRef;
...
client.get().uri(...).exchangeToMono(response -> {

   return response.bodyToMono(typeRef);
});

The above works fine when the JSON response is an array, but (understandably) fails when it's not: org.springframework.core.codec.DecodingException: JSON decoding error: Cannot deserialize value of type `java.util.ArrayList<...>` from Object value (token `JsonToken.START_OBJECT`)

I've tried the following approaches to resolve, but none have succeeded:

  1. Enabling the ACCEPT_SINGLE_VALUE_AS_ARRAY option. I think this isn't working because it's not a field that may or may not be a list, but the entire JSON body.
  2. Creating a wrapper class MyList<T> that contains a single List<T> field. This fails to deserialize as there is no inner field in the JSON to map - perhaps there's some way I can fix this with annotations?
  3. Checking if the response body is a list or not, and deserialize in two different ways. This code fails because response.bodyToMono can't be executed twice for Flux-y reasons (it's null the second time), so the following doesn't work:
Class<T> theClazz;
...
response.bodyToMono(String.class).flatMap(b -> b.startWith("[") ? response.bodyToMono(typeRef) : response.bodyToMono(theClazz).map(List::of))
  1. Writing my own BodyExtractor. I couldn't get the Jackson ObjectMapper to play nicely with the typeRef. Using ResolvableType.forType(typeRef).getRawClass() with the mapper picks up that there's a List, but doesn't properly deserialize the contents (instead putting it into a Map)

Any suggestions of how I can resolve this?

I feel like there must be a simple solution that I'm missing due to ignorance - any help much appreciated!

Overlord_Dave
  • 894
  • 10
  • 27

1 Answers1

0

In the end I used an ObjectMapper directly as below:

ObjectMapper jsonMapper = new ObjectMapper();

return response.bodyToMono(String.class).map(m -> {

    try {

        if (m.startsWith("[")) {

            final T[] objects = (T[]) jsonMapper.readValue(m, clazz.arrayType());

            return Arrays.asList(objects);

        } else {

            return List.of(jsonMapper.readValue(m, clazz));
        }

    } catch (JsonProcessingException e) {

        throw new RuntimeException(e);
    }
});

The major disadvantage of this approach is that the mapper is not the same instance used by the Spring environment and does not match the configuration specified in any properties. There is some information in this answer about accessing that mapper, but so far doesn't seem like there's a consistent approach that always works: How do I obtain the Jackson ObjectMapper in use by Spring 4.1?

Not marking as answered as I hope there's a cleaner solution than the above, but it does work so long as you're not relying on properties to configure your JSON parsing.

Overlord_Dave
  • 894
  • 10
  • 27