Rails 没有正确地从 jQuery 解码 JSON (数组变成带有整数键的散列)

每次我想用 jQuery to Rails 发布一个 JSON 对象数组时,都会遇到这个问题。 如果我对数组进行字符串化,我可以看到 jQuery 正在正确地完成它的工作:

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

但是如果我只是把数组作为 AJAX 调用的数据发送出去:

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

然而,如果我只发送一个普通数组,它就可以工作:

"shared_items"=>["entity_253"]

为什么 Rails 将数组更改为那个奇怪的散列?我想到的唯一原因是 Rails 无法正确理解内容,因为这里没有类型(有没有办法在 jQuery 调用中设置它?):

Processing by SharedListsController#create as

谢谢!

更新: 我将数据作为数组而不是字符串发送,数组是使用 .push()函数动态创建的。用 $.post$.ajax试过了,结果相同。

28256 次浏览

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.

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

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.

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.

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
}

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

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