If I had the following map:
%{
"example[field_one][0][field_name]" => "some value",
"example[field_one][1][field_name]" => "another value",
"example[field_two]" => "third value",
"different_field" => "different_value"
}
How could I convert it the following structure:
%{
"example" => %{
"field_one" => %{
"0" => %{
"field_name" => "some_value"
},
"1" => %{
"field_name" => "another_value"
}
},
"field_two" => "third_value"
},
"different_field" => "different_value"
}
The use case is some values being grabbed out of a form with their name
as per the structure used by Phoenix forms. I need to map is back to a nested structure that I can cast with a Changeset. I’ve looked through the Phoenix.HTML.Form codebase and can’t seem to find the function that would do this, so I assume it comes from the browser in the correct format?
Cheers, Jamie
Is this actually from a phoenix form? If so this should be happening automatically.
It’s coming from a Phoenix form, but I’m grabbing the values with JS and sending then back over a socket. I’m hacking together something to replace my current implementation with Drab whilst I wait for LiveView, so thought I’d see if I could solve it with regular JS and channels.
So far I’ve got this:
Enum.reduce(attrs, %{"example" => %{}}, fn {key, value}, acc ->
path =
key
|> String.split(["[", "]"]) # Remove the []
|> Enum.reject(fn x -> x == "" end) # Remove the "" left behind in List
put_in(acc, path, value)
end)
but the issue is that I need to specify an empty map when creating the accumulator. Otherwise I get an error trying to insert the first nested key on nil
.
To update, I found this question answered by @sasajuric. Modifying the answer to my specific situation I ended up with this, which gives me the result I was looking for.
def to_map(input) do
Enum.reduce(input, %{},
fn({key, value}, intermediate_map) ->
merge(intermediate_map, create_path(key), value)
end)
end
defp create_path(key) do
key
|> String.split(["[", "]"]) # Remove the []
|> Enum.reject(fn x -> x == "" end) # Remove the "" left behind in List
end
defp merge(map, [leaf], value), do: Map.put(map, leaf, value)
defp merge(map, [node | remaining_keys], value) do
inner_map = merge(Map.get(map, node, %{}), remaining_keys, value)
Map.put(map, node, inner_map)
end
When trying other methods, it was the creation of the empty map on keys with other nested values that caused me issues. I was trying to solve the problem by using the Kernel’s put_in
and get_and_update_in
functions which really don’t like nil
values!
Sasa’s solution treats the keys as a list, and generates the inner_map
first. I was trying to do it the other way, using Enum.reduce
and lots of Kernel functions.
Every day’s a school day!
2 Likes
But doesn’t Phoenix have helpers that parse such HTTP-friendly structures?
1 Like
It must do, but I can’t seem to find them. They obviously exist somewhere between the form submission and the controller params, but where…
Even if I could use them, it would feel odd to use them in this context; although referencing the “official” way this gets done would be interesting.
1 Like
Why would it be odd though? These are obviously HTTP form parameters. It would make perfect sense to call a Phoenix helper for it… if we could actually find it, that is. 
1 Like
If it’s a Phoenix helper then yeah, that’d be great and make sense. My search took me into Plug.Parser at which point I decided I didn’t have enough coffee onboard and to just work it out myself! 
1 Like
I haven’t tried it, but I suspect a healthy dose of Plug.Conn.Query.decode/1
would help.
Update. I tried it now 
input = %{
"example[field_one][0][field_name]" => "some value",
"example[field_one][1][field_name]" => "another value",
"example[field_two]" => "third value",
"different_field" => "different_value"
}
Enum.reduce(input, %{}, fn {k,v}, acc -> Plug.Conn.Query.decode("#{k}=#{v}",
acc)end)
yields
%{
"different_field" => "different_value",
"example" => %{
"field_one" => %{
"0" => %{"field_name" => "some value"},
"1" => %{"field_name" => "another value"}
},
"field_two" => "third value"
}
}
It’d be easier if your input weren’t already broken on the equals sign.
Ironically, I find lack of caffeination tends to drive me toward finding the code that already does it rather that brute force a new way.
2nd edit:
Oh, we can simplify it even more!
Enum.reduce(input, %{}, &Plug.Conn.Query.decode_pair/2)
5 Likes
I knew it! I’m bookmarking Plug.Conn.Query
.
BTW, I’m also much more inclined to reuse people’s open-sourced work, and with me it’s not related to coffee at all. It’s just the right thing to do!
Thank you.

1 Like
That’s brilliant, thank you! Can confirm that it “Just Works”
I’m actually managing the serialising of the form data in the browser, so could opt for a =
separated string, but with the decode_pair
option I’ll probably leave it as is. Thanks again!
2 Likes