Misleading ecto error: unknown field `password` in %App.User{password: nil

Happens when trying to Ecto.Changeset.change/1 a changeset.

Example:

attrs = %{"password" => "asdf"}
%App.User{}
|> Ecto.Changeset.cast(attrs, [:password])
|> Ecto.Changeset.change(attrs)

To reproduce: https://github.com/syfgkjasdkn/ecto-error

Does the %App.User{} struct has field :password? Can you show your whole struct? Can you also quote whole error?

Does the %App.User{} struct has field :password ?

Yes.

Can you show your whole struct? Can you also quote whole error?

It’s not relevant, the field is definitely present.

** (ArgumentError) unknown field `password` in %App.User{password: nil, phone: nil, ...}

Your whole code is probably relevant, because if I do it like this:

Interactive Elixir (1.7.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule User do
...(1)>   use Ecto.Schema
...(1)> 
...(1)>   schema "users" do
...(1)>     field :password, :string
...(1)>     field :phone, :string
...(1)>   end
...(1)> 
...(1)>   def x do
...(1)>     attrs = %{"password" => "asdf"}
...(1)>     %User{}
...(1)>     |> Ecto.Changeset.cast(attrs, [:password])
...(1)>     |> Ecto.Changeset.change(attrs)
...(1)>   end
...(1)> end
warning: redefining module User (current version loaded from _build/dev/lib/office/ebin/Elixir.User.beam)
  iex:1

{:module, User,
 <<70, 79, 82, 49, 0, 0, 12, 144, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 201,
   0, 0, 0, 49, 11, 69, 108, 105, 120, 105, 114, 46, 85, 115, 101, 114, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:x, 0}}
iex(2)> User.x
#Ecto.Changeset<
  action: nil,
  changes: %{:password => "asdf", "password" => "asdf"},
  errors: [],
  data: #User<>,
  valid?: true
>

It works without any error.

1 Like
defmodule Demo do
  defmodule Schema do
    use Ecto.Schema

    schema "asd" do
      field(:something, :string)
    end
  end

  def error do
    attrs = %{"something" => "asdf"}

    %Schema{}
    |> Ecto.Changeset.cast(attrs, [:something])
    |> Ecto.Changeset.change(attrs)
  end
end
iex(4)> Demo.error
** (ArgumentError) unknown field `something` in %Demo.Schema{__meta__: #Ecto.Schema.Metadata<:built, "asd">, id: nil, something: nil}
    (ecto) lib/ecto/changeset.ex:1141: Ecto.Changeset.put_change/7
    (stdlib) maps.erl:232: :maps.fold_1/3
    (ecto) lib/ecto/changeset.ex:384: Ecto.Changeset.change/2

Will push to github now.

iex(4)> defmodule Demo do
...(4)>   defmodule Schema do
...(4)>     use Ecto.Schema
...(4)> 
...(4)>     schema "asd" do
...(4)>       field(:something, :string)
...(4)>     end
...(4)>   end
...(4)> 
...(4)>   def error do
...(4)>     attrs = %{"something" => "asdf"}
...(4)> 
...(4)>     %Schema{}
...(4)>     |> Ecto.Changeset.cast(attrs, [:something])
...(4)>     |> Ecto.Changeset.change(attrs)
...(4)>   end
...(4)> end
{:module, Demo,
 <<70, 79, 82, 49, 0, 0, 5, 148, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 158,
   0, 0, 0, 16, 11, 69, 108, 105, 120, 105, 114, 46, 68, 101, 109, 111, 8, 95,
   95, 105, 110, 102, 111, 95, 95, 7, 99, ...>>, {:error, 0}}
iex(5)> Demo.error
#Ecto.Changeset<
  action: nil,
  changes: %{:something => "asdf", "something" => "asdf"},
  errors: [],
  data: #Demo.Schema<>,
  valid?: true
>

Work for me, tag me in post if you push it to github, I’ll look at your whole code.

1 Like

We seem to have different elixir versions. I’m on 1.9.1.

If someone on 1.9.1 is able to reproduce it, I’ll open an issue on ecto repo.

I got the error with your repo, trying to get down to what seems to be the problem, and why it worked when I pasted code to iex in some other project :thinking:

1 Like

The problem is ecto looking for "something" (binary) key in a struct, at least that’s what Ecto.Changeset.put_change/7 (the last invoked function according to the stacktrace) is called with.

iex(1)> Rexbug.start("Ecto.Changeset.put_change/7")
{107, 1}
iex(2)> Demo.error
** (ArgumentError) unknown field `something` in %Demo.Schema{__meta__: #Ecto.Schema.Metadata<:built, "asd">, id: nil, something: nil}
    (ecto) lib/ecto/changeset.ex:1141: Ecto.Changeset.put_change/7
    (stdlib) maps.erl:232: :maps.fold_1/3
    (ecto) lib/ecto/changeset.ex:384: Ecto.Changeset.change/2

# 21:56:22 #PID<0.271.0> IEx.Evaluator.init/4
# Ecto.Changeset.put_change(%Demo.Schema{__meta__: #Ecto.Schema.Metadata<:built, "asd">, id: nil, something: nil}, %{something: "asdf"}, [], true, "something", "asdf", nil)

and “prettifying” the last line:

Ecto.Changeset.put_change(
  %Demo.Schema{__meta__: #Ecto.Schema.Metadata<:built, "asd">, id: nil, something: nil}, 
  %{something: "asdf"},
  [],
  true,
  "something", # <-- what I think is the culprit
  "asdf",
  nil
)

Excerpt from the source code of the failing function:

  # called directly from get_changed/6, not from `put_change/3`
  defp put_change(data, changes, errors, valid?, key, value, {tag, relation})
       when tag in @relations do
    original = Map.get(data, key)
    current = Relation.load!(data, original)

    case Relation.change(relation, value, current) do
      {:ok, change, relation_valid?} when change != original ->
        {Map.put(changes, key, change), errors, valid? and relation_valid?}
      {:error, error} ->
        {changes, [{key, error} | errors], false}
      # ignore or ok with change == original
      _ ->
        {Map.delete(changes, key), errors, valid?}
    end
  end

  defp put_change(data, _changes, _errors, _valid?, key, _value, nil) do
    raise ArgumentError, "unknown field `#{key}` in #{inspect(data)}"
  end

Actually the problem is due to {tag, relation} being nil which in turn is due to there being no type in changeset.types for the key "something" (it should’ve been an atom).

1 Like

Opened an issue: https://github.com/elixir-ecto/ecto/issues/3134

Might be a different ecto version as well.

Yeah, it seems I tried on some old ecto 2.2.

I tried to figure something out but I didn’t get to anything more than you did.

And it actually IMHO boils down to the fact that we have in Elixir maps that can have strings or atoms as keys, or even mix them. We can have specs that define for dialyzer what given function receive, but I don’t think we have a way to write guards that will test if maps keys are only binaries, or only atoms.

EDIT:
Actually if you comment out this lines in ecto/changeset.ex

defp put_change(data, _changes, _errors, _valid?, key, _value, nil) do
    raise ArgumentError, "unknown field `#{key}` in #{inspect(data)}"
  end

it works, so maybe something else should be checked instead of checking if last argument is nil, but it’s too late for me to figure out if commenting this out does not break something else.

1 Like

What’s the intention of the last line here? cast has already incorporated the typecast value from attrs into the changeset.

Ecto.Changeset.change/2 specifies that the keys should be atoms and includes the corresponding typespec:

@spec change(Ecto.Schema.t | t | {data, types}, %{atom => term} | Keyword.t) :: t

Edit: see also the fix

Extra edit: here’s what adding Dialyzer to the example project and running mix dialyzer prints out:

Total errors: 1, Skipped: 0, Unnecessary Skips: 0
done in 0m1.27s
lib/demo.ex:10:no_return
Function error/0 has no local return.
________________________________________________________________________________
done (warnings were emitted)
2 Likes

What’s the intention of the last line here? cast has already incorporated the typecast value from attrs into the changeset.

I’m not sure, I wasn’t the original author of that function, but it was me who had to debug it :slight_smile:

The new error message in https://github.com/elixir-ecto/ecto/commit/9fca4a94e7fb790a7cd4a4fcaaabf551f55b6206 should make the debugging a bit easier.

Solved in https://github.com/elixir-ecto/ecto/commit/9fca4a94e7fb790a7cd4a4fcaaabf551f55b6206

3 Likes