Error json encoding struct - `no function clause matching in Jason.Encoder.App.Item."-inlined-encode/2-"/2

I’ve researched for a while but I can’t seem to get this to work properly and it’s been a frustrating experience, as everything I do keeps getting me this error.

I have the following schema:

  @derive {Jason.Encoder, except: [:__meta__, :__struct__, :timer, :inserted_at, :updated_at]}
  schema "items" do
    field :person_id, :integer
    field :status, :integer
    field :text, :string

    has_many :timer, Timer
    many_to_many(:tags, Tag, join_through: ItemTag, on_replace: :delete)

    timestamps()
  end

I’m using the @derive annotation to properly decode so I can return the item to the user.
I have this simple function that preloads the tags associated to an item accordingly.

  def get_item(id, preload_tags \\ false ) do
    item = Item
    |> Repo.get(id)

    if(preload_tags == true) do
      item |> Repo.preload(tags: from(t in Tag, order_by: t.text))
    else
      item
    end
  end

The problem is that, in the API controller…

          # `item` is the object returned from `get_item`. Assume an `item` is found.
          item ->
            if retrieve_tags do
              json(conn, item)
            else
              item = Map.drop(item, [:tags, :timer])
              json(conn, item)
            end

if I try to return an item without tags (else statement), I keep getting the same error.

no function clause matching in Jason.Encoder.App.Item."-inlined-encode/2-"/2

If I try to to not drop the :tags property, I get the oh-so familiar error of Jason.Encoder not being able to encode the association.

** (RuntimeError) cannot encode association :tags from App.Item to JSON because the association was not loaded.

You can either preload the association:

    Repo.preload(App.Item, :tags)

Or choose to not encode the association when converting the struct to JSON by explicitly listing the JSON fields in your schema:

    defmodule App.Item do
      # ...

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

I want to be able to just return the item without tags if the user doesn’t want to. Returning with tags works fine, it’s just that the without tags always errors out with the aforementioned error.

Is my @derive annotation wrong? It’s the only way I found to serialize tags property either if it exists or doesn’t.

Thank you very much and I hope you have a wonderful day!

An update on this:

Apparently, if I remove the :__struct__ field, it works.

          item ->
            if retrieve_tags do
              json(conn, item)
            else
              item = Map.drop(item, [:tags, :__struct__])
              json(conn, item)
            end

This is weird, because Jason.Encoder’s documentation explicitly says:

By default all keys except the :__struct__ key are encoded.

Perhaps I’m going about this the wrong way :thinking:

There’s a chance that when you specify keys to not encode with :except, the default gets overridden? I’m not super familiar with Jason internals. In either case, I’d recommend opening an issue in the repo, at the very least I bet the error message could be improved :wink:

1 Like

I would try removing the special fields from the derive line. No need to be clever, this should be handled for you automatically.

1 Like

Thanks for the answer, guys!

The reason this stopped working in the else statement of

          item ->
            if retrieve_tags do
              json(conn, item)
            else
              item = Map.drop(item, [:tags, :__struct__])
              json(conn, item)
            end

is because when I use Map.drop/2 to remove the keys, the item is no longer a struct. Jason.Encode only works on structs of the specified Item schema, hence why it fails.

I understand why it fails but it’s a wee annoying that there’s no simple way of “bypassing” or doing a custom implementation whenever a field has an Ecto.NotLoaded value.

Basically…

item #=> %App.Item{
  __meta__: #Ecto.Schema.Metadata<:loaded, "items">,
  id: 1,
  person_id: 0,
  status: 2,
  text: "random text",
  timer: #Ecto.Association.NotLoaded<association :timer is not loaded>,
  tags: #Ecto.Association.NotLoaded<association :tags is not loaded>,
  inserted_at: ~N[2023-01-26 17:05:45],
  updated_at: ~N[2023-01-26 17:05:45]
}

it fails in tags fields because @derive {Jason.Encoder, except: [:timer, :inserted_at, :updated_at]} doesn’t know what to do with the Ecto.Association.NotLoaded value.

Thanks for the response though, guys! Thoroughly appreciated!