Ecto.Enum In Schemaless Changesets

Beginning in Ecto 3.5, Ecto added Ecto.Enum as a new field type to use with schemas. This allows you to cast values to a known list of accepted values and have the values be atoms. Prior, you would have had to create your own Ecto type and implement the Ecto.Type behaviour. Using Ecto.Enum is very straightforward. Here's how you'd use it in a schema:

defmodule MyApp.User do
  use Ecto.Schema
  
  schema "users" do
    # ...
    field :type, Ecto.Enum, values: [:regular, :staff, :admin, :superuser]
  end
end

Schemaless Changesets

Schemaless changesets are a very powerful tool in Ecto. You can create an ephermal schema that you can use for validations and casting. This is extremely useful when dealing with data that's coming from outside your system. Here's an example:

import Ecto.Changeset

params = %
  "old_password" => "password1",
  "new_password" => "password2",
  "new_password_confirmation" => "password2"
}

initial_data = %{}

field_types = %{
  old_password: :string,
  new_password: :string,
  new_password_confirmation: :string
}

{initial_data, field_types}
|> cast(params, ~w(old_password password new_password_confirmation)a)
|> validate_required(~w(old_password new_password new_password_confirmation)a)
|> validate_length(:new_password, min: 8)
|> validate_confirmation(:new_password)

Using Ecto.Enum in a Schemaless Changeset

The underlying type of Ecto.Enum is actually a parameterized type (also new in Ecto 3.5). Ecto.ParamaterizedType is a behaviour, similar to Ecto.Type, that allows you to configure a type from a set of initialization options. Using a parameterized type requires a little extra syntax to work by using a three-element tuple:

types = %{
  event_type: {:parameterized, Ecto.Enum, Ecto.Enum.init(values: ~w(short medium long)a)}
}

The first element lets Ecto know that you want to use a parameterized type. The second element tells Ecto which module to use for casting data. The third element is the configuration for the type. With Ecto.Enum, the third element can be crafted by calling init on the Ecto.Enum module.

Here's how it would look alltogether in a changeset:

import Ecto.Changeset

initial_data = %{}

params = %{"event_type" => "short"}

types = %{
  event_type: {:parameterized, Ecto.Enum, Ecto.Enum.init(values: ~w(short medium long)a)}
}

{initial_data, types}
|> cast(params, [:event_type])
|> validate_required([:event_type])
# ...

Summary

Ecto.Enum is a great addition in Ecto 3.5. Even though it isn't obvious how to use Ecto.Enum in a schemaless changeset, adding a few extra bits of information will enable you to use it. If you haven't already tried using a schemless changeset, try them out the next time you want validate external data or even replace an embedded schema that is used in a similar fashion.