Impossible de désérialiser l'instance de java.util.ArrayList en dehors du jeton START_OBJECT

129

J'essaye de POSTER un Listdes objets personnalisés. Mon JSON dans le corps de la requête est le suivant:

{
    "collection": [
        {
            "name": "Test order1",
            "detail": "ahk ks"
        },
        {
            "name": "Test order2",
            "detail": "Fisteku"
        }
    ]
}

Code côté serveur qui gère la demande:

import java.util.Collection;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;


@Path(value = "/rest/corder")
public class COrderRestService {

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response postOrder(Collection<COrder> orders) {
        StringBuilder stringBuilder = new StringBuilder();
        for (COrder c : orders) {
            stringBuilder.append(c.toString());
        }
        System.out.println(stringBuilder);
        return Response.ok(stringBuilder, MediaType.APPLICATION_JSON).build();
    }
}

Entité COrder:

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class COrder {
    String name;
    String detail;

    @Override
    public String toString() {
        return "COrder [name=" + name + ", detail=" + detail
                + ", getClass()=" + getClass() + ", hashCode()=" + hashCode()
                + ", toString()=" + super.toString() + "]";
    }
}

Mais une exception est levée:

SEVERE: Failed executing POST /rest/corder
org.jboss.resteasy.spi.ReaderException: org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
 at [Source: org.apache.catalina.connector.CoyoteInputStream@6de8c535; line: 1, column: 1]
    at org.jboss.resteasy.core.MessageBodyParameterInjector.inject(MessageBodyParameterInjector.java:183)
    at org.jboss.resteasy.core.MethodInjectorImpl.injectArguments(MethodInjectorImpl.java:88)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:111)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:280)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:234)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:221)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:356)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:179)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:220)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:724)
isah
la source

Réponses:

156

Le problème est le JSON - cela ne peut pas, par défaut, être désérialisé en un Collectioncar ce n'est pas en fait un tableau JSON - qui ressemblerait à ceci:

[
    {
        "name": "Test order1",
        "detail": "ahk ks"
    },
    {
        "name": "Test order2",
        "detail": "Fisteku"
    }
]

Puisque vous ne contrôlez pas le processus exact de désérialisation (RestEasy le fait), une première option serait d'injecter simplement le JSON en tant que Stringpuis de prendre le contrôle du processus de désérialisation:

Collection<COrder> readValues = new ObjectMapper().readValue(
    jsonAsString, new TypeReference<Collection<COrder>>() { }
);

Vous perdriez un peu de la commodité de ne pas avoir à faire cela vous-même, mais vous régleriez facilement le problème.

Une autre option - si vous ne pouvez pas changer le JSON - serait de construire un wrapper pour s'adapter à la structure de votre entrée JSON - et de l'utiliser à la place de Collection<COrder>.

J'espère que cela t'aides.

Eugen
la source
1
Super, j'ai enveloppé dans la collection car il y avait un exemple dans les documents Resteasy, mais c'était avec XML.
isah
2
@isah nice, pouvez-vous partager ce lien resteasy ici s'il vous plaît
nanospeck
à bien y penser, il. Je suis à peu près sûr d'avoir le bon code et de le déboguer pendant des heures et de changer mes codes en cours de route. Il s'est avéré que je manquais juste les crochets pour indiquer que mon message est un tableau. Eh bien, je suppose que c'est une malédiction pour les débutants, LOL. À toute personne qui découvre JSON et Spring Data, ne suivez pas mes traces. :(
iamjoshua
Le code ne fonctionne pas pour moi, dans le contrôleur quel devrait être le code attendu?
prem30488
63

Au lieu du document JSON, vous pouvez mettre à jour l'objet ObjectMapper comme ci-dessous:

ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
Salah Atwa
la source
1
Merci, la réponse m'a aidé
Jesús Sánchez
Fantastique! Vous avez sauvé un jour.
Herve Mutombo le
merci, veuillez vous référer à notre site mboot.herokuapp.com , nous publions un article lié à java - spring-boot
Salah Atwa le
8

Cela fonctionnera:

Le problème peut survenir lorsque vous essayez de lire une liste avec un seul élément en tant que JsonArray plutôt que JsonNode ou vice versa.

