How to convert map keys from strings to atoms in Elixir

Elixir

Elixir Problem Overview


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

Elixir Solutions


Solution 1 - Elixir

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

Solution 2 - Elixir

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}

Solution 3 - Elixir

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.

Solution 4 - Elixir

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}

Solution 5 - Elixir

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

Solution 6 - Elixir

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.

Solution 7 - Elixir

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

Solution 8 - Elixir

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!

Solution 9 - Elixir

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

Solution 10 - Elixir

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

Solution 11 - Elixir

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(%{})

Solution 12 - Elixir

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

Solution 13 - Elixir

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.

Solution 14 - Elixir

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)

Solution 15 - Elixir

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNoDisplayNameView Question on Stackoverflow
Solution 1 - ElixirLenin Raj RajasekaranView Answer on Stackoverflow
Solution 2 - ElixirDebajitView Answer on Stackoverflow
Solution 3 - ElixirGazlerView Answer on Stackoverflow
Solution 4 - ElixirRoman BedichevskiiView Answer on Stackoverflow
Solution 5 - ElixirmomoView Answer on Stackoverflow
Solution 6 - ElixirphilosodadView Answer on Stackoverflow
Solution 7 - ElixirJason AxelsonView Answer on Stackoverflow
Solution 8 - ElixirAkshay RathodView Answer on Stackoverflow
Solution 9 - ElixirZubair NabiView Answer on Stackoverflow
Solution 10 - ElixirRodrigo Zampieri CastilhoView Answer on Stackoverflow
Solution 11 - ElixirChris AlmeidaView Answer on Stackoverflow
Solution 12 - ElixirBetreeView Answer on Stackoverflow
Solution 13 - ElixirHo-Sheng HsiaoView Answer on Stackoverflow
Solution 14 - ElixirrharariView Answer on Stackoverflow
Solution 15 - ElixirsdcView Answer on Stackoverflow