26

A one-element JSON array that I'm trying to unmarshal:

[
   {
      "id":"42",
      "status":"Active",
      "name":"purple monkey dishwasher"
   }
]

The corresponding Java class (getters & setters omitted for brevity):

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Badge
{
    @XmlElement(name="id")
    private String id;

    @XmlElement(name="status")
    private Status status;

    @XmlElement(name="name")
    private String name;

    public static enum Status
    {
        Active,
        NotActive
    }
}

The Jersey Client code which makes an HTTP request and is supposed to unmarshal the above JSON into a one-element List<Foo>:

Client client = Client.create();
WebResource apiRoot = client.resource("http://localhost:9000/api");
List<Badge> badges = apiRoot.path("/badges").get(new GenericType<List<Badge>>(){});

The last line, specifically the WebResource#get() call, throws the following exception:

javax.xml.bind.UnmarshalException: unexpected element (uri:"", local:"status"). Expected elements are <{}badge>
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.handleEvent(UnmarshallingContext.java:662)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportError(Loader.java:258)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportError(Loader.java:253)
    at com.sun.xml.bind.v2.runtime.unmarshaller.Loader.reportUnexpectedChildElement(Loader.java:120)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext$DefaultRootLoader.childElement(UnmarshallingContext.java:1063)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext._startElement(UnmarshallingContext.java:498)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallingContext.startElement(UnmarshallingContext.java:480)
    at com.sun.xml.bind.v2.runtime.unmarshaller.InterningXmlVisitor.startElement(InterningXmlVisitor.java:75)
    at com.sun.xml.bind.v2.runtime.unmarshaller.StAXStreamConnector.handleStartElement(StAXStreamConnector.java:247)
    at com.sun.xml.bind.v2.runtime.unmarshaller.StAXStreamConnector.bridge(StAXStreamConnector.java:181)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:369)
    at com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal(UnmarshallerImpl.java:341)
    at com.sun.jersey.core.provider.jaxb.AbstractListElementProvider.readFrom(AbstractListElementProvider.java:232)
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:552)
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:522)
    at com.sun.jersey.api.client.WebResource.handle(WebResource.java:617)
    at com.sun.jersey.api.client.WebResource.get(WebResource.java:191)
    at com.redacted.badge.client.BadgerImpl.findAllBadges(BadgerImpl.java:105)
    at com.redacted.webapp.admin.BadgeAction.unspecified(BadgeAction.java:40)
    at org.apache.struts.actions.DispatchAction.dispatchMethod(DispatchAction.java:245)
    at org.apache.struts.actions.DispatchAction.execute(DispatchAction.java:170)
    at org.apache.struts.chain.commands.servlet.ExecuteAction.execute(ExecuteAction.java:58)
    at org.apache.struts.chain.commands.AbstractExecuteAction.execute(AbstractExecuteAction.java:67)
    at org.apache.struts.chain.commands.ActionCommandBase.execute(ActionCommandBase.java:51)
    at org.apache.commons.chain.impl.ChainBase.execute(ChainBase.java:190)
    at org.apache.commons.chain.generic.LookupCommand.execute(LookupCommand.java:304)
    at org.apache.commons.chain.impl.ChainBase.execute(ChainBase.java:190)
    at org.apache.struts.chain.ComposableRequestProcessor.process(ComposableRequestProcessor.java:283)
    at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1913)
    at org.apache.struts.action.ActionServlet.doGet(ActionServlet.java:449)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:617)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:119)
    at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:55)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.MemberFilter.doFilter(MemberFilter.java:83)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.AuthFilter.doFilter(AuthFilter.java:113)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.displaytag.filter.ResponseOverrideFilter.doFilter(ResponseOverrideFilter.java:125)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.LanguageHandlingFilter.doFilter(LanguageHandlingFilter.java:151)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.SetCharacterEncodingFilter.doFilter(SetCharacterEncodingFilter.java:146)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.PartnerFilter.doFilter(PartnerFilter.java:59)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.redacted.webapp.filter.SessionStatusFilter.doFilter(SessionStatusFilter.java:113)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:470)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
    at com.googlecode.psiprobe.Tomcat60AgentValve.invoke(Tomcat60AgentValve.java:30)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:298)
    at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:859)
    at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:588)
    at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
    at java.lang.Thread.run(Thread.java:680)