Puisque vous ne pouvez pas savoir avec certitude si la liste retournée contient un seul élément (donc le json ressemble à ceci {...} ) ou plusieurs éléments (et le json ressemble à ceci [{...}, {... }] ) - vous devrez vérifier à l'exécution le type de l'élément.

Ça devrait ressembler à ça:

(Remarque: dans cet exemple de code, j'utilise com.fasterxml.jackson)

String jsonStr = response.readEntity(String.class);
ObjectMapper mapper = new ObjectMapper();
JsonNode rootNode = mapper.readTree(jsonStr);

// Start by checking if this is a list -> the order is important here:                      
if (rootNode instanceof ArrayNode) {
    // Read the json as a list:
    myObjClass[] objects = mapper.readValue(rootNode.toString(), myObjClass[].class);
    ...
} else if (rootNode instanceof JsonNode) {
    // Read the json as a single object:
    myObjClass object = mapper.readValue(rootNode.toString(), myObjClass.class);
    ...
} else {
    ...
}
Bar Naor
la source
7

Lié à la réponse d'Eugen, vous pouvez résoudre ce cas particulier en créant un objet POJO wrapper contenant un Collection<COrder>comme variable membre. Cela guidera correctement Jackson pour placer les Collectiondonnées réelles dans la variable membre du POJO et produire le JSON que vous recherchez dans la requête API.

Exemple:

public class ApiRequest {

   @JsonProperty("collection")
   private Collection<COrder> collection;

   // getters
}

Définissez ensuite le type de paramètre de COrderRestService.postOrder()comme étant votre nouveau ApiRequestPOJO wrapper au lieu de Collection<COrder>.

Adil B
la source
2

J'ai rencontré le même problème ces jours-ci et peut-être que des détails supplémentaires pourraient être utiles à quelqu'un d'autre.

Je cherchais des directives de sécurité pour les API REST et j'ai traversé un problème très intriguant avec les tableaux json. Vérifiez le lien pour plus de détails, mais en gros, vous devez les envelopper dans un objet comme nous l'avons déjà vu dans cette question.

Donc, au lieu de:

  [
    {
      "name": "order1"
    },
    {
      "name": "order2"
    }
  ]

Il est conseillé de toujours faire:

  {
    "data": [
      {
        "name": "order1"
      },
      {
        "name": "order2"
      }
    ]
  }

C'est assez simple lorsque vous effectuez un GET , mais cela pourrait vous poser des problèmes si, à la place, vous essayez de POST / PUT le même json.

Dans mon cas, j'avais plus d'un GET qui était une liste et plus d'un POST / PUT qui recevraient le même json.

Donc, ce que je finis par faire était d'utiliser un objet Wrapper très simple pour une liste :

public class Wrapper<T> {
  private List<T> data;

  public Wrapper() {}

  public Wrapper(List<T> data) {
    this.data = data;
  }
  public List<T> getData() {
    return data;
  }
  public void setData(List<T> data) {
    this.data = data;
  }
}

La sérialisation de mes listes a été faite avec un @ControllerAdvice :

@ControllerAdvice
public class JSONResponseWrapper implements ResponseBodyAdvice<Object> {

  @Override
  public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    return true;
  }

  @Override
  @SuppressWarnings("unchecked")
  public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    if (body instanceof List) {
      return new Wrapper<>((List<Object>) body);
    }
    else if (body instanceof Map) {
      return Collections.singletonMap("data", body);
    }  
    return body;
  }
}

Ainsi, toutes les listes et cartes étaient enveloppées sur un objet de données comme ci-dessous:

  {
    "data": [
      {...}
    ]
  }

La désérialisation était toujours par défaut, en utilisant simplement de Wrapper Object:

@PostMapping("/resource")
public ResponseEntity<Void> setResources(@RequestBody Wrapper<ResourceDTO> wrappedResources) {
  List<ResourceDTO> resources = wrappedResources.getData();
  // your code here
  return ResponseEntity
           .ok()
           .build();
}

C'était ça! J'espère que ça aide quelqu'un.

Remarque: testé avec SpringBoot 1.5.5.RELEASE .

