Better Jason encoder for schemas automatically

I got this warning

warning: the Jason.Encoder protocol has already been consolidated, an implementation for Any has no effect. If you want to implement protocols after compilation or during tests, check the “Consolidation” section in the documentation for Kernel.defprotocol/2 lib/encoder.ex:1

warning: redefining module Jason.Encoder.Any (current version loaded from /Users/dev/projects/haitracker.com/haitracker-be/_build/dev/lib/jason/ebin/Elixir.Jason.Encoder.Any.beam) lib/encoder.ex:1

with the following encoder

defimpl Jason.Encoder, for: Any do
  def encode(%{__struct__: _} = struct, _options) do
    skip_keys =
      case struct.__struct__ do
        Haitracker.User ->
          [
            :local_password_hash,
            :login_status_message
          ]

        # TODO: define skip keys for each model and pass to this function
        _whatever ->
          []
      end

    struct
    |> Map.from_struct()
    |> sanitize_map(skip_keys)
    |> Jason.encode!()
  end

  defp sanitize_map(map, skip_keys) do
    filter = fn {key, val} ->
      cond do
        key in [:__meta__, :__struct__] ->
          false

        is_map(val) ->
          Ecto.assoc_loaded?(val)

        key not in skip_keys ->
          true

        true ->
          false
      end
    end

    map
    |> Enum.filter(filter)
    |> Enum.into(%{})
  end
end

I am wondering if there is a better way to encode all of the schemas I have using Jason with just single encoder config file so that all of the encoding options are defined at one place.

Requirement is to exclude __meta__ and __struct__ fields and if some association is not loaded, we also need to skip these fields. you can call these keys to be global skip keys and values. and I want to exclude few keys per schema/model. i.e local skip_keys

:wave:

Why not use @derive for each supported type? Sure, it’s not as automatic as what you want, but it’d be less error-prone since it’s more explicit. And it would usually be just a few lines of code, which you can actually automate with a macro for your ecto schemas.

It would be probably more performant than what you’ve shown above as well …

What is the use case here? Are you defining these encoders to return JSON for an HTTP endpoint? If so you probably want to look into json views which deal with a variety of downsides that the encoder approach creates.

Yes.

But I am using just one single common view and want to do generic code because code is being used for admin panel. and logic is mostly same

Because sometimes lets say I have the associations loaded based on some condition and sometimes not. I need to make it generic so that I can use for admin panel. Using @derive means static.

You can make any function generic by putting it somewhere common and calling it from both views. You don’t need to use an encoder to do this.

You can define an encoder for ecto’s Ecto.Association.NotLoaded then.

1 Like

Can I skip the field completely because if I encode Ecto.Association.NotLoaded and return nil that is totally incorrect. Association is just not loaded, doesn’t mean its having nil value

You can return "NotLoaded". Following your logic, skipping it would be incorrect as well (the field does exist).

1 Like

this seems workable but problem is Front end devs wants consistent behaviour. i.e if some field is object, it should always be an object.

If that’s your requirement it seems that you need to load the association(s).

Then maybe return an object

{
  name: "Association.NotLoaded",
  __field__: "friends"
}