Rendering a view throws error no function clause matching in Ecto.assoc_loaded

I’m rendering a view but it is throwing error while association loading. Not sure what I’m missing. Been breaking my head but unable to find solution.

Matter of fact it was working fine before with Phoenix 1.3.0 and now run into this problem after upgrading Phoenix to 1.6.15

Error:

[error] #PID<0.11572.0> running PxrfWeb.Endpoint (connection #PID<0.11571.0>, stream id 1) terminated
Server: localhost:4001 (http)
Request: GET /api/v2/media/{media_id}
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Ecto.assoc_loaded?/1
        (ecto 3.9.5) lib/ecto.ex:568: Ecto.assoc_loaded?(%{id: nil, name: nil, profile_image_media: nil})
        (pxrf 2.0.1) lib/pxrf_web/views/user_view.ex:82: PxrfWeb.UserView.render/2
        (pxrf 2.0.1) lib/pxrf_web/views/media_view.ex:321: PxrfWeb.MediaView.render/2
        (phoenix_view 2.0.2) lib/phoenix_view.ex:557: Phoenix.View.render_to_iodata/3
        (phoenix 1.6.16) lib/phoenix/controller.ex:772: Phoenix.Controller.render_and_send/4
        (pxrf 2.0.1) lib/pxrf_web/controllers/media/media_controller.ex:1: PxrfWeb.MediaController.action/2
        (pxrf 2.0.1) lib/pxrf_web/controllers/media/media_controller.ex:1: PxrfWeb.MediaController.phoenix_controller_pipeline/2
        (phoenix 1.6.16) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
        (pxrf 2.0.1) lib/pxrf_web/endpoint.ex:1: PxrfWeb.Endpoint.plug_builder_call/2
        (pxrf 2.0.1) lib/plug/debugger.ex:136: PxrfWeb.Endpoint."call (overridable 3)"/2
        (pxrf 2.0.1) lib/pxrf_web/endpoint.ex:1: PxrfWeb.Endpoint.call/2
        (phoenix 1.6.16) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4
        (stdlib 4.1.1) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

user_controller.ex

def show(conn, %{"id" => id}) do
    current_user = conn.assigns[:current_user]
    user_with_media =
      case is_nil(current_user) do
        true ->
          from(
            m in Media,
            left_join: u in assoc(m, :user),
            left_join: o in assoc(u, :organization),
            left_join: om in assoc(u, :profile_image_media),
            where: m.id == ^id,
            select: %{
              id: m.id,
              title: m.title,
              description: m.description,
              user: %{
                u
                | is_followed: false,
                  organization: %{ # <--- Not all the keys from the schema are mapped here. Only those necessary.
                    id: o.id,
                    name: o.name,
                    profile_image: %{
                         id: om.id,
                         url: om.url, 
                         height: om.height, 
                         weight: om.width
                      }
                    }
                },
              }
            }
          )
          |> Repo.one()
       
      false -> .....
       # block of code
   end
   
  conn |> render("show.json", user: user_with_media)
end

user_view.ex

def render("show.json", %{user: user}) do
    %{
      id: user.id,
      email: user.email,
      first_name: user.first_name,
      last_name: user.last_name,
      account_type: user.account_type,
      username: user.username,
      organization:
        if Ecto.assoc_loaded?(user.organization) do
          render_one(user.organization, OrganizationView, "organization.json", as: :organization)
        else
          nil
        end
     }
end

Is it because that I’m not using all the keys from the organization schema? Do I need to have all the key defined here as well inside the controller?

That almost seems to be the case.

It looks like you either need a list or a struct to pass in the Ecto.assoc_loaded?/1 fn.

Looking at the select documentation, I think you can put a organization: %Organization{} in the select statement within your user_controller.ex and then it should work

Upon updating and it throws Jason Encoder error.

Updated:

Error:

[info] Sent 500 in 372ms
[error] #PID<0.19924.0> running PxrfWeb.Endpoint (connection #PID<0.19923.0>, stream id 1) terminated
Server: localhost:4001 (http)
Request: GET /api/v2/media/{media_id}
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %Pxrf.Web.Media.UserSelection{id: "xxxxx-xxdx-xxx-xxxx", has_model_property: false} of type Pxrf.Web.UserSelection (a struct), Jason.Encoder protocol must always be explicitly implemented.

