Problem
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
}
end
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.
Solution
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)
else
Gettext.dgettext(ContactWeb.Gettext, "errors", msg, opts)
end
end
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)
}
end
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.