Merge maps and struct

Hello there!

I’m trying to merge struct into a map by a form using Phoenix, by I didn’t encounter a way to make this by a “shortcut”.

The module looks like this:

defmodule Foo do
  defstruct foo: ""
end

In a Phoenix form I receive this as params on submit:

%{"foo" => "bar"} = params

The expected result is:

%Foo{foo: "bar"}

By Map.merge(%Foo{}, params) the fields are “duplicated” and the output is:

%{
  :__struct__ => Foo,
  :foo => "",
  "foo" => "bar"
}

By struct(%Foo{}, params) the foo param is not changed:

%Foo{foo: ""}

The way I found to get the expected result is mapping key params to atom:

params = Map.new(fn {k, v} -> {k |> String.to_atom(), v} end)

Then both Map.merge/2 and struct/2 give me the expected result %Foo{foo: "bar"}.

If I change my module to this:

defmodule Foo do
  defstruct "foo": ""
end

Then I receive a warning:

found quoted keyword “foo” but the quotes are not required. Note that keywords are always atoms, even when quoted. Similar to atoms, keywords made exclusively of ASCII letters, numbers, and underscores do not require quotes

…and receive unexpected results from both functions.

So, what’s the reason for a key when is_bitstring not be converted to atom by default?
Or what I’m doing wrong?

"foo" is string. And needs => operator for definition of map.

:foo is atom. Hence it can be defined in map via two ways.

  1. :foo => "bar"
  2. foo: "bar"

Note the space after : in 2.

1 Like

Params are not converted to atoms by design as a reminder that it’s untrusted data coming from the outside world, so you need to sanitize and cast the data into a trusted format.

Also be aware of the security concers of String.to_atom Preventing atom exhaustion | EEF Security WG

6 Likes

Ok, that’s a good reason.
Thanks!

1 Like

You should do it with a changeset…

iex(1)> defmodule Foo do
...(1)> defstruct foo: ""
...(1)> end
{:module, Foo,
 <<70, 79, 82, 49, 0, 0, 6, 196, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 192,
   0, 0, 0, 19, 10, 69, 108, 105, 120, 105, 114, 46, 70, 111, 111, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, %Foo{foo: ""}}
iex(2)> changeset = Ecto.Changeset.cast {%Foo{}, %{foo: :string}}, %{"foo"=>"koko"}, [:foo]
#Ecto.Changeset<
  action: nil,
  changes: %{foo: "koko"},
  errors: [],
  data: #Foo<>,
  valid?: true
>
iex(3)> Ecto.Changeset.apply_action changeset, :create
{:ok, %Foo{foo: "koko"}}

In this case, a schemaless changeset.

9 Likes

Yeah! I was trying to do this without Ecto, but I think this is the best way/solution using Phoenix. Thanks!

You can use the ecto library (general validation et. al. stuff) without using the ecto_sql one (DB stuff) easily. Changeset resides in the former.

1 Like