How to get struct from map - elixir?

Well that’s because handle_in receives a map with string keys.
voughtdq came up with Poison.decode … as a nice solution (map to struct).
Using Poison.decode eliminates the need for boilerplate code in the struct definition, yay!
So that’s the only reason why I would want to use it.

I tend to use Ecto and embedded schemas for map -> struct and boundary validation like this:

defmodule MyApp.MyContext do
  def do_the_thing(params \\ %{}) when is_map(params) do
    with {:ok, command} <- MyApp.MyContext.Commands.DoTheThing.new(params) do
      # fun business logic here
    end
  end
end

defmodule MyApp.MyContext.Commands.DoTheThing do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :field_1, :string
    field :field_2, :integer
  end

  def new(params) do
    command = changeset(params)
    case command.valid? do
      true  -> {:ok, apply_changes(command)}
      false -> {:error, command.errors}
    end
  end

  defp changeset(params) do
    %__MODULE__{}
    |> cast(params, [:field_1, :field_2])
    |> validate_required([:field_1, :field_2])
  end
end

It doesn’t feel so dirty to do this since Ecto 3.0 and the :ecto_sql separation.

I’ve found this to be a good way to accept large form inputs - maybe not as practical for smaller sets of parameters.

2 Likes

Tnx, I understand your point, it’s probably better to use something solid like ecto.
Your new function is similar to initializing a struct in Go, is this typical also for Elixir?

Most of my channel functions accept just a few parameters and is_binary is fine most of the time.
I just don’t like defining the map as an argument and all of the guards; it doesn’t look nice.

still a little bit dirty :wink:

@primary_key false
embedded_schema do ...

@MrDoops suppose I would really like to embed my structures in my channel modules,
and I wouldn’t want to define new and changeset functions for all of my embeds.
Would it be crazy to do something like this:

defmodule MyApp.SomeChannel do
  use Phoenix.Channel
  
  defmodule SigninData do
    use Ecto.Schema
    use MyApp.Macros.Structure
      
    @primary_key false
    embedded_schema do
      field :email, :string
      field :password, :string
    end
  end
  
  ...
  
  def handle_in("signin", %{} = params, socket) do
    params
    |> SigninData.new
    |> ... authenticate ...
    |> ... reply ...
  end
end

using this macro:

defmodule MyApp.Macros.Structure do
  import Ecto.Changeset
  
  defmacro __using__(_opts) do
    quote do
      def new(params) do
        params |> MyApp.Macros.Structure.new(unquote(__CALLER__.module))
      end
    end
  end
  
  def new(params, module) when is_map(params), do: params |> changeset(module) |> structure
  def new(_params, _module), do: {:error, :invalid}
  
  defp changeset(params, module) do
    with fields <- module.__schema__(:fields) do
      struct(module)
      |> cast(params, fields)
      |> validate_required(fields)
    end         
  end
  
  defp structure(%Ecto.Changeset{valid?: true} = changeset), do: {:ok, apply_changes(changeset)}
  defp structure(%Ecto.Changeset{} = changeset), do: {:error, changeset.errors}
  defp structure(_noop), do: {:error, :unprocessable_entity}
end

?

2 Likes

It’s mostly a subjective thing, but I prefer not to have the Phoenix / Web layer know how Identity / Authentication is implemented. I’d rather do something like:

def handle_in("signin", %{} = params, socket) do
  case Identity.authenticate(params) do
     {:ok, ...} -> ...
     {:error, ...} -> ...
  end
end
1 Like

Fair point, I would actually also delegate the actual authentication process.
I was trying to better define the channel api.

I’m looking into Absynthe / GraphQL to better define my api.
It seems to make the structs inside channels thing irrelevant and provides a ton of functionality like user input validation.

2 Likes

Not sure if this is exactly your problem, but since I was searching for "How to convert a map to a User struct, and this topic popped up, I thought that I would add what I eventually did that worked perfectly:

Map.merge(%User{}, map_defaults)

This is dangerous as it doesn’t check that the keys in map_defaults actually correspond to keys of the struct. You may end up with a broken struct which is annoying to debug. What you should do is:

s = struct!(User, map_defaults)

This will ensure that keys that are in @enforce_keys must be exist and there are no keys that don’t exist in the struct. Otherwise it will raise an error. There is also Kernel.struct/2 (i.e. without exlamation mark) that will just discard unknown keys and won’t enforce @enforce_keys.

4 Likes

Much better yes. I didn’t know this one. I wonder why the web searches didn’t pull this up, if it was that easy to do. First I’ve heard of ‘struct/2’ thanks!

I encountered a similar problem today. Wanted a minimal set of fields to process frequently so created a smaller struct, with only a subset of key/value pairs of the full one. Then I fetch data from the DB in a manner very similar to the one described here:

IOW I am getting a Map (well, list of maps in that particular example there) with only the key/value pairs I am interested in and pass it to struct!2

struct!(%MyMinimalStruct{}, my_map_from_db_query)

Seems almost suspiciously trivial… any gotchas?

Alternative could be to create another “model” with limited Ecto.Schema defnition. Might check that route too

You can do from x in query, select: %MyMinimalStruct{id: x.id, name: x.name}. A bit more verbose than the map/2 syntax, but no later mapping needed. If MyMinimalStruct is also a schema module there’s also Repo.load.

1 Like

Excellent! Verbosity difference with minimal fieldset is a non-issue. Something like this:

MyModel
|> select([mm], %MyMinimalStruct{id: mm.id, name: mm.name}
|> Repo.get_by!(id: id)

LGTM. And can be used instead of converting maps in cases where only the (minimal) struct is needed. :+1:

I think this approach is the right fit for me. It preserves Ecto’s type casting without relying on validations that feel out of place when loading from the DB. A changeset makes sense in the app-to-DB direction, but going the other way it’s redundant since the data has already passed validation (at least if Ecto is the only DB writer — otherwise falling back to a proper changeset is fine).

pry(2)> 
Ecto.Changeset.cast(struct(schema_module), params, schema_module.__schema__(:fields)) 
|> Ecto.Changeset.apply_changes()
%Mozek.V3.Orchestrator.Rule.Instance.RequestSchema{
  __meta__: #Ecto.Schema.Metadata<:built, "v3_rule_instance_requests">,
  id: 207,
  freshness_stat_id: 718,
  inserted_at: ~U[2025-08-22 11:34:45Z]
}

A utility function like this works well — it handles type casting and embeds (associations could be added similarly if needed):

@spec cast_to_struct(module(), map()) :: struct()
def cast_to_struct(schema_module, params) when is_atom(schema_module) and is_map(params) do
  unless has_changeset?(schema_module) do
    raise "Schema #{inspect(schema_module)} does not have a changeset/2 function"
  end

  fields = schema_module.__schema__(:fields) -- schema_module.__schema__(:embeds)

  struct(schema_module)
  |> Ecto.Changeset.cast(params, fields)
  |> then(fn cs ->
    Enum.reduce(schema_module.__schema__(:embeds), cs, fn embed, acc ->
      Ecto.Changeset.cast_embed(acc, embed)
    end)
  end)
  |> Ecto.Changeset.apply_changes()
end