Creating a struct from a map with string keys

5,723 views
Skip to first unread message

Simone Carletti

unread,
Jan 13, 2016, 5:11:27 AM1/13/16
to elixir-lang-talk
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

José Valim

unread,
Jan 13, 2016, 5:19:03 AM1/13/16
to elixir-l...@googlegroups.com
That's pretty much the way to go except you can do everything in one pass:


    def to_struct(kind, attrs) do
      struct = struct(kind)
      Enum.reduce Map.to_list(struct), struct, fn {k, _}, acc ->
        case Map.fetch(attrs, Atom.to_string(k)) do
          {:ok, v} -> %{acc | k => v}
          :error -> acc
        end
      end
    end

On line 5 I used Elixir's v1.2 k => v syntax for updates but you can use Map.put/3 if you want to support earlier versions.



José Valim
Skype: jv.ptec
Founder and Director of R&D

--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/28ba8289-dbcd-4242-a601-d69b30f0ecd7%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Simone Carletti

unread,
Jan 13, 2016, 5:44:27 AM1/13/16
to elixir-lang-talk, jose....@plataformatec.com.br
Thanks for the swift response José.

For the sake of my curiosity, how do you feel about the proposal of enhancing struct/2? I know that as a language designer it's important to resist the temptation to introduce a lot of features that may be beneficial just in a few cases, but I have a feeling it may be a common issue.
What's your opinion?

-- Simone

Alexei Sholik

unread,
Jan 13, 2016, 6:02:04 AM1/13/16
to elixir-l...@googlegroups.com
Depending on your use case, you may want to error on a key that is not present in the struct instead of ignoring it. Or your JSON keys may not map directly to the field names in the struct, in which case instead of using String.to_atom you'd need to match on each string individually and return a corresponding atom for it.

Elixir makes a good point by forcing the user to make an explicit decision. If the language made making that decision for the user by providing some builtin option, I wouldn't call that an enhancement.

Rickard Andersson

unread,
Jan 13, 2016, 2:24:00 PM1/13/16
to elixir-l...@googlegroups.com
Is there any reason you wouldn't simply make functions for the data you
do want to convert explicitly?

def to_user(%{"id" => id, "name" => name}) do
%User{id: id, name: name}
end

Is it really advisable to run through all your data and convert to
structs if the above is too hard to write for the data you do have?

It's assertive in that it assures that what you want is in the data,
specific enough in that you're not making massive generalizations and
running everything through it, meaning you'll touch lots of parts with
the code and I don't see how it's wasteful in the least.

// Rickard.
> >> *José Valim*
> >>> <http://stackoverflow.com/questions/31990134/how-to-convert-map-keys-from-strings-to-atoms-in-elixir> where
> >>> <https://groups.google.com/d/msgid/elixir-lang-talk/28ba8289-dbcd-4242-a601-d69b30f0ecd7%40googlegroups.com?utm_medium=email&utm_source=footer>
> >>> .
> >>> For more options, visit https://groups.google.com/d/optout.
> >>>
> >>
> >> --
> > You received this message because you are subscribed to the Google Groups
> > "elixir-lang-talk" group.
> > To unsubscribe from this group and stop receiving emails from it, send an
> > email to elixir-lang-ta...@googlegroups.com.
> > To view this discussion on the web visit
> > https://groups.google.com/d/msgid/elixir-lang-talk/5c514446-b71c-40d5-89ec-5f8228054c1d%40googlegroups.com
> > <https://groups.google.com/d/msgid/elixir-lang-talk/5c514446-b71c-40d5-89ec-5f8228054c1d%40googlegroups.com?utm_medium=email&utm_source=footer>
> > .
> >
> > For more options, visit https://groups.google.com/d/optout.
> >
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/CAAPY6ePptcw0YRQVS8Mv1UU2qWK-MivBryETt-zC72G8nV0dxw%40mail.gmail.com.

Redvers Davies

unread,
Jan 15, 2016, 10:38:36 AM1/15/16
to elixir-l...@googlegroups.com
Rickard,

Explicit is always better than implicit so I'm right there with you.
Unfortunately in my world I fairly often come across APIs in which
most fields in each record are optional and when there's no data they
just don't put in that field.

That means that post-JSX the only way to really deal with that data
explicitly is to do many, many calls to "Map and friends"[tm] to
extract and assemble the struct. That's hugely verbose which isn't a
bad thing as it's self documenting.

In other cases I'm set the struct with my default values for any field
that's not included and then used struct/2.

I'm reminded of the statement I heard Joe make about OTP being a
framework and that you shouldn't be scared to create your own gen_x
modules. The provided ones are not the "final answer"[tm].



Red
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-talk/20160113192257.GA21667%40omniknight.
Reply all
Reply to author
Forward
0 new messages