When building JSON APIs you often find yourself in a situation when you have part of the JSON already encoded and want to embed it in a bigger structure. A common solution is to decode the encoded part and embed the outer structure just to encode it back again. It’s obvious how that back-and-forth decoding and encoding is wasteful. Fortunately with poison we have a much better alternative.
Poison encoding is based on protocols, this means we can define how a particular
Elixir struct should be encoded - and it doesn’t have to be a structural
encoding - it can be something completely unrelated to the internal
representation. We’re going to leverage that here, by creating a JSONFragment
struct and implementing the Poison.Encoder
for it to return the encoded JSON
“fragment”.
defmodule JSONFragment do
defstruct iodata: []
def new(iodata) when is_binary(iodata) or is_list(iodata) do
%__MODULE__{iodata: iodata}
end
end
defimpl Poison.Encoder, for: JSONFragment do
def encode(%{iodata: iodata}, _options) do
iodata
end
end
And that’s all. Simple, wasn’t it? That’s the power of protocol-based encoding.
How do we use it? Let’s see couple examples how we could use it in Phoenix. We can use it for a part of response in a Phoenix view:
def render("show.json", %{user: user, json: json}) do
%{id: user.id,
name: user.name,
data: JSONFragment.new(json)}
end
Or to respond directly in a Phoenix channel:
def handle_in("load_data", _payload, socket) do
data = load_json_string()
{:reply, {:ok, JSONFragment.new(data)}, socket}
end
Or in any other place that’s going to encode our data with poison. One thing you may have noticed, is that I accept a binary or a list instead of a string, and call the value “iodata” - it’s an optimisation for constant-time string concatenation common in Elixir and Erlang libraries. You can read more about it in the excellent article by Nathan Long.