Nested Hash Params with Named Routes in Ruby on Rails

Thu, 16 Nov 2006 20:41:00 -0800
ruby, rails, plugin, patch, routing

I've never (as of rev 5522) been able to pass a nested hash as parameters to url_for and have it flatten it properly on the generated URL. I encountered the problem again the other day and, having had just about enough, decided to try and nip it in the bud.

The problem I was having occurred when tried to make an HTTP request to a named route such as:

people_url(:person => { :name => 'Bob', :profession => 'Developer' })

This translates to a URL such as http://localhost:3000/people?person=nameBobprofessionDeveloper for a POST request. This obviously doesn't decode the hash in a format that can be encoded back its original form.

When I've encountered this problem in the past, I could never find any information from someone who's actually solved the problem. There are a few patches submitted as tickets to the Rails devs, but one seems to be a duplicate of the other, and neither seems to fix this problem on Edge, at least not for my particular use case.

The root of this particular problem lies in a method in the action_controller/routing.rb file within actionpack/actioncontroller, in the options_as_params of the RoutingSet class. The method, without comments, looks like this:

1
2
3
4
5
6
7




def options_as_params(options)
  options_as_params = options[:controller] ? { :action => "index" } : {}
  options.each do |k, value|
    options_as_params[k] = value.to_param
  end
  options_as_params
end

The problem lies on line 4 in the case where value is a Hash. In that case, the to_param method simply converts the Hash instance into its String representation, which is just a pile of keys and values mashed together, as in my people_url example.

To solve this problem, I changed the method to look like this:

def options_as_params(options)
  options_as_params = options[:controller] ? { :action => "index" } : {}
  options.parameterize do |param_key, param_value|
    options_as_params[param_key] = param_value.to_param
  end
  options_as_params
end

The parameterize method is an extension I made to the Hash class that essentially flattens a hash like:

{
  :person => {
    :name => 'Bob',
    :profession => 'Developer'
  }
}

into a new Hash that looks like:

{
  :'person[name]' => 'Bob',
  :'person[profession]' => 'Developer'
}

The new Hash is in a format that can be decoded into a URL query string that will be properly encoded back into a Hash on the request. My Hash extension for the solution looks like this:

class Hash

  def flatten(superkey)
    flattened_hash = {}
    self.each do |key, value|
      flattened_hash["#{superkey}[#{key}]".to_sym] = value
    end
    flattened_hash
  end

  def parameterize
    self.each do |k, value|
      if value.is_a?(Hash)
        value.flatten(k).each { |fk, fv| yield fk, fv }
      else
        yield k, value
      end
    end
  end

end

Note that this has only been tested to work for a single-level nested Hash, as that satisfied my needs at the time I developed the solution.