97

What is the way to convert %{"foo" => "bar"} to %{foo: "bar"} in Elixir?

1
  • 3
    Warning: [String.to_atom/1] creates atoms dynamically and atoms are not garbage-collected. Therefore, string should not be an untrusted value, such as input received from a socket or during a web request. Consider using to_existing_atom/1 instead. hexdocs.pm/elixir/String.html#to_atom/1 Commented Oct 31, 2019 at 17:57

17 Answers 17

107

Use Comprehensions:

iex(1)> string_key_map = %{"foo" => "bar", "hello" => "world"} %{"foo" => "bar", "hello" => "world"} iex(2)> for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), val} %{foo: "bar", hello: "world"} 
Sign up to request clarification or add additional context in comments.

7 Comments

how to store it into some variable !
how to convert below into strings of atoms [%{"foo" => "bar", "hello" => "world"},%{"foo" => "baromater", "hello" => "nope"}]
Here is a function definition for a recursive helper: def keys_to_atoms(string_key_map) when is_map(string_key_map) do for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)} end def keys_to_atoms(value), do: value
Warning! You shouldn't call this on untrusted user input because atom are not garbage collected and could cause you to run into the limit of the number of atoms allowed on the BEAM: hexdocs.pm/elixir/String.html#to_atom/1
Just note that this does not convert deep/recursive
|
71

I think the easiest way to do this is to use Map.new:

%{"a" => 1, "b" => 2} |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) %{a: 1, b: 2} 

3 Comments

I like this. Because if you are piping, it gets difficult with the for loop.
Just note that this does not convert deep/recursive
Also probably better to use String.to_existing_atom/1 (see docs for String.to_existing_atom/1)
26

You can use a combination of Enum.reduce/3 and String.to_atom/1

%{"foo" => "bar"} |> Enum.reduce(%{}, fn {key, val}, acc -> Map.put(acc, String.to_atom(key), val) end) %{foo: "bar"} 

However you should be wary of converting to atoms based in user input as they will not be garbage collected which can lead to a memory leak. See this issue.

You can use String.to_existing_atom/1 to prevent this if the atom already exists.

3 Comments

Yeah I know about memory leak,same thing was in ruby before 2.2 but thank you anyway
Yes, it was the same thing in Ruby but Elixir is not Ruby and in general this pattern is extremely discouraged in Elixir. The only case I can think it makes sense on top of my mind is when loading data into structs (and then there are safer ways to do it)
@JoséValim what's the safer way to load data into structs?
14

Snippet below converts keys of nested json-like map to existing atoms:

iex(2)> keys_to_atoms(%{"a" => %{"b" => [%{"c" => "d"}]}}) %{a: %{b: [%{c: "d"}]}} 
 def keys_to_atoms(json) when is_map(json) do Map.new(json, &reduce_keys_to_atoms/1) end def reduce_keys_to_atoms({key, val}) when is_map(val), do: {String.to_existing_atom(key), keys_to_atoms(val)} def reduce_keys_to_atoms({key, val}) when is_list(val), do: {String.to_existing_atom(key), Enum.map(val, &keys_to_atoms(&1))} def reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val} 

1 Comment

In my opinion, this one is written is the best.
9

To build on @emaillenin's answer, you can check to see if the keys are already atoms, to avoid the ArgumentError that is raised by String.to_atom when it gets a key that is already an atom.

for {key, val} <- string_key_map, into: %{} do cond do is_atom(key) -> {key, val} true -> {String.to_atom(key), val} end end 

Comments

8

You can use the Jason library.

michalmuskala/jason

%{ "key" => "1", "array" => [%{"key" => "1"}], "inner_map" => %{"another_inner_map" => %{"key" => 100}} } |> Jason.encode!() |> Jason.decode!(keys: :atoms) %{array: [%{key: "1"}], inner_map: %{another_inner_map: %{key: 100}}, key: "1"} 

