Why can't we have embeds_many :foo, :map where the embeds is a list of non-homogenous maps?

I have question on the limitations of embeds_many. I want non-homogenous embeds many.

Despite being able to define how to handle casting from the with option in embeds_many, casting fails when the type for the field is :map.

defmodule Container do
  use Ecto.Schema
  embedded_schema do
    embeds_many :foos_and_bars, :map
  end
end

defmodule Foo do
  use Ecto.Schema
  embedded_schema do
    field :position, :integer
    field :unique_data, :list
  end
end

defmodule Bar do
  use Ecto.Schema
  embedded_schema do
    field :position, :integer
    field :more_unique_data, :bool
  end

The issue arises when I need to call cast_embed on the Container to validate foos_and_bars

container = %Container{foos_and_bars: [%Bar{position: 1, more_unique_data: true}]}
params = %{
  foos_and_bars: %{
    "1" => %{more_unique_data: false}
}}

Ecto.Changeset.cast(container, params, [])
|> Ecto.Changeset.cast_embed(:foos_and_bars, with: fn foo_or_bar, params ->
  case foo_or_bar do
    foo = %Foo{} -> Foo.changeset(foo, params)
    bar = %Bar{} -> Foo.changeset(bar, params)
  end
end)

The cast_embed throws (UndefinedFunctionError) function :map.__schema__/1 is undefined (module :map is not available) :map.__schema__(:primary_key) as it expects the field to be a schema not just a plain :map.

My question is why is this the case? I understand that there may not be an obvious use for field backed by a database to need this type flexiblity but when changesets are the first class supported method of input validation in Phoenix these issues arrive. The wider context is for my application we have an ordered list of inputs. Some those “inputs” are nested input lists. To maintain the order correctly inside of the inputs_for we need to store an Enumerable with two types (a plain input or nested group of inputs). Problems arrise when casting the changeset as described above.

I can further understand why this error could get thrown if Ecto.Changeset.cast_embed was not called alongside the with option. The with allows you to define how to handle casting for each item in the embeds. which in my opinion should allow me to have case that matches on type and delegates to those modules changeset functions.

embeds_many :foos_and_bars, :map should either raise an exception or the case should be handled as described above.

1 Like

This sounds like exactly what polymorphic_embed was designed to do:

Without the “type” field that that uses, it’s not possible to figure out which schema the results should be when retrieving a schema that says embeds_many

If you really care exclusively about writing these shapes, consider saying field :foos_and_bars, :map and then manually using Foo and Bar’s changesets to produce maps in cast_embed.

2 Likes

Nice library, interesting that Ecto doesn’t support this behavior itself though. The polymorphic embeds inputs_for is a nice feature to support as well to easily switch off type. That is my exact use case.