Phoenix Views for JSON APIs
When doing JSON APIs in phoenix one will eventually hit the fact that by default
structs cannot be encoded to json by Jason
, the default json library of phoenix.
Looking at the README of Jason
this is quickly resolved by doing something like
this:
defmodule User do
@derive Jason.Encoder
defstruct [:id, :name, :title, :coordinates]
end
This does work, but it also has quite a drawback: Protocol implementations are module based and therefore global. One cannot have a given struct encode to multiple different json representations. This might not sound that problematic at first, as one usually hits that problem trying to get the struct to convert to the first json form required in a project – though that might not stay to be the only one.
Changing requirements
The User
struct of the past section could be used in multiple parts of an
application. Let’s consider the application holding a blog, which users can publish on,
and also a map of registered users to find people by location. Both should be powered
by individual api endpoints.
By implementing Jason.Encoder
the users can be encoded to json directly in the
controller:
# BlogController
def index(conn, _) do
json(conn, %{authors: Users.list_users()})
end
# MapController
def index(conn, _) do
json(conn, %{users: Users.list_users()})
end
This will encode all users to json, but including all struct fields on both endpoints, even though only the blog is concerned about titles and coordinates only being relevant to the map component.
If we want to remove the :coordinates
field from the authors this would be possible
by adjusting how the Jason.Encoder
implemenation works. Though it would remove
the coordinates for the map’s users list as well. Additionally it’s not great to need
to adjust a core business logic module to cater to a need of the web api.
Phoenix Views
A great solution to the problem here is using the phoenix view layer instead of
the Jason.Encoder
protocol. Often the view layer of phoenix is seen as a part
only needed for HTML based websites or even more specific for handling templates,
but the view layer is very useful even beyond those use-cases.
The phoenix view layer has two important pieces to it. The template engines
(.eex, .exs, .leex, .heex, …), which turn template files into functions on the
view module – not so important here – and format encoders. Format
encoders turn the values returned by MyAppWeb.SomeView.render/2
functions
into iodata to send back as the http response.
That’s how a map returned from such a function is turned into a json string
if the format is .json
or how for .html
the html encoding is applied. One
can even add custom format encoders (e.g. for .xml
or .mjml
).
Rendering JSON
How would using the phoenix view layer look like for our example. Let’s start with a view module for each of the controllers.
# BlogView
def render("index.json", %{authors: authors}) do
%{
authors: render_many(authors, __MODULE__, "author.json", as: :author)
}
end
def render("author.json", %{author: author}) do
%{
id: author.id,
name: shorten_firstname(author.name),
title: author.title
}
end
defp shorten_firstname(name) do
[first, rest] = String.split(name, " ", parts: 2)
<<letter::binary-size(1), _rest>> = first
"#{letter}. #{rest}"
end
This shows not only how the returned fields can be limited, but also how the format of a field can be adjusted to what needs to be returned. Using views for converting the struct to the returned map of data provides a lot of flexibility and also a place to segment such endpoint specific implementation details into.
This not only prevents controllers to get more complex, but also view modules can be used by multiple controllers. So composition and reuse is supported.
# MapView
def render("index.json", %{users: users}) do
%{
users: render_many(users, __MODULE__, "user.json", as: :user)
}
end
def render("user.json", %{user: user}) do
%{
id: user.id,
name: user.name,
coordinates: user.coordinates
}
end
This one shows the map view. There doesn’t seem to be much new here, but consider that the coordinates are just returned as is, even if in a real world implementation it’s likely a struct as well.
Using the view layer makes most sense for domain models of an application.
Auxiliary structs, which mostly represent complex values like coordinates or
Decimal
structs still benefit from global Jason.Encoder
implementations given
there’s hardly any use in encoding just parts of the data they hold. Those structs
are more akin to a single value represented by a struct of details and less a
container of multiple distinct pieces of data.
Takeaway
Phoenix views are generally a great way to handle the transformation from
application level data to an exchange format sent to other parties. It’s kind of
the inverse of Ecto.Changesets
, which bring data into a system, while views
provide data to the outside. Both are in my opionion nice ways of forming an so
called anti-corruption layer in an application to separate the outside world
from core data formats and making outside change easier to handle.
Starting with phoenix 1.6 the view layer got extracted from phoenix the framework, so it can be included wherever useful, similar to how ecto can be useful even without a database.