You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Phoenix视图渲染报错:Protocol.UndefinedError(Enumerable未实现)

问题:Phoenix API渲染响应时遇到Enumerable未实现错误

本人是Elixir、Phoenix/Ecto及Erlang新手,目前正通过TDD方式搭建全新API。参照现有可用示例定义Model、View和Controller,但创建Conversation并尝试渲染响应时,持续遇到Protocol.UndefinedError: protocol Enumerable not implemented错误。

以下是相关代码片段及错误输出:

路由代码

scope "/conversations" do
  post "/", ConversationController, :create
end

控制器代码

defmodule ThingyWeb.ConversationController do
  use ThingyWeb, :controller

  alias Thingy.Conversations.Conversation
  alias Thingy.Repo

  def create(conn, conversation_params) do
    case Thingy.Auth.validate_session(conn) do
      {:ok, _} ->
        result =
          %Conversation{}
          |> Conversation.changeset(conversation_params)
          |> Repo.insert()

        case result do
          {:ok, conversation} ->
            IO.puts("store to db went well, now trying to render response")
            IO.inspect(conversation)
            conn
            |> put_status(201)
            |> render("created.json", conversation)  # 问题所在行

          {:error, _} ->
            conn
            |> put_status(400)
            |> render("error.json", %{message: "Unable to process conversation as provided"})
        end

      {:error, "No token"} ->
        conn
        |> put_status(401)
        |> render("error.json", %{message: "Authentication failed (no token)"})

      {:error, "Not found"} ->
        conn
        |> put_status(404)
        |> render("error.json", %{message: "Authentication failed (not found)"})
    end
  end
end

模型代码

defmodule Thingy.Conversations.Conversation do
  use Ecto.Schema
  import Ecto.Changeset

  schema "conversations" do
    field :title, :string
    field :start_date_time, :utc_datetime

    timestamps()
  end

  @doc false
  def changeset(conversation, attrs) do
    conversation
    |> cast(attrs, [:title, :start_date_time])
    |> validate_required([:title, :start_date_time])
  end
end

视图代码

defmodule ThingyWeb.ConversationView do
  use ThingyWeb, :view

  def render("error.json", %{message: message}) do
    %{
      errors: [message]
    }
  end

  def render("created.json", %{conversation: conversation}) do
    render_one(conversation, ConversationView, "conversation.json")
  end

  def render("conversation.json", %{conversation: conversation}) do
    %{
      id: conversation.id,
      title: conversation.title
    }
  end
end

测试代码

describe "Conversation operations" do
  setup [:login_user]

  test "able to provide details of a new conversation, and receive the assigned id in response",
       %{
         conn: conn,
         authentication: authentication
       } do
        IO.puts("starting problem test")
    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> authentication.meallogger_token)
      |> put_req_header("content-type", "application/json")
      |> post(
        Routes.conversation_path(conn, :create),
        %{
          title: "new conversation",
          start_date_time: "2024-06-14T15:30:00Z",
        }
      )

    resp = json_response(conn, 201)

    assert %{
             "id" => _id,
           } = resp

    case validate_return_properties(resp, @expected_create_response_properties) do
      {:error, extra_keys} ->
        assert false, "there were extraneous keys in the json response: #{extra_keys}"
    end
  end
end

错误输出

starting problem test
store to db went well, now trying to render response
%Metabite.Conversations.Conversation{
  __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">,
  id: 50,
  title: "new conversation",
  start_date_time: ~U[2024-06-14 15:30:00Z],
  inserted_at: ~N[2024-06-07 06:30:42],
  updated_at: ~N[2024-06-07 06:30:42]
}
Mix task exited with reason
normal
returning code 0

1) test Conversation operations able to provide details of a new conversation, and receive the assigned id in response (ThingyWeb.ConversationControllerTest)
     test/thingy_web/controllers/conversation_controller_test.exs:50
     ** (Protocol.UndefinedError) protocol Enumerable not implemented for %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1}, body_params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, cookies: %{}, halted: false, host: "www.example.com", method: "POST", owner: #PID<0.587.0>, params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, path_info: ["api", "v1", "conversations"], path_params: %{}, port: 80, private: %{ThingyWeb.Router => {[], %{PhoenixSwagger.Plug.SwaggerUI => []}}, :phoenix_view => ThingyWeb.ConversationView, :phoenix_template => "created.json", :phoenix_router => ThingyWeb.Router, :phoenix_endpoint => ThingyWeb.Endpoint, :phoenix_action => :create, :phoenix_controller => ThingyWeb.ConversationController, :before_send => [#Function<0.54455629/1 in Plug.Telemetry.call/2>], :plug_session_fetch => #Function<1.76384852/1 in Plug.Session.fetch_session/1>, :plug_skip_csrf_protection => true, :phoenix_recycled => true, :phoenix_request_logger => {"request_logger", "request_logger"}, :phoenix_format => "json", :phoenix_layout => {ThingyWeb.LayoutView, :app}}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{}, req_headers: [{"accept", "application/json"}, {"authorization", "Bearer auth-token-123"}, {"content-type", "application/json"}], request_path: "/api/v1/conversations", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "F9alGnAy2l0PbroAAAbB"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: 201}, __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1} of type Thingy.Conversations.Conversation (a struct). This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, JasonV.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Stream
     code: |> post(
     stacktrace:
       (elixir 1.16.3) lib/enum.ex:1: Enumerable.impl_for!/1
       (elixir 1.16.3) lib/enum.ex:166: Enumerable.reduce/3
       (elixir 1.16.3) lib/enum.ex:4396: Enum.reverse/1
       (elixir 1.16.3) lib/enum.ex:3726: Enum.to_list/1
       (elixir 1.16.3) lib/map.ex:224: Map.new_from_enum/1
       (phoenix_view 2.0.2) lib/phoenix_view.ex:370: Phoenix.View.render/3
       (phoenix_view 2.0.2) lib/phoenix_view.ex:557: Phoenix.View.render_to_iodata/3
       (phoenix 1.6.15) lib/phoenix/controller.ex:772: Phoenix.Controller.render_and_send/4
       (thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.action/2
       (thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.phoenix_controller_pipeline/2
       (phoenix 1.6.15) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
       (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.plug_builder_call/2
       (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint."call (overridable 3)"/2
       (thingy 0.1.1) deps/plug/lib/plug/debugger.ex:136: ThingyWeb.Endpoint."call (overridable 4)"/2
       (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.call/2
       (phoenix 1.6.15) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
       test/thingy_web/controllers/conversation_controller_test.exs:60: (test)

解决方案

问题出在控制器的render调用逻辑上:

render("created.json", conversation)

Phoenix的render/3函数要求第二个参数是包含模板所需变量的映射(map),而非直接传入struct。当你直接传递conversation这个Ecto struct时,Phoenix会尝试将其转换为映射,但Ecto struct本身并非Enumerable类型,因此抛出Protocol.UndefinedError。

修改控制器中的这一行代码,将struct包装成键值对形式的映射:

render("created.json", %{conversation: conversation})

修改后,视图中的render("created.json", %{conversation: conversation})就能正确接收conversation变量,调用render_one渲染单个资源,问题即可解决。


内容的提问来源于stack exchange,提问作者Timo Newton

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 16:07:02