I've tried a variety of combinations of annotations on Badge, or using an array instead of GenericType:

List<Badge> badges = Arrays.asList(apiRoot.path("/badges").get(Badge[].class));

or using an intermediate ClientResponse:

GenericType<List<Badge>> type = new GenericType<List<Badge>>(){};
ClientResponse clientResponse = apiRoot.path("/badges").get(ClientResponse.class);
List<Badge> badges = clientResponse.getEntity(type);

but none so far have solved the problem.

Even more confounding is the fact that my existing setup has no problems unmarshalling JSON-encoded Badges which are inside of other structures, like this:

{
   "userid":"123456789",
   "userbadges":[
      {
         "badge":{
              "id":"42",
              "status":"Active",
              "name":"purple monkey dishwasher"
         },
         "earned":"2012-03-06 18:16:18.172"
      }
   ]
}

What am I doing wrong?

Charles
  • 50,943
  • 13
  • 104
  • 142
Matt Ball
  • 354,903
  • 100
  • 647
  • 710

6 Answers6

23

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.

Community
  • 1
  • 1
Matt Ball
  • 354,903
  • 100
  • 647
  • 710
14

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


UPDATE

In GlassFish 4 EclipseLink JAXB (MOXy) is the default JSON-binding provider used by Jersey:

bdoughan
  • 147,609
  • 23
  • 300
  • 400
  • 2
    Thanks, I'll give this a shot. I'm curious to know _why_ this doesn't work out-of-the-box – do you know? Also, I knew it was only a matter of time 'til you posted an answer `:)` – Matt Ball Mar 12 '12 at 13:33
  • @MДΓΓБДLL - I have updated my answer. I believe it is not working out of the box because your then items in your collection do not have `badge` as a root element. – bdoughan Mar 12 '12 at 19:08
  • 1
    Oh, interesting. Taking another look at the difference between the JSON I can unmarshall, and the JSON I can't, I see what you mean. – Matt Ball Mar 12 '12 at 19:32
  • This approach is NOT the standard one. To produce & consume standard JSON into Jersey one have to use Jackson! This is easy, quick, and was designed as this by the Jersey team. – yves amsellem Mar 19 '12 at 10:13
  • @yvesamsellem - (-1?) This approach uses the standard 'MessageBodyReader/Writer` interfaces to interact with the Jersey client API and clearly demonstrates that you do NOT need to use Jackson to produce & consume standard JSON into Jersey. An earlier version of MOXy is already in GlassFish (http://blog.bdoughan.com/2012/02/glassfish-312-is-full-of-moxy.html), and a version containing the JSON binding will be included in a future GlassFish release. The stack trace clearly indicated that a JAXB implementation was being used with the JSON and MOXy supports more JAXB annotations than Jackson does. – bdoughan Mar 19 '12 at 10:30
  • @BlaiseDoughan Yes, is uses a standard approach to (un)marshall representations but IT IS NOT the standard approach to (un)marshall JSON in Jersey: Jackson is. – yves amsellem Mar 19 '12 at 10:59
  • @yvesamsellem by "standard," you mean _de facto_ standard, right? `:)` – Matt Ball Mar 19 '12 at 11:02
  • @MДΓΓ БДLL yes, I mean built in; designed this way by the Jersey developers – yves amsellem Mar 19 '12 at 17:58
  • @yvesamsellem - FYI, Tweet from Jersey regarding MOXy: https://twitter.com/gf_jersey/status/218036096758579202 – bdoughan Jul 05 '12 at 19:18
11

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:

  1. configuring JAXB to be more standard (see here for more);
  2. 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>>() {});
Community
  • 1
  • 1
