I am trying to encode a structure into json format using the Jason library. However, this is not working as expected.
Let's assume I have this struct:
defmodule Test do
defstruct [:foo, :bar, :baz]
end
And that when using Jason.enconde(%Test{foo: 1, bar: 2, baz:3 })
I want this json to be created:
%{"foo" => 1, "banana" => 5}
It is my understanding that to achieve this I need to implement the Jason.Enconder
protocol in my struct:
https://hexdocs.pm/jason/Jason.Encoder.html
defmodule Test do
defstruct [:foo, :bar, :baz]
defimpl Jason.Encoder do
@impl Jason.Encoder
def encode(value, opts) do
Jason.Encode.map(%{foo: Map.get(value, :foo), banana: Map.get(value, :bar, 0) + Map.get(value, :baz, 0)}, opts)
end
end
end
However, this will not work:
Jason.encode(%Test{foo: 1, bar: 2, baz: 3})
{:error,
%Protocol.UndefinedError{
description: "Jason.Encoder protocol must always be explicitly implemented.\n\nIf you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:\n\n @derive {Jason.Encoder, only: [....]}\n defstruct ...\n\nIt is also possible to encode all fields, although this should be used carefully to avoid accidentally leaking private information when new fields are added:\n\n @derive Jason.Encoder\n defstruct ...\n\nFinally, if you don't own the struct you want to encode to JSON, you may use Protocol.derive/3 placed outside of any module:\n\n Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])\n Protocol.derive(Jason.Encoder, NameOfTheStruct)\n",
protocol: Jason.Encoder,
value: %Test{bar: 2, baz: 3, foo: 1}
}}
From what I understand, it looks like I can only select/exclude keys to serialize, I cannot transform/add new keys.
Since I own the structure in question, using Protocol.derive
is not necessary.
However I fail to understand how I can leverage the Jason.Encoder
protocol to achieve what I want.
My guess is, this is due to writing the protocol inside a test file. Protocol consolidation happens before the test file executes, so the protocol never becomes part of the compiled codebase.
To elaborate with an example...
I did the following in a Phoenix app
defmodule Foo do
defstruct [:a, :b]
defimpl Jason.Encoder do
def encode(%Foo{a: a, b: b}, opts) do
Jason.Encode.map(%{"a" => a, "b" => b}, opts)
end
end
end
defmodule FooTest do
use ExUnit.Case
defmodule Bar do
defstruct [:c, :d]
defimpl Jason.Encoder do
def encode(%Bar{c: c, d: d}, opts) do
Jason.Encode.map(%{"c" => c, "d" => d}, opts)
end
end
end
test "encodes Foo" do
%Foo{a: 1, b: 2} |> Jason.encode!() |> IO.inspect()
end
test "encodes Bar" do
%Bar{c: 5, d: 6} |> Jason.encode!()
end
end
Running this test fule, results in the "encodes Foo" passing, but "encodes Bar" fails with a warning
warning: the Jason.Encoder protocol has already been consolidated, an implementation for FooTest.Bar has no effect. If you want to implement protocols after compilation or during tests, check the "Consolidation" section in the Protocol module documentation
followed by an error in the test
** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %FooTest.Bar{c: 5, d: 6} of type FooTest.Bar (a struct), Jason.Encoder protocol must always be explicitly implemented.
This is because of protocol consolidation happening, causing the Bar protocol to not be compiled.
You can turn off protocol consolidation in the test environment, by adding the following to mix.exs
def project do
# ...
consolidate_protocols: Mix.env() != :test,
#...
end
If you do that, the protocol will compile and both tests will pass.
However, the solution is probably to just not write the struct/protocol directly in the test file.