92

Every time I want to POST an array of JSON objects with jQuery to Rails, I have this problem. If I stringify the array I can see that jQuery is doing its work correctly:

"shared_items"=>"[{\"entity_id\":\"253\",\"position\":1},{\"entity_id\":\"823\",\"position\":2}]"

But if I just send the array it as the data of the AJAX call I get:

"shared_items"=>{"0"=>{"entity_id"=>"253", "position"=>"1"}, "1"=>{"entity_id"=>"823", "position"=>"2"}}

Whereas if I just send a plain array it works:

"shared_items"=>["entity_253"]

Why is Rails changing the array to that strange hash? The only reason that comes to mind is that Rails can't correctly understand the contents because there is no type here (is there a way to set it in the jQuery call?):

Processing by SharedListsController#create as 

Thank you!

Update: I'm sending the data as an array, not a string and the array is created dynamically using the .push() function. Tried with $.post and $.ajax, same result.

oxfist
  • 749
  • 6
  • 22
aledalgrande
  • 5,167
  • 3
  • 37
  • 65

7 Answers7

170

In case someone stumbles upon this and wants a better solution, you can specify the "contentType: 'application/json'" option in the .ajax call and have Rails properly parse the JSON object without garbling it into integer-keyed hashes with all-string values.

So, to summarize, my problem was that this:

$.ajax({
  type : "POST",
  url :  'http://localhost:3001/plugin/bulk_import/',
  dataType: 'json',
  data : {"shared_items": [{"entity_id":"253","position":1}, {"entity_id":"823","position":2}]}
});

resulted in Rails parsing things as:

Parameters: {"shared_items"=>{"0"=>{"entity_id"=>"253", "position"=>"1"}, "1"=>{"entity_id"=>"823", "position"=>"2"}}}

