Validating API payloads with Ecto schemaless changesets.

The Ecto library is one of Elixir’s secret weapons and many software engineers are becoming aware of its strengths as a general schema validation tool and not just an ORM. If you are thinking of using Ecto to validate the schemas of your HTTP API payloads (either body or query) then I’d encourage you to do so since my experience has been overwhelmingly positive. If you’re already using Ecto for database queries then using it for data validation let’s you leverage your existing experience with the library and has the added benefit of keeping your dependencies leaner and meaner. This is a quick post to highlight an approach to doing it with Ecto schemaless changesets and why doing so might be better than using full Ecto schemas if your schemas do not map directly to a table in a database.

What is a schemaless changeset?

A schemaless changeset is a changeset that validates data without requiring you to define an Ecto.Schema struct as a base. It’s useful if you’re validating data not directly connected to a database.

Advantages of schemaless changesets

  1. Flexibility: Schemaless changesets allow you to cast your data onto a map instead of a struct which means you will only be returned values for the properties that you passed in. This is especially useful when arguments are optional and you want to be able to distinguish between an argument that was passed as nil and an argument that wasn’t passed at all. For example, in a PUT request passing null for a property might indicate that you want to set that property to null whereas not passing that property at all indicates that you don’t want to make changes to the property.

  2. Clarity: It is clearer from reading a schemaless changeset that it does not represent a database table and exists for data validation.

Let me show you what they look like in practice - this is an example validator that validates the query parameters for a list of blog articles.

defmodule Web.Validators.ArticleListParams do
  alias Ecto.Changeset
  import Ecto.Changeset

  # Define your data schema
  @types %{
    sort: :string,
    include_published: :boolean
  }

  def changeset(attrs) do
    # Perform validations as normal
    {%{}, @types}
    |> cast(attrs, Map.keys(@types))
    |> validate_required([:sort, :include_published])
    |> validate_inclusion(:sort, ["date", "author"])
  end

  def validate_params(params) do
    params
    |> changeset()
    |> apply_action(nil)
  end
end

and here’s how you use them:

# Passing a single argument. Note that unlike full changesets which
# return elixir structs with nil values for unpassed items, schemaless
# changesets return only the data you pass in.
Web.Validators.ArticleListParams.validate_params(%{
    "sort": "date"
})
-> {:ok, %{sort: "date"}}


# Here's how errors are returned
Web.Validators.ArticleListParams.validate_params(%{
    sort: 123
})
-> {:error,
 #Ecto.Changeset<
   action: nil,
   changes: %{},
   errors: [sort: {"is invalid", [type: :string, validation: :cast]}],
   data: %{},
   valid?: false
 >}

Full documentation can be found in the Ecto documentation.

eternal cyclic renewal