Search code examples
javahttphttpclientjava-11java-http-client

How to map a JSON response to a Java class using Java 11 HttpClient and Jackson?


I'm new to the Java 11 HttpClient and would like to give it a try. I have a simple GET request that return JSON and I would like to map the JSON response to a Java class called Questionnaire.

I understand that I can turn the response out of box into a String or an input stream like this

HttpRequest request = HttpRequest.newBuilder(new URI(String.format("%s%s", this.baseURI, "/state")))
          .header(ACCEPT, APPLICATION_JSON)
          .PUT(noBody()).build();

HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());

How can I write something that converts the JSON string to my Questionnaire class like this?

HttpResponse<Questionnaire> response = this.client.send(request, HttpResponse.BodyHandlers./* what can I do here? */);

I use Jackson to transform JSON into Java class instances. Is there Jackson support for the new Java standard HttpClient yet?

UPDATE 1 I was not precise enough, sorry about that. I am looking for a blocking get example. I was aware of http://openjdk.java.net/groups/net/httpclient/recipes.html#jsonGet


Solution

  • Solution for Java 11 HttpClient::sendAsync only

    Based on this link you can do something like this :

    public static void main(String[] args) throws IOException, URISyntaxException, ExecutionException, InterruptedException {
            UncheckedObjectMapper uncheckedObjectMapper = new UncheckedObjectMapper();
    
            HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
                    .header("Accept", "application/json")
                    .build();
    
            Model model = HttpClient.newHttpClient()
                    .sendAsync(request, HttpResponse.BodyHandlers.ofString())
                    .thenApply(HttpResponse::body)
                    .thenApply(uncheckedObjectMapper::readValue)
                    .get();
    
            System.out.println(model);
    
    }
    
    class UncheckedObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
            /**
             * Parses the given JSON string into a Map.
             */
            Model readValue(String content) {
                try {
                    return this.readValue(content, new TypeReference<Model>() {
                    });
                } catch (IOException ioe) {
                    throw new CompletionException(ioe);
                }
            }
    
    }
    
    class Model {
            private String userId;
            private String id;
            private String title;
            private boolean completed;
    
    
        //getters setters constructors toString
    }
    

    I used some dummy endpoint which provides sample JSON input and sample model class to map the response directly to Model class using Jackson.

    Solution for Java 11 HttpClient::send and HttpClient::sendAsync

    I found a way by defining custom HttpResponse.BodyHandler :

    public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<W> {
    
        private Class<W> wClass;
    
        public JsonBodyHandler(Class<W> wClass) {
            this.wClass = wClass;
        }
    
        @Override
        public HttpResponse.BodySubscriber<W> apply(HttpResponse.ResponseInfo responseInfo) {
            return asJSON(wClass);
        }
    
        public static <T> HttpResponse.BodySubscriber<T> asJSON(Class<T> targetType) {
            HttpResponse.BodySubscriber<String> upstream = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
    
            return HttpResponse.BodySubscribers.mapping(
                    upstream,
                    (String body) -> {
                        try {
                            ObjectMapper objectMapper = new ObjectMapper();
                            return objectMapper.readValue(body, targetType);
                        } catch (IOException e) {
                            throw new UncheckedIOException(e);
                        }
                    });
        }
    }
    

    Then I call it :

    public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
    
        HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
                    .header("Accept", "application/json")
                    .build();
    
        Model model = HttpClient.newHttpClient()
                    .send(request, new JsonBodyHandler<>(Model.class))
                    .body();
    
        System.out.println(model);
    
    }
    

    The response is :

    Model{userId='1', id='1', title='delectus aut autem', completed=false}
    

    The JavaDoc of HttpResponse.BodySubscribers::mapping was particulary useful to solve this. It can be further improved to use HttpResponse.BodySubscribers::ofInputStream instead of HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8) to define the BodySubscriber for the JsonBodyHandler.