How to Write Custom Validations for Ecto Changesets
Tags: elixir, phoenix, ecto
Published: 2021-01-10 10:52:36

> Written for Phoenix 1.5.7 & Ecto 3.5.5

Ecto [changesets](https://hexdocs.pm/ecto/Ecto.Changeset.html) provide common validation options but we can also write our own. Before we get into writing our own though, we need to understand how (most of) the default validators work. Let's have a look at the definition of `validate_inclusion` for example:

```elixir
def validate_inclusion(changeset, field, data, opts \\ []) do
  validate_change changeset, field, {:inclusion, data}, fn _, value ->
    if value in data,
      do: [],
      else: [{field, {message(opts, "is invalid"), [validation: :inclusion, enum: data]}}]
  end
end
```

We can see that the actual logic is wrapped inside `validate_change/4`. `validate_change/3` and `validate_change/4` both take a changeset, a field, and a validator function (`validate_change/4` takes metadata as its 3rd paramater). `validate_change` will then check for the presence of  the `field` in the changeset and if it exists and the change value is not nil then it will run the `validator`. The `validator` must return either an empty list if the validation passed or a list of errors which will be appended to the `:errors` field of the changeset and `valid?` will be set to false.

With that in mind, let's write our own validation. Below we have a basic module with a schema and a `changeset` function for our `tutorials` table:

```elixir
defmodule MyApp.Tutorials.Tutorial do
  use Ecto.Schema
  import Ecto.Changeset

  schema "tutorials" do
    field :name, :string
    field :is_useful, :boolean

    timestamps()
  end

  @doc false
  def changeset(tutorial, attrs) do
    tutorial
    |> cast(attrs, [:name, :is_useful])
    |> validate_required([:name, :is_useful])
  end
end
```

To add a custom validation, we'll write a function that takes the `Changeset` as its first parameter and then the field atom:

```elixir
defmodule MyApp.Tutorials.Tutorial do
  
  # ...

  def validate_is_useful(changeset, field) when is_atom(field) do
    validate_change(changeset, field, fn field, value ->
      case value do
        true ->
          []

        false ->
          [{field, "tutorials should be useful"}]
      end
    end)
  end
end
```

All we need to do now is update our `changeset` function to include the new validation:

```elixir
defmodule MyApp.Tutorials.Tutorial do
  
  # ...

  @doc false
  def changeset(tutorial, attrs) do
    tutorial
    |> cast(attrs, [:name, :is_useful])
    |> validate_required([:name, :is_useful])
    |> validate_is_useful(:is_useful)
  end

  # ...

end
```

You could also write a custom validation by simply getting the field value you want from the changeset using [`get_field/3`](https://hexdocs.pm/ecto/Ecto.Changeset.html#get_field/3), and that does have its palce, but by leveraging `validate_change` when the field you're validating exists and has a value your `validator` will be run but if you're generating a changeset with no `changes`, for example to pass to a form in Phoenix, then the validator won't run and raise an error when the field who's value you're trying to get doesn't exist.

That's all there is to it really! For further reading, check out the [`Ecto.Changeset`](https://hexdocs.pm/ecto/Ecto.Changeset.html) docs, they're very good. If you have any questions, don't hesitate to ask 💬