@derive in use-able module doesn't work

Deriving Jason.Encoder by default excludes the :__struct__ key. Suppose I wanted to create a useable module that, when used, derives Jason.Encoder with all keys, including :__struct__. How to do that? I tried the following:

defmodule JsonWithStruct do
  defmacro __using__(_) do
    quote do
      @before_compile unquote(__MODULE__)
    end
  end

  defmacro __before_compile__(_) do
    quote do
      @derive IO.inspect({Jason.Encoder, only: Map.keys(@__struct__)})
    end
  end
end
defmodule SomeStruct do
  use JsonWithStruct
  defstruct [:foo, :bar]
end

When compiling, it outputs:

{Jason.Encoder, [only: [:__struct__, :bar, :foo]]}

as expected. However, the protocol is still somehow not implemented.

iex(74)> %SomeStruct{} |> Jason.encode!()       
** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %SomeStruct{bar: nil, foo: nil} of type SomeStruct (a struct), Jason.Encoder protocol must always be explicitly implemented.
iex(73)> Jason.Encoder.impl_for %SomeStruct{}
Jason.Encoder.Any

Why is it not working? I suspect it has to do with protocol compilation having different nuances than regular modules?

@derive must come before the defstruct call. Moving it to __using__ should be enough.

5 Likes

Thank you! Unfortunately that doesn’t work because @__struct__ is not defined yet in __using__)). Is there another way to get the struct keys?

No, but instead you can implement the encoder “manually”:

defmacro __using__(_) do
  module = __CALLER__.module

  quote do
    defimpl Jason.Encoder do
      def encode(%unquote(module){} = data, opts) do
        Jason.Encode.map(data, opts)
      end
    end
  end
end

Disclaimer: I haven’t tested this code, but it should work with some small adjustments.

3 Likes

Indeed that did work! Made a few adaptations, main point being can’t do %unquote(module){} because the struct isn’t available yet.

defimpl Jason.Encoder do
  def encode(data, opts) do
    Jason.Encode.map(Map.put(data, "__struct__", unquote(module)), opts)
  end
end

Thank you!