Writing Custom Validations With Ecto 3

Published February 25, 2019 by Toran Billups

Recently I launched my first Phoenix app Elixir Match and along the way I found a handful of situations that didn't offer any useful search results. I've since decided to pull off an item from my backlog and blog about it weekly to help fill the void and push something useful to the top for anyone who might follow in my footsteps.

The problem

Today I'll be showing how to write a custom validation for Ecto 3 and Phoenix 1.4. In my specific example the signup form required an "invite code" that players would use to create an account. I wanted an excuse to learn more about Ecto so I decided to write this logic as part of the changeset. The unit tests below shows how this changeset will function when the invite code is both correct and incorrect.

    defmodule Match.UsersTest do
      use Match.DataCase, async: true
      alias Match.User
      @id "A4E3400CF711E76BBD86C57CA"
      @valid_attrs %{id: @id, username: "toran", password: "abcd1234"}
      test "changeset is fine with correct invite code" do
        attrs = @valid_attrs |> Map.put(:invite, "elixir2019")
        changeset = User.changeset(%User{}, attrs)
        assert changeset.valid?
        assert Map.get(changeset, :errors) == []
      test "changeset is invalid if invite is wrong" do
        attrs = @valid_attrs |> Map.put(:invite, "foo")
        changeset = User.changeset(%User{}, attrs)
        refute changeset.valid?
        assert Map.get(changeset, :errors) == [invite: {"invalid invite code", []}]

Reading the documentation combined with a little trial and error helped me find a solution that wasn't all that different than what I found looking at previous versions of Elixir and Ecto but enough that I wanted to blog about it for anyone else scratching their head like I was.

    defmodule Match.User do
      use Ecto.Schema
      import Ecto.Changeset
      def changeset(user, params \\ %{}) do
          |> cast(params, [:id, :username, :password, :icon, :invite, :hash])
          |> validate_required([:username, :password])
          |> validate_invite
      defp validate_invite(%Ecto.Changeset{} = changeset) do
        value = Map.get(changeset.changes, :invite)
        case value == "elixir2019" do
          true -> changeset
          false -> add_error(changeset, :invite, "invalid invite code")

You can find the full source code for this custom validation on github

Buy Me a Coffee

Twitter / Github / Email