Elixir Multiple Jason Encoders for same struct

919 Views Asked by At

Suppose I'm building an API that is versioned (as an example let's use a user object):

%User{
  id: "b2507407-891b-486e-aaf8-ba262c16d618"
  first_name: "John",
  last_name: "Doe",
  email: "[email protected]"
}

My initial thought was to have multiple encoders for different versions of the API I would run different Jason encoders:

defimpl Jason.EncoderV1, for: __MODULE__ do
  def encode(user, opts) do
    Jason.Encode.map(%{name: "#{user.first_name} #{user.last_name}"}, opts)
  end
end

defimpl Jason.EncoderV2, for: __MODULE__ do
  def encode(user, opts) do
    Jason.Encode.map(%{first_name: first_name, last_name: last_name}, opts)
  end
end

I did not see any reference in the Jason docs that would allow this.

2

There are 2 best solutions below

1
On

You should somehow tell Jason what version do you want to use. Jason.Encoder.encode/2 has the second parameter for this.

defimpl Jason.Encoder, for: __MODULE__ do
  def encode(user, opts) do
    {v, opts} = Keyword.pop(opts, :version, :v1)

    v
    |> case do
      :v2 -> %{first_name: first_name, last_name: last_name}
      _ -> %{name: "#{user.first_name} #{user.last_name}"}
    end
    |> Jason.Encode.map(opts)
  end
end

And call it like Jason.encode!(any, version: :v2).

Sidenote: defimpl expects the module that defines a protocol as a first parameter, one cannot pass whatever, like inexisting Jason.EncoderV2 there.

1
On

Your sample code poses a question that is more of a question about Elixir protocols, but be careful: the Jason package defines its own protocol which is not subject to your versioning -- i.e. your implementations of it would always use defimpl Jason.Encoder.

However, you could define different implementations for different structs, e.g.

defimpl Jason.Encoder, for: UserV1 do
  def encode(user, opts) do
    Jason.Encode.map(%{name: "#{user.first_name} #{user.last_name}"}, opts)
  end
end

defimpl Jason.Encoder, for: UserV2 do
  def encode(user, opts) do
    Jason.Encode.map(%{first_name: first_name, last_name: last_name}, opts)
  end
end

Or, leverage the options (2nd argument) as Aleksei has pointed out in his answer.

In other words, you can't change the name of the protocol (because it is out of your control), but you can change the functionality by providing different names of different modules (which you do control).

Simpler solutions here are less glamorous than protocol implementations:

  1. you could simply define JSON views for your different use cases
  2. you could write custom v1 and v2 functions that would convert the %User{} struct to a regular map (with whatever fields are needed for each), and then rely on the standard JSON encoding as provided by your JSON library (Jason in your case) and its encoding protocol.