Passing Rails controller params to Sidekiq

Passing Rails controller params to Sidekiq jobs

If we pass Rails controller params, directly to a Sidekiq worker then they are not parsed correctly by Sidekiq when the job is executed. Let's see an example.

class UsersController < ApplicationController
  def create
    CreateUserWorker.perform_async(user_params)
  end
  
  private
  
    def user_params
      params.require(:user).permit(:name, :email)
    end
end
 
class CreateUserWorker
  include Sidekiq::Worker
  
  def perform(params)
    User.create!(params)
  end
end

This is because Sidekiq expects the arguments to be primitive types such as Hash, String, Boolean, Array etc.

Sidekiq uses JSON.generate to generate JSON and then pushes the job data to Redis. When the job is pulled from Redis for execution, Sidekiq parses the saved job data using JSON.parse and then passes it to the worker.

Let's see what is the output of JSON.generate with controller params as argument.

JSON.generate user_params
=> "\"{\\\"user\\\"=>{\\\"name\\\"=>\\\"Prathamesh\\\", \\\"email\\\"=>\\\"[email protected]\\\"}}\""

This is not the expected output because when Sidekiq will parse it, we will not get the ActionController::Parameters back.

job_data = JSON.generate user_params

JSON.parse job_data
=> "{\"user\"=>{\"name\"=>\"Prathamesh\", \"email\"=>\"[email protected]\"}}"

We can see that the data is not parsed correctly, because the JSON payload was not generated correctly.

Ruby's JSON.generate method treats certain primitive objects such as Hash, String, True, False as special cases when generating JSON representation. Whereas for custom objects, it checks whether the object responds to to_json or not. If yes then it returns the output of calling to_json on that object. If the object does not respond to the to_json method, then it generates its JSON representation considering the object as String. This code can be found here.

In case of ActionController::Parameters objects, Ruby generates JSON representation considering it as String. Even though ActionController::Parameters gets to_json method from Active Support, still Ruby is not able to figure out that ActionController::Parameters objects respond to to_json.

# https://github.com/ruby/ruby/blob/8e517942656f095af2b3417f0df85ae0b216002a/ext/json/generator/generator.c#L1018

else if (rb_respond_to(obj, i_to_json)) {
        tmp = rb_funcall(obj, i_to_json, 1, Vstate);
        Check_Type(tmp, T_STRING);
        fbuffer_append_str(buffer, tmp);
    }

As ActionController::Parameters respond to to_json it should go into this branch of code but somehow it goes into the final else code branch.

I am not able to figure out why that is happening. Rohit Kumar pointed in comments on this post that when you call JSON.generate, the to_json method is not called on the instance of ActionController::Parameters which is the reason why Ruby does not use the to_json output defined by Active Support for serialization. I have not fully understood yet what does rb_funcall(obj, i_to_json, 1, Vstate); outputs in this case.

So to fix this, we need to pass the params as Hash object to the Sidekiq job. A simple way will be to pass the params after calling to_h on them.

class UsersController < ApplicationController
  def create
    CreateUserWorker.perform_async(user_params.to_h)
  end
  
  private
  
    def user_params
      params.require(:user).permit(:name, :email)
    end
end
Importantly, to_h will result into an error when we have not permitted any of the parameters. You can also call to_unsafe_h to pass all the parameters without permitting.

Before Rails 5, ActionController::Parameters was inheriting from Hash class so the JSON serialization and deserialization was working properly without converting the params to a Hash. But now because ActionController::Parameters do not inherit from Hash anymore, we need to pass it to Sidekiq worker as a Hash.