1 Comment

Very nice. This approach can be modified, from atoms to atoms!, which switches it to String.to_existing_atom/1 and avoids leaking atoms. Documentation: hexdocs.pm/jason/Jason.html#decode/2
5

There's a library for this, https://hex.pm/packages/morphix. It also has a recursive function for embedded keys.

Most of the work is done in this function:

defp atomog(map) do atomkeys = fn {k, v}, acc -> Map.put_new(acc, atomize_binary(k), v) end Enum.reduce(map, %{}, atomkeys) end defp atomize_binary(value) do if is_binary(value), do: String.to_atom(value), else: value end 

Which is called recursively. After reading @Galzer's answer I'll probably convert this to use String.to_existing_atom soon.

2 Comments

How is this recursive? atomog(%{"a" => %{"b" => 2}}) returns %{a: %{"b" => 2}}
the atomog function is called as part of a recursive function, it is not itself recursive. Check the morphix code itself for more detail.
4

First of all, @Olshansk's answer worked like a charm for me. Thank you for that.

Next, since the initial implementation provided by @Olshansk was lacking support for list of maps, below is my code snippet extending that.

def keys_to_atoms(string_key_map) when is_map(string_key_map) do for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)} end def keys_to_atoms(string_key_list) when is_list(string_key_list) do string_key_list |> Enum.map(&keys_to_atoms/1) end def keys_to_atoms(value), do: value 

This the sample I used, followed by the output after passing it to the above function - keys_to_atoms(attrs)

# Input %{ "school" => "School of Athens", "students" => [ %{ "name" => "Plato", "subjects" => [%{"name" => "Politics"}, %{"name" => "Virtues"}] }, %{ "name" => "Aristotle", "subjects" => [%{"name" => "Virtues"}, %{"name" => "Metaphysics"}] } ] } # Output %{ school: "School of Athens", students: [ %{name: "Plato", subjects: [%{name: "Politics"}, %{name: "Virtues"}]}, %{name: "Aristotle", subjects: [%{name: "Virtues"}, %{name: "Metaphysics"}]} ] } 

The explanation for this is very simple. The first method is the heart of everything which is invoked for the input of the type map. The for loop destructures the attributes in key-value pairs and returns the atom representation of the key. Next, while returning the value, there are three possibilities again.

  1. The value is yet another map.
  2. The value is a list of maps.
  3. The value is none of the above, it's primitive.

So this time, when the keys_to_atoms method is invoked while assigning value, it may invoke one of the three methods based on the type of input. The methods are organized in the snippet in a similar order.

Hope this helps. Cheers!

Comments

4

Here's a version of @emaillenin's answer in module form:

defmodule App.Utils do # Implementation based on: http://stackoverflow.com/a/31990445/175830 def map_keys_to_atoms(map) do for {key, val} <- map, into: %{}, do: {String.to_atom(key), val} end def map_keys_to_strings(map) do for {key, val} <- map, into: %{}, do: {Atom.to_string(key), val} end end 

Comments

2
defmodule Service.MiscScripts do @doc """ Changes String Map to Map of Atoms e.g. %{"c"=> "d", "x" => %{"yy" => "zz"}} to %{c: "d", x: %{yy: "zz"}}, i.e changes even the nested maps. """ def convert_to_atom_map(map), do: to_atom_map(map) defp to_atom_map(map) when is_map(map), do: Map.new(map, fn {k, v} -> {String.to_atom(k), to_atom_map(v)} end) defp to_atom_map(v), do: v end 

1 Comment

Perfect is you're dealing with nested maps (aka recursion)
1
m = %{"key" => "value", "another_key" => "another_value"} k = Map.keys(m) |> Enum.map(&String.to_atom(&1)) v = Map.values(m) result = Enum.zip(k, v) |> Enum.into(%{}) 

Comments

1

I really liked Roman Bedichevskii's answer ... but I needed something that will thoroughly atomize the keys of deeply nested yaml files. This is what I came up with:

