Ecto error on loading custom Ecto.Type from Embedded Schema

Hey folks, I’m trying to use a custom Ecto.Type inside an Embedded Schema, but I’m getting a wierd error:

** (ArgumentError) cannot load `"10.00"` as type DomainTypes.Money for field `amount` in schema ApiEcommerce.Payments.Charge.Status
     stacktrace:
       (ecto) lib/ecto/schema.ex:1494: Ecto.Schema.load!/5
       (ecto) lib/ecto/schema.ex:1468: anonymous fn/5 in Ecto.Schema.__unsafe_load__/4
       (elixir) lib/enum.ex:1899: Enum."-reduce/3-lists^foldl/2-0-"/3
       (ecto) lib/ecto/schema.ex:1455: Ecto.Schema.__unsafe_load__/3
       (elixir) lib/enum.ex:1294: Enum."-map/2-lists^map/1-0-"/2
       (ecto) lib/ecto/type.ex:460: Ecto.Type.load_embed/3
       (ecto) lib/ecto/type.ex:659: Ecto.Type.process_loaders/3
       (ecto) lib/ecto/schema.ex:1490: Ecto.Schema.load!/5
       (ecto) lib/ecto/schema.ex:1442: Ecto.Schema.safe_load_zip/4
       (ecto) lib/ecto/schema.ex:1443: Ecto.Schema.safe_load_zip/4
       (ecto) lib/ecto/schema.ex:1430: Ecto.Schema.__safe_load__/6
       (ecto) lib/ecto/repo/queryable.ex:282: Ecto.Repo.Queryable.process_source/6
       (ecto) lib/ecto/repo/queryable.ex:170: Ecto.Repo.Queryable.preprocess/5
       (postgrex) lib/postgrex/query.ex:77: DBConnection.Query.Postgrex.Query.decode_map/3
       (postgrex) lib/postgrex/query.ex:64: DBConnection.Query.Postgrex.Query.decode/3
       (db_connection) lib/db_connection.ex:1019: DBConnection.decode/6
       (ecto) lib/ecto/adapters/postgres/connection.ex:98: Ecto.Adapters.Postgres.Connection.execute/4
       (ecto) lib/ecto/adapters/sql.ex:256: Ecto.Adapters.SQL.sql_call/6
       (ecto) lib/ecto/adapters/sql.ex:436: Ecto.Adapters.SQL.execute_or_reset/7
       (ecto) lib/ecto/repo/queryable.ex:133: Ecto.Repo.Queryable.execute/5

As the error states is something on load from the database. This is my schema and my custom type

defmodule DomainTypes.Money do
  @behaviour Ecto.Type

  def type, do: :decimal

  def cast(value) when is_integer(value) do
    {:ok, Decimal.new(1, value, -2)}
  end

  def cast(%Decimal{} = value) do
    {:ok, value}
  end

  def cast(_), do: :error

  def load(value) when is_integer(value) do
    {:ok, Decimal.new(1, value, -2)}
  end

  def load(%Decimal{} = value) do
    {:ok, value}
  end

  def load(_), do: :error

  def dump(%Decimal{} = value) do
    {:ok, value}
  end

  def dump(_), do: :error
end

schema "charges" do
    # ...
    embeds_many :statuses, Status do
      field(:type, :string)
      field(:amount, DomainTypes.Money)
      field(:acquirer_identifier, :string)
      field(:acquirer_unique_sequential_number, :string)

      timestamps()
    end

    # ...
  end

It looks like your load function does not handle binaries (strings). I would put this before the def load(_), do: :error bit, like so:

def load(text) when is_binary(text) do
  case Decimal.parse(text) do
    {:ok, %Decimal{} = value} ->
      load(value)
    {:error, _} ->
      :error
  end
end

Custom types in embedded schemas do not use load/dump. They just json encode the runtime data and use the cast callback to load it again.

4 Likes

I am having the exact same issue. Is there a solution for this? We need to implement the load functions if I am correct, so does this mean it is not possible to store an own Ecto.Type inside an embedded field?

Edit: I just found the final paragraph of https://hexdocs.pm/ecto/Ecto.Schema.html#embeds_one/3, but I do not understand which actions I have to undertake to make this work. Let me know if this is the wrong topic for this.

Having in mind that embdes are stored as JSONB (Elixir Map) inside Postgres, then perhaps you need to derive a protocol to make your JSON encoder work with the custom type? We can’t know more if you don’t give us more context.

Thanks for the reply and sorry for my lack of context, I will keep this in mind the next time I post. I tried what you suggested and learned a few things along the way, but it did not solve my specific problem.

A colleague of mine found out that things went wrong at the casting level, as I was pattern matching in the cast function clause on my own Ecto.Type. As it turns out and as pointed out, loading the embedded struct runs it through the cast again which was impossible as the data from the db was just a map hence the matching failed.

Overloading the cast function to work both ways solved the problem.

2 Likes