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.