web-dev-qa-db-fra.com

Impossible de décompresser un tableau d'objets JSON à l'aide de Jersey Client

Un tableau JSON à un élément que j'essaie de dissocier:

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

La classe Java correspondante (getters & setters omis pour plus de brièveté):

@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
    }
}

Le code client Jersey qui émet une requête HTTP est supposé pour décomparer le code JSON ci-dessus en un List<Foo> à un élément:

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

La dernière ligne, en particulier l'appel WebResource#get(), lève l'exception suivante:

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)

J'ai essayé diverses combinaisons d'annotations sur Badge ou en utilisant un tableau au lieu de GenericType:

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

ou en utilisant une variable ClientResponse:

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

mais aucun n'a jusqu'ici résolu le problème.

Encore plus déconcertant est le fait que ma configuration existante a aucun problème unmarshalling Badges codés JSON qui sont à l'intérieur d'autres structures, comme ceci:

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

Qu'est-ce que je fais mal?

26
Matt Ball

J'ai pu résoudre ce problème avec un effort minimal en utilisant JacksonJsonProvider en tant que fournisseur MessageBody(Reader|Writer) pour l'instance du client Jersey:

ClientConfig cfg = new DefaultClientConfig();
cfg.getClasses().add(JacksonJsonProvider.class);
Client client = Client.create(cfg);

L’application MessageBodyReader de Jackson semble être plus sage que celle de Jersey JSON.

Merci à Comment personnaliser la sérialisation d’une liste d’objets JAXB en JSON? pour m'avoir orienté dans la direction de Jackson.

23
Matt Ball

Remarque: Je suis le EclipseLink JAXB (MOXy) lead et membre du groupe JAXB (JSR-222) .

Vous pouvez utiliser l'extension de liaison JSON ajoutée au composant MOXy dans EclipseLink 2.4 pour gérer ce cas d'utilisation:

Démo

L’API client Jersey vous permet d’utiliser la même variable MessageBodyReaderMessageBodyWriter côté serveur côté client. 

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

Vous trouverez ci-dessous une variable MessageBodyReaderMessageBodyWriter générique qui peut être utilisée avec n’importe quel serveur/client pour activer MOXy en tant que fournisseur de liaisons JSON.

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

}

_/ Pour plus d'informations


METTRE À JOUR

Dans GlassFish 4, EclipseLink JAXB (MOXy) est le fournisseur de liaison JSON par défaut utilisé par Jersey:

13
Blaise Doughan

Par défaut, Jersey utilise JAXB pour le processus de (dés) triage et, malheureusement, le processeur JAXB JSON n’est pas standard (les tableaux one-element sont ignorés, les tableaux vides sont transformés en tableau vide à un élément ... ).

Donc, vous avez deux choix:

  1. configurer JAXB pour qu'il soit plus standard ( voir ici pour plus);
  2. en utilisant Jackson au lieu de JAXB - que je recommande.

L'utilisation de Jackson côté client se fait de la manière suivante:

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

J'ai eu un problème similaire, et a été résolu avec ce qui suit

  1. Faire un résolveur de contexte JAXB comme ceci

    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. Ajout du résolveur de contexte à votre client

    ClientConfig config = new DefaultClientConfig();
    config.getClasses().add(JAXBContextResolver.class);
    
    Client client = Client.create(config);
    
  3. Maintenant, vous pouvez obtenir le tableau d'objets

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

Et si vous voulez une liste, utilisez simplement

   Arrays.asList(badges)
6
gurbieta

Importer cette

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

et ceci est le code pour 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);
}
1
user2116367

Cela peut être dû à un problème dans lequel le producteur ne code pas correctement une liste de singleton dans JSON. Voir cet article pour une explication plus complète et la solution proposée.

D'après ce que décrit l'article et le message d'erreur, je suppose que les éléments suivants sont produits à la place:

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

Selon l'article, la solution réside dans l'extension et la personnalisation du fournisseur afin de corriger le formatage des listes de singleton et des listes vides en JSON.

Malheureusement, l'article est en allemand, que j'ai dû traduire moi-même. Veuillez me faire savoir s'il ne résout pas votre problème. Dans ce cas, le mérite revient à Dirk Dittmar, l'auteur de l'article.

PS - Si vous utilisez Chrome pour traduire la page comme je l'ai fait, veillez à revenir à l'original pour afficher les extraits de code, car certaines parties d'entre eux sont par erreur "traduites" en espaces.

0
Paul Bellora