How to map a JSON response to a Java class using Java 11 HttpClient and Jackson?
Simplifying @michalk solution for Java 11 HttpClient::send
HttpService Class Example:
public class HttpService {
private final HttpClient httpClient= HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
public HttpService() {}
public <T> T sendGetRequest(String url, Class<T> responseType) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create(url)).header("Accept", "application/json").build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return new ObjectMapper().readValue(response.body(), responseType);
}
public <T> List<T> sendGetListRequest(String url, Class<T> responseType) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder().GET().uri(URI.create(url)).header("Accept", "application/json").build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(response.body(), objectMapper.getTypeFactory().constructCollectionType(List.class, responseType));
}}
Model Class Example:
public class Model {
private String id;
public Model() {}
public String getId() { return this.id; }
public void setId(String id) { this.id = id; }
@Override
public String toString() { return "Model{" + "id='" + id + '\'' + '}'; }}
Sending HTTP GET request:
public class Main {
public static void main(String[] args) {
try {
HttpService httpService = new HttpService();
Model model = httpService.sendGetRequest("http://localhost:8080/api/v1/models/1", Model.class);
System.out.println("Single Object:" + model);
System.out.print('\n');
List<Model> models = httpService.sendGetListRequest("http://localhost:8080/api/v1/models", Model.class);
for(Model m: models) { System.out.println("Object:" + m); }
}
catch (IOException | InterruptedException e) {
System.err.println("Failed to send GET request: " + e.getMessage());
}
}}
Response:
Single Object: Model{id='1'}
Object: Model{id='1'}
Object: Model{id='2'}
Object: Model{id='3'}
Required Maven Dependency (pom.xml):
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.3</version>
</dependency>
If you're OK with including a dependency, check out Methanol (disclaimer: I'm the library's author). The library has special BodyHandler
implementations for object mapping. You can add JSON support by installing the jackson adapter.
var request = MutableRequest.GET("https://example.com")
.header("Accept", "application/json");
var modelResponse = client.send(request, MoreBodyHandlers.ofObject(Model.class));
// Use TypeRef<T> for complex types
var modelListResponse = client.send(request, MoreBodyHandlers.ofObject(new TypeRef<List<Model>>() {}));
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
.