harbu.org

JAX-RS Map Message Body Reader

March 10, 2016

Recently I was using Jersey to implement a client for a RESTful web service in Java. Jersey implements the JAX-RS (Java API for RESTful Web Services) API specification which defines an unified interface for writing RESTful web services and clients. Using JAX-RS leads to some rather nifty code, even if we are working in the ceremonial language also known as Java:

public MyBean fetchData(String baseUrl) {
    Client client = ClientBuilder.newClient();
    WebTarget target = client.target(baseUrl).path("books")
        .queryParam("key", "fake-api-key")
        .queryParam("region", "Finland");
    return target.request(MediaType.APPLICATION_JSON)
        .header("User-Agent", "MyBot/1.0 (+http://www.domain.com)")
        .get(MyBean.class)
}

The code above instantiates a new REST HTTP client, builds a target URL, performs a GET on said target URL, and finally de-serializes the response JSON into a bean.

But what if instead of MyBean you'd want a java.util.Map so that you don't need to write tedious POJOs. Perhaps the response JSON consists of multiple levels and the single value that you are interested is buried deep inside the hierarchy. Well you could do the following.

return target.request(MediaType.APPLICATION_JSON)
    .header("User-Agent", "MyBot/1.0 (+http://www.domain.com)")
    .get(Map.class)

However, this isn't enough. In addition, you need to tell Jersey how to convert JSON into a Map. The following MessageBodyReader.java will do just that. It reads the HTTP response into a string and then transforms it into a java.util.Map.

import com.google.gson.Gson;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Scanner;

public class MapMessageBodyReader implements MessageBodyReader<Map> {
    @Override
    public boolean isReadable(Class<?> type,
                              Type genericType,
                              Annotation[] annotations,
                              MediaType mediaType) {
        return Map.class.isAssignableFrom(type);
    }

    @Override
    public Map readFrom(Class<Map> type,
                        Type genericType,
                        Annotation[] annotations,
                        MediaType mediaType,
                        MultivaluedMap<String, String> httpHeaders,
                        InputStream entityStream) throws IOException,
                          WebApplicationException {
        Scanner s = new Scanner(entityStream).useDelimiter("\\A");
        String jsonString = s.hasNext() ? s.next() : "";

        Gson gson = new Gson(); // ... or any JSON library
        return gson.fromJson(jsonString, Map.class);
    }
}

You will also need to register the mapper with Jersey. Or at least I had to, since Jersey wouldn't automatically detect it even with appropriate class-level annotations set.

Client client = ClientBuilder.newBuilder()
    .register(MapMessageBodyReader.class)
    .build();