DEV Community

Cover image for ecto's cast/4 function explained
Senyo SimpsonšŸŒ¹
Senyo SimpsonšŸŒ¹

Posted on • Originally published at oom.senyosimpson.com

ecto's cast/4 function explained

The typical changeset in Ecto is created via the cast/4function.

cast(data, params, permitted, opts \\ [])
Enter fullscreen mode Exit fullscreen mode

I've hardly found its usage intuitive.

def register_user(attrs) do
  %User{}
  |> User.registration_changeset(attrs)
  |> Repo.insert()
end

def registration_changeset(user, attrs, opts \\ []) do
  user
  |> cast(attrs, [:email, :password])
  |> validate_email(opts)
  |> validate_password(opts)
end
Enter fullscreen mode Exit fullscreen mode

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

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :age, :integer
  end
end
Enter fullscreen mode Exit fullscreen mode

And say we do this

iex> changeset = Ecto.Changeset.cast(%User{}, %{age: "0"}, [:age]) 
Enter fullscreen mode Exit fullscreen mode

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.

iex> changeset
#Ecto.Changeset<
  action: nil,
  changes: %{age: 0}, # type is cast to an integer
  errors: [],
  data: #User<>,
  valid?: true
>
Enter fullscreen mode Exit fullscreen mode

We'll see this fail if our data cannot be converted into an integer with no changes applied.

iex> changeset = Ecto.Changeset.cast(%User{}, %{age: "a"}, [:age]) 
iex> changeset
#Ecto.Changeset<
  action: nil,
  changes: %{},
  errors: [age: {"is invalid", [type: :integer, validation: :cast]}],
  data: #User<>,
  valid?: false
>
Enter fullscreen mode Exit fullscreen mode

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.

iex> changeset = Ecto.Changeset.cast(%User{age: 24}, %{age: "0"}, [:age]) 
iex> changeset
#Ecto.Changeset<
  action: nil,
  changes: %{age: 0},
  errors: [],
  data: #User<>,
  valid?: true
>
Enter fullscreen mode Exit fullscreen mode

More writing

For more of my writing, check out my newsletter: off the wire

Top comments (0)