Convert a nested struct into a nested map

Hello guys, can you please help turn this map into a pure map. Some of the members of the map are still structs.

%{
  callback_query: nil,
  channel_post: nil,
  chosen_inline_result: nil,
  edited_message: nil,
  inline_query: nil,
  message: %Nadia.Model.Message{
    audio: nil,
    caption: nil,
    channel_chat_created: nil,
    chat: %Nadia.Model.Chat{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      photo: nil,
      title: nil,
      type: "private",
      username: "ChristianTovar"
    },
    contact: nil,
    date: 1562605521,
    delete_chat_photo: nil,
    document: nil,
    edit_date: nil,
    entities: nil,
    forward_date: nil,
    forward_from: nil,
    forward_from_chat: nil,
    from: %Nadia.Model.User{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      username: "ChristianTovar"
    },
    group_chat_created: nil,
    left_chat_member: nil,
    location: nil,
    message_id: 714,
    migrate_from_chat_id: nil,
    migrate_to_chat_id: nil,
    new_chat_member: nil,
    new_chat_photo: [],
    new_chat_title: nil,
    photo: [],
    pinned_message: nil,
    reply_to_message: nil,
    sticker: nil,
    supergroup_chat_created: nil,
    text: "google meets",
    venue: nil,
    video: nil,
    voice: nil
  },
  update_id: 412827321
}

Hello,

Welcome to the forum

This may help you
from_struct(struct)View Source

from_struct(atom() | struct()) :: map()

Converts a struct to map.

It accepts the struct module or a struct itself and simply removes the __struct__ field from the given struct or from a new struct generated from the given module.

Example

defmodule User do
  defstruct [:name]
end

Map.from_struct(User)
#=> %{name: nil}

Map.from_struct(%User{name: "john"})
#=> %{name: "john"}

I actually did that for the outer map. It was a struct, however that function does not turn inner struct into maps…

You need to implement the nesting on your own. But that is a lot more expensive, as it needs to reconstruct the maps from scratch

1 Like

Wouldn’t s/he be able to use

variable_with_struct
|> Enum.each
|> Map.from_struct

Like this?
https://hexdocs.pm/elixir/Enum.html#each/2

Also if i am mistaken please correct me(still learning to think in elixir and the language)

No, Enum.each/2 returns :ok. Also you need to decide for each key in the map/struct if you want to convert it or not.

1 Like

Here is example working code:

# First of all minimal definition in order to make code compillable
defmodule Nadia.Model.Chat do
  defstruct [:first_name, :id, :last_name, :photo, :title, :type, :username]
end

defmodule Nadia.Model.User do
  defstruct [:first_name, :id, :last_name, :username]
end

defmodule Nadia.Model.Message do
  defstruct [
    :audio,
    :caption,
    :channel_chat_created,
    :chat,
    :contact,
    :date,
    :delete_chat_photo,
    :document,
    :edit_date,
    :entities,
    :forward_date,
    :forward_from,
    :forward_from_chat,
    :from,
    :group_chat_created,
    :left_chat_member,
    :location,
    :message_id,
    :migrate_from_chat_id,
    :migrate_to_chat_id,
    :new_chat_member,
    :new_chat_photo,
    :new_chat_title,
    :photo,
    :pinned_message,
    :reply_to_message,
    :sticker,
    :supergroup_chat_created,
    :text,
    :venue,
    :video,
    :voice
  ]
end

# Here is code which would work only for your specific case
defmodule Example do
  def sample(map) do
    message = ensure_map(map.message)
    chat = ensure_map(message.chat)
    from = ensure_map(message.from)
    updated_message = %{message | chat: chat, from: from}
    %{map | message: updated_message}
  end

  defp ensure_map(%{__struct__: _} = struct), do: Map.from_struct(struct)
  defp ensure_map(data), do: data
end

# Here is more dynamic way
# No matter how many maps and lists have you nested
# It will iterate over all map or list elements and finally ensure that they are not structs
# NOTE: If you want you can add extra guard to limit possible __struct__ value
# For example you probably do not want to create maps from Date, DateTime, NaiveDateTime and Time structs
defmodule ExampleDynamic do
  def sample(map), do: :maps.map(&do_sample/2, map)

  def do_sample(_key, value), do: ensure_nested_map(value)

  defp ensure_nested_map(list) when is_list(list), do: Enum.map(list, &ensure_nested_map/1)

  # NOTE: In pattern-matching order of function guards is important!
  # @structs [Date, DateTime, NaiveDateTime, Time]
  # defp ensure_nested_map(%{__struct__: struct} = data) when struct in @structs, do: data

  defp ensure_nested_map(%{__struct__: _} = struct) do
    map = Map.from_struct(struct)
    :maps.map(&do_sample/2, map)
  end

  defp ensure_nested_map(data), do: data
end

