Prettify Ecto errors

Written by: David Roman

By Sergey Gernyak, Back-End Engineer.


I came into Elixir from the Ruby and Rails world.

RoR has many cool libraries out of box, and, well, actually we are spoiled by the proposed functionality of them. Ecto was created with another philosophy to the ActiveRecord, but the ideas around the flow are similar.

For example, validation and getting error messages. In RoR using the ActiveRecord library you do the following:

post = Post.create title: '', body: ''
post.valid? # => false
post.errors.full_messages # => ["Title should not be blank!"]

With Ecto, this kind of code can look like the following:

{:error, changeset} = %Post{}
                      |> Post.changeset(%{title: "", body: ""})
                      |> Repo.insert
changeset.valid? # => false
changeset.errors # => [title: {"should not be blank", []}]

But… there is no built-in approach to get a complete list of error messages. There is only the traverse_errors/2 function where you can find an example of how to go through each error and process it.

During the development of one of my Elixir applications, I wrote the code below as preparation to prettify errors:


defmodule EctoHelper do
  @moduledoc """
  Provides helper functions
  """

  @doc """
  Prettifies changeset error messages.
  By default `changeset.errors` returns errors as keyword list, where key is name of the field
  and value is part of message. For example, `[body: "is required"]`.
  This method transforms errors in list which is ready to pass it, for example, in response of
  a JSON API request.
  ## Example of basic usage
  ```elixir
  EctoHelper.pretty_errors([body: "is required"]) # => ["Body is required"]
  ```
  ## Example of usage with interpolations
  ```elixir
  EctoHelper.pretty_errors([login: {"should be at most %{count} character(s)", [count: 10]}])
  # => ["Login should be at most 10 character(s)"]
  ```
  """
  @spec pretty_errors(Map.t) :: [String.t]
  def pretty_errors(errors) do
    errors
    |> Enum.map(&do_prettify/1)
  end

  defp do_prettify({field_name, message}) when is_bitstring(message) do
    human_field_name = field_name
                        |> Atom.to_string
                        |> String.replace("_", " ")
                        |> String.capitalize
    human_field_name <> " " <> message 
  end
  defp do_prettify({field_name, {message, variables}}) do
    compound_message = do_interpolate(message, variables)
    do_prettify({field_name, compound_message})
  end

  defp do_interpolate(string, [{name, value} | rest]) do
    n = Atom.to_string(name)
    msg = String.replace(string, "%{#{n}}", do_to_string(value))
    do_interpolate(msg, rest)
  end
  defp do_interpolate(string, []), do: string

  defp do_to_string(value) when is_integer(value), do: Integer.to_string(value)
  defp do_to_string(value) when is_bitstring(value), do: value
  defp do_to_string(value) when is_atom(value), do: Atom.to_string(value)
end
defmodule EctoHelperTest do
  use ExUnit.Case, async: true

  test "prettify simple errors" do
    res = EctoHelper.pretty_errors([body: "is required"])
    assert ["Body is required"] = res
  end

  test "prettify errors with variables" do
    res = EctoHelper.pretty_errors([
      login: {"should be at most %{count} character(s)", [count: 10]},
      message_body: "should not be blank"
    ])
    assert String.equivalent?(Enum.at(res, 0), "Login should be at most 10 character(s)")
    assert String.equivalent?(Enum.at(res, 1), "Message body should not be blank")
  end
end

I hope this helps.

Enjoy! Happy hacking!

(Visited 321 times, 1 visits today)
Last modified: April 17, 2020
Author info
David Roman
David is our content guru. He helps us and our clients with crafting content strategies and producing content that might actually stop you from scrolling. He also enjoys green tea and using his analogue camera.
Close