153
votes

I'm trying to POST a List of custom objects. My JSON in request body is this:

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

Server side code that handles the request:

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

Entity 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() + "]";
    }
}

But an exception is thrown:

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)
13

13 Answers

171
votes

The problem is the JSON - this cannot, by default, be deserialized into a Collection because it's not actually a JSON Array - that would look like this:

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

Since you're not controlling the exact process of deserialization (RestEasy does) - a first option would be to simply inject the JSON as a String and then take control of the deserialization process:

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

You would loose a bit of the convenience of not having to do that yourself, but you would easily sort out the problem.

Another option - if you cannot change the JSON - would be to construct a wrapper to fit the structure of your JSON input - and use that instead of Collection<COrder>.

Hope this helps.

89
votes

Instead of the JSON document, you can update the ObjectMapper object like below :

ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
11
votes

This will work:

The problem may happen when you're trying to read a list with a single element as a JsonArray rather than a JsonNode or vice versa.

Since you can't know for sure if the returned list contains a single element (so the json looks like this {...}) or multiple elements (and the json looks like this [{...},{...}]) - you'll have to check in runtime the type of the element.

It should look like this:

(Note: in this code sample I'm using 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 {
    ...
}
10
votes

Related to Eugen's answer, you can solve this particular case by creating a wrapper POJO object that contains a Collection<COrder> as its member variable. This will properly guide Jackson to place the actual Collection data inside the POJO's member variable and produce the JSON you are looking for in the API request.

Example:

public class ApiRequest {

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

   // getters
}

Then set the parameter type of COrderRestService.postOrder() to be your new ApiRequest wrapper POJO instead of Collection<COrder>.

4
votes

As mentioned above the following would solve the problem: mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

However in my case the provider did this [0..1] or [0..*] serialization rather as a bug and I could not enforce fixing. On the other hand it did not want to impact my strict mapper for all other cases which needs to be validated strictly.

So I did a Jackson NASTY HACK (which should not be copied in general ;-) ), especially because my SingleOrListElement had only few properties to patch:

@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());
  }
3
votes

I ran into this same problem these days and maybe some more detail might be helpful to somebody else.

I was looking some security guidelines for REST APIs and crossed a very intriguing issue with json arrays. Check the link for details, but basically, you should wrap them within an Object as we already saw in this post question.

So, instead of:

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

It's advisable that we always do:

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

This is pretty straight-forward when you doing a GET, but might give you some trouble if, instead, your trying to POST/PUT the very same json.

In my case, I had more than one GET that was a List and more than one POST/PUT that would receive the very same json.

So what I end up doing was to use a very simple Wrapper object for a List:

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

Serialization of my lists were made with a @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;
  }
}

So, all the Lists and Maps where wrapped over a data object as below:

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

Deserialization was still default, just using 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();
}

That was it! Hope it helps someone.

Note: tested with SpringBoot 1.5.5.RELEASE.

2
votes

After struggling with this thing for WAY too long, here is the super easy solution.

My controller was looking for

@RequestBody List<String> ids

and I had the request body as

{
    "ids": [
        "1234",
        "5678"
     ]
}

and the solution was to change the body simply to

["1234", "5678"]

Yup. Just that easy.

0
votes

I had this issue on a REST API that was created using Spring framework. Adding a @ResponseBody annotation (to make the response JSON) resolved it.

0
votes

Normally we face this issue when there is a problem mapping JSON node with that of Java object. I faced the same issue because in the swagger the node was defined as of Type array and the JSON object was having only one element , hence the system was having difficulty in mapping one element list to an array.

In Swagger the element was defined as

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

While it should be

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

And TestNew should be of type array

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

Same issue:

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

What caused it was the following:

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

In my test I purposely set the request as null (no content POST). As previously mentioned, the cause for the OP was the same because the request didn't contain a valid JSON, so it couldn't be automatically identified as an application/json request which was the limitation on the server (consumes = "application/json"). A valid JSON request would be. What fixed it was populating an entity with null body and json headers explicitly.

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

In my case, the error was being shown because when I was reading my JSON file using Jackson library, my JSON file contained only 1 object. Hence it started with "{" and ended with "}". But while reading it and storing it in a variable, I was storing it in an Array object (As in my case, there could be more than 1 object).

Hence, I added "[" in the start and "]" in the end of my JSON file to convert it into an array of object and it worked perfectly fine without any error.

0
votes
@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List< COrder > orders;