Reginaldo Santos
la source
1
la classe wrapper est authentique
Omid Rostami
0

J'ai eu ce problème sur une API REST créée à l'aide du framework Spring. L'ajout d'une annotation @ResponseBody (pour rendre la réponse JSON) l'a résolu.

Fais Will
la source
0

Normalement, nous sommes confrontés à ce problème lorsqu'il y a un problème de mappage du nœud JSON avec celui de l'objet Java. J'ai rencontré le même problème parce que dans le swagger, le nœud était défini comme un tableau Type et que l'objet JSON n'avait qu'un seul élément, le système avait donc du mal à mapper une liste d'éléments à un tableau.

Dans Swagger, l'élément était défini comme

Test:
 "type": "array",
 "minItems": 1,
 "items": {
   "$ref": "#/definitions/TestNew"
  }

Alors que ça devrait être

Test:
    "$ref": "#/definitions/TestNew"

Et TestNewdevrait être de type array

Ambuj Sinha
la source
0
Dto response = softConvertValue(jsonData, Dto.class);


     public static <T> T softConvertValue(Object fromValue, Class<T> toValueType) 
        {
            ObjectMapper objMapper = new ObjectMapper();
            return objMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                    .convertValue(fromValue, toValueType);
        }
Dipen Chawla
la source
0

Même problème:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.UUID` out of START_OBJECT token

La cause en était la suivante:

ResponseEntity<UUID> response = restTemplate.postForEntity("/example/", null, UUID.class);

Dans mon test, j'ai volontairement défini la demande comme nulle (pas de contenu POST). Comme mentionné précédemment, la cause de l'OP était la même car la requête ne contenait pas de JSON valide, elle ne pouvait donc pas être automatiquement identifiée comme une requête application / json, ce qui était la limitation du serveur ( consumes = "application/json"). Une demande JSON valide serait. Ce qui a résolu le problème, c'était de remplir explicitement une entité avec un corps nul et des en-têtes json.

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity request = new HttpEntity<>(null, headers);
ResponseEntity<UUID> response = restTemplate.postForEntity("/example/", request, UUID.class);
Tanel
la source
0

Dans mon cas, l'erreur était affichée car lorsque je lisais mon fichier JSON à l'aide de la bibliothèque Jackson, mon fichier JSON ne contenait qu'un seul objet. Par conséquent, il a commencé par "{" et s'est terminé par "}". Mais en le lisant et en le stockant dans une variable, je le stockais dans un objet Array (comme dans mon cas, il pourrait y avoir plus d'un objet).

Par conséquent, j'ai ajouté "[" au début et "]" à la fin de mon fichier JSON pour le convertir en un tableau d'objets et cela a parfaitement fonctionné sans aucune erreur.

Akshay Chopra
la source
0

Comme mentionné ci-dessus, ce qui suit résoudrait le problème: mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

Cependant, dans mon cas, le fournisseur a fait cette sérialisation [0..1] ou [0 .. *] plutôt comme un bogue et je n'ai pas pu appliquer la correction. Par contre cela ne voulait pas impacter mon mappeur strict pour tous les autres cas qui doivent être validés strictement.

J'ai donc fait un Jackson NASTY HACK (qui ne devrait pas être copié en général ;-)), surtout parce que mon SingleOrListElement n'avait que quelques propriétés à corriger:

@JsonProperty(value = "SingleOrListElement", access = JsonProperty.Access.WRITE_ONLY)
private Object singleOrListElement; 

public List<SingleOrListElement> patch(Object singleOrListElement) {
  if (singleOrListElement instanceof List) {
    return (ArrayList<SingleOrListElement>) singleOrListElement;
  } else {
    LinkedHashMap map = (LinkedHashMap) singleOrListElement;
    return Collections.singletonList(SingletonList.builder()
                            .property1((String) map.get("p1"))
                            .property2((Integer) map.get("p2"))
                            .build());
  }
Phirzel
la source
-1

@JsonFormat (avec = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) Liste privée des commandes <COrder>;

Suhas Saheer
la source
Veuillez fournir une réponse complète avec du code à l'intérieur des blocs de code et du texte évaluant l'exactitude de votre réponse
Ioannis Barakos