I recently had to deal with camelCase-formated JSON while working on REST
payloads. I am using an embedded schema and Ecto.Changeset
to standardize
the params map into an internal struct. However, I prefer to use the language
snake_case convention for fields.
At first, I added the following three lines to the controller; naively
converting keys to snake_case using Macro.underscore/1
.
iex(1)> params = %{"email" => "john.doe@email.com", "firstName" => "John", "lastName" => "Doe"}
iex(2)> Enum.reduce(params, %{}, fn {key, value}, acc ->
...(2)> Map.put(acc, Macro.underscore(key), value)
...(2)> end)
%{"email" => "john.doe@email.com", "first_name" => "John", "last_name" => "Doe"}
This pattern is so common, there had to be a better way… I knew Elixir has
special forms of for
to deal with Enumerable
, but I’m not using for
enough so I went back to the documentation:
Getting Started > Comprehensions
[…] However, the result of a comprehension can be inserted into different data structures by passing the
:into
option to the comprehension.
⮑ https://elixir-lang.org/getting-started/comprehensions.html#the-into-option
The previous code block could be expressed in a single line!
iex(3)> for {key, value} <- params, into: %{}, do: {Macro.underscore(key), value}
%{"email" => "john.doe@email.com", "first_name" => "John", "last_name" => "Doe"}
It works, but only for map with a single level of keys: values
! Let’s add
recursion on values
so we can apply the same logic recursively to nested maps.
And extract our code to a Plug
so it can be included in a router pipeline
or directly at the controller level.
defmodule Foo.Plugs.SnakeCaseParams do
def call(%{params: params} = conn, _opts) do
%{conn | params: convert(params)}
end
defp convert(params) when is_map(params) do
for {key, value} <- params, into: %{}, do: {Macro.underscore(key), convert(value)}
end
defp convert(params) when is_list(params) do
for value <- params, do: convert(value)
end
defp convert(params), do: params
end
It’s as simple as that 🤓