Override Protocol implementation without warning?

My usecase is, I want to override Ecto.Association.NotLoaded's Jason.Encoder implementation, so it returns nil instead of raising. It works if I simply just do it, but not without warnings.

defimpl Jason.Encoder, for: Ecto.Association.NotLoaded do
  def encode(_struct, _opts), do: "null"
end
warning: redefining module Jason.Encoder.Ecto.Association.NotLoaded (current version loaded from _build/dev/lib/ecto/ebin/Elixir.Jason.Encoder.Ecto.
Association.NotLoaded.beam)
  lib/app_web/views/not_loaded.ex:1
warning: this clause cannot match because a previous clause at line 1 always matches
  deps/jason/lib/encoder.ex:1

So the question is, is it possible to do this without warnings?

2 Likes

You can set the compiler option :ignore_module_conflict. You can do that in your mix file by setting it in the project settings under the :elixirc_options key or by setting it in the code with Code.compiler_options/1. With the latter, you may be able to disable it temporarily to allow your one use.

However, I don’t think you’ll be able to get past the second warning.

Also, depending on how elixir handles consolidating protocols, it could be the case that your clause will be the one that never matches, because it already matched the pre-existing one.

My clause actually matches fine.

I guess I was more like wondering whether this is a bad thing to do. Or is there a proper way to do this. Or should protocol implementations be allowed to be overridden.

EDIT: forgot to say thanks!

Having your database schemas automatically serialized to JSON can be convenient. This is technically leaking an implementation detail though. If you change your database, you may not want to change your API. That may not matter for every API though.

One way to address the issue you’re having is to derive a JSON encoding for your schema, that excludes the fields/relationships you’re not going to load.

defmodule User do
  use Ecto.Schema
  @derive {Jason.Encoder, except: [:association_im_not_loading]}

  schema "users" do
  end
end

If you sometimes render the schema in different ways, it’s probably better to do the transformation in your views. This would also be the way you’d use if you want to protect your API from changes to your database. You can do something like:

user
|> Map.from_struct()
|> Map.drop([:field1, :field2])

# or

Map.take(user, [:field1, :field2])

Maybe?

I do need the associations from time to time, so they can’t simply be excluded. I actually explicitly include them in the derive.

This would have to be done at view layer.
Preferably my view layer should not care about what data is preloaded and what is not. (so I won’t have to do this specifically for every different case)

Still wondering if there is a way to override Protocol implementation in general, if anyone knows. Thanks.

5 Likes

In general overridding a protocol like this is probably a bad idea. Especially for structs of libraries, as it would be hard to predict the consequences of that. And in general Protcols are 1:1 from type to implementation.

BUT FEAR NOT you can solve your particular problem by implementing the protocol for each schema manually like so:

      defimpl Jason.Encoder, for: YourSchema do
        def encode(struct, opts) do
           Map.from_struct(struct)
           |> Enum.into(fn
             {key, % Ecto.Association.NotLoaded{} -> {key, "not_loaded"}
             {key, value} -> {key, value}
             end, %{})
           |> Jason.Encode.map(opts)
        end
      end

If you don’t want to have to copy paste this every where I would create a using macro like this:

defmodule Db.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @type t :: %__MODULE__{}
      defimpl Jason.Encoder do
        def encode(struct, opts) do
           Map.from_struct(struct)
           |> Enum.into(fn
             {key, % Ecto.Association.NotLoaded{} -> {key, "not_loaded"}
             {key, value} -> {key, value}
             end, %{})
           |> Jason.Encode.map(opts)
        end
      end
    end
  end
end

Then instead of use Ecto.Schema in your modules do use Db.Schema

2 Likes