Trying to create an api json in phoenix, gettin error `protocol Jason.Encoder not implemented for `

Hello everyone,

I’m actually studying elixir and phoenix, I’m testing phoenix
I’m learning how to create a rest API JSON

I’m trying to get users and show it as JSON but when try to use json(conn, users) I get this error

protocol Jason.Encoder not implemented for %Test.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 3, email: "test@hotmail.com", encrypted_password: "qowejoqiwjeo", valid: true, password: nil, password_confirmation: nil, created_at: ~N[2023-10-05 20:00:46], updated_at: ~N[2023-10-05 20:00:46]} of type Test.Accounts.User (a struct), Jason.Encoder protocol must always be explicitly implemented.

Here is my controller

defmodule TestWeb.TestController do
  use TestWeb, :controller

  alias Test.Contexts.Users

  def test1(conn, _params) do
    users = Users.list_users() |> IO.inspect(label: "testcontroller")
    json(conn, users)
  end
end

The output of IO.inspect

testcontroller: [
  %Test.Accounts.User{
    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
    id: 3,
    email: "test@hotmail.com",
    encrypted_password: "qowejoqiwjeo",
    valid: true,
    password: nil,
    password_confirmation: nil,
    created_at: ~N[2023-10-05 20:00:46],
    updated_at: ~N[2023-10-05 20:00:46]
  }
]

I discover that if I delete some elements from the struct

defmodule TestWeb.TestController do
  use TestWeb, :controller

  alias Test.Contexts.Users

  def test1(conn, _params) do
    users = Users.list_users() |> IO.inspect(label: "testcontroller")
    # This way 
    # users = users |> Enum.map(&(Map.delete(&1, :__meta__) |> Map.from_struct())) 
    # or this way
    users = users |> Enum.map(&Map.drop(&1, [:__meta__, :__struct__]))
    json(conn, users)
  end
end

I got the result in explorer

[{"id":3,"valid":true,"password":null,"email":"test@hotmail.com","password_confirmation":null,"created_at":"2023-10-05T20:00:46","encrypted_password":"123123","updated_at":"2023-10-05T20:00:46"},{"id":4,"valid":false,"password":null,"email":"test@gmail.com","password_confirmation":null,"created_at":"2023-10-05T20:39:23","encrypted_password":"123123123","updated_at":"2023-10-05T20:39:23"}]

My questions are the next

  • I don’t know if I’m doing it correctly, but this is the right way?
  • I need always to delete :__meta__ and :__struct__ and others elements to work correctly?
  • I discovery that you can create a module to have the bodies format o similar to this:
defmodule TestWeb.TestJSON do
  def all(%{users: users}) do
    for(user <- users, do: data(user)
  end
  
  defp data(%User{} = user) do
    %{
      id: user.id,
      emai: user.email,
      valido: user.valid,
      creado: user.created_at,
      modificado: user.updated_at
    }
  end
end

# And use in the controller like this...
defmodule TestWeb.TestController do
  use TestWeb, :controller

  alias Test.Contexts.Users

  def test1(conn, _params) do
    users = Users.list_users()
    render(conn, :all, users: users)
  end
end

  • Another question, is there a way to render, sending all users like this render(conn, :all, users) and receive like this def all(users) do ?

No need to touch special struct keys… You could always use Map.from_struct

The way to render struct to json is with the @derive attribute

Or in a module that produces your custom json (like in api views…)

2 Likes

Just do:

users = users |> Enum.map(&Map.from_struct/1)
json(conn, users)

Unless you really want a custom encoder – which would likely just skip a few fields? – then that’s a good way to do it.

1 Like

If I only use

users = users |> Enum.map(&Map.from_struct/1)
json(conn, users)

I get an error of

cannot encode metadata from the :__meta__ field for Test.Accounts.User to JSON. This metadata is used internally by Ecto and should never be exposed externally.

You can either map the schemas to remove the :__meta__ field before encoding to JSON, or explicit list the JSON fields in your schema:

    defmodule Test.Accounts.User do
      # ...

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

That’s why I use this:

users = users |> Enum.map(&(Map.from_struct(&1)|>Map.delete( :__meta__)))
# or
users = users |> Enum.map(&Map.drop(&1, [:__meta__, :__struct__]))