Ruby: How to turn a hash into HTTP parameters?

148,099

Solution 1

Update: This functionality was removed from the gem.

Julien, your self-answer is a good one, and I've shameless borrowed from it, but it doesn't properly escape reserved characters, and there are a few other edge cases where it breaks down.

require "addressable/uri"
uri = Addressable::URI.new
uri.query_values = {:a => "a", :b => ["c", "d", "e"]}
uri.query
# => "a=a&b[0]=c&b[1]=d&b[2]=e"
uri.query_values = {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]}
uri.query
# => "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
uri.query_values = {:a => "a", :b => {:c => "c", :d => "d"}}
uri.query
# => "a=a&b[c]=c&b[d]=d"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}}
uri.query
# => "a=a&b[c]=c&b[d]"
uri.query_values = {:a => "a", :b => {:c => "c", :d => true}, :e => []}
uri.query
# => "a=a&b[c]=c&b[d]"

The gem is 'addressable'

gem install addressable

Solution 2

For basic, non-nested hashes, Rails/ActiveSupport has Object#to_query.

>> {:a => "a", :b => ["c", "d", "e"]}.to_query
=> "a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e"
>> CGI.unescape({:a => "a", :b => ["c", "d", "e"]}.to_query)
=> "a=a&b[]=c&b[]=d&b[]=e"

http://api.rubyonrails.org/classes/Object.html#method-i-to_query

Solution 3

If you are using Ruby 1.9.2 or later, you can use URI.encode_www_form if you don't need arrays.

E.g. (from the Ruby docs in 1.9.3):

URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => "ruby", "lang" => "en")
#=> "q=ruby&lang=en"
URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
#=> "q=ruby&q=perl&lang=en"
URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
#=> "q=ruby&q=perl&lang=en"

You'll notice that array values are not set with key names containing [] like we've all become used to in query strings. The spec that encode_www_form uses is in accordance with the HTML5 definition of application/x-www-form-urlencoded data.

Solution 4

No need to load up the bloated ActiveSupport or roll your own, you can use Rack::Utils.build_query and Rack::Utils.build_nested_query. Here's a blog post that gives a good example:

require 'rack'

Rack::Utils.build_query(
  authorization_token: "foo",
  access_level: "moderator",
  previous: "index"
)

# => "authorization_token=foo&access_level=moderator&previous=index"

It even handles arrays:

Rack::Utils.build_query( {:a => "a", :b => ["c", "d", "e"]} )
# => "a=a&b=c&b=d&b=e"
Rack::Utils.parse_query _
# => {"a"=>"a", "b"=>["c", "d", "e"]}

Or the more difficult nested stuff:

Rack::Utils.build_nested_query( {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}] } )
# => "a=a&b[][c]=c&b[][d]=d&b[][e]=e&b[][f]=f"
Rack::Utils.parse_nested_query _
# => {"a"=>"a", "b"=>[{"c"=>"c", "d"=>"d", "e"=>"e", "f"=>"f"}]}

Solution 5

Here's a short and sweet one liner if you only need to support simple ASCII key/value query strings:

hash = {"foo" => "bar", "fooz" => 123}
# => {"foo"=>"bar", "fooz"=>123}
query_string = hash.to_a.map { |x| "#{x[0]}=#{x[1]}" }.join("&")
# => "foo=bar&fooz=123"
Share:
148,099

Related videos on Youtube

Julien Genestoux
Author by

Julien Genestoux

Cofounder of Superfeedr, acquired by Medium.

Updated on October 02, 2020