@doc """ Safe version, will only atomize to an existing key """ def atomize_keys(map) when is_map(map), do: Map.new(map, &atomize_keys/1) def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys/1) def atomize_keys({key, val}) when is_binary(key), do: atomize_keys({String.to_existing_atom(key), val}) def atomize_keys({key, val}), do: {key, atomize_keys(val)} def atomize_keys(term), do: term @doc """ Unsafe version, will atomize all string keys """ def unsafe_atomize_keys(map) when is_map(map), do: Map.new(map, &unsafe_atomize_keys/1) def unsafe_atomize_keys(list) when is_list(list), do: Enum.map(list, &unsafe_atomize_keys/1) def unsafe_atomize_keys({key, val}) when is_binary(key), do: unsafe_atomize_keys({String.to_atom(key), val}) def unsafe_atomize_keys({key, val}), do: {key, unsafe_atomize_keys(val)} def unsafe_atomize_keys(term), do: term 

It's main limitation is that if you feed it a tuple {key, value} and the key is a binary, it will atomize it. That is something you want for keyword lists, but it is probably someone's edge case. In any case, YAML and JSON files don't have a concept of a tuple, so for processing those, it won't matter.

Comments

1

Here is what I use to recursively (1) format map keys as snakecase and (2) convert them to atoms. Keep in mind that you should never convert non-whitelisted user data to atoms as they are not garbage collected.

defp snake_case_map(map) when is_map(map) do Enum.reduce(map, %{}, fn {key, value}, result -> Map.put(result, String.to_atom(Macro.underscore(key)), snake_case_map(value)) end) end defp snake_case_map(list) when is_list(list), do: Enum.map(list, &snake_case_map/1) defp snake_case_map(value), do: value 

Comments

1

I like to use Enum.into/3 so that I can easily choose between Map, Keyword or any other Collectable

%{"foo" => "bar"} |> Enum.into(Map.new(), fn {k, v} -> {String.to_atom(k), v} end) %{foo: "bar"} 
%{"foo" => "bar"} |> Enum.into(Keyword.new(), fn {k, v} -> {String.to_atom(k), v} end) [foo: "bar"] 

Comments

0

when you have a map inside another map

def keys_to_atom(map) do Map.new( map, fn {k, v} -> v2 = cond do is_map(v) -> keys_to_atom(v) v in [[nil], nil] -> nil is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end) true -> v end {String.to_atom("#{k}"), v2} end ) end 

sample:

my_map = %{"a" => "1", "b" => [%{"b1" => "1"}], "c" => %{"d" => "4"}} 

result

%{a: "1", b: [%{b1: "1"}], c: %{d: "4"}} 

note: the is_list will fail when you have "b" => [1,2,3] so you can comment/remove this line if this is the case:

# is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end) 

Comments

0

We found ourselves doing this a lot in various Elixir/Phoenix projects ...
so we created a tested+documented utility function Useful.atomize_map_keys/1
that considers all the answers in this thread.

Install by adding this line to the deps your `mix.exs:

{:useful, "~> 1.0.8"}, 

Then use:

my_map = %{"name" => "Alex", "age": 17} Useful.atomize_map_keys(my_map) %{name: "Alex", age: 17} 

We find this a lot clearer when reading code and allows for pipes e.g:

MyApp.fetch_json_data() |> Jason.decode() |> Useful.atomize_map_keys() 

1 Comment

You can use |> Jason.decode(keys: &String.to_atom/1) that will do that for you while decoding (therefore saving memory and processing time)
0

This answer only applies to Ecto, but I hope it saves someone some of the time I lost.

I was doing something complicated with a form for a record with children (nested properties). I had managed to remove some calls to changeset() while smashing things with a hammer, and that led to the forms using string keys while the tests used atoms. Once I restored the changeset() calls in the Repo code, then both paths were happy.

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.