Nested Hash Params with Named Routes in Ruby on Rails 3

Posted by Ryan Kinderman Fri, 17 Nov 2006 04:41:00 GMT

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.

Trackbacks

Use the following link to trackback from your own site:
http://kinderman.net/trackbacks?article_id=nested-hash-params-with-named-routes-in-ruby-on-rails&day=16&month=11&year=2006

Comments

Leave a comment

  1. roger pack 12 months later:

    I did the same thing (1.2.3 still shows this problem). Instead mine was to do something like this

    def flatteninteriorhashes_to_singles(params) out = {} for key, possiblecombinedpoorly_value in params value = params[key] if value.class.ancestors.include? Hash for keyinner, valueinner in value out["#{key}[#{keyinner}]"] = valueinner # they had better not be 3dd or we're stuck again. Fix when necessary end else out[key] = value end end out end

    To avoid having to rewrite ruby code shared among developers. Cheers!

    linkto 'whatever', flatteninteriorhashesto_singles(params).merge({:page => 3})

    Note that this doesn't convert them to :symbols (so it's deficient in that way).

  2. roger pack 12 months later:

    oops I meant

    def flatten_interior_hashes_to_singles(params)
        out = {}
        for key, possible_combined_poorly_value in params
          value = params[key]
          if value.class.ancestors.include? Hash
             for key_inner, value_inner in value
                out["#{key}[#{key_inner}]"] = value_inner # they had better not be 3dd or we're stuck again.  Fix when necessary
              end
           else
           out[key] = value
           end
        end
        out
      end
    

    used like

    linkto 'something', flatteninteriorhashestosingles(params).merge({:newparam => new_setting})
    or what have you.

  3. macournoyer about 1 year later:

    I think you should be able to achieve the same result by just overriding Hash#to_param

Comments