Lately there has been discussion around rendering Ecto models for a JSON style API in Phoenix. I am part of the core Phoenix team, and I typically use Phoenix to build API’s. I am here to show you an example of using views to render JSON.
Right now there is a very common meme to use Poison Protocols to render your JSON object.
Here is an example from the # elixir-lang IRC channel:
Using the Phoenix.Controller.json/2 to return directly from your controller actions.
And while this is perfectly valid Elixir and Phoenix, in this blog post I want to highly advise against using this method.
So you might be thinking what’s wrong with that method? It shows off the power of Elixir Protocols and handles your model in every case.
By using protocols, you are coupling the presentation or view to the model. Phoenix is an MVC framework, and models will always need to viewed differently in different contexts.
So what’s the better way?
Phoenix Views of course!
Let’s start with the view. We’ll use Paul’s example from above.
Let’s discuss how this is different.
- Explicitly rendering your data via function call. No protocols, only functions.
- All of your helper functions defined in the view are automatically available.
- If you had to modify your output, it would be obvious what to do next. Go check the view.
So this is a pretty simple example, but even in it there lie some bugs.
The first and less interesting bug, is what happens if my associations are not pre-loaded?
In both examples Ecto will raise an exception.
How are the associated model’s user
and comments
rendered?
In the Poison example, it would attempt to apply the encoder protocol on the associations struct, and if it couldn’t, it would fall back to the basic struct. In our example, the same thing would happen! We haven’t explicitly defined the user
or comment
implementation.
Here is how I would update our example to properly render the associations:
You’ll notice my view code has grown quite a bit. I added an encode function for simplicity of calling it from other views. I might even redefine the encode functions to look like this. Because I find it more obvious what is going on and can see at a glance what the JSON body might look like.
I want to point your attention to my encode_lite/1
function in the UserView
. My CommentView
only requires the basic info about a user. To accomplish this I defined a new function body and called it.
In the case of the Poison.Encoder I would need to explicitly call Poison.encode!(comment.user, lite: true)
and have a conditional in my user implementation to handle the option of lite.
As the API project grows and your services become more complex, cases like these become the rule instead of the exception. Views give you the flexibility required to do basically anything you want, it’s just function calls and maps.
Next time I’ll show a more complex example that uses the JSON API spec and I’ll show off some helpers I’ve built to reduce some of the obvious duplication in my views.