Let's assume I have a struct defined as it follows:
defmodule User do
defstruct [:id, :name]
@type t :: %__MODULE__{id: integer, name: String.t}
end
Keys are atoms, as it seems to be the most common approach. I need to create a new User struct from a payload I receive from an API. Since the payload is in JSON, it is serialized into a map where the keys are strings and not atoms. When I use struct/2 to create the struct, this is the result:
iex> struct(User, %{"id" => 1, "name" => "weppos"})
%User{id: nil, name: nil}
Indeed, it works if the map keys are atoms:
iex> struct(User, %{id: 1, name: "weppos"})
%User{id: 1, name: "weppos"}
iex> struct(User, %{id: 1, name: "weppos", foo: "bar"})
%User{id: 1, name: "weppos"}
My immediate reaction was to convert the keys from string to atoms. However, given how this affects performances in the Ruby world, I assumed it could be a similar issues and I started digging deeper. I found
this question where José wrote:
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)
Unfortunately, I was not able to find any extra reference. However, it seems my case matched exactly what José was talking about.
Hence I wrote a little method that tries to emulate struct/2, with the following goals in mind:
- take a map %{String.t => any} and a Struct name (where fields are defined with atoms) as inputs
- compare the fields in the Struct with the keys in the map, and discard the unknown fields (to avoid leaking unnecessary atoms)
- create an "instance" of the Struct and set the proper fields
Here's a stub:
def to_struct(kw, struct) do
res = struct(struct)
Map.keys(res)
|> Enum.filter(fn(x) -> Map.has_key?(kw, to_string(x)) end)
|> Enum.reduce(res, fn(x, acc) -> Map.put(acc, x, kw[to_string(x)]) end)
end
It works™, but it seems convoluted. I wonder, is there a better way to achieve this? Moreover, does it make sense to enhance struct/2 to be able to pass an option to convert the keys?
Thanks,
-- Simone