Transforming list of structs to map

Hi,
Is there an easier way to convert a list of structs to map. Extracting fields from the struct and in the nested struct and make it a map.

The list of struct:

[
  %Kaiwa.Chat.Message{
    __meta__: #Ecto.Schema.Metadata<:loaded, "messages">,
    id: 1,
    inserted_at: ~N[2020-04-03 11:46:24],
    room: #Ecto.Association.NotLoaded<association :room is not loaded>,
    room_id: 1,
    text: "this is a test message",
    updated_at: ~N[2020-04-03 11:46:24],
    user: %Kaiwa.Accounts.User{
      __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
      id: 1,
      inserted_at: ~N[2020-04-02 04:10:00],
      messages: #Ecto.Association.NotLoaded<association :messages is not loaded>,
      name: "Mr Testing",
      password: nil,
      password_hash: "$argon2id$v=19$m=131072,t=8,p=4$26a9JgOy+rmuhjQ7WZD1bg$ZJe55e9UdfrWqm0E4rMnGFG7mA/Wyr7fWdi1f1tZP+8",
      rooms: #Ecto.Association.NotLoaded<association :rooms is not loaded>,
      updated_at: ~N[2020-04-02 04:10:00],
      username: "test" 
    },
    user_id: 1
  }
] 

My attempt:

Enum.map(messages, fn message ->
  user = Map.from_struct(message.user)

  %{
    message_id: message.id,
    text: message.text,
    created_at: message.inserted_at,
    user_id: user.id,
    name: user.name
  }
end)

Expected result:

[
  %{
    created_at: ~N[2020-04-03 11:46:24],
    message_id: 1,
    name: "Mr Testing",
    text: "this is a test message", 
    user_id: 1
  }
]

Is there a better way to achieve the expected result or my attempt is okay?

2 Likes

I think your version is ok

This is going to include a lot of keys you don’t want like __meta__ and password_hash (very bad). If you’re trying to create a JSON response for an API I recommend just explicitly making maps for each of the things you want to expose.

1 Like

Thank you for the advised.

I’m wondering if there’s an easy way to picking out the values you needed in a struct directly because I just convert the struct to map so I can easily access the fields I want to encode.

user_map = Map.take(user, [:name, :id, :username])
3 Likes

Thank you.

I would be more comfortable with something that asserts the shape of the data it receives so I can catch bugs early:

    Enum.map(
      messages,
      fn %{
           id: message_id,
           text: text,
           inserted_at: created_at,
           user: %{id: user_id, name: name}
         } ->
        %{
          message_id: message_id,
          text: text,
          created_at: created_at,
          user_id: user_id,
          name: name
        }
      end
    )

If even one message doesn’t comply with the expected structure you’ll get an appropriate error. Although to be fair, your code would also blow up but IMO in a slightly less intuitive fashion. I’d prefer mine because it also makes intent clearer.

1 Like