ecto's cast/4 explained

2 minute read

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>