Convert map with nested string keys to nested map

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. :003:

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! :rofl:

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 :slight_smile:

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. :024: :049:

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