# Your example data which is needed for compilation
data = %{
  callback_query: nil,
  channel_post: nil,
  chosen_inline_result: nil,
  edited_message: nil,
  inline_query: nil,
  message: %Nadia.Model.Message{
    audio: nil,
    caption: nil,
    channel_chat_created: nil,
    chat: %Nadia.Model.Chat{
      first_name: "Christian",
      id: 543_211_234,
      last_name: "Tovar",
      photo: nil,
      title: nil,
      type: "private",
      username: "ChristianTovar"
    },
    contact: nil,
    date: 1_562_605_521,
    delete_chat_photo: nil,
    document: nil,
    edit_date: nil,
    entities: nil,
    forward_date: nil,
    forward_from: nil,
    forward_from_chat: nil,
    from: %Nadia.Model.User{
      first_name: "Christian",
      id: 543_211_234,
      last_name: "Tovar",
      username: "ChristianTovar"
    },
    group_chat_created: nil,
    left_chat_member: nil,
    location: nil,
    message_id: 714,
    migrate_from_chat_id: nil,
    migrate_to_chat_id: nil,
    new_chat_member: nil,
    new_chat_photo: [],
    new_chat_title: nil,
    photo: [],
    pinned_message: nil,
    reply_to_message: nil,
    sticker: nil,
    supergroup_chat_created: nil,
    text: "google meets",
    venue: nil,
    video: nil,
    voice: nil
  },
  update_id: 412_827_321
}

# Here we are ensuring that both ways gives exactly same results
Example.sample(data) == ExampleDynamic.sample(data)

Please let me know if you have any questions.

Related documentation:

  1. &:maps.map/2
  2. &Enum.map/2
  3. &Map.from_struct/1
  4. Guards
  5. Map (describes specific update syntax)
12 Likes

Outstanding! Bravo! Thanks for the help, very complete module indeed.

:wave:

Why do you want to convert these structs to maps? Maybe there’s an easier way to accomplish what you want … Personally, I don’t remember having problems with :nadia's structs.

2 Likes
  def map_from_struct(%_{__meta__: %{__struct__: _}} = schema) when is_map(schema) do
    schema
    |> Map.from_struct()
    |> from_ecto_struct()
  end

  def map_from_struct(map) when is_map(map) do
    map
    |> Enum.reduce(%{}, fn {key, value}, acc ->
      value =
        case value do
          %_{__meta__: %{__struct__: _}} ->
            from_ecto_struct(value)

          value when is_list(value) ->
            value |> Enum.map(&from_ecto_struct/1)

          _ ->
            value
        end

      Map.put_new(acc, key, value)
    end)
  end

We write a lib of utils around ecto https://github.com/annatel/antl_utils_ecto

one of the functionality is
AntlUtilsEcto.map_from_struct(my_schema)

1 Like

The simplest solution that I found is using Poison.

struct = %{
  callback_query: nil,
  channel_post: nil,
  chosen_inline_result: nil,
  edited_message: nil,
  inline_query: nil,
  message: %Nadia.Model.Message{
    audio: nil,
    caption: nil,
    channel_chat_created: nil,
    chat: %Nadia.Model.Chat{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      photo: nil,
      title: nil,
      type: "private",
      username: "ChristianTovar"
    },
    contact: nil,
    date: 1562605521,
    delete_chat_photo: nil,
    document: nil,
    edit_date: nil,
    entities: nil,
    forward_date: nil,
    forward_from: nil,
    forward_from_chat: nil,
    from: %Nadia.Model.User{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      username: "ChristianTovar"
    },
    group_chat_created: nil,
    left_chat_member: nil,
    location: nil,
    message_id: 714,
    migrate_from_chat_id: nil,
    migrate_to_chat_id: nil,
    new_chat_member: nil,
    new_chat_photo: [],
    new_chat_title: nil,
    photo: [],
    pinned_message: nil,
    reply_to_message: nil,
    sticker: nil,
    supergroup_chat_created: nil,
    text: "google meets",
    venue: nil,
    video: nil,
    voice: nil
  },
  update_id: 412827321
}

{:ok, json_string} = struct |> Poison.encode
{:ok, map} = json_string |> Poison.decode
map 

Here is another take on the topic

defmodule MapFromDeepStruct do
  def from_deep_struct(%{} = map), do: convert(map)

  defp convert(data) when is_struct(data) do
    data |> Map.from_struct() |> convert()
  end

  defp convert(data) when is_map(data) do
    for {key, value} <- data, reduce: %{} do
      acc ->
        case key do
          :__meta__ ->
            acc

          other ->
            Map.put(acc, other, convert(value))
        end
    end
  end

  defp convert(other), do: other
end

The tests are here Convert deeply nested Elixir struct into map Ā· GitHub

Another option is to use Nestru. It supports transformation in both ways. Encoding from nested struct into a map can be like the following.

defmodule Nadia.Model.Chat do
  @derive Nestru.Encoder
  defstruct [:first_name, :id, :last_name, :photo, :title, :type, :username]
end

defmodule Nadia.Model.User do
  @derive Nestru.Encoder
  defstruct [:first_name, :id, :last_name, :username]
