Encoding Ecto Validation Errors in Phoenix 1.3

October 23, 2017 • 3 minute read • @mitchhanbergAnalytics


I recently ran into this error while implementing the first endpoint of my Phoenix JSON API.

** (Poison.EncodeError) unable to encode value: {:username, {"has already been taken", []}}

After a bit of googling and detective work, I found the offending piece of code, located in my error_view.ex file.

def render("409.json", %{changeset: changeset}) do
    status: "failure",
    errors: changeset.errors # this line causes the error

This function handles rendering the JSON payload that the controller sends back to the client when there is an error.

The errors property of the changeset struct is a keyword list* of error's, with error being a type defined in the Changeset module.

@type error :: {String.t, Keyword.t}

Poison is not able to encode this, so a Poison.EncodeError error is raised.

* It's important to remember that a keyword list is a list of 2-item tuples with the first item of the tuple being an atom. So the error we originally saw was the key-value pair that couldn't be encoded, shown in tuple form.


If you created your Phoenix app when Phoenix was at v1.3, then you should have this function in the /lib/your_app_web/views/error_helpers.ex file. If not, go ahead and paste it in that file.

@doc """
  Translates an error message using gettext.
  def translate_error({msg, opts}) do
    # Because error messages were defined within Ecto, we must
    # call the Gettext module passing our Gettext backend. We
    # also use the "errors" domain as translations are placed
    # in the errors.po file.
    # Ecto will pass the :count keyword if the error message is
    # meant to be pluralized.
    # On your own code and templates, depending on whether you
    # need the message to be pluralized or not, this could be
    # written simply as:
    #     dngettext "errors", "1 file", "%{count} files", count
    #     dgettext "errors", "is invalid"
    if count = opts[:count] do
      Gettext.dngettext(ContactWeb.Gettext, "errors", msg, msg, count, opts)
      Gettext.dgettext(ContactWeb.Gettext, "errors", msg, opts)

And then we make the following change.

def render("409.json", %{changeset: changeset}) do
    status: "failure",
-   errors: changeset.errors # this line causes the error
+   errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)

Here we use the Ecto.Changeset.traverse_errors/2 function to apply the translate_errors/1 function to each error, which will return a map that can then be encoded by Poison.

Here is the JSON that we can now render and send to the client!

  "status": "failure",
  "errors": {
    "email": [
        "has already been taken"

If you found this helpful, please let me know! You can find me on twitter as @mitchhanberg or you can shoot me an email.

If you want to stay current with what I'm working on and articles I write, join my mailing list!

I seldom send emails, and I will never share your email address with anyone else.