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) == []
      end
    
      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", []}]
      end
    end
  

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
        user
          |> cast(params, [:id, :username, :password, :icon, :invite, :hash])
          |> validate_required([:username, :password])
          |> validate_invite
      end
    
      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")
        end
      end
    end
  

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


Buy Me a Coffee

Twitter / Github / Email