elixirphoenix-frameworkelixir-poison

Poison unable to encode error message


I am using phoenix as a JSON API. One of my models is as below which works well unless the unique_constraint check fails. When that happens I get the following error:

(Poison.EncodeError) unable to encode value: {"Email address is already registered", []}

Model:

defmodule MyApp.Registration do
  use MyApp.Web, :model

  @derive {Poison.Encoder, only: [:name, :email, :category]}
  schema "registrations" do
    field :name, :string
    field :category, :string
    field :email, :string

    timestamps
  end

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, ~w(name email category), [])
    |> validate_length(:name, min: 1, max: 240)
    |> unique_constraint(:email, message: "Email address is already registered")
  end

end

Controller:

def create(conn, registration_params) do
  changeset = Registration.changeset(%Registration{}, registration_params)
  case  Repo.insert(changeset) do
    {:ok, _registration} ->
      # Success 
    {:error, error} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(MyApp.ErrorView, "generic.json", error: error)
  end
end

View:

def render("generic.json", error) do
  error
end

I think I probably need to somehow add the error messages to the Poison.Encoder, only: [] list but I am unsure how to do that.

Edit

I should clarify that if I don't specify a custom error I still get the same error with the generic error message.


Solution

  • I think this is done automatically if you use mix phoenix.gen.json but the changeset errors need to be translated to json.

    View:

    def translate_errors(changeset) do
      Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
    end
    
    def render("error.json", %{changeset: changeset}) do
      # When encoded, the changeset returns its errors
      # as a JSON object. So we just pass it forward.
      %{errors: translate_errors(changeset)}
    end
    

    Controller:

    def create(conn, registration_params) do
      changeset = Registration.changeset(%Registration{}, registration_params)
      case  Repo.insert(changeset) do
        {:ok, _registration} ->
          # Success 
        {:error, changeset} ->
          conn
          |> put_status(:unprocessable_entity)
          |> render(MyApp.ErrorView, "error.json", changeset: changeset)
      end
    end
    

    EDIT

    Adding the translate_error\1 function definition. This is contained within a module called MyApp.ErrorHelpers which is imported into the view function definition in either web/my_app.ex or lib/my_app_web.ex depending on your version of phoenix.

    lib/my_app_web/views/error_helpers.ex:

    defmodule MyAppWeb.ErrorHelpers do
      def translate_error({msg, opts}) do
        if count = opts[:count] do
          Gettext.dngettext(MyAppWeb.Gettext, "errors", msg, msg, count, opts)
        else
          Gettext.dgettext(MyAppWeb.Gettext, "errors", msg, opts)
        end
      end
    end