What turns URL params like http://site.com/people?name=bobo into { :name => “bobo” } in Ruby?

And what turns extra-weird Rails or Sinatra params like /path?people[][name]=bobo&people[][first_love]=cheese?

What the Hell is that?

Googling “rails query param names with square brackets” doesn’t help much. And Rails doesn’t make it easy to find docs for this.

So what’s going on with the weird params?

Rails uses Rack to parse them, so it’s (mostly) the same in Sinatra, Padrino or your ruby web framework of choice as well.

What Does the Code Say?

Rack is really poorly documented. But the query param code is short, so instead of my usual "rebuild it" approach, let’s go right to the source – Rack::Utils.parse_nested_query:

1
2
3
4
5
6
7
8
9
10
11
def parse_nested_query(qs, d = nil)
  params = KeySpaceConstrainedParams.new

  (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
    k, v = p.split('=', 2).map { |s| unescape(s) }

    normalize_params(params, k, v)
  end

  return params.to_params_hash
end

That’s just parsing the parameters by splitting on ampersand and semicolon. What about the square braces? They’re handled in normalize_params (see below for abridged source, or GitHub for full). It gets called once for each parameter name.

What Does normalize_params Do?

First it checks the param name for anything that’s not square-braces, and then read whatever is after that (called “after”).

If the “after” is nothing there are no square braces — great, assign the parameter, you’re done.

If “after” is empty square-braces, that parameter is an array. Start with it being an empty array, and then append to it. So if you parsed p[]=a&p[]=b&p[]=c, you’d get {"p" => ["a", "b", "c"] }.

If “after” starts with empty square braces and then has more after it, it’s nested. So the parameter is still an array, and could be something like an array of arrays. The code handles this by recursing.

So if you parsed p[][a]=a&p[][b]=b&p[][c]=c, you’d get { "p" => [{ "a" => "a", "b" => "b", "c" => "c"]}.

Feeling a little confused? Scroll down for another approach — play with it in irb!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def normalize_params(params, name, v = nil)
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
  k = $1 || ''
  after = $' || ''

  return if k.empty?

  if after == ""
    params[k] = v
  elsif after == "[]"
    params[k] ||= []
    params[k] << v
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
    child_key = $1
    params[k] ||= []
    if params_hash_type?(params[k].last)
      && !params[k].last.key?(child_key)
      normalize_params(params[k].last, child_key, v)
    else
      params[k] << normalize_params(params.class.new, child_key, v)
    end
  else
    params[k] ||= params.class.new
    params[k] = normalize_params(params[k], after, v)
  end

  return params
end

Using irb

Since this is Ruby, pop open irb and require “rack”.

We’ll define a convenience function called p() because I hate typing.

Then we’ll check to make sure we were right up above:

1
2
3
4
5
6
7
8
9
10
11
Athanor:rulers noah$ irb
1.9.3p125 :001 > require "rack"
 => true
1.9.3p125 :002 > def p(params)
1.9.3p125 :003?>   Rack::Utils.parse_nested_query(params)
1.9.3p125 :004?>   end
 => nil
1.9.3p125 :005 > p("p[]=a&p[]=b&p[]=c")
 => {"p"=>["a", "b", "c"]}
1.9.3p125 :006 > p("p[][a]=a&p[][b]=b&p[][c]=c")
 => {"p"=>[{"a"=>"a", "b"=>"b", "c"=>"c"}]}

Looks pretty good. Let’s try skipping the first open brackets:

1
2
3
4
1.9.3p125 :007 > p("p[x]=1&p[y]=2")
 => {"p"=>{"x"=>"1", "y"=>"2"}}
1.9.3p125 :008 > p("p[123]=bobo")
 => {"p"=>{"123"=>"bobo"}}

So that’s what we usually see in Rails with a structure.

What else can we get by playing with this?

1
2
3
4
5
6
7
8
1.9.3p125 :017 > p("x[][y][w]=1&x[][z]=2&x[][y][w]=3&x[][z]=4")
 => {"x"=>[{"y"=>{"w"=>"3"}, "z"=>"2"}, {"z"=>"4"}]}
1.9.3p125 :018 > p("x[][z][w]=1&x[][z]=2&x[][y][w]=3&x[][z]=4")
 => {"x"=>[{"z"=>{"w"=>"1"}}, {"z"=>"2", "y"=>{"w"=>"3"}}, {"z"=>"4"}]}
1.9.3p125 :019 > p("x[][z][w]=1&x[][z][j]=2&x[][y][w]=3&x[][z]=4")
 => {"x"=>[{"z"=>{"w"=>"1", "j"=>"2"}, "y"=>{"w"=>"3"}}, {"z"=>"4"}]}
1.9.3p125 :020 > p("x[][z][w]=1&x[][z][j]=2&x[][y][w]=3&x[][b]=4")
 => {"x"=>[{"z"=>{"w"=>"1", "j"=>"2"}, "y"=>{"w"=>"3"}, "b"=>"4"}]}

Have fun! I hope you’ve learned some interesting things about Rack parameter parsing… And how to figure it out next time you see somebody doing something that looks complicated with Rails parameters!

Your first message has a download URL.

Comments