Skip to content

Instantly share code, notes, and snippets.

@kipcole9
Last active October 24, 2023 22:13
Show Gist options
  • Save kipcole9/0bd4c6fb6109bfec9955f785087f53fb to your computer and use it in GitHub Desktop.
Save kipcole9/0bd4c6fb6109bfec9955f785087f53fb to your computer and use it in GitHub Desktop.
Helpers for Elixir Maps: underscore, atomise and stringify map keys
defmodule Map.Helpers do
@moduledoc """
Functions to transform maps
"""
@doc """
Convert map string camelCase keys to underscore_keys
"""
def underscore_keys(nil), do: nil
def underscore_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {Macro.underscore(k), underscore_keys(v)} end)
|> Enum.map(fn {k, v} -> {String.replace(k, "-", "_"), v} end)
|> Enum.into(%{})
end
# Walk the list and atomize the keys of
# of any map members
def underscore_keys([head | rest]) do
[underscore_keys(head) | underscore_keys(rest)]
end
def underscore_keys(not_a_map) do
not_a_map
end
@doc """
Convert map string keys to :atom keys
"""
def atomize_keys(nil), do: nil
# Structs don't do enumerable and anyway the keys are already
# atoms
def atomize_keys(struct = %{__struct__: _}) do
struct
end
def atomize_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {String.to_atom(k), atomize_keys(v)} end)
|> Enum.into(%{})
end
# Walk the list and atomize the keys of
# of any map members
def atomize_keys([head | rest]) do
[atomize_keys(head) | atomize_keys(rest)]
end
def atomize_keys(not_a_map) do
not_a_map
end
@doc """
Convert map atom keys to strings
"""
def stringify_keys(nil), do: nil
def stringify_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {Atom.to_string(k), stringify_keys(v)} end)
|> Enum.into(%{})
end
# Walk the list and stringify the keys of
# of any map members
def stringify_keys([head | rest]) do
[stringify_keys(head) | stringify_keys(rest)]
end
def stringify_keys(not_a_map) do
not_a_map
end
@doc """
Deep merge two maps
"""
def deep_merge(left, right) do
Map.merge(left, right, &deep_resolve/3)
end
# Key exists in both maps, and both values are maps as well.
# These can be merged recursively.
defp deep_resolve(_key, left = %{}, right = %{}) do
deep_merge(left, right)
end
# Key exists in both maps, but at least one of the values is
# NOT a map. We fall back to standard merge behavior, preferring
# the value on the right.
defp deep_resolve(_key, _left, right) do
right
end
end
@ukchukx
Copy link

ukchukx commented Jun 8, 2017

Hello @kipcole9, your key atomization implementation was far more elegant than mine. Thanks for providing some code for me to learn from.
There's a minor bug:
ArgumentError is thrown when String.to_atom/1 is called with an atom key.
We need to add a clause to check for that.

def atomize_keys(map = %{}) do
    map
    |> Enum.map(fn 
        {k, v} when is_atom(k) -> {k, atomize_keys(v)} 
        {k, v} -> {String.to_atom(k), atomize_keys(v)} 
    end)
    |> Enum.into(%{})
  end

Thanks.

@Abica
Copy link

Abica commented Nov 1, 2017

Nice work. The only change I made to this was to allow stringify_keys to work when the key is already a string. I did this with the following changes:

  def stringify_keys(map = %{}) do
    map
    |> Enum.map(fn {k, v} -> {stringify_key(k), stringify_keys(v)} end)
    |> Enum.into(%{})
  end

  defp stringify_key(key) when is_atom(key), do: Atom.to_string(key)
  defp stringify_key(key), do: key

@pedroassumpcao
Copy link

Thanks @kipcole9 and @Abica!

@rhnonose
Copy link

rhnonose commented Jan 2, 2018

I added extra clausules for stringify_keys to ignore ecto schemas and structs:
def stringify_keys(schema = %{__meta__: _}), do: schema
def stringify_keys(struct = %{struct: _}), do: struct

@mkaplan9
Copy link

helpful resource, thanks

@moxley
Copy link

moxley commented Oct 5, 2018

Thanks for the code! By replacing Atom.to_string(k) with to_string(k), you'll cover cases where some keys are already strings. to_string/1 is part of the Kernel module.

@moxley
Copy link

moxley commented Oct 5, 2018

Just want to point out as well, it's generally a bad idea to blindly convert strings to atoms. It's "extremely discouraged", according to José Valim (creator of Elixir). This is because atoms are not garbage collected, and there's the risk of the VM crashing because of increasing number of atoms generated.

...in general this pattern is extremely discouraged in Elixir... – José Valim
https://stackoverflow.com/questions/31990134/how-to-convert-map-keys-from-strings-to-atoms-in-elixir

@mmendez512
Copy link

You can also use String.to_existing_atom\1 to prevent atom size from reaching the limit. If the key is not an existing atom then use the String.to_atom first (otherwise it'll throw an Argument Exception). Here's a simple function to handle both cases:

def safe_to_atom(k) do try do String.to_existing_atom(k) rescue ArgumentError -> String.to_atom(k) end end

@mmendez512
Copy link

But you still want to make sure the data you're converting comes from a trusted source

@onixus74
Copy link

Thank you!

@jeremyjh
Copy link

jeremyjh commented Dec 2, 2019

@mmendez512 the whole point of String.to_existing_atom is for when you cannot not trust the input data, such as when it is sent from a web browser. Catching the exception and falling back to to_atom is redundant, you may as well just use to_atom when you trust the input data, and you can just let it fail when you do not trust the input data and get non-matching keys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment