0

I am trying to implement a scenario where I want to be able to send an array of JSON object as request body to Spring MVC controller.

I have gone through the following posts

  1. Custom HttpMessageConverter with @ResponseBody to do Json things
  2. Howto get rid of <mvc:annotation-driven />?
  3. http://prasanthnath.wordpress.com/2013/08/23/type-conversion-in-spring/#more-199
  4. http://gerrydevstory.com/2013/08/14/posting-json-to-spring-mvc-controller/
  5. Spring-Returning json with @ResponseBody when the Accept header is */* throws HttpMediaTypeNotAcceptableException

But none of the suggestions are working. I apologize if I missed any other posts.

The idea is that there are two controller functions that will

  1. fetch data from the database The controller will query the database and return a List of objects to be serialized.
  2. accept the JSON response and convert them to list of objects to be inserted into the database.

The first one works without any explicit serialization on my part.

@RequestMapping("/config")
public class ConfigController {

  @Autowired
  private final Service service;

   // This works. I don't know why.
   @RequestMapping("/fetch", method=RequestMethod.GET)
   @ResponseBody
   @ResponseStatus(HttpStatus.OK)
   public String readConfigProperties() throws Exception {
     ImmutableList<Config> configObjects = this.service.readConfiguration();
     return configObjects;
   }
}

I am having trouble taking the JSON response passed in request body and make them available as a list of objects. The controller function seems to be passing a list of linked hashmaps which is not what I want. This is raising a ClassCastException. The function set up is given below

UPDATE: I used @ResponseBody annotation in a previous version of this post. I changed the post to use @RequestBody, but no impact.

@RequestMapping(method=RequestMethod.POST, consumes={"application/json"}, value="/update}
@ResponseStatus(HttpStatus.OK)
public void updateConfig(@RequestBody List<Config> configList) throws Exception {
  this.service.updateConfiguration(configList);

}

In this case configList is a list of LinkedHashMap objects and so it causes a ClassCastException to be thrown. I don't know why.

My headers are as follows:

Content-Type: application/json; charset=utf-8

Stack trace:

java.lang.ClassCastException: java.util.LinkedHashMap incompatible with com.kartik.springmvc.model.Config
    com.kartik.springmvc.service.ConfigPropertyService.upsertConfigurationProperties(ConfigPropertyService.java:56)
    com.kartik.springmvc.controller.ConfigController .upsertConfigurationProperties(ConfigController .java:86)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:88)
    sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:55)
    java.lang.reflect.Method.invoke(Method.java:613)
    org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:213)
    org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
    org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
    org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:923)
    org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:852)
    org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
    org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:646)

My converters and controller specific configuration

   <context:annotation-config />
    <context:component-scan base-package="com.kartik.springmvc.controller" />
    <bean class="com.kartik.springmvc.controller.AppConfigPropertiesConverter" id="appConfigPropertiesConverter"/>

    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
         <property name="messageConverters">
            <list>
                <ref bean="appConfigPropertiesConverter" />
                <bean  class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
            </list>
        </property>
    </bean>
    <mvc:annotation-driven/>

My converter implementation is given below. UPDATE: This class is not invoked.

public class AppConfigPropertiesConverter extends AbstractHttpMessageConverter<Object> {

  public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

  private Gson gson = new Gson();
  /**
   * Construct a new {@code GsonHttpMessageConverter}.
   */
  public AppConfigPropertiesConverter() {
    super(new MediaType("application", "json", DEFAULT_CHARSET), new MediaType(
        "application", "*+json", DEFAULT_CHARSET));
  }

  /** Supports only {@link Config} instances. */
  @Override
  protected boolean supports(Class<?> clazz) {
    // TODO Auto-generated method stub
    return true;
  }

  /**
   * Converts to a list of {@Config}
   */
  @Override
  protected Object readInternal(
      Class<?> clazz, HttpInputMessage inputMessage)
          throws IOException, HttpMessageNotReadableException {
    TypeToken<?> token = TypeToken.get(clazz);
    System.out.println("#################################################################3");
    Reader jsonReader =
        new InputStreamReader(inputMessage.getBody(), DEFAULT_CHARSET.displayName());
    System.out.println("####################################################################");
    try {
      return this.gson.fromJson(jsonReader, token.getType());
    } catch (JsonParseException ex) {
      throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
    }
  }

  /**
   * Write the json reprsentation to {@link OutputStream}.
   * 
   * @param config object to be serialized
   * @param output http output message
   */
  @Override
  protected void writeInternal(
      Object config, HttpOutputMessage outputMessage)
          throws IOException, HttpMessageNotWritableException {
    outputMessage.getBody().write(
        this.gson.toJson(config).getBytes(DEFAULT_CHARSET.displayName()));
  }

}