Comments

  • Julien Genestoux
    Julien Genestoux over 3 years

    That is pretty easy with a plain hash like

    {:a => "a", :b => "b"} 
    

    which would translate into

    "a=a&b=b"
    

    But what do you do with something more complex like

    {:a => "a", :b => ["c", "d", "e"]} 
    

    which should translate into

    "a=a&b[0]=c&b[1]=d&b[2]=e" 
    

    Or even worse, (what to do) with something like:

    {:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]
    

    Thanks for the much appreciated help with that!

    • CookieOfFortune
      CookieOfFortune about 15 years
      It sounds like you want to convert JSON into HTTP params... perhaps you need a different encoding?
    • Julien Genestoux
      Julien Genestoux about 15 years
      Hum, this is actually not Json, but a Ruby Hash... not sure I understand why encoding matters here.
    • Noach Magedman
      Noach Magedman about 11 years
      The answer by lmanners ought to be promoted. There are a lot of great roll-your-own answers here (many with high scores) but ActiveSupport has since added standardized support for this, rendering the conversation moot. Unfortunately, lmanner's answer is still buried down the list.
    • ian
      ian about 11 years
      @Noach in my opinion, any answer that says to rely on a library that heavily monkey patches core classes should remain buried. The justification for a huge number of those patches is shaky at best (take a look at Yehuda Katz's comments in this article), this being an excellent example. YMMV, but for me, something with a class method or that doesn't open Object and Hash, and where the authors wouldn't say "just don't clash with us!" would be much, much better.
  • Julien Genestoux
    Julien Genestoux about 15 years
    Unfortunately, this deosn't work when we have a nested Array inside the params (see example #2)... :(
  • Julien Genestoux
    Julien Genestoux about 15 years
    Thx! What are the edge cases where my solution breaks? so I can do add it to the specs?
  • Bob Aman
    Bob Aman about 15 years
    It doesn't handle booleans, and this is clearly undesirable: {"a" => "a&b=b"}.to_params
  • tokland
    tokland over 13 years
    Why do you say it's broken? the output you showed is ok, isn't?
  • Gabe Martin-Dempesy
    Gabe Martin-Dempesy over 13 years
    I just tried it and you seem to be right. Maybe my statement was originally due to the way an earlier version of rails parsed the query string (I seemed to recall it overwriting the previous 'b' values). Started GET "/?a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e" for 127.0.0.1 at 2011-03-10 11:19:40 -0600 Processing by SitesController#index as HTML Parameters: {"a"=>"a", "b"=>["c", "d", "e"]}
  • Ernest
    Ernest about 12 years
    URI.escape != CGI.escape and for URL you want the first one.
  • Ernest
    Ernest about 12 years
    And does not do any king of escaping.
  • svale
    svale about 12 years
    Actually not, @Ernest. When e.g. embedding another url as a parameter to your url (lets say this is the return url to be redirected to after login) URI.escape will keep the '?' and '&' of the embedded url in place breaking the surrounding url, while CGI.escape will correctly tuck them away for later as %3F and %26. CGI.escape("http://localhost/search?q=banana&limit=7") => "http%3A%2F%2Flocalhost%2Fsearch%3Fq%3Dbanana%26limit%3D7" URI.escape("http://localhost/search?q=banana&limit=7") => "http://localhost/search?q=banana&limit=7"
  • oif_vet
    oif_vet almost 12 years
    FYI, unfortunately this behavior has been removed from Addressable as of 2.3 (github.com/sporkmonger/addressable/commit/…)
  • sheldonh
    sheldonh over 11 years
    @oif_vet Could you say what behaviour has been removed? Bob's suggested appraoch of using the addressable gem to solve the original poster's problem works for me as of addressable-2.3.2.
  • Bob Aman
    Bob Aman over 11 years
    @sheldonh, no, @oif_vet is correct. I removed this behavior. Deeply nested structures are no longer supported in Addressable as inputs to the query_values mutator.
  • Ed Ruder
    Ed Ruder over 11 years
    Your nested example demonstrates that it doesn't work properly--when you start, :b is an array of two hashes. You end up with :b being an array of one bigger hash.
  • ian
    ian over 11 years
    @EdRuder there's no properly because there's no accepted standard. What it does show is that it's a lot closer than anyone else's attempt, judging by the other answers.
  • davidgoli
    davidgoli about 11 years
    This method is deprecated since Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
  • ian
    ian about 11 years
    @davidgoli Erm, not in Rack it's not github.com/rack/rack/blob/1.5.2/lib/rack/utils.rb#L140. If you want to use it in Rails, surely it's as simple as require 'rack'? It must be there, considering all the major Ruby web frameworks are built on top of Rack now.
  • Danyel
    Danyel almost 11 years
    +1, this is by far the best. It doesn't depend on any sources outside of Ruby itself.
  • Duke
    Duke almost 11 years
    +1 works fine with '{:a => "a", :b => {:c => "c", :d => true}, :e => []}' example
  • user208769
    user208769 over 10 years
    Does not seem to work with ruby 2.0 - the hash {:c => "c", :d => true} appears to be inspected, so sent through as a string.
  • Bo Jeanes
    Bo Jeanes over 10 years
    @user208769, I just tried in Ruby 2.0 and it seems to work fine. $ ruby -ruri -e 'puts RUBY_VERSION; puts URI.encode_www_form({:c => "c", :d => true})' # outputs 2.0.0 c=c&d=true
  • user208769
    user208769 over 10 years
    It was a section of the larger snippet above - ruby -ruri -e 'puts RUBY_VERSION; puts URI.encode_www_form({:a => "a", :b => {:c => "c", :d => true}, :e => []})' # outputs 2.0.0 a=a&b=%7B%3Ac%3D%3E%22c%22%2C+%3Ad%3D%3Etrue%7D&
  • Matt Huggins
    Matt Huggins over 10 years
    Note that this has different results for array values than both Addressable::URI and ActiveSupport's Object#to_query.
  • jrochkind
    jrochkind about 10 years
    you really ought to make sure you are properly URI-escaping your keys and values though. Even for simple cases. It'll bite you.
  • Kelvin
    Kelvin about 9 years
    @EdRuder ActiveSupport's to_query also merges the 2 arrays (v4.2).
  • Sam
    Sam over 8 years
    what goes wrong if there are nested hashes? Why I can't use this when there are nested hashes? To me, it just url escapes the nested hash, there should be no problem using this in http request.
  • puchu
    puchu over 7 years
    It turns space into "+". This is only for "x-www-form-urlencoded" data.
  • Dorian
    Dorian over 6 years
    Without Rails: require 'active_support/all' is needed
  • Dorian
    Dorian over 6 years
    Without Rails: require 'active_support/all' is needed
  • jsmartt
    jsmartt about 4 years
    At least with Rails 5.2 to_query does not handle nil values properly. { a: nil, b: '1'}.to_query == "a=&b=1", but Rack and CGI both parse a= as an empty string, not nil. I'm not sure about support for other servers, but with rails, the correct query string should be a&b=1. I think it's wrong that Rails can't produce a query string that's correctly parsed by itself...