whereas this (NOTE: we're now stringifying the javascript object and specifying a content type, so rails will know how to parse our string):

$.ajax({
  type : "POST",
  url :  'http://localhost:3001/plugin/bulk_import/',
  dataType: 'json',
  contentType: 'application/json',
  data : JSON.stringify({"shared_items": [{"entity_id":"253","position":1}, {"entity_id":"823","position":2}]})
});

results in a nice object in Rails:

Parameters: {"shared_items"=>[{"entity_id"=>"253", "position"=>1}, {"entity_id"=>"823", "position"=>2}]}

This works for me in Rails 3, on Ruby 1.9.3.

swajak
  • 2,669
  • 3
  • 20
  • 22
  • This answer is superior to the chosen one. Let's upboat it to the top! – event_jr Nov 05 '12 at 13:23
  • Accepting this as this is what Rails really needs to work properly with JSON. You have to a similar thing if you're testing a JSON Rails API with RSpec/Cucumber. RSpec: set `@request.env['HTTP_ACCEPT'] = "application/json"`. Cucumber: pass `'HTTP_ACCEPT' => 'application/json'` in the headers. – aledalgrande Dec 26 '12 at 16:26
  • 2
    This does not seem like the right behavior for Rails, given that receiving JSON from JQuery is a pretty common case for Rails apps. Is there a defensible rationale as to why Rails behaves in this way? It seems that the client-side JS code is having to unnecessarily jump through hoops. – Jeremy Burton Mar 23 '14 at 00:34
  • 1
    I think in the case above the culprit is jQuery. iirc jQuery wasn't setting the `Content-Type` of the request to `application/json`, but was instead sending the data as an html form would? Hard to think back this far. Rails was working just fine, after the `Content-Type` was set to the correct value, and the data being sent was valid JSON. – swajak Mar 23 '14 at 09:32
  • I get javascript error: "Request header field Content-Type is not allowed by Access-Control-Allow-Headers." When I set header value in rails response Access-Control-Allow-Headers to "Origin, X-Requested-With, Content-Type, Accept" -> it reports "No 'Access-Control-Allow-Origin' header is present on the requested resource.". Although, "Access-Control-Allow-Origin" response header IS also being set to "*". – AndroC Apr 26 '14 at 12:00
  • OK, it turns out I forgot to stringify data passed to ajax call. But still, there were steps I had to do that weren't mentioned in the answer. Namely, I had to set response header on rails side: headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept' – AndroC Apr 26 '14 at 12:38
  • @ancajic if you had to set that header, then you are making a cross-domain request, which is not an example every request follows, and brings up all sorts of other considerations. In fact, I'd assume there are whole questions revolving around rails and CORS. – swajak Apr 28 '14 at 17:54
  • Yes, I am using cross-domain requests. Using token authentication it has become fairly common practice, so I thought my comment would help someone in future. – AndroC Apr 28 '14 at 20:33
  • Your solution causes Chrome to send an OPTIONS request instead of POST request. I didn't even know that HTTP has an OPTIONS method. The result is 404. – Paul Aug 08 '14 at 12:02
  • P.S. It was because of same domain policy. After putting the thst form into web application everything is working fine. – Paul Aug 08 '14 at 13:21
  • 3
    Want to note not only did @swajak stringify the object but also specify `contentType: 'application/json'` – Roger Lam Jun 01 '15 at 02:56
  • 1
    Thanks @RogerLam, I've updated the solution to describe that better I hope – swajak Jun 01 '15 at 22:49
  • This is so damn obscure but such a common pattern nowadays. Where was this ever documented! Awesome answer, thanks. Still required with jQuery 1.11 and Rails 4.2. – MSC Jul 21 '16 at 11:39
  • Why isn't this weird Rails behavior documented anywhere! I've been hitting my head against a wall for far too long trying to figure this out. THANK YOU. – aronmoshe_m Mar 13 '21 at 02:06
13

Slightly old question, but I fought with this myself today, and here's the answer I came up with: I believe this is slightly jQuery's fault, but that it's only doing what is natural to it. I do, however, have a workaround.

Given the following jQuery ajax call:

$.ajax({
   type : "POST",
   url :  'http://localhost:3001/plugin/bulk_import/',
   dataType: 'json',
   data : {"shared_items": [{"entity_id":"253","position":1},{"entity_id":"823","position":2}]}

});

The values jQuery will post will look something like this (if you look at the Request in your Firebug-of-choice) will give you form data that looks like:

shared_items%5B0%5D%5Bentity_id%5D:1
shared_items%5B0%5D%5Bposition%5D:1

If you CGI.unencode that you'll get

shared_items[0][entity_id]:1
shared_items[0][position]:1

I believe this is because jQuery thinks that those keys in your JSON are form element names, and that it should treat them as if you had a field named "user[name]".

So they come into your Rails app, Rails sees the brackets, and constructs a hash to hold the innermost key of the field name (the "1" that jQuery "helpfully" added).

Anyway, I got around this behavior by constructing my ajax call the following way;

$.ajax({
   type : "POST",
   url :  'http://localhost:3001/plugin/bulk_import/',
   dataType: 'json',
   data : {"data": JSON.stringify({"shared_items": [{"entity_id":"253","position":1},{"entity_id":"823","position":2}])},
  }
});

Which forces jQuery to think that this JSON is a value that you want to pass, entirely, and not a Javascript object it must take and turn all the keys into form field names.

However, that means things are a little different on the Rails side, because you need to explicitly decode the JSON in params[:data].

But that's OK:

ActiveSupport::JSON.decode( params[:data] )

TL;DR: So, the solution is: in the data parameter to your jQuery.ajax() call, do {"data": JSON.stringify(my_object) } explicitly, instead of feeding the JSON array into jQuery (where it guesses wrongly what you want to do with it.

RyanWilcox
  • 13,890
  • 1
  • 36
  • 60
8

I just ran into this issue with Rails 4. To explicitly answer your question ("Why is Rails changing the array to that strange hash?"), see section 4.1 of the Rails guide on Action Controllers:

To send an array of values, append an empty pair of square brackets "[]" to the key name.

The problem is, jQuery formats the request with explicit array indices, rather than with empty square brackets. So, for example, instead of sending shared_items[]=1&shared_items[]=2, it sends shared_items[0]=1&shared_items[1]=2. Rails sees the array indices and interprets them as hash keys rather than array indices, turning the request into a weird Ruby hash: { shared_items: { '0' => '1', '1' => '2' } }.

If you don't have control of the client, you can fix this problem on the server side by converting the hash to an array. Here's how I did it:

shared_items = []
params[:shared_items].each { |k, v|
  shared_items << v
}
jessepinho
  • 5,580
  • 1
  • 19
  • 19
1

following method could be helpful if you use strong parameters

def safe_params
  values = params.require(:shared_items)
  values = items.values if items.keys.first == '0'
  ActionController::Parameters.new(shared_items: values).permit(shared_items: [:entity_id, :position]).require(:shared_items)
end
Eugene
  • 991
  • 10
  • 10
0

Use the rack-jquery-params gem (disclaimer: I'm the author). It fixes your issue of arrays becoming hashes with integer keys.

Caleb Clark
  • 271
  • 3
  • 2
0

Have you considered doing parsed_json = ActiveSupport::JSON.decode(your_json_string)? If you're sending stuff the other way about you can use .to_json to serialise the data.

Michael De Silva
  • 3,808
  • 1
  • 20
  • 24
  • 1
    Yes considered, but wanted to know if there is a "right way" to do this in Rails, without having to manually encode/decode. – aledalgrande Jun 20 '11 at 13:51
0

Are you just trying to get the JSON string into a Rails controller action?

I'm not sure what Rails is doing with the hash, but you might get around the problem and have more luck by creating a Javascript/JSON object (as opposed to a JSON string) and sending that through as the data parameter for your Ajax call.

myData = {
  "shared_items":
    [
        {
            "entity_id": "253",
            "position": 1
        }, 
        {
            "entity_id": "823",
            "position": 2
        }
    ]
  };

If you wanted to send this via ajax then you would do something like this:

$.ajax({
    type: "POST",
    url: "my_url",    // be sure to set this in your routes.rb
    data: myData,
    success: function(data) {          
        console.log("success. data:");
        console.log(data);
    }
});

Note with the ajax snippet above, jQuery will make an intelligent guess on the dataType, although it's usually good to specify it explicitly.

Either way, in your controller action, you can get the JSON object you passed with the params hash, i.e.

params[:shared_items]

E.g. this action will spit your json object back at you:

def reply_in_json
  @shared = params[:shared_items]

  render :json => @shared
end
australis
  • 431
  • 5
  • 10