How to Write Custom Validations for Ecto Changesets
Written for Phoenix 1.5.7 & Ecto 3.5.5
Ecto changesets 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:
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:
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:
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:
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
, 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
docs, they're very good. If you have any questions, don't hesitate to ask on dev.to 💬