Why won't cast_embed call my function?

This is probably something silly and simple that I’m missing, but I’m going on many hours and have been unable to figure this one out.

I have an inline embeds_one in my schema.

    @primary_key {:id, :binary_id, autogenerate: true}
    embeds_one :preferences, Preferences, on_replace: :mark_as_invalid  do  
      field :theme, :string
    end

I then try to cast it in my changeset:

  def changeset(user, params)
    ...
    |> cast_embed(:preferences, with: &preference_changeset/2)
    ...
  end

  def preference_changeset(preferences, params) do
    preferences
    |> cast(params, [:theme])
    |> validate_theme()
  end

After much code tracing I disovered that cast_embed is adding an error to the changeset, but it never calls my preference_changeset function. The only error message is

{:error,
 #Ecto.Changeset<
   action: :update,
   changes: %{},
   errors: [preferences: {"is invalid", [validation: :embed, type: :map]}],
   data: #MyApp.Accounts.User<>,
   valid?: false
 >}

So I have a couple of questions:

  1. Why isn’t my preference_changeset function being called by cast_embed?
  2. Why is the validation failing? It seems like it might be mad about the type but it’s declared as a :map and receives a map.

FWIW, that exact error is produced in the Ecto tests when passing a string instead of a map:

Thanks. I am getting increasingly confident that my function isn’t being called because the type is “wrong” and the cast fails early. I’ve tried with a bunch of different ways however, and it’s been ineffective. I’m definitely passing a map. I’ve tried nesting it and flattening it also, to no avail.

I wonder if there is a bug somewhere. I just moved “preferences” to be a full schema module instead of inline embedded, and all the code that was failing before is working now without changes. Possibly I screwed up the inline declaration?

Regardless, I’ll just keep preferences defined in a module.