One workaround is to define an Ecto.Type
. My example here isn’t well organized (the type here should be a different module, OptionList
), but that’s fine for an example:
defmodule Test.Flow do
use Ecto.Schema
import Ecto.Changeset
defmodule Option do
use Ecto.Schema
use Ecto.Type
import Ecto.Changeset
@primary_key false
embedded_schema do
field :value, :string
end
def changeset(opts \\ %__MODULE__{}, attrs) do
cast(opts, attrs, [:value])
end
@impl Ecto.Type
def type, do: {:array, :map}
@impl Ecto.Type
def cast([_ | _] = list) do
cond do
Enum.all?(list, &is_struct(&1, __MODULE__)) -> {:ok, list}
Enum.all?(list, &is_map/1) -> load(list)
true -> :error
end
end
def cast(nil), do: {:ok, nil}
def cast(_), do: :error
@impl Ecto.Type
def load(nil), do: {:ok, nil}
def load(data) when is_list(data) do
{
:ok,
Enum.map(data, fn entry ->
struct!(
__MODULE__,
for {k, v} <- entry do
{String.to_existing_atom(k), v}
end
)
end)
}
end
@impl Ecto.Type
def dump([_ | _] = list) do
if Enum.all?(list, &match?(%__MODULE__{}, &1)) do
{:ok, Enum.map(list, &Map.from_struct/1)}
else
:error
end
end
def dump(nil), do: {:ok, nil}
def dump(_), do: :error
end
schema "flows" do
field :options, Option
end
def changeset(flows \\ %__MODULE__{}, attrs) do
cast(flows, attrs, [:id, :options])
end
end
It’s a lot of work, and I suspect that some of it could be wrapped in a macro.
I fear that the lesson here is to avoid embeds_many
if your column is intentionally nullable, because Ecto works against your design in this case.