It is in the Ecto.Enum documentation.
OK, now I understood Ecto.Enum
- turns out it does exactly what I want:
defmodule Person do
use Ecto.Schema
@primary_key false
embedded_schema do
field(:id, :integer)
field(:name, :string, null: false)
field(:age, :integer)
field(:job, Ecto.Enum, values: Job.ids())
field(:hobbies, {:array, Ecto.Enum}, values: Hobby.ids())
field(:friends, {:array, :integer})
end
end
defmodule Hobby do
@hobbies [:hobby_painting, :hobby_freeclimbing, :hobby_stampcollecting]
def ids(), do: @hobbies
end
iex(1)> data = %{id: 1, name: "Bob", age: "18", job: "job_programmer", friends: [2, 4711], hobbies: ["hobby_freeclimbing", "hobby_painting"]}
iex(2)> p = Ecto.Changeset.cast(%Person{}, data, Map.keys(data)) |> Ecto.Changeset.apply_changes()
%Person{
age: 18,
friends: [2, 4711],
hobbies: [:hobby_freeclimbing, :hobby_painting],
id: 1,
job: :job_programmer,
name: "Bob"
}
Are you using typed_embedded_schema
here?
One alternative valid approach is to do the serialization with Nestru like the following:
defmodule Person do
defstruct [:id, :name, :age, :job, :hobbies, :friends]
defimpl Nestru.Decoder do
def from_map_hint(_value, _context, _map) do
{:ok, %{hobbies: &safe_to_list_of_atoms(&1), job: &safe_to_atom(&1)}}
end
defp safe_to_list_of_atoms(list) do
atoms =
Enum.reduce_while(list, [], fn item, acc ->
case safe_to_atom(item) do
{:ok, atom} -> {:cont, [atom | acc]}
{:error, _message} = error -> {:halt, error}
end
end)
if match?({:error, _message}, atoms) do
atoms
else
{:ok, Enum.reverse(atoms)}
end
end
defp safe_to_atom(binary) do
try do
{:ok, String.to_existing_atom(binary)}
rescue
ArgumentError -> {:error, "No atom exists for the value #{inspect(binary)}."}
end
end
end
end
So for the data with binaries for existing atoms, it returns the struct like that:
iex(1)> [:hobby_painting, :hobby_freeclimbing, :hobby_stampcollecting]
iex(2)> :job_programmer
iex(3)> data = %{id: 1, name: "Bob", age: "18", job: "job_programmer", friends: [2, 4711], hobbies: ["hobby_freeclimbing", "hobby_painting"]}
iex(4)> Nestru.from_map(data, Person)
{:ok,
%Person{
age: "18",
friends: [2, 4711],
hobbies: [:hobby_freeclimbing, :hobby_painting],
id: 1,
job: :job_programmer,
name: "Bob"
}}
And for the data with binary having no equivalent atom, it will fail like the following:
iex(5)> data = %{id: 1, name: "Bob", age: "18", job: "job_programmer", friends: [2, 4711], hobbies: ["hobby_freeclimbing", "hobby_gardening"]}
iex(6)> {:error, error} = Nestru.from_map(data, Person)
{:error,
%{
get_in_keys: [#Function<8.5372299/3 in Access.key!/1>],
message: "No atom exists for the value \"hobby_gardening\".",
path: [:hobbies]
}}
And it’s even possible to get the failed piece of the map like that :
iex(7)> get_in(data, error.get_in_keys)
["hobby_freeclimbing", "hobby_gardening"]
back to the original question…
my idea:
- decode to a map with atoms as keys
- merge it with a new struct
@doc ~S"""
Decode to Struct.
## Examples
iex> Test.decode_struct("{\"age\":123,\"name\":\"foo\"}", keys: :atoms)
%User{age: 123, name: "foo"}
"""
def decode_struct(json_str, opts \\ %{}) do
%User{} |> Map.merge(Jason.decode!(json_str, opts))
end
You could use struct!/2
to cast a struct and not allow unknown keys to be added to the struct
%User{} |> Map.merge(%{unknown: "works"})
will yield %User{unknown: "works"}
But struct!(User, %{unknown: "works"})
will raise a KeyError.
thanks!