Rails Tricky Error: No Implicit Conversion From Symbol to Integer

In working with Ruby on the new Write.app API, I’ve come across a number of problems related to using it as a pure RESTful API server rather than a full web app with views. Because the new Write.app is an API server only it comes with some challenges and troubleshooting common issues becomes a bit harder sometimes. In this case I had a problem with POST’ing JSON data to a controller which accepted nested attributes and getting a ‘No implicit conversion from symbol to integer’ error in return. Turns out it was a simple mistake with a very simple solution that trips up a lot of developers.

For the code here we’re using Rails 4 and Ruby 2.0.

We were trying to send 3 pieces of data to the API and have it create both a new user and an API key for that user in a different table. Rather than calling a bunch of methods on different controllers we wanted the User model and API Key model to be updated in one fell swoop. In our example here we’re using a simplified version of the code that illustrates the problem clearly and leaves out the extra stuff. If you end up implementing similar code in your own projects please be sure to take the time to do proper validation.

So in this case we have an example controller that accepts POST parameters in JSON format that should create a new user account as well as a new row in the API key table.

This is our JSON POST request to api/users:

1
2
3
4
5
6
7
8
{
  "user": {
      "username": "example",
      "password": "strongPass0rd!",
      "password_confirmation": "strongPass0rd",
      "email": "example@example.com"
  }
}

Here is our controller which will handle the above JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Api::V1::UsersController < ApplicationController
  respond_to :json

  def create
    @user = User.new(user_params)
    render :json => @user.save
  end

  private
    def user_params
      params[:user][:keys_attributes] = [{
        "secret_key" => 12345678,
        "role" => 1
        }]
      params.require(:user).permit(:username, :password, :password_confirmation, :email, keys_attributes: [:secret_key, :role])
    end
end

When you POST to api/users with a username, password, and a password confirmation you’ve got all you need to create a new user. Because we’re using Rails 4 you there’s a private method which invokes Rails’ new Strong Parameters feature which lets us choose which parameters a user is allowed to submit in a form. The private user_params method first sets the default values for creating a new API key and appends them to the params[:user] hash which contains the JSON data we just POST’d. Then we return which parameters from our newly enhanced :user parameter hash we’ll allow to be sent to the model.

As you can see in the controller above, we have added the :keys_attributes hash to the :user params hash. We do this because the User model accepts nested attributes for the Keys model. Whenever a model accepts nested attributes you get a new *_attributes writer method for those associated models. In this case we have a keys_attributes method because each user has_many Keys and the User model can now pass attributes on to the Key model thereby creating, updating, and destroying associated database records in different tables at the same time. In our call to set up Strong Parameters we include those keys_attributes and then specify which keys from that hash are allowed to be passed in to the model.

To understand how these models are related, here is a simplified version of the database tables containing only the bare minimum columns needed:

Users table

id username password_digest email

Keys table

id secret_key user_id

These two tables are associated with each other through the user_id column in the Keys table. user_id acts as the foreign key to associate a key with a user.

Our models for User and Key look like this:

1
2
3
4
5
6
7
8
9
10
11
# User Model
class User < ActiveRecord::Base
  has_many :keys
  accepts_nested_attributes_for :keys
  has_secure_password # No need to validate the presence of the password fields as this method does that for you
end

# API Key Model
class Key < ActiveRecord::Base
  belongs_to :user
end

So our User has_many keys and each Key belongs_to one user. The User model accepts_nested_attributes_for modifying keys. The associations between the models and the fact that the User model will accept attributes for the Key model means that we can now create a new user and give them an API key on the fly. This code works like a charm but only because up till now I’ve only showed the correct code.

So what was the problem?

The problem that arose the first time around was that our keys_attributes was being interpreted as an array and not a hash. The original version of keys_attributes looked like this:

1
2
3
4
params[:user][:keys_attributes] = {
  :secret_key => 12345678,
  :role => 1
}

The difference is incredibly subtle…

Whenever we tried to POST this data to api/users/ the create method would return false even though we provided all the required data and set up everything else correctly. After a bit of digging around we found that this error is often associated with nested forms being implemented incorrectly Specifically the fields_for block in the form’s ERB file. But we’re not using Rails views since this is an API server. There had to be an underlying cause that developers weren’t aware of because of all the magic that Rails hides from you. Sure, this may present itself as a problem with nested forms but no one could explain why. Finally, the answer was found on a Ruby forum.

The issue is that the code above was seen as an array and arrays cannot have named keys. Rails expected params[:user][:keys_attributes] to use integers as keys (like an array does) but instead it found symbols. So it was taking our symbols, trying to convert them into integers, then throwing an error when it couldn’t. Symbols are confusing for many people new to Ruby. In short, a symbol is just an immutable string that starts with a colon (:) and contains no spaces. They’re often used as keys for hashes because they’re more performant than normal strings.

So when Ruby saw params[:user][:keys_attributes][:secret_key] it was trying to take the last key and turn it into params[:user][:keys_attributes][0] or at least find some integer value it could be converted to.

The solution was to fix how we were declaring the :keys_attributes so that it could be interpreted correctly as a hash. There’s a very small difference between the correct and incorrect implementations of our code below so please note the inclusion of the brackets in the correct code…

1
2
3
4
5
6
7
8
9
10
11
# WRONG!
params[:user][:keys_attributes] = {
  :secret_key => 12345678,
  :role => 1
}

# RIGHT!
params[:user][:keys_attributes] = [{
  "secret_key" => 12345678,
  "role" => 1
}]

And that was all it took for the error message to go away. Now we’re building more functionality into the app and hope to get a beta version out for people to use in the next couple of months. Hopefully this post will make its way into Google and people are able to find one more article with a solution to this problem, which so far I’ve seen most associated with Rails 4.

Web development

« Ruby's ||= (OR/Equals) Explained List of Rails Status Code Symbols »

Comments