`Ecto.Changeset.validate_change` and setting fields to `nil`

Question / comment / maybe bug report

I was surprised that this didn’t work:

def validate_not_cleared(changeset) do
  validate_change(changeset, :some_field, fn _, _new_value ->
    if changeset.data.some_field do
      [some_field: "can't be changed once set"]
    else
      []
    end
  end)
end

It successfully adds an error to the changeset when some_field is being changed to a non-nil value, but if the changeset is clearing the value it doesn’t run:

%SomeStruct{some_field: "foo"}
|> Ecto.Changeset.change(%{foo: nil})
|> validate_not_cleared()
# gives valid?: true

Here’s what I’m using instead, for the moment:

def validate_not_cleared(changeset) do
  case fetch_change(changeset, :some_field) do
    {:ok, _new_value} ->
      if changeset.data.some_field do
        add_error(changeset, :some_field, "can't be changed once set")
      else
        changeset
      end

    :error ->
      changeset
  end
end

EDIT: this is also distinct from how Ecto.Changeset.update_change works, which uses fetch_change

1 Like

I think the field’s name is :some_field, not :foo

As the docs say (emphasis mine):

It invokes the validator function to perform the validation only if a change for the given field exists and the change value is not nil.

https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_change/3

That’s because most validations do not want to run on nil. You have one of the few exceptions. :slight_smile:

2 Likes

Oops, that was a typo between the real code and the sanitized-for-public consumption version! Good catch!

1 Like

I’m not 100% convinced by this logic, but it makes some sense. But then I had to trim the value being changed:

defp trim_some_field_change(changeset) do
  update_change(changeset, :some_field, fn
    nil -> nil
    v -> String.trim(v)
  end)
end

Here I actually don’t want to run update_change on nil but it uses fetch so I have to guard against it.

IMO either both of these should see the “value → nil” change for some_field or neither, but not one of each.