UPDATE: Added the service layer

   public class ConfigPropertyService implements IConfigPropertyService {

  private final Log LOG = LogFactory.getLog(ConfigPropertyService.class);

  private final IConfigPropertyDao<Config> configPropertyDao;

  /**
   * Returns the instance of Dao.
   */
  @Override
  public IConfigPropertyDao<Config> getConfigPropertyDao() {
    return this.configPropertyDao;
  }

  /**
   * Initializes the data access tier.
   */

      public ConfigPropertyService(IConfigPropertyDao<Config> configPropertyDao) {
        this.configPropertyDao = Objects.requireNonNull(configPropertyDao);
      }

  /**
   * {@inheritDoc}
   * @throws ConfigServiceException if the resources can't be cleaned up successfully
   */
  @Override
  public void upsertConfigurationProperties(
      ImmutableList<Config> configModels) {

    for (Config config: configModels) {
         // This for loop throws an exception.
    }

    // Step # 2: Updating the properties. 
    this.configPropertyDao.upsertProperties(configModels);
  }


  /**
   * {@inheritDoc}
   */
  @Override
  public ImmutableList<ConfigModel> readConfigProperties() {
    return this.configPropertyDao.readProperties();

  }

}

My request body is given as follows. It is a String body with Content-Type: application/json; charset=UTF-8

[{"propertyName":"anchorIndexingFilter.deduplicate","propertyValue":"false","propertyDescription":"With this enabled the indexer will case-insensitive deduplicate hanchors\n  before indexing. This prevents possible hundreds or thousands of identical anchors for\n  a given page to be indexed but will affect the search scoring (i.e. tf=1.0f).\n  ","editable":true}]   
halfer
  • 19,824
  • 17
  • 99
  • 186
Kartik
  • 2,541
  • 2
  • 37
  • 59
  • Try using `@RequestBody` instead of `@ResponseBody` in the `updateConfig` method – geoand Aug 07 '14 at 07:48
  • How you are sending the JSON data ? can you show that code ? show `Config` class. I am not sure but you ll have to use `JSonArray` to receive List of data in `JSON`. that variable ll get data as JSON and then just simple manipulate that `JSON` object. – user3145373 ツ Aug 07 '14 at 07:50
  • Stack trace seems unrelated with shown code : `ClassCastException` occurs in `ConfigPropertyService.upsertConfigurationProperties` (where is it ?) called from `AppConfigPropertiesController.upsertConfigurationProperties` (what is that ?) – Serge Ballesta Aug 07 '14 at 07:59
  • geoand: I use @RequestBody annotation but no impact. I have updated the post. user3145373: I use a REST client to set up a String body with Content-Type: application/json; charset=UTF-8 – Kartik Aug 07 '14 at 08:01
  • @SergeBallesta Bad copy paste on my part. I have two controllers that connect to different tables but have the same functionality. ConfigService is the service delegate I used to connect to db. I will update the config xml with other injected beans. – Kartik Aug 07 '14 at 08:12
  • What are line 56 in ConfigPropertyService and 86 in ConfigController ? – Serge Ballesta Aug 07 '14 at 10:33
  • Line # 56: the for-each loop. Line # 56: Invokes the service class. – Kartik Aug 07 '14 at 17:49

3 Answers3

0

The stacktrace says ....LinkedHashMap incompatible with com.kartik.springmvc.model.Config.

The error occurs in for (Config config: configModels) where configModels comes from controller through public void updateConfig(@RequestBody List<Config> configList)

It now make sense : Spring sees a json string in the request body and fully explode it in a list of maps. You asked for a List and got a List. Since Java manages generics through type erasure, all lists are compatible at run time.

