Cannot unmarshal a JSON array of objects using Jer

2019-01-22 18:49发布

问题:

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?

回答1:

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.



回答2:

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:

  • http://blog.bdoughan.com/2013/06/moxy-is-new-default-json-binding.html


回答3:

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


回答4:

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)


回答5:

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


回答6:

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.