9

I am trying to send a HashMap or any other Map implementation from ajax to a Spring MVC controller

Here's the detail of how I do it :

the Ajax call is as follow

var tags = {};
tags["foo"] = "bar";
tags["whee"] = "whizzz";


$.post("doTestMap.do",   {"tags" : tags }, function(data, textStatus, jqXHR) {
if (textStatus == 'success') {
    //handle success
    console.log("doTest returned " + data);
} else {
    console.err("doTest returned " + data);
}
}); 

then on the controller side I have :

@RequestMapping(value="/publisher/doTestMap.do", method=RequestMethod.POST)
public @ResponseBody String doTestMap(@RequestParam(value = "tags", defaultValue = "") HashMap<String,String> tags, HttpServletRequest request) {  //

    System.out.println(tags);

    return "cool";
} 

Unfortunately I systematically get

org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [java.util.Map]: no matching editors or conversion strategy found

What am I doing wrong ?

Thank you.

azpublic
  • 1,404
  • 4
  • 20
  • 42
  • Looks like spring reads "tags" param as string, not as map. That means spring have no default message converter, that converts string to map. So you need to write custom MessageConverter and register it in application – yname Aug 16 '13 at 19:29
  • seriously ? Doesn't spring support mapping to a Map out of the box ? – azpublic Aug 16 '13 at 20:40
  • How is `tags` even serialized with jquery `post`? It looks like json. Maybe you are looking for `@RequestBody`. – Sotirios Delimanolis Aug 16 '13 at 21:03
  • what type is `tags` on the javascript side? The resulting map is going to map what to what? – soulcheck Aug 16 '13 at 23:28

5 Answers5

13

Binding a map in a spring controller is supported the same way as binding an array. No special converter needed!

There is one thing to keep in mind though:

  • Spring uses command object(s) as a top level value holder. A command object can be any class.

So all you need is a wrapper class (TagsWrapper) which holds a field of type Map<String, String> called tags. The same approach you take to bind an array.

This is explained pretty well in the docs but i kept forgetting the need of the wrapper object once in a while ;)

The second thing you need to change is the way you submit the tags values:

  • use one form parameter per map key and not a full string representation of the complete map.
  • one input value should look like this:

      <input type="text" name="tags[key]" value="something">
    

If tags is a map in a wrapper this works out of the box for form submits.

AdrieanKhisbe
  • 3,899
  • 8
  • 37
  • 45
Martin Frey
  • 10,025
  • 4
  • 25
  • 30
  • Yes this is it ! If you wrap the Map inside a @ModelAttribute annotated argument of any class it works. And this is crazy because if you just put the map as an argument to the controller method it does not bind propperly and creates an empty BindingAwareModelMap. + note also the comment about the form parameters is true. Thanks a lot ! – azpublic Aug 17 '13 at 15:52
6

Here's my completed answer with code as per Martin Frey's help :

javascript side (note how the tags values are populated):

var data = {
   "tags[foo]" : "foovalue", 
   "tags[whizz]" : "whizzvalue" 
}

$.post("doTestMap.do",   data , function(data, textStatus, jqXHR) {
    ...
}); 

And then on the controller side :

@RequestMapping(value="/publisher/doTestMap.do", method=RequestMethod.POST)
public @ResponseBody String doTestMap(@ModelAttribute MyWrapper wrapper, HttpServletRequest request) {
} 

and create your wrapper class with Map inside of it :

class MyWrapper {

    Map<String,String> tags;

   +getters and setters

}

Then you'll get your map populated appropriately ...

azpublic
  • 1,404
  • 4
  • 20
  • 42
6

This could be late in the response. However, it might help someone. I had a similar problem and this is how i fixed it. On JS: My Map Looks Like,

var values = {};
values[element.id] = element.value;

Ajax:

        $.ajax({
            type : 'POST',
            url : 'xxx.mvc',
            data : JSON.stringify(values),              
            error : function(response) {
                alert("Operation failed.");
            },
            success : function(response) {
                alert("Success");
            },
            contentType : "application/json",
            dataType : "json"
        });

Controller:

@RequestMapping(value = "/xxx.mvc", method=RequestMethod.POST)
    @ResponseBody
    public Map<String, Object> getValues(@RequestBody Map<String, Object> pvmValues, final HttpServletRequest request, final HttpServletResponse response) {
System.out.println(pvmValues.get("key"));
}
JoelC
  • 3,664
  • 9
  • 33
  • 38
SJ Arvind
  • 61
  • 1
  • 1
  • on Javascript, use some thing like this; var values = {}; values.elementid=elementvalue; instead of values[elementid]=elementvalue; – Raghu Dec 12 '16 at 06:42
  • we were using spring security and the above code gave 403 exception. so we had to follow this answer along with the above changes.https://stackoverflow.com/questions/19091206/403-forbidden-error-with-ajax-get-request-spring/33810866#33810866 – Raghu Dec 12 '16 at 10:29
0

You're sending a javascript array tags, which by default will be url-encoded by jQuery as a series of parameters named tags[]. Not sure if this is what you wanted.

You get an error, because spring doesn't have default conversion strategy from multiple parameters with the same name to HashMap. It can, however, convert them easily to List, array or Set.

So try this:

@RequestMapping(value="/publisher/doTestMap.do", method=RequestMethod.POST)
public @ResponseBody String doTestMap(@RequestParam(value = "tags[]") Set<String> tags, HttpServletRequest request) {  //

    System.out.println(tags); //this probably won't print what you want

    return "cool";
} 
soulcheck
  • 36,297
  • 6
  • 91
  • 90
0

The best way of doing this would be encoding the object using JSON and decoding it.

You need two libraries. On the client side you need json2.js. Some browsers seem to natively do this by now. But having this is the safer way.

On the server you need jackson.

On the client you encode your map and pass that as parameter:

var myEncodedObject = JSON.stringify(tags);

On the server you receive the parameter as a string and decode it:

ObjectMapper myMapper = new ObjectMapper();
Map<String, Object> myMap = myMapper.readValue(tags, new TypeReference<Map<String, Object>>);

There may be some way in Spring to make it convert automatically, but this is the gist of it.

ced-b
  • 3,957
  • 1
  • 27
  • 39