If you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:

    @derive {Jason.Encoder, only: [....]}
    defstruct ...

It is also possible to encode all fields, although this should be used carefully to avoid accidentally leaking private information when new fields are added:

    @derive Jason.Encoder
    defstruct ...

Finally, if you don't own the struct you want to encode to JSON, you may use Protocol.derive/3 placed outside of any module:

    Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])
    Protocol.derive(Jason.Encoder, NameOfTheStruct)
. This protocol is implemented for the following type(s): Any, Any, Atom, Atom, BitString, BitString, Date, Date, DateTime, DateTime, Decimal, Decimal, Ecto.Association.NotLoaded, Ecto.Schema.Metadata, Ecto.Schema.Metadata, Float, Float, Integer, Integer, Jason.Fragment, Jason.Fragment, Jason.OrderedObject, Jason.OrderedObject, List, List, Map, Map, Money, Money, NaiveDateTime, NaiveDateTime, Time, Time
        (jason 1.4.0) lib/jason.ex:213: Jason.encode_to_iodata!/2
        (phoenix 1.6.16) lib/phoenix/controller.ex:772: Phoenix.Controller.render_and_send/4
        (pxrf) lib/pxrf_web/controllers/media/media_controller.ex:1: PxrfWeb.MediaController.action/2
        (pxrf) lib/pxrf_web/controllers/media/media_controller.ex:1: PxrfWeb.MediaController.phoenix_controller_pipeline/2
        (phoenix 1.6.16) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
        (pxrf) lib/pxrf_web/endpoint.ex:1: PxrfWeb.Endpoint.plug_builder_call/2
        (pxrf) lib/plug/debugger.ex:136: PxrfWeb.Endpoint."call (overridable 3)"/2
        (pxrf) lib/pxrf_web/endpoint.ex:1: PxrfWeb.Endpoint.call/2
        (phoenix 1.6.16) lib/phoenix/endpoint/cowboy2_handler.ex:54: Phoenix.Endpoint.Cowboy2Handler.init/4

FWIW, it appears that the specific change from Phoenix 1.3 → 1.6 that triggered your original issue was this one:

In Ecto 2, calling Ecto.assoc_loaded?/1 would accept anything and return true as long as it wasn’t an unloaded association struct - so the else path in your show.json wouldn’t ever be encountered.


Regarding your second error, I’m guessing the upgrade from Phoenix 1.3 → 1.6 also included switching to Jason instead of Poison. One of the biggest differences is that Jason doesn’t automatically handle structs, which produces the exact error that you’re seeing.

Not sure how to handle the Jason error.

I have the following in my schema which is embedded

    embeds_one :user_selection, UserSelection, on_replace: :delete do
      field(:has_model, :boolean)
      field(:license, License)
    end

It throws the following error while rendering.

Request: GET /api/v2/media/{media_id}
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %Pxrf.Web.Media.UserSelection{id: "xxx-xxxx-xxxxx-xxxxx", has_model: false, license: :licensed_user} of type Pxrf.Web.Media.UserSelection (a struct), Jason.Encoder protocol must always be explicitly implemented.

If you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:

    @derive {Jason.Encoder, only: [....]}
    defstruct ...

,......

Do I have to encode this implicitly? If so how to deal with this?

Under the hood, embeds_one with block is defining a module containing an embedded_schema call.

The error message is telling you that that module needs to have the Jason.Encoder protocol implemented for it; normally this would be done with the @derive attribute mentioned. The “standard” place to define that attribute would be just before embedded_schema, but embeds_one doesn’t provide an extension point there.

There are two ways to approach this:

  • notice that @derive only needs to appear before defstruct is called in the module - and that happens after the END of the block in embedded_schema. So you can write this (see this thread for more discussion):
    embeds_one :user_selection, UserSelection, on_replace: :delete do
      @derive Jason.Encoder
      field(:has_model, :boolean)
      field(:license, License)
    end
    
  • use the “from the outside” method with Protocol.derive after embeds_one:
    embeds_one :user_selection, UserSelection, on_replace: :delete do
      @derive Jason.Encoder
      field(:has_model, :boolean)
      field(:license, License)
    end
    
    Protocol.derive(Jason.Encoder, UserSelection)
    
    I have not actually tried this.

Hey… that worked… Awesome :slight_smile: I replaced it everywhere I have an embedded_schema.