# `Ecto.Enum` maps atoms to integer columns

Status columns tend to rot the same way everywhere: an integer in the database, a string in the params, an atom in the business logic, and `String.to_atom/1` calls scattered across three contexts to bridge them. `Ecto.Enum` collapses all of it into the field definition — atoms in Elixir, integers in Postgres, with the conversion and the validation handled for you.

```elixir
defmodule MyApp.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset

  schema "articles" do
    field :title, :string
    field :status, Ecto.Enum, values: [draft: 0, review: 1, published: 2]
  end

  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :status])
    |> validate_required([:title, :status])
  end
end
```

The keyword-list form is the one worth knowing. `values: [:draft, :review, :published]` stores strings; `values: [draft: 0, review: 1, published: 2]` stores integers while your code still sees atoms:

```elixir
changeset = Article.changeset(%Article{}, %{"title" => "Hi", "status" => "published"})
changeset.changes
# => %{status: :published, title: "Hi"}

Ecto.Type.dump(Article.__schema__(:type, :status), :published)
# => {:ok, 2}
```

A form posts `"published"`, the changeset holds `:published`, the column stores `2`. No conversion code in between.

## Validation comes free

An unknown value is rejected by `cast/3` itself — you do not need `validate_inclusion/3`:

```elixir
changeset = Article.changeset(%Article{}, %{"title" => "Hi", "status" => "archived"})

changeset.valid?
# => false

changeset.errors
# => [status: {"is invalid", ...}]
```

That check also protects you from the atom-exhaustion problem the naive version has, because no atom is ever created from user input — the value is looked up against the declared set.

## The introspection functions

This is the part that pays off in templates and tests:

```elixir
Ecto.Enum.values(Article, :status)
# => [:draft, :review, :published]

Ecto.Enum.mappings(Article, :status)
# => [draft: 0, review: 1, published: 2]

Ecto.Enum.dump_values(Article, :status)
# => [0, 1, 2]
```

`values/2` builds your select options straight from the schema, so adding a status to the field definition adds it to every form at once:

```heex
<.input field={@form[:status]} type="select" options={Ecto.Enum.values(Article, :status)} />
```

For a translated label per status, keep the atoms as the source of truth and translate at the edge rather than storing display strings in the column.

The migration side stays ordinary — the column is just an integer:

```elixir
def change do
  alter table(:articles) do
    add :status, :integer, null: false, default: 0
  end
end
```

One thing to plan for: the integers are a storage contract. Reordering the keyword list silently rewrites the meaning of every existing row, so append new values with new numbers rather than renumbering the existing ones.

[`Ecto.Enum` docs](https://hexdocs.pm/ecto/Ecto.Enum.html)


---

Created by: almirsarajcic
Date: August 21, 2026
URL: https://elixirdrops.net/d/Cog9EhkR