yves amsellem
  • 7,106
  • 5
  • 44
  • 68
  • 1
    Yup, as [my self-answer](http://stackoverflow.com/a/9725228/139010) said, I'm trying out Jackson, which seems to be much less quirky. – Matt Ball Mar 16 '12 at 13:32
  • Great to know. Using a custom JacksonProvider is only necessary when you want to custom the (un)marshalling process. If you don't, using the default one provided by Jersey is enough (JSONConfiguration feature). – yves amsellem Mar 16 '12 at 13:43
  • As it happens, I am actually using a custom provider, so that I can use JAXB annotations alongside Jackson annotations: http://wiki.fasterxml.com/JacksonJAXBAnnotations#Registering_JAXB_annotation_introspector – Matt Ball Mar 16 '12 at 14:11
  • The default Jersey Jackson provider is configured to use JAXB annotations alongside Jackson ones ;-) The only issue is that the exact configuration of this provider is not documented :-( – yves amsellem Mar 17 '12 at 12:26
6

I had a similar problem, and was resolved with the following

  1. Make a JAXB context resolver like this

    import java.util.ArrayList;
    import java.util.List;
    
    import javax.ws.rs.ext.ContextResolver;
    import javax.ws.rs.ext.Provider;
    import javax.xml.bind.JAXBContext;
    
    import com.sun.jersey.api.json.JSONConfiguration;
    import com.sun.jersey.api.json.JSONJAXBContext;
    
    @Provider
    public class JAXBContextResolver implements ContextResolver<JAXBContext> {
    
        private JAXBContext       context;
    
        private Class<?>[]        types    = { Badge.class };
    
        private List<Class<?>>    classes    = new ArrayList<Class<?>>();
    
        public JAXBContextResolver() throws Exception {
            this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), types);
    
            for (Class<?> clazz : types) {
                classes.add(clazz);
            }
        }
    
        public JAXBContext getContext(Class<?> objectType) {
            return classes.contains(objectType) ? context : null;
        }
    
    }
    
  2. Added the context resolver to your client

    ClientConfig config = new DefaultClientConfig();
    config.getClasses().add(JAXBContextResolver.class);
    
    Client client = Client.create(config);
    
  3. Now you can get the objects array

    WebResource apiRoot = client.resource("http://localhost:9000/api");
    Badge[] badges = apiRoot.path("/badges").get(Badge[].class);
    

And if you want a list, simply use

   Arrays.asList(badges)
gurbieta
  • 866
  • 1
  • 8
  • 22
1

Import this

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.17</version>
    <scope>compile</scope>
</dependency>

and this is the code to unmarshall

import com.sun.jersey.api.json.JSONJAXBContext;
import com.sun.jersey.api.json.JSONUnmarshaller;
public static <T> T unmarshalJson(String jsonTxt, Class<T> clazz) throws JAXBException {
    JSONJAXBContext jctx = new JSONJAXBContext(clazz);
    JSONUnmarshaller unm = jctx.createJSONUnmarshaller();
    return (T)unm.unmarshalFromJSON(new StringReader(jsonTxt), clazz);
}
Lance Roberts
  • 22,383
  • 32
  • 112
  • 130
0

This may have to do with an issue in which the producer doesn't properly encode a singleton list into JSON. See this article for a fuller explanation and proposed solution.

Based on what the article describes, and from the error message, I'm guessing the following is being produced instead:

{
   {
      "id":"42",
      "status":"Active",
      "name":"purple monkey dishwasher"
   }
}

According to the article, the solution lies in extending and customizing the provider to correct how singleton lists and empty lists are formatted into JSON.

Unfortunately the article is in German, which I had to translate for myself - let me know if it doesn't actually address your problem. If it does, credit goes to Dirk Dittmar, the article's author.

PS - if you use Chrome to translate the page like I did, make sure to switch back to the original to see the code snippets as parts of them get mistakenly "translated" into whitespace.

Paul Bellora
  • 54,340
  • 18
  • 130
  • 181