240

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!

4
  • It sounds like you want to convert JSON into HTTP params... perhaps you need a different encoding? Commented Apr 28, 2009 at 16:21
  • Hum, this is actually not Json, but a Ruby Hash... not sure I understand why encoding matters here. Commented Apr 28, 2009 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. Commented May 16, 2013 at 9: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), 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. Commented May 16, 2013 at 14:40

13 Answers 13

320

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

require 'active_support/all'
query = {:a => "a", :b => ["c", "d", "e"]}
query.to_query
# "a=a&b%5B%5D=c&b%5B%5D=d&b%5B%5D=e"
CGI.unescape(query.to_query)
# "a=a&b[]=c&b[]=d&b[]=e"

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

Sign up to request clarification or add additional context in comments.

5 Comments

Why do you say it's broken? the output you showed is ok, isn't?
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"]}
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.
Without Rails: require 'active_support/all' is needed
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...
177

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.

7 Comments

+1, this is by far the best. It doesn't depend on any sources outside of Ruby itself.
+1 works fine with '{:a => "a", :b => {:c => "c", :d => true}, :e => []}' example
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.
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&
Note that this has different results for array values than both Addressable::URI and ActiveSupport's Object#to_query.
|
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

5 Comments

Thx! What are the edge cases where my solution breaks? so I can do add it to the specs?
It doesn't handle booleans, and this is clearly undesirable: {"a" => "a&b=b"}.to_params
FYI, unfortunately this behavior has been removed from Addressable as of 2.3 (github.com/sporkmonger/addressable/commit/…)
@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, 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.
72

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"}]}

5 Comments

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.
@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.
This method is deprecated since Rails 2.3.8: apidock.com/rails/Rack/Utils/build_query
@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.
@EdRuder ActiveSupport's to_query also merges the 2 arrays (v4.2).
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"

Comments

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

2 Comments

Unfortunately, this deosn't work when we have a nested Array inside the params (see example #2)... :(
And does not do any king of escaping.
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

Comments

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

2 Comments

URI.escape != CGI.escape and for URL you want the first one.
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"
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.

1 Comment

you really ought to make sure you are properly URI-escaping your keys and values though. Even for simple cases. It'll bite you.
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"

1 Comment

Without Rails: require 'active_support/all' is needed
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"

Comments

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)

Comments

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.