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();