You have 2 major ways of fix it :

  • the manual way (ugly, but simple and robust) : take the json String as a ... String and manually convert it. You know what it should be and what you want, so you will easily do the conversion.

    public void updateConfig(@RequestBody String jsonString ) {
        ... do actual conversion
    

    If converting to a String causes problem, conversion to ByteArray will allways be possible because you will use a ByteArrayHttpMessageConverter under the hood

  • the clever way : find the way the conversion occurs in spring binding and manage to have it generating a true List<Config>. As Spring natively supports Jackson or Jackson2, you could try to customize it. Alternatively you can use your own converter :

    • create a HttpMessageConverter bean - ok, you have one
    • declare an explicit RequestMappingHandlerAdapter and inject your message converter in it
    • do not forget to inject any other message converter you could use in your application as you are overring Spring default conversion

    it is way cleaner but a rather advanced configuration.

References : Spring reference manual - Wev MVC Framework/Mapping the request body with the @RequestBody annotation - Remoting and web services using Spring/HTTP Message Conversion

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
0

The issue is with your method signature you are passing LinkedHashMap as a parameter but in your method you are receiving List of config objects List this is causing class cast exception

public void updateConfig(@RequestBody List<Config> configList) 

Change above to something like this not exactly

public void updateConfig(@RequestBody LinkedHashMap<K,V> configList)
Ramzan Zafar
  • 1,562
  • 2
  • 17
  • 18
  • RamzanZafar - If I change the parameter type to @RequestBody String configList, it returns a stingified json array. I don't know why I am getting a List of LinkedHashMap objects. I want list of Config objects. – Kartik Aug 08 '14 at 01:32
0

After much permutation and combination, I used a custom deserializer. It works as below

public class AppConfigDeserializer implements JsonDeserializer<List<Config>> {

 /**
  * Creates an collection of {@link Config}.
  * 
  * <p>If the stringified json element represents a json array as in {@code [{'foo': 'bar'}]}, then
  * the serialized instance will be an instance of {@link JsonArray} comprising of
  * {@link JsonObject}
  * 
  * <p>If th stringified json element represents a json object as in {@code {'foo': 'bar'}}, then
  * the serialized instance will be an instance of {@link JsonObject). 
  * 
  */
  @Override
  public List<Config> deserialize(JsonElement json, Type typeOfT,
      JsonDeserializationContext context) throws JsonParseException {
    json = Objects.requireNonNull(json);
    List<Config> configs = new ArrayList<>();
    if (JsonArray.class.isAssignableFrom(json.getClass())) {
        JsonArray jsonArray = (JsonArray) json;
        for (JsonElement jsonElement : jsonArray) {
          JsonObject jsonObject = (JsonObject) jsonElement.getAsJsonObject();
          // Initialize the list of models with values to defaulting to primitive values.
          configs.add(
          new Config(
              jsonObject.get("appName") != null ?
                  jsonObject.get("appName").getAsString() : null,
              jsonObject.get("propertyName") != null ?
                  jsonObject.get("propertyName").getAsString() : null,
              jsonObject.get("propertyValue") != null ?
                  jsonObject.get("propertyValue").getAsString() : null,
              jsonObject.get("propertyDescription") != null ?
                  jsonObject.get("propertyDescription").getAsString() : null,
              jsonObject.get("editable") != null ?
                  jsonObject.get("editable").getAsBoolean() : false,
              jsonObject.get("updated") != null ?
                jsonObject.get("updated").getAsBoolean() : false));
        }
    } else if (JsonObject.class.isAssignableFrom(json.getClass())) {
      // Just a simple json string.
      JsonObject jsonObject = (JsonObject) json.getAsJsonObject();
      configs.add(new Config(
                jsonObject.get("appName") != null ?
                    jsonObject.get("appName").getAsString() : null,
                jsonObject.get("propertyName") != null ?
                    jsonObject.get("propertyName").getAsString() : null,
                jsonObject.get("propertyValue") != null ?
                    jsonObject.get("propertyValue").getAsString() : null,
                jsonObject.get("propertyDescription") != null ?
                    jsonObject.get("propertyDescription").getAsString() : null,
                jsonObject.get("editable") != null ?
                    jsonObject.get("editable").getAsBoolean() : false,
                jsonObject.get("updated") != null ?
                  jsonObject.get("updated").getAsBoolean() : false));
  }
    return configs;
  }

}

In my converter implementation, I reference the converter clas.

public class AppConfigPropertiesConverter
    extends AbstractHttpMessageConverter<List<Config>> {

  public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

  private Gson gson = null;

  private final GsonBuilder gsonBuilder = new GsonBuilder();

  private final TypeToken<List<Config>> token = new TypeToken<List<Config>>() {};
  /**
   * Construct a new {@code GsonHttpMessageConverter}.
   */
  public AppConfigPropertiesConverter() {
    super(new MediaType("application", "json", DEFAULT_CHARSET), new MediaType(
        "application", "*+json", DEFAULT_CHARSET));
  }

  /**
   * Initializes the type adapters to inorder to deserialize json arrays or json objects.
   */
  public void initializeAdapters() {
   this.gsonBuilder.registerTypeAdapter(token.getType(), new AppConfigDeserializer());
   this.gson = this.gsonBuilder.create();
  }

  /** Supports only {@link L} instances. */
  @Override
  protected boolean supports(Class<?> clazz) {
    // TODO Auto-generated method stub
    return List.class.isAssignableFrom(clazz);
  }

  /**
   * Converts the serialized input to a list of objects.
   * 
   * @param clazz class to be serialized into
   * @param inputMessage message to be read from
   */
  @Override
  protected List<Config> readInternal(
      Class<? extends List<Config>> clazz, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {

    Reader jsonReader =
        new InputStreamReader(inputMessage.getBody(), DEFAULT_CHARSET.displayName());
    return this.gson.fromJson(jsonReader, this.token.getType());
  }

  /**
   * Converts an instance of immutable list to json response.
   * 
   * @param configs list of objects to be serialized
   * @param outputMessage output message to write to
   * @throws IOException thrown if the object can not be serialized
   * @throws HttpMessageNotWritableException if the object can not be written
   */
  @Override
  protected void writeInternal(
      List<Config> configs, HttpOutputMessage outputMessage)
      throws IOException, HttpMessageNotWritableException {
    outputMessage.getBody().write(
        this.gson.toJson(
            configs, this.token.getType()).getBytes(DEFAULT_CHARSET.displayName()));
  }
}

This worked for me. Although I should have used https://github.com/spring-projects/spring-android/blob/master/spring-android-rest-template/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java instead.

Kartik
  • 2,541
  • 2
  • 37
  • 59