Ecto changesets provide built-in validations like validate_required/2 and validate_length/3, but sometimes you need custom validation logic. In this post, we'll explore how to implement custom validations in Ecto.

Let's say we have a User schema, and we need to ensure that the age field is at least 18.

We can define the schema like this and add a the custom validator validate_age:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :age, :integer
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :age])
    |> validate_required([:name, :age])
    |> validate_age()
  end

  defp validate_age(changeset) do
    validate_change(changeset, :age, fn :age, age ->
      if age < 18 do
        [{:age, "must be at least 18"}]
      else
        []
      end
    end)
  end
end

Another approach is to check the value directly and add an error manually:

defp validate_age(changeset) do
  age = get_change(changeset, :age, get_field(changeset, :age))

  if age && age < 18 do
    add_error(changeset, :age, "must be at least 18")
  else
    changeset
  end
end

When to use each approach:

  • validate_change/3 is useful for concise validations that return lists of errors.
  • add_error/3 is better when checking multiple fields or requiring more control over the validation logic.