234

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!

Flip
  • 6,233
  • 7
  • 46
  • 75
Julien Genestoux
  • 31,046
  • 20
  • 66
  • 93
  • It sounds like you want to convert JSON into HTTP params... perhaps you need a different encoding? – CookieOfFortune Apr 28 '09 at 16:21
  • Hum, this is actually not Json, but a Ruby Hash... not sure I understand why encoding matters here. – Julien Genestoux Apr 28 '09 at 16:51
  • 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. – Noach Magedman May 16 '13 at 09:49
  • 2
    @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](http://yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice/)), 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. – ian May 16 '13 at 14:40

14 Answers14

315

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

Dorian
  • 22,759
  • 8
  • 120
  • 116
Gabe Martin-Dempesy
  • 7,687
  • 4
  • 33
  • 24
  • 1
    Why do you say it's broken? the output you showed is ok, isn't? – tokland Mar 10 '11 at 13:14
  • 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"]} – Gabe Martin-Dempesy Mar 10 '11 at 17:21
  • 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. – Sam Dec 02 '15 at 01:13
  • 3
    Without Rails: `require 'active_support/all'` is needed – Dorian Nov 20 '17 at 22:15
  • 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... – jsmartt Jun 18 '20 at 22:18
173

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.

Bo Jeanes
  • 6,294
  • 4
  • 42
  • 39
  • 11
    +1, this is by far the best. It doesn't depend on any sources outside of Ruby itself. – Danyel Aug 21 '13 at 22:44
  • +1 works fine with '{:a => "a", :b => {:c => "c", :d => true}, :e => []}' example – Duke Sep 19 '13 at 05:57
  • 1
    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. – user208769 Dec 13 '13 at 23:29
  • @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` – Bo Jeanes Dec 15 '13 at 00:32
  • 1
    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&` – user208769 Dec 16 '13 at 19:40
  • 3
    Note that this has different results for array values than both `Addressable::URI` and ActiveSupport's `Object#to_query`. – Matt Huggins Feb 14 '14 at 18:56
  • It turns space into "+". This is only for "x-www-form-urlencoded" data. – puchu Feb 14 '17 at 14:37
89

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
Bob Aman
  • 32,839
  • 9
  • 71
  • 95
  • 1
    Thx! What are the edge cases where my solution breaks? so I can do add it to the specs? – Julien Genestoux May 05 '09 at 18:09
  • 2
    It doesn't handle booleans, and this is clearly undesirable: {"a" => "a&b=b"}.to_params – Bob Aman May 06 '09 at 17:29
  • 5
    FYI, unfortunately this behavior has been removed from Addressable as of 2.3 (https://github.com/sporkmonger/addressable/commit/f51e290b5f68a98293327a7da84eb9e2d5f21c62) – oif_vet Sep 13 '12 at 23:03
  • 2
    @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. – sheldonh Jan 16 '13 at 12:26
  • 1
    @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. – Bob Aman Jan 23 '13 at 00:07
70

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"}]}
ian
  • 12,003
  • 9
  • 51
  • 107
  • 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. – Ed Ruder Feb 05 '13 at 23:13
  • 3
    @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. – ian Feb 06 '13 at 00:35
  • 1
    This method is deprecated since Rails 2.3.8: http://apidock.com/rails/Rack/Utils/build_query – davidgoli May 06 '13 at 22:09
  • 8
    @davidgoli Erm, not in ***Rack*** it's not https://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. – ian May 16 '13 at 13:10
  • 1
    @EdRuder ActiveSupport's `to_query` also merges the 2 arrays (v4.2). – Kelvin Jun 22 '15 at 19:29
12

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"
Hubro
  • 56,214
  • 69
  • 228
  • 381
10

Steal from Merb:

# File merb/core_ext/hash.rb, line 87
def to_params
  params = ''
  stack = []

  each do |k, v|
    if v.is_a?(Hash)
      stack << [k,v]
    else
      params << "#{k}=#{v}&"
    end
  end

  stack.each do |parent, hash|
    hash.each do |k, v|
      if v.is_a?(Hash)
        stack << ["#{parent}[#{k}]", v]
      else
        params << "#{parent}[#{k}]=#{v}&"
      end
    end
  end

  params.chop! # trailing &
  params
end

See http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html

Avdi
  • 18,340
  • 6
  • 53
  • 62
5
class Hash
  def to_params
    params = ''
    stack = []

    each do |k, v|
      if v.is_a?(Hash)
        stack << [k,v]
      elsif v.is_a?(Array)
        stack << [k,Hash.from_array(v)]
      else
        params << "#{k}=#{v}&"
      end
    end

    stack.each do |parent, hash|
      hash.each do |k, v|
        if v.is_a?(Hash)
          stack << ["#{parent}[#{k}]", v]
        else
          params << "#{parent}[#{k}]=#{v}&"
        end
      end
    end

    params.chop! 
    params
  end

  def self.from_array(array = [])
    h = Hash.new
    array.size.times do |t|
      h[t] = array[t]
    end
    h
  end

end
Julien Genestoux
  • 31,046
  • 20
  • 66
  • 93
4
{:a=>"a", :b=>"b", :c=>"c"}.map{ |x,v| "#{x}=#{v}" }.reduce{|x,v| "#{x}&#{v}" }

"a=a&b=b&c=c"

Here's another way. For simple queries.

Roman Kiselenko
  • 43,210
  • 9
  • 91
  • 103
  • 2
    you really ought to make sure you are properly URI-escaping your keys and values though. Even for simple cases. It'll bite you. – jrochkind May 05 '14 at 20:54
4

I know this is an old question, but I just wanted to post this bit of code as I could not find a simple gem to do just this task for me.

module QueryParams

  def self.encode(value, key = nil)
    case value
    when Hash  then value.map { |k,v| encode(v, append_key(key,k)) }.join('&')
    when Array then value.map { |v| encode(v, "#{key}[]") }.join('&')
    when nil   then ''
    else            
      "#{key}=#{CGI.escape(value.to_s)}" 
    end
  end

  private

  def self.append_key(root_key, key)
    root_key.nil? ? key : "#{root_key}[#{key.to_s}]"
  end
end

Rolled up as gem here: https://github.com/simen/queryparams

svale
  • 264
  • 1
  • 3
  • 1
    `URI.escape != CGI.escape` and for URL you want the first one. – Ernest May 09 '12 at 13:16
  • 2
    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"` – svale May 23 '12 at 19:08
3

The best approach it is to use Hash.to_params which is the one working fine with arrays.

{a: 1, b: [1,2,3]}.to_param
"a=1&b[]=1&b[]=2&b[]=3"
Dorian
  • 22,759
  • 8
  • 120
  • 116
fhidalgo
  • 129
  • 5
3
require 'uri'

class Hash
  def to_query_hash(key)
    reduce({}) do |h, (k, v)|
      new_key = key.nil? ? k : "#{key}[#{k}]"
      v = Hash[v.each_with_index.to_a.map(&:reverse)] if v.is_a?(Array)
      if v.is_a?(Hash)
        h.merge!(v.to_query_hash(new_key))
      else
        h[new_key] = v
      end
      h
    end
  end

  def to_query(key = nil)
    URI.encode_www_form(to_query_hash(key))
  end
end

2.4.2 :019 > {:a => "a", :b => "b"}.to_query_hash(nil)
 => {:a=>"a", :b=>"b"}

2.4.2 :020 > {:a => "a", :b => "b"}.to_query
 => "a=a&b=b"

2.4.2 :021 > {:a => "a", :b => ["c", "d", "e"]}.to_query_hash(nil)
 => {:a=>"a", "b[0]"=>"c", "b[1]"=>"d", "b[2]"=>"e"}

2.4.2 :022 > {:a => "a", :b => ["c", "d", "e"]}.to_query
 => "a=a&b%5B0%5D=c&b%5B1%5D=d&b%5B2%5D=e"
mhorbul
  • 146
  • 6
3

If you are in the context of a Faraday request, you can also just pass the params hash as the second argument and faraday takes care of making proper param URL part out of it:

faraday_instance.get(url, params_hsh)
Yo Ludke
  • 2,149
  • 2
  • 23
  • 38
0

I like using this gem:

https://rubygems.org/gems/php_http_build_query

Sample usage:

puts PHP.http_build_query({"a"=>"b","c"=>"d","e"=>[{"hello"=>"world","bah"=>"black"},{"hello"=>"world","bah"=>"black"}]})

# a=b&c=d&e%5B0%5D%5Bbah%5D=black&e%5B0%5D%5Bhello%5D=world&e%5B1%5D%5Bbah%5D=black&e%5B1%5D%5Bhello%5D=world
John
  • 32,403
  • 80
  • 251
  • 422
0
2.6.3 :001 > hash = {:a => "a", :b => ["c", "d", "e"]}
=> {:a=>"a", :b=>["c", "d", "e"]}
2.6.3 :002 > hash.to_a.map { |x| "#{x[0]}=#{x[1].class == Array ? x[1].join(",") : x[1]}" 
}.join("&")
=> "a=a&b=c,d,e"
saqib
  • 111
  • 2
  • 5