Converting a snake_case map keys to camelCase is a pretty common task in the snake-case-style languages working with the JavaScript frontend. Here are the basics in understanding how you can convert maps to camelCase style in Elixir.
Imagine we have a similar map:
data = %{
data: %{
id: 1,
type: "documents",
attributes: %{
author: %{
first_name: "Jo",
last_name: "Nesbo"
}
}
}
}
And we want to endup with the JSON like (let’s ignore the actual JSON conversion ala Jason.encode
):
{
"data": {
"id": "1",
"type": "documents",
"attributes": {
"author": {
"firstName": "Jo",
"lastName": "Nesbo"
}
}
}
}
There are basically three things we need to take into account:
- converting anything (string) to camelCase
- converting an Elixir map keys to camelCase
- finally deeply converting an Elixir map to camelCase
How do we do that?
For converting a simple string on its own we there is a macro camelize
that works with strings and atoms:
Macro.camelize("foo_bar")
Unfortunatelly it capitalizes the first character and does not support Unicode. From the docs:
This function was designed to camelize language identifiers/tokens, that’s why it belongs to the
Macro
module. Do not use it as a general mechanism for camelizing strings as it does not support Unicode or characters that are not valid in Elixir identifiers.
Luckily there is Inflex library that can camelize strings (among other useful things):
iex(3)> Inflex.camelize("first_name", :lower)
"firstName"
As you can see, it does have a :lower
option which is exactly what we want.
The implementation is interesting too as it’s actually very short:
defmodule Inflex.Camelize do
@moduledoc false
def camelize(word, option \\ :upper) do
case Regex.split(~r/(?:^|[-_])|(?=[A-Z])/, to_string(word)) do
words ->
words
|> Enum.filter(&(&1 != ""))
|> camelize_list(option)
|> Enum.join()
end
end
defp camelize_list([], _), do: []
defp camelize_list([h | tail], :lower) do
[lowercase(h)] ++ camelize_list(tail, :upper)
end
defp camelize_list([h | tail], :upper) do
[capitalize(h)] ++ camelize_list(tail, :upper)
end
def capitalize(word), do: String.capitalize(word)
def lowercase(word), do: String.downcase(word)
end
It converts the string to list of words and uses the [head | tail]
convention to support the :lower
option. Then capitalizes every word and joins them.
Next is to actually know how to do anything with keys and theirs values. In Elixir we can use for
:
for {key, val} <- map, into: %{} do
{key, val}
end
map
here would be our data
variable. We are returning the same map.
Let’s try to add Inflex in the mix:
for {key, val} <- map, into: %{} do
{Inflex.camelize(key, :lower), val}
end
This would indeed camelize the keys, but left out all the values including our nested maps.
To that end we need to also deeply convert our values:
defp camel_cased_map_keys(map) when is_map(map) do
for {key, val} <- map, into: %{} do
{Inflex.camelize(key, :lower), camel_cased_map_keys(val)}
end
end
defp camel_cased_map_keys(val), do: val
# and then
data |> camel_cased_map_keys()
If the value is another map, we recursively call this function again. If not, we keep the value intact by leveraging pattern matching.
This looks good unless we realize that data and datetime structs are maps too! Luckily we can pattern match on these structs and skip them as well:
defp camel_cased_map_keys(%Date{} = val), do: val
defp camel_cased_map_keys(%DateTime{} = val), do: val
defp camel_cased_map_keys(%NaiveDateTime{} = val), do: val
defp camel_cased_map_keys(map) when is_map(map) do
for {key, val} <- map, into: %{} do
{Inflex.camelize(key, :lower), camel_cased_map_keys(val)}
end
end
defp camel_cased_map_keys(val), do: val
In some projects we might want to add a case for Ecto.DateTime
or Timex.Ecto.DateTime
as well.
If you stuble upon Caramelize library, note that it uses the above mentioned Macro.camelize
underneath and so it does not support Unicode.
If you need to be automatically changing these keys back and forth for API requests, you should do a Plug-based approach. Look at Accent plug for inspiration. And if you are using JSON:API spec I added support for camelCase to ja_serializer.