end

defmodule Nadia.Model.Message do
  @derive Nestru.Encoder
  defstruct [
    :audio,
    :caption,
    :channel_chat_created,
    :chat,
    :contact,
    :date,
    :delete_chat_photo,
    :document,
    :edit_date,
    :entities,
    :forward_date,
    :forward_from,
    :forward_from_chat,
    :from,
    :group_chat_created,
    :left_chat_member,
    :location,
    :message_id,
    :migrate_from_chat_id,
    :migrate_to_chat_id,
    :new_chat_member,
    :new_chat_photo,
    :new_chat_title,
    :photo,
    :pinned_message,
    :reply_to_message,
    :sticker,
    :supergroup_chat_created,
    :text,
    :venue,
    :video,
    :voice
  ]
end

struct = %{
  callback_query: nil,
  channel_post: nil,
  chosen_inline_result: nil,
  edited_message: nil,
  inline_query: nil,
  message: %Nadia.Model.Message{
    audio: nil,
    caption: nil,
    channel_chat_created: nil,
    chat: %Nadia.Model.Chat{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      photo: nil,
      title: nil,
      type: "private",
      username: "ChristianTovar"
    },
    contact: nil,
    date: 1562605521,
    delete_chat_photo: nil,
    document: nil,
    edit_date: nil,
    entities: nil,
    forward_date: nil,
    forward_from: nil,
    forward_from_chat: nil,
    from: %Nadia.Model.User{
      first_name: "Christian",
      id: 543211234,
      last_name: "Tovar",
      username: "ChristianTovar"
    },
    group_chat_created: nil,
    left_chat_member: nil,
    location: nil,
    message_id: 714,
    migrate_from_chat_id: nil,
    migrate_to_chat_id: nil,
    new_chat_member: nil,
    new_chat_photo: [],
    new_chat_title: nil,
    photo: [],
    pinned_message: nil,
    reply_to_message: nil,
    sticker: nil,
    supergroup_chat_created: nil,
    text: "google meets",
    venue: nil,
    video: nil,
    voice: nil
  },
  update_id: 412827321
}

encoding looks like:

iex> Nestru.to_map(struct)
{:ok,
 %{
   callback_query: nil,
   channel_post: nil,
   chosen_inline_result: nil,
   edited_message: nil,
   inline_query: nil,
   message: %{
     audio: nil,
     caption: nil,
     channel_chat_created: nil,
     chat: %{
       first_name: "Christian",
       id: 543211234,
       last_name: "Tovar",
       photo: nil,
       title: nil,
       type: "private",
       username: "ChristianTovar"
     },
     contact: nil,
     date: 1562605521,
     delete_chat_photo: nil,
     document: nil,
     edit_date: nil,
     entities: nil,
     forward_date: nil,
     forward_from: nil,
     forward_from_chat: nil,
     from: %{first_name: "Christian", id: 543211234, last_name: "Tovar", username: "ChristianTovar"},
     group_chat_created: nil,
     left_chat_member: nil,
     location: nil,
     message_id: 714,
     migrate_from_chat_id: nil,
     migrate_to_chat_id: nil,
     new_chat_member: nil,
     new_chat_photo: [],
     new_chat_title: nil,
     photo: [],
     pinned_message: nil,
     reply_to_message: nil,
     sticker: nil,
     supergroup_chat_created: nil,
     text: "google meets",
     venue: nil,
     video: nil,
     voice: nil
   },
   update_id: 412827321
 }}

Does anyone know if Jason can do what the poison example or nestru does?

Looks like yes

https://hexdocs.pm/jason/Jason.Encoder.html

1 Like

I needed this functionality recently; thank you for writing it!

I also needed to account for lists, so I added:

defp convert(data) when is_list(data) do
  Enum.map(data, fn item ->
    convert(item)
  end)
end
1 Like

Still feels a bit verbose

def convert(data) when is_struct(data) do
  data |> Map.from_struct |> convert
end

def convert(data) when is_map(data) do
  Map.new(data, fn {k, v} -> {k, convert(v)})
end

def convert(data) when is_list(data) do
  Enum.map(data, &convert/1)
end

def convert (data), do: data
2 Likes

This is extremely frustrating. I have a struct with struct fields and other varied fields like regexes and maps and lists and tuples.

I have tried jason and nestru. Both render things differently across elixir version.

Do i really need to handcraft the struct to json/string representation?

for context working on

And its extremely hard to get tests to pass in CICD. Everything can be green locally on my newer elixir version 1.17 … but in github actions tests fails beause struct hash is different

bah…

wierd thing is that hash tests passes on older elixir versions … i think its at least map field ordering issue.

I mean how hard can it be

hmmmm … otp 26 …

hmmmmm

ref Blog Post: Taking Control of Map Sort Order in Elixir

ok, solved with the MapFromDeepStruct which i shamelessly stole and transformed to a ListFromDeepStruct. Thing is, maps ordering are unstable. Lists not. Then i do inspect(list) and hash it