Cannot unmarshal a JSON array of objects using Jersey Client
I was able to solve this with minimal effort by using JacksonJsonProvider
as the MessageBody(Reader|Writer)
provider for the Jersey Client instance:
ClientConfig cfg = new DefaultClientConfig();cfg.getClasses().add(JacksonJsonProvider.class);Client client = Client.create(cfg);
Jackson's MessageBodyReader
implementation appears to be more well-behaved than the Jersey JSON one.
Thanks to How can I customize serialization of a list of JAXB objects to JSON? for pointing me in the Jackson direction.
Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.
You can use the JSON Binding extension that is being added to the MOXy component in EclipseLink 2.4 to handle this use case:
Demo
The Jersey client API allows you to leverage the same MessageBodyReader
/MessageBodyWriter
from the server side on the client side.
package forum9627170;import java.util.List;import org.example.Customer;import com.sun.jersey.api.client.*;import com.sun.jersey.api.client.config.*;public class Demo { public static void main(String[] args) { ClientConfig cc = new DefaultClientConfig(); cc.getClasses().add(MOXyJSONProvider.class); Client client = Client.create(cc); WebResource apiRoot = client.resource("http://localhost:9000/api"); List<Badge> badges = apiRoot.path("/badges").accept("application/json").get(new GenericType<List<Badge>>(){}); for(Badge badge : badges) { System.out.println(badge.getId()); } }}
MOXyJSONProvider
Below is a generic MessageBodyReader
/MessageBodyWriter
that could be used with any server/client to enable MOXy as the JSON binding provider.
package forum9627170;import java.io.*;import java.lang.annotation.Annotation;import java.lang.reflect.ParameterizedType;import java.lang.reflect.Type;import javax.xml.transform.stream.StreamSource;import javax.ws.rs.*;import javax.ws.rs.core.*;import javax.ws.rs.ext.*;import javax.xml.bind.*;@Provider@Produces(MediaType.APPLICATION_JSON)@Consumes(MediaType.APPLICATION_JSON)public class MOXyJSONProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object>{ @Context protected Providers providers; public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return true; } public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { try { Class domainClass = getDomainClass(genericType); Unmarshaller u = getJAXBContext(domainClass, mediaType).createUnmarshaller(); u.setProperty("eclipselink.media-type", mediaType.toString()); u.setProperty("eclipselink.json.include-root", false); return u.unmarshal(new StreamSource(entityStream), domainClass).getValue(); } catch(JAXBException jaxbException) { throw new WebApplicationException(jaxbException); } } public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return true; } public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { try { Marshaller m = getJAXBContext(getDomainClass(genericType), mediaType).createMarshaller(); m.setProperty("eclipselink.media-type", mediaType.toString()); m.setProperty("eclipselink.json.include-root", false); m.marshal(object, entityStream); } catch(JAXBException jaxbException) { throw new WebApplicationException(jaxbException); } } public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } private JAXBContext getJAXBContext(Class<?> type, MediaType mediaType) throws JAXBException { ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, mediaType); JAXBContext jaxbContext; if(null == resolver || null == (jaxbContext = resolver.getContext(type))) { return JAXBContext.newInstance(type); } else { return jaxbContext; } } private Class<?> getDomainClass(Type genericType) { if(genericType instanceof Class) { return (Class) genericType; } else if(genericType instanceof ParameterizedType) { return (Class) ((ParameterizedType) genericType).getActualTypeArguments()[0]; } else { return null; } }}
For More Information
- MOXy as Your JAX-RS JSON Provider - Client Side
- MOXy as Your JAX-RS JSON Provider - Server Side
- Specifying EclipseLink MOXy as Your JAXB Provider
UPDATE
In GlassFish 4 EclipseLink JAXB (MOXy) is the default JSON-binding provider used by Jersey:
By default, Jersey is using JAXB for the (un)marshalling process, and unfortunately, JAXB JSON processor is not standard (one-element arrays are ignored, empty arrays are transformed into a one-element empty array...).
So, you've got two choices:
- configuring JAXB to be more standard (see here for more);
- using Jackson instead of JAXB — which I recommend.
Using Jackson client-side is done the following way:
ClientConfig clientConfig = new DefaultClientConfig();clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);Client client = Client.create(clientConfig);List<Badge> badges = client.resource("/badges").getEntity(new GenericType<List<Badge>>() {});