ecto's cast/4 explained
The typical changeset in Ecto is created via the cast/4
function.
1cast(data, params, permitted, opts \\ [])
I’ve hardly found its usage intuitive.
1def register_user(attrs) do
2 %User{}
3 |> User.registration_changeset(attrs)
4 |> Repo.insert()
5end
6
7def registration_changeset(user, attrs, opts \\ []) do
8 user
9 |> cast(attrs, [:email, :password])
10 |> validate_email(opts)
11 |> validate_password(opts)
12end
Why are we casting with an empty User
? What do we actually use attrs
for?
As it turns out, it’s not even mildly confusing. The purpose of the cast/4
function is as the name
says on the box: to cast data from one type into the other. It’s necessary as we may receive data
from external sources with the wrong type i.e an integer sent as a string from a form. Keep in mind
though, we’re still dealing with a changeset, whose sole purpose is to track changes to fields in
our data.
Per the function signature, cast/4
casts the fields of params
into the expected types specified
by data
, while tracking changes to the data
. The only changes that are tracked are those
specified by the atoms passed via permitted
.
Take this Ecto schema
1defmodule User do
2 use Ecto.Schema
3 import Ecto.Changeset
4
5 schema "users" do
6 field :age, :integer
7 end
8end
And say we do this
1iex> changeset = Ecto.Changeset.cast(%User{}, %{age: "0"}, [:age])
The parameter age
is a string. The cast/4
function will convert it into an integer. And since
we’ve passed in an empty User
, we’ll get a changeset reflecting the changes we’ve made i.e adding
a parameter age with the value of 0.
1iex> changeset
2#Ecto.Changeset<
3 action: nil,
4 changes: %{age: 0}, # type is cast to an integer
5 errors: [],
6 data: #User<>,
7 valid?: true
8>
We’ll see this fail if our data cannot be converted into an integer with no changes applied.
1iex> changeset = Ecto.Changeset.cast(%User{}, %{age: "a"}, [:age])
2iex> changeset
3#Ecto.Changeset<
4 action: nil,
5 changes: %{},
6 errors: [age: {"is invalid", [type: :integer, validation: :cast]}],
7 data: #User<>,
8 valid?: false
9>
If we already have a populated User
type and we pass in valid data, we get our change tracking as
expected and the data is cast if need be.
1iex> changeset = Ecto.Changeset.cast(%User{age: 24}, %{age: "0"}, [:age])
2iex> changeset
3#Ecto.Changeset<
4 action: nil,
5 changes: %{age: 0},
6 errors: [],
7 data: #User<>,
8 valid?: true
9>