Rendering structs in phoenix 1.5

After update to phoenix 1.5 (from 1.4) my controllers fail to render data structs. I’m not sure if this is a bug in pheonix code, documentation or am I doing something wrong/misunderstanding (most likely).

When using phoenix 1.4 I was able to:
in controller

  def forum_example(conn, params) do
    result = MyApp.calculate_response(params) # result is Struct

    conn
    |> Conn.put_status(result.status)
    |> put_view(MyApp.JsonView)
    |> render("forum_example.json", result)
  end

then in view:

  def render("forum_example.json", %{values: value} = _result) do
    %{
      value: value |> build_for_view() |> filter()
    }
  end

Basically I would pass MyApp.Struct around and pattern match against needed values in my MyApp.JsonView function call.
This worked fine with phoenix 1.4 and also this adheres to Phoenix.Controller.render/3 function spec which says that map() is accepted as assigns param (we know that struct is a map).

But it does not work in phoenix 1.5, because of this change
This change uses Map.new() to “convert” assigns to map, instead of just checking if assigns are map or list and doing :maps.from_list in latter case. But Map.new() accepts only Enumerable.t(), which structs are not. So this breaks specification of Phoenix.Controller.render/3 which says that maps are accepted (and structs, by extension?). Also it breaks my code :expressionless:

So is this bug in code, bug in documentation or am I just misunderstanding and doing it incorrectly?

1 Like

Map implements Enumerable, and structs are maps, so they implement it too when seen as maps. EDIT: structs in general do not implement Enumerable, (unless one implements it for a particular struct) you are right, as @LostKobrakai also noted below.

That’s not correct here. Protocol dispatching doesn’t happen on the level of beam datatypes, therefore structs are not Enumerable.t automatically. It doesn’t make much sense to e.g. enumerate over a %Task{} and %Date.Range{} or %MapSet{} don’t just enumerate over struct fields as well.

1 Like

Oh, you are totally right, I posted without thinking it through, thanks for correcting me :slightly_smiling_face:

It’s for sure a breaking change, but I think the intend was always for the function to receive data, which is enumerable – a collection of various data needed to render the view. It already was always normalized to a map. Most often you’ll see a keyword list used to send down many things to the view.

Yep, I’d agree it’s a breaking change, even though the fact that structs could be passed before was probably incidental. The documentation saying that a Map can be passed is technically correct, as you can pass a map, but possibly confusing in light of the “structs are maps“ notion.

The problem is that the “structs are maps” is not necessarily true, depending on context. For the purpose of dispatching, they are not maps. I definitely got confused by this in my comment before.

You surely know already, but for the benefits of others hitting the same issue, one solution would be to turn the struct into a map with Map.from_struct/1.

1 Like

Yes, Map.from_struct/1 works fine. Implementing protocol for my structs also worked (copy/pasted elixir’s own enumerable implementation for Map.
I have a lot of controllers that pass the struct to render/3 :sweat_smile:

I’ll open a issue or even do a PR then. I suppose documentation should be more clear for controller’s render/3.

2 Likes