We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Schemaless changesets validate forms without the database
almirsarajcic
0 comments
Copy link
Need to validate form data, preview changes, or validate multi-step workflows without hitting the database? Use schemaless changesets with Ecto.Changeset.apply_action/2 to get validation errors and transformed data.
# ✅ Schemaless changeset for contact form validation
defmodule MyApp.ContactForm do
import Ecto.Changeset
@types %{
name: :string,
email: :string,
message: :string,
subscribe: :boolean
}
def changeset(params) do
{%{}, @types} # Empty map + type definition
|> cast(params, Map.keys(@types)) # Cast params to types
|> validate_required([:name, :email, :message]) # Required fields
|> validate_format(:email, ~r/@/) # Email format
|> validate_length(:message, min: 10, max: 1000) # Message length
end
def validate(params) do
params
|> changeset()
|> apply_action(:insert) # Returns {:ok, data} or {:error, changeset}
end
end
Why this works:
{%{}, @types}- Creates changeset without Ecto schema (just a map + type spec)apply_action(:insert)- Validates and returns clean data, or errors if invalid- No database - Perfect for forms that don’t map to a single table
Using in LiveView forms:
defmodule MyAppWeb.ContactLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(ContactForm.changeset(%{})))}
end
def handle_event("validate", %{"contact" => params}, socket) do
changeset =
params
|> ContactForm.changeset()
|> Map.put(:action, :validate) # Show errors immediately
{:noreply, assign(socket, form: to_form(changeset))}
end
def handle_event("submit", %{"contact" => params}, socket) do
case ContactForm.validate(params) do
{:ok, valid_data} ->
# Send email, save to external API, etc.
send_contact_email(valid_data)
{:noreply, put_flash(socket, :info, "Message sent!")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
end
Multi-step form validation:
defmodule MyApp.RegistrationWizard do
import Ecto.Changeset
@step1_types %{name: :string, email: :string}
@step2_types %{company: :string, role: :string}
@step3_types %{password: :string, password_confirmation: :string}
def validate_step(1, params) do
{%{}, @step1_types}
|> cast(params, [:name, :email])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> apply_action(:insert) # Check if step 1 is valid
end
def validate_step(2, params) do
{%{}, @step2_types}
|> cast(params, [:company, :role])
|> validate_required([:company, :role])
|> apply_action(:insert)
end
def validate_step(3, params) do
{%{}, @step3_types}
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_length(:password, min: 12)
|> validate_confirmation(:password)
|> apply_action(:insert)
end
def complete_registration(all_params) do
# All steps validated, now save to database
with {:ok, step1} <- validate_step(1, all_params),
{:ok, step2} <- validate_step(2, all_params),
{:ok, step3} <- validate_step(3, all_params) do
# Combine validated data and insert
user_params = step1 |> Map.merge(step2) |> Map.merge(step3)
%User{}
|> User.changeset(user_params)
|> Repo.insert()
end
end
end
API parameter validation:
defmodule MyApp.API.SearchParams do
import Ecto.Changeset
@types %{
query: :string,
page: :integer,
per_page: :integer,
sort_by: :string,
order: :string
}
def validate(params) do
{%{}, @types}
|> cast(params, Map.keys(@types))
|> validate_required([:query])
|> validate_number(:page, greater_than: 0)
|> validate_number(:per_page, greater_than: 0, less_than_or_equal_to: 100)
|> validate_inclusion(:sort_by, ["relevance", "date", "title"])
|> validate_inclusion(:order, ["asc", "desc"])
|> apply_defaults()
|> apply_action(:insert)
end
defp apply_defaults(changeset) do
changeset
|> put_default(:page, 1)
|> put_default(:per_page, 20)
|> put_default(:sort_by, "relevance")
|> put_default(:order, "desc")
end
defp put_default(changeset, field, value) do
case get_field(changeset, field) do
nil -> put_change(changeset, field, value)
_ -> changeset
end
end
end
The power of apply_action:
# Returns {:ok, %{name: "Alice", email: "alice@example.com"}} if valid
ContactForm.validate(%{"name" => "Alice", "email" => "alice@example.com"})
# Returns {:error, %Ecto.Changeset{}} with errors if invalid
ContactForm.validate(%{"name" => "", "email" => "invalid"})
When to use schemaless changesets:
- ✅ Contact forms, search forms, filters (no database backing)
- ✅ Multi-step wizards (validate each step independently)
- ✅ API parameter validation (type coercion + validation)
- ✅ Form previews (validate before commit)
- ✅ External API integrations (validate before sending)
Pro tips:
- Use
:insert,:update, or:deletewithapply_action- they’re semantically equivalent for schemaless changesets - Combine multiple changesets with
withfor complex validation flows - Add custom validators with
validate_change/3just like schema changesets - Use
Ecto.Changeset.traverse_errors/2to format errors for APIs
Links:
copied to clipboard