How to get struct from map - elixir?

ecto
maps
structs
phoenix

#1

Lets say i have map like this fetching from my database

%{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => "XXXX@gmail.com", "name" => "Hariharasudhan", "password_hash" => "XXXX", "phone" => "XXXX"}

and i have struct like follow
> @primary_key{:id, :binary_id, autogenerate: true}
schema “users” do
field :name, :string
field :email, :string
field :phone, :string
field :password_hash, :string
end’

what is best way to convert Map to User Struct,

note:
i need to do this convertion on each request after authentication , so i need effective way to achieve this?


#2

That’s what https://hexdocs.pm/elixir/Kernel.html#struct/2 does.


#3

@manukall If i try struct(%User{}, %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => "XXXX@gmail.com", "name" => "Hariharasudhan", "password_hash" => "XXXX", "phone" => "XXXX"} ) am getting output like

%User{meta: #Ecto.Schema.Metadata<:built, “users”>, countryCode: nil, email: nil, id: nil, name: nil, password_hash: nil, phone: nil}

it doesn’t set any value from map to struct


#4

Keys from the map must be atoms, as only atoms are allowed in struct definition.


#5

@matteosister yes, but map returned from db has string keys, Is there is any way to conevrt??


#7

I use this code.

with original version coming from:
https://groups.google.com/forum/#!msg/elixir-lang-talk/6geXOLUeIpI/L9einu4EEAAJ


#8

Wow, I’m pretty surprised that this isn’t built in, or at least available as part of Phoenix since incoming json data is not exactly an unexpected occurence in that context. Sure, it’s simple to manually add, and I understand the point about using strings instead of atoms for external data. Still, from an ergonomic point of view it’s super weird because it forces you to repeat your attributes (and makes refactoring harder).

Or maybe I’m missing something? I’m still very much a newb when it comes to Elixir.


#9

Poison let’s you do this ergonomically:

Poison.decode!(~s({"name": "Devin Torres", "age": 27}), as: %Person{})

#10

Many thanks! :pray:t2:

I knew I was missing something.


#11

Ah, as it turns out that won’t work for my purposes. I’m not trying to grab the values from a literal and when if I use inspect to get a string dump of the map, it’s obviously not going to come out as proper JSON. Ah well. I’ll write a clever macro at some point, I reckon.


#12

Untested, but it seems like cast and apply_changes should do what you want:

%User{} |> Ecto.Changeset.cast(map, User.__schema__(:fields)) |> Ecto.Changeset.apply_changes()


#13

I cannot figure it out which solution is better, the one presented or the one José Valim created? Seems they are doing slightly different things.


#14

Or you could use the Morphix library.


#15

This decode as stuff is pretty cool.
So why is it not part of Phoenix? (or am I missing something)
This won’t work:

defmodule Stuff do
  defstruct foo: nil, bar: nil
end

def handle_in("send_stuff", %Stuff{} = my_stuff, socket), do: something_with(my_stuff)

The incoming json is already decoded, so I would have to do something like:

def handle_in("send_stuff", %{} = my_stuff, socket) do
  my_stuff
  |> Poison.encode
  |> (fn ({:ok, json}) -> Poison.decode(json, as: %Stuff{}) end).()
  |> something_with
end

My solution is to not use structs, which I don’t like, I would love to use structs.
%Stuff{} = my_stuff is a lot more concise than %{"foo" => foo, "bar" => bar}

In case of many parameters, the function definitions become unreadable and ugly,
which is not an enjoyable kind of pattern matching.

Am I missing something?


#16

The decode as stuff will not compile in conjunction with @enforce_keys.
Probably this is related to the absence of the %Stuff{} = my_stuff feature.


#17

At the risk of stating the obvious

defmodule Stuff do
  defstruct foo: nil, bar: nil

  def from_map(%{"foo" => foo, "bar" => bar}),
    do: %Stuff{foo: foo, bar: bar}

  # alternatively - return some error
  def from_map(m),
    do: m

  #...

end

# ...

def handle_in("send_stuff", stuff, socket) do
  with %Stuff{} = my_stuff <- Stuff.from_map(stuff) do
    something_with(my_stuff)

  else
     # do something else with "stuff" argument ...

  end
end

# ... or

def handle_in("send_stuff", stuff, socket) do
  case Stuff.from_map(stuff) do
    %Stuff{} = my_stuff ->
      something_with(my_stuff)

    # other options ...
    _ ->
       # ...

  end
end

Kernel.with/1


#18

Tnx for your suggestions, much appreciated.
I’m trying to avoid boilerplate code in struct definitions and
if/else/with/case statements in functions (I prefer pattern matching).

I am thinking about something like this, using a helper function:

def to_struct(%{} = params, kind) do
  params
  |> Poison.encode
  |> (fn {:ok, json} -> json end).()
  |> Poison.decode(%{as: kind, keys: :atoms!})
  |> (fn {:ok, decoded} -> decoded end).()
end

def handle_in("send_stuff", %{} = m, socket) do
  m |> to_struct(%Stuff{}) |> something_with
end

defp something_with(%Stuff{} = stuff), do: ...

Or maybe I should just skip decoding/encoding and do something like this:

// javascript
let data = { "json": JSON.stringify(actual_stuff) };
// push...

// elixir
def handle_in("send_stuff", %{"json" => json}, socket) do
  json |> Poison.decode(%{as: %Stuff{}, keys: :atoms!}) |> something_with
end

Any thoughts?


#19

Not sure you want to, but you could also use an ecto embedded schema definition, which would allow you to just cast the json into a “structure” (including type conversion)


#20

Surely ecto provides some very nice functionality that could be used in this context.
Also I would never persist anything without using ecto, embedded or otherwise.

I was thinking about defining embedded modules with structs inside a module that uses Phoenix.Channel.
The structs would tell me about the kind of stuff coming in and the function definitions would be short.
So it’s more about clarity than anything else.


#21

I must be missing something - why does Poison have get involved in to_struct? I suspect it’s because you really want to use the Poison.Decode.transform/2 functionality.

Given that the transform process relies on empty structs (possibly nested) I’d be tempted to just code the necessary boilerplate conversions without Poison - that code can go into a separate module from the struct definition. If there is an opportunity to eliminate the boilerplate in the future, great - otherwise :man_